evennia.help.models¶
Models for the help system.
The database-tied help system is only half of Evennia’s help functionality, the other one being the auto-generated command help that is created on the fly from each command’s __doc__ string. The persistent database system defined here is intended for all other forms of help that do not concern commands, like information about the game world, policy info, rules and similar.
- class evennia.help.models.HelpEntry(*args, **kwargs)[source]¶
Bases:
SharedMemoryModelA generic help entry.
- An HelpEntry object has the following properties defined:
key - main name of entry help_category - which category entry belongs to (defaults to General) entrytext - the actual help text permissions - perm strings
- Method:
access
- db_key¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- db_help_category¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- db_entrytext¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- db_lock_storage¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- db_tags¶
Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example:
class Pizza(Model): toppings = ManyToManyField(Topping, related_name='pizzas')
**Pizza.toppings** and **Topping.pizzas** are **ManyToManyDescriptor** instances.
Most of the implementation is delegated to a dynamically defined manager class built by **create_forward_many_to_many_manager()** defined below.
- db_date_created¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- objects = <evennia.help.manager.HelpEntryManager object>¶
- property date_created¶
Return the field in localized time based on settings.TIME_ZONE.
- access(accessing_obj, access_type='read', default=True)[source]¶
Determines if another object has permission to access this help entry.
- Accesses used by default:
‘read’ - read the help entry itself. ‘view’ - see help entry in help index.
- Parameters:
accessing_obj (Object or Account) – Entity trying to access this one.
access_type (str) – type of access sought.
default (bool) – What to return if no lock of access_type was found.
- property search_index_entry¶
Property for easily retaining a search index entry for this object.
- web_get_admin_url()[source]¶
Returns the URI path for the Django Admin page for this object.
ex. Account#1 = ‘/admin/accounts/accountdb/1/change/’
- Returns:
path (str) – URI path to Django Admin page for object.
- classmethod web_get_create_url()[source]¶
Returns the URI path for a View that allows users to create new instances of this object.
ex. Chargen = ‘/characters/create/’
For this to work, the developer must have defined a named view somewhere in urls.py that follows the format ‘modelname-action’, so in this case a named view of ‘character-create’ would be referenced by this method.
ex.
url(r'characters/create/', ChargenView.as_view(), name='character-create')
If no View has been created and defined in urls.py, returns an HTML anchor.
This method is naive and simply returns a path. Securing access to the actual view and limiting who can create new objects is the developer’s responsibility.
- Returns:
path (str) – URI path to object creation page, if defined.
- web_get_detail_url()[source]¶
Returns the URI path for a View that allows users to view details for this object.
ex. Oscar (Character) = ‘/characters/oscar/1/’
For this to work, the developer must have defined a named view somewhere in urls.py that follows the format ‘modelname-action’, so in this case a named view of ‘character-detail’ would be referenced by this method.
ex.
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$', CharDetailView.as_view(), name='character-detail')
If no View has been created and defined in urls.py, returns an HTML anchor.
This method is naive and simply returns a path. Securing access to the actual view and limiting who can view this object is the developer’s responsibility.
- Returns:
path (str) – URI path to object detail page, if defined.
- web_get_update_url()[source]¶
Returns the URI path for a View that allows users to update this object.
ex. Oscar (Character) = ‘/characters/oscar/1/change/’
For this to work, the developer must have defined a named view somewhere in urls.py that follows the format ‘modelname-action’, so in this case a named view of ‘character-update’ would be referenced by this method.
ex.
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$', CharUpdateView.as_view(), name='character-update')
If no View has been created and defined in urls.py, returns an HTML anchor.
This method is naive and simply returns a path. Securing access to the actual view and limiting who can modify objects is the developer’s responsibility.
- Returns:
path (str) – URI path to object update page, if defined.
- web_get_delete_url()[source]¶
Returns the URI path for a View that allows users to delete this object.
ex. Oscar (Character) = ‘/characters/oscar/1/delete/’
For this to work, the developer must have defined a named view somewhere in urls.py that follows the format ‘modelname-action’, so in this case a named view of ‘character-detail’ would be referenced by this method.
ex.
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/delete/$', CharDeleteView.as_view(), name='character-delete')
If no View has been created and defined in urls.py, returns an HTML anchor.
This method is naive and simply returns a path. Securing access to the actual view and limiting who can delete this object is the developer’s responsibility.
- Returns:
path (str) – URI path to object deletion page, if defined.
- get_absolute_url()¶
Returns the URI path for a View that allows users to view details for this object.
ex. Oscar (Character) = ‘/characters/oscar/1/’
For this to work, the developer must have defined a named view somewhere in urls.py that follows the format ‘modelname-action’, so in this case a named view of ‘character-detail’ would be referenced by this method.
ex.
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$', CharDetailView.as_view(), name='character-detail')
If no View has been created and defined in urls.py, returns an HTML anchor.
This method is naive and simply returns a path. Securing access to the actual view and limiting who can view this object is the developer’s responsibility.
- Returns:
path (str) – URI path to object detail page, if defined.
- exception DoesNotExist¶
Bases:
ObjectDoesNotExist
- exception MultipleObjectsReturned¶
Bases:
MultipleObjectsReturned
- property entrytext¶
A wrapper for getting database field db_entrytext.
- get_next_by_db_date_created(*, field=<django.db.models.fields.DateTimeField: db_date_created>, is_next=True, **kwargs)¶
- get_previous_by_db_date_created(*, field=<django.db.models.fields.DateTimeField: db_date_created>, is_next=False, **kwargs)¶
- property help_category¶
A wrapper for getting database field db_help_category.
- id¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- property key¶
A wrapper for getting database field db_key.
- property lock_storage¶
A wrapper for getting database field db_lock_storage.
- path = 'evennia.help.models.HelpEntry'¶
- typename = 'SharedMemoryModelBase'¶
- class evennia.help.models.AliasHandler(obj)[source]¶
Bases:
TagHandlerA handler for the Alias Tag type.
- class evennia.help.models.ContentType(id, app_label, model)[source]¶
Bases:
Model- exception DoesNotExist¶
Bases:
ObjectDoesNotExist
- exception MultipleObjectsReturned¶
Bases:
MultipleObjectsReturned
- app_label¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- property app_labeled_name¶
- get_all_objects_for_this_type(**kwargs)[source]¶
Return all objects of this type for the keyword arguments given.
- get_object_for_this_type(using=None, **kwargs)[source]¶
Return an object of this type for the keyword arguments given. Basically, this is a proxy around this object_type’s get_object() model method. The ObjectNotExist exception, if thrown, will not be caught, so code that calls this method should catch it.
- id¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- logentry_set¶
Accessor to the related objects manager on the reverse side of a many-to-one relation.
In the example:
class Child(Model): parent = ForeignKey(Parent, related_name='children')
**Parent.children** is a **ReverseManyToOneDescriptor** instance.
Most of the implementation is delegated to a dynamically defined manager class built by **create_forward_many_to_many_manager()** defined below.
- model¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- property name¶
- objects = <django.contrib.contenttypes.models.ContentTypeManager object>¶
- permission_set¶
Accessor to the related objects manager on the reverse side of a many-to-one relation.
In the example:
class Child(Model): parent = ForeignKey(Parent, related_name='children')
**Parent.children** is a **ReverseManyToOneDescriptor** instance.
Most of the implementation is delegated to a dynamically defined manager class built by **create_forward_many_to_many_manager()** defined below.
- class evennia.help.models.HelpEntryManager(*args, **kwargs)[source]¶
Bases:
TypedObjectManagerThis HelpEntryManager implements methods for searching and manipulating HelpEntries directly from the database.
These methods will all return database objects (or QuerySets) directly.
Evennia-specific: find_topicmatch find_apropos find_topicsuggestions find_topics_with_category all_to_category search_help (equivalent to evennia.search_helpentry)
- all_to_category(default_category)[source]¶
Shifts all help entries in database to default_category. This action cannot be reverted. It is used primarily by the engine when importing a default help database, making sure this ends up in one easily separated category.
- Parameters:
default_category (str) – Category to move entries to.
- create_help(key, entrytext, category='General', locks=None, aliases=None, tags=None)[source]¶
Create a static help entry in the help database. Note that Command help entries are dynamic and directly taken from the __doc__ entries of the command. The database-stored help entries are intended for more general help on the game, more extensive info, in-game setting information and so on.
- Parameters:
key (str) – The name of the help entry.
entrytext (str) – The body of te help entry
category (str, optional) – The help category of the entry.
locks (str, optional) – A lockstring to restrict access.
aliases (list of str, optional) – List of alternative (likely shorter) keynames.
tags (lst, optional) – List of tags or tuples (tag, category).
- Returns:
help (HelpEntry) – A newly created help entry.
- find_apropos(topicstr)[source]¶
Do a very loose search, returning all help entries containing the search criterion in their titles.
- Parameters:
topicstr (str) – Search criterion.
- Returns:
matches (HelpEntries) – Query results.
- find_topicmatch(topicstr, exact=False)[source]¶
Searches for matching topics or aliases based on player’s input.
- Parameters:
topcistr (str) – Help topic to search for.
exact (bool, optional) – Require exact match (non-case-sensitive). If False (default), match sub-parts of the string.
- Returns:
matches (HelpEntries) – Query results.
- find_topics_with_category(help_category)[source]¶
Search topics having a particular category.
- Parameters:
help_category (str) – Category query criterion.
- Returns:
matches (HelpEntries) – Query results.
- find_topicsuggestions(topicstr)[source]¶
Do a fuzzy match, preferably within the category of the current topic.
- Parameters:
topicstr (str) – Search criterion.
- Returns:
matches (Helpentries) – Query results.
- class evennia.help.models.LockHandler(obj)[source]¶
Bases:
objectThis handler should be attached to all objects implementing permission checks, under the property ‘lockhandler’.
- __init__(obj)[source]¶
Loads and pre-caches all relevant locks and their functions.
- Parameters:
obj (object) – The object on which the lockhandler is
defined.
- add(lockstring, validate_only=False)[source]¶
Add a new lockstring to handler.
- Parameters:
lockstring (str or list) – A string on the form “<access_type>:<functions>”. Multiple access types should be separated by semicolon (;). Alternatively, a list with lockstrings.
validate_only (bool, optional) – If True, validate the lockstring but don’t actually store it.
- Returns:
success (bool) –
- The outcome of the addition, False on
error. If validate_only is True, this will be a tuple (bool, error), for pass/fail and a string error.
- append(access_type, lockstring, op='or')[source]¶
Append a lock definition to access_type if it doesn’t already exist.
- Parameters:
access_type (str) – Access type.
lockstring (str) – A valid lockstring, without the operator to link it to an eventual existing lockstring.
op (str) – An operator ‘and’, ‘or’, ‘and not’, ‘or not’ used for appending the lockstring to an existing access-type.
Note
The most common use of this method is for use in commands where the user can specify their own lockstrings. This method allows the system to auto-add things like Admin-override access.
- cache_lock_bypass(obj)[source]¶
We cache superuser bypass checks here for efficiency. This needs to be re-run when an account is assigned to a character. We need to grant access to superusers. We need to check both directly on the object (accounts), through obj.account and using the get_account() method (this sits on serversessions, in some rare cases where a check is done before the login process has yet been fully finalized)
- Parameters:
obj (object) – This is checked for the is_superuser property.
- check(accessing_obj, access_type, default=False, no_superuser_bypass=False)[source]¶
Checks a lock of the correct type by passing execution off to the lock function(s).
- Parameters:
accessing_obj (object) – The object seeking access.
access_type (str) – The type of access wanted.
default (bool, optional) – If no suitable lock type is found, default to this result.
no_superuser_bypass (bool) – Don’t use this unless you really, really need to, it makes supersusers susceptible to the lock check.
Notes
A lock is executed in the follwoing way:
Parsing the lockstring, we (during cache) extract the valid lock functions and store their function objects in the right order along with their args/kwargs. These are now executed in sequence, creating a list of True/False values. This is put into the evalstring, which is a string of AND/OR/NOT entries separated by placeholders where each function result should go. We just put those results in and evaluate the string to get a final, combined True/False value for the lockstring.
The important bit with this solution is that the full lockstring is never blindly evaluated, and thus there (should be) no way to sneak in malign code in it. Only “safe” lock functions (as defined by your settings) are executed.
- check_lockstring(accessing_obj, lockstring, no_superuser_bypass=False, default=False, access_type=None)[source]¶
Do a direct check against a lockstring (‘atype:func()..’), without any intermediary storage on the accessed object.
- Parameters:
accessing_obj (object or None) – The object seeking access. Importantly, this can be left unset if the lock functions don’t access it, no updating or storage of locks are made against this object in this method.
lockstring (str) – Lock string to check, on the form “access_type:lock_definition” where the access_type part can potentially be set to a dummy value to just check a lock condition.
no_superuser_bypass (bool, optional) – Force superusers to heed lock.
default (bool, optional) – Fallback result to use if access_type is set but no such access_type is found in the given lockstring.
access_type (str, bool) – If set, only this access_type will be looked up among the locks defined by lockstring.
- Returns:
access (bool) – If check is passed or not.
- delete(access_type)¶
Remove a particular lock from the handler
- Parameters:
access_type (str) – The type of lock to remove.
- Returns:
success (bool) –
- If the access_type was not found
in the lock, this returns False.
- get(access_type=None)[source]¶
Get the full lockstring or the lockstring of a particular access type.
- Parameters:
access_type (str, optional)
- Returns:
lockstring (str) –
- The matched lockstring, or the full
lockstring if no access_type was given.
- remove(access_type)[source]¶
Remove a particular lock from the handler
- Parameters:
access_type (str) – The type of lock to remove.
- Returns:
success (bool) –
- If the access_type was not found
in the lock, this returns False.
- replace(lockstring)[source]¶
Replaces the lockstring entirely.
- Parameters:
lockstring (str) – The new lock definition.
- Returns:
success (bool) – False if an error occurred.
- Raises:
LockException – If a critical error occurred. If so, the old string is recovered.
Bases:
ModelBase class for idmapped objects. Inherit from this.
Bases:
object
This is called when the idmapper cache is flushed and allows customized actions when this happens.
- Returns:
do_flush (bool) –
- If True, flush this object as normal. If
False, don’t flush and expect this object to handle the flushing on its own.
Method to store an instance in the cache.
- Parameters:
instance (Class instance) – the instance to cache.
new (bool, optional) – this is the first time this instance is cached (i.e. this is not an update operation like after a db save).
Delete the object, clearing cache.
Method to flush an instance from the cache. The instance will always be flushed from the cache, since this is most likely called from delete(), and we want to make sure we don’t cache dead objects.
Flush this instance from the instance cache. Use force to override the result of at_idmapper_flush() for the object.
This will clean safe objects from the cache. Use force keyword to remove all objects, safe or not.
Return the objects so far cached by idmapper for this class.
Method to retrieve a cached instance by pk value. Returns None when not found (which will always be the case when caching is disabled for this class). Please note that the lookup will be done even when instance caching is disabled.
Central database save operation.
Notes
Arguments as per Django documentation. Calls self.at_<fieldname>_postsave(new) (this is a wrapper set by oobhandler: self._oob_at_<fieldname>_postsave())
- class evennia.help.models.Tag(*args, **kwargs)[source]¶
Bases:
ModelTags are quick markers for objects in-game. An typeobject can have any number of tags, stored via its db_tags property. Tagging similar objects will make it easier to quickly locate the group later (such as when implementing zones). The main advantage of tagging as opposed to using tags is speed; a tag is very limited in what data it can hold, and the tag key+category is indexed for efficient lookup in the database. Tags are shared between objects - a new tag is only created if the key+category combination did not previously exist, making them unsuitable for storing object-related data (for this a regular Attribute should be used).
The ‘db_data’ field is intended as a documentation field for the tag itself, such as to document what this tag+category stands for and display that in a web interface or similar.
The main default use for Tags is to implement Aliases for objects. this uses the ‘aliases’ tag category, which is also checked by the default search functions of Evennia to allow quick searches by alias.
- exception DoesNotExist¶
Bases:
ObjectDoesNotExist
- exception MultipleObjectsReturned¶
Bases:
MultipleObjectsReturned
- accountdb_set¶
Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example:
class Pizza(Model): toppings = ManyToManyField(Topping, related_name='pizzas')
**Pizza.toppings** and **Topping.pizzas** are **ManyToManyDescriptor** instances.
Most of the implementation is delegated to a dynamically defined manager class built by **create_forward_many_to_many_manager()** defined below.
- channeldb_set¶
Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example:
class Pizza(Model): toppings = ManyToManyField(Topping, related_name='pizzas')
**Pizza.toppings** and **Topping.pizzas** are **ManyToManyDescriptor** instances.
Most of the implementation is delegated to a dynamically defined manager class built by **create_forward_many_to_many_manager()** defined below.
- db_category¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- db_data¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- db_key¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- db_model¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- db_tagtype¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- helpentry_set¶
Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example:
class Pizza(Model): toppings = ManyToManyField(Topping, related_name='pizzas')
**Pizza.toppings** and **Topping.pizzas** are **ManyToManyDescriptor** instances.
Most of the implementation is delegated to a dynamically defined manager class built by **create_forward_many_to_many_manager()** defined below.
- id¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- msg_set¶
Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example:
class Pizza(Model): toppings = ManyToManyField(Topping, related_name='pizzas')
**Pizza.toppings** and **Topping.pizzas** are **ManyToManyDescriptor** instances.
Most of the implementation is delegated to a dynamically defined manager class built by **create_forward_many_to_many_manager()** defined below.
- objectdb_set¶
Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example:
class Pizza(Model): toppings = ManyToManyField(Topping, related_name='pizzas')
**Pizza.toppings** and **Topping.pizzas** are **ManyToManyDescriptor** instances.
Most of the implementation is delegated to a dynamically defined manager class built by **create_forward_many_to_many_manager()** defined below.
- objects = <django.db.models.manager.Manager object>¶
- scriptdb_set¶
Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example:
class Pizza(Model): toppings = ManyToManyField(Topping, related_name='pizzas')
**Pizza.toppings** and **Topping.pizzas** are **ManyToManyDescriptor** instances.
Most of the implementation is delegated to a dynamically defined manager class built by **create_forward_many_to_many_manager()** defined below.
- class evennia.help.models.TagHandler(obj)[source]¶
Bases:
objectGeneric tag-handler. Accessed via TypedObject.tags.
- __init__(obj)[source]¶
Tags are stored internally in the TypedObject.db_tags m2m field with an tag.db_model based on the obj the taghandler is stored on and with a tagtype given by self.handlertype
- Parameters:
obj (object) – The object on which the handler is set.
- add(key=None, category=None, data=None)[source]¶
Add a new tag to the handler.
- Parameters:
key (str or list) – The name of the tag to add. If a list, add several Tags.
category (str, optional) – Category of Tag. None is the default category.
data (str, optional) – Info text about the tag(s) added. This can not be used to store object-unique info but only eventual info about the tag itself.
Notes
If the tag + category combination matches an already existing Tag object, this will be re-used and no new Tag will be created.
- all(return_key_and_category=False, return_objs=False)[source]¶
Get all tags in this handler, regardless of category.
- Parameters:
return_key_and_category (bool, optional) – Return a list of tuples [(key, category), …].
return_objs (bool, optional) – Return tag objects.
- Returns:
tags (list) –
- A list of tag keys [tagkey, tagkey, …] or
a list of tuples [(key, category), …] if return_key_and_category is set.
- batch_add(*args)[source]¶
Batch-add tags from a list of tuples.
- Parameters:
*args (tuple or str) – Each argument should be a tagstr keys or tuple (keystr, category) or (keystr, category, data). It’s possible to mix input types.
Notes
This will generate a mimimal number of self.add calls, based on the number of categories involved (including None) (data is not unique and may be overwritten by the content of a latter tuple with the same category).
- batch_remove(*args)[source]¶
Batch-remove tags from a list of tuples.
- Parameters:
*args (tuple or str) – Each argument should be a tagstr keys or tuple (keystr, category) or (keystr, category, data) (the data field is ignored, only keystr/category matters). It’s possible to mix input types.
- clear(category=None)[source]¶
Remove all tags from the handler.
- Parameters:
category (str, optional) – The Tag category to limit the request to. Note that None is the valid, default category.
- get(key=None, default=None, category=None, return_tagobj=False, return_list=False, raise_exception=False)[source]¶
Get the tag for the given key, category or combination of the two.
- Parameters:
key (str or list, optional) – The tag or tags to retrieve.
default (any, optional) – The value to return in case of no match.
category (str, optional) – The Tag category to limit the request to. Note that None is the valid, default category. If no key is given, all tags of this category will be returned.
return_tagobj (bool, optional) – Return the Tag object itself instead of a string representation of the Tag.
return_list (bool, optional) – Always return a list, regardless of number of matches.
raise_exception (bool, optional) – Raise AttributeError if no matches are found.
- Returns:
tags (list) –
- The matches, either string
representations of the tags or the Tag objects themselves depending on return_tagobj. If ‘default’ is set, this will be a list with the default value as its only element.
- Raises:
AttributeError – If finding no matches and raise_exception is True.
- has(key=None, category=None, return_list=False)[source]¶
Checks if the given Tag (or list of Tags) exists on the object.
- Parameters:
key (str or iterable) – The Tag key or tags to check for. If None, search by category.
category (str, optional) – Limit the check to Tags with this category (note, that None is the default category).
- Returns:
has_tag (bool or list) –
- If the Tag exists on this object or not.
If tag was given as an iterable then the return is a list of booleans.
- Raises:
ValueError – If neither tag nor category is given.
- remove(key=None, category=None)[source]¶
Remove a tag from the handler based ond key and/or category.
- Parameters:
key (str or list, optional) – The tag or tags to retrieve.
category (str, optional) – The Tag category to limit the request to. Note that None is the valid, default category
Notes
If neither key nor category is specified, this acts as .clear().
- class evennia.help.models.lazy_property(func: Callable[[...], TProp], name=None, doc=None)[source]¶
Bases:
Generic[TProp]Delays loading of property until first access. Credit goes to the Implementation in the werkzeug suite: http://werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
This should be used as a decorator in a class and in Evennia is mainly used to lazy-load handlers:
@lazy_property def attributes(self): return AttributeHandler(self)
Once initialized, the AttributeHandler will be available as a property “attributes” on the object. This is read-only since this functionality is pretty much exclusively used by handlers.
- evennia.help.models.reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None, *, query=None, fragment=None)[source]¶
- evennia.help.models.slugify(value, allow_unicode=False)[source]¶
Convert to ASCII if ‘allow_unicode’ is False. Convert spaces or repeated dashes to single dashes. Remove characters that aren’t alphanumerics, underscores, or hyphens. Convert to lowercase. Also strip leading and trailing whitespace, dashes, and underscores.