Upgrade evennia dependencies and RUN MIGRATIONS. Update requirement to Django 5.3, which has some backwards-incompatible index changes

This commit is contained in:
Griatch 2025-03-01 23:23:41 +01:00
parent 4269745d3e
commit 0f28eb1ac3
12 changed files with 701 additions and 588 deletions

View file

@ -2,9 +2,11 @@
## Main branch
Updated dependencies: Twisted >24 (<25). Python 3.10, 3.11, 3.12, 3.13. Will
drop 3.10 support as part of next major release.
Updated dependencies: Django >5.1 (<5,2), Twisted >24 (<25).
Python versions: 3.11, 3.12, 3.13.
- Feat (backwards incompatible): RUN MIGRATIONS (`evennia migrate`): Now requiring Django 5.1 (Griatch)
- Feat (backwards incompatible): Drop support and testing for Python 3.10 (Griatch)
- [Feat][pull3719]: Support Python 3.13. (0xDEADFED5)
- [Feat][pull3633]: Default object's default descs are now taken from a `default_description`
class variable instead of the `desc` Attribute always being set (count-infinity)
@ -13,8 +15,7 @@ drop 3.10 support as part of next major release.
strings instead of `None` if no name is provided, also enforce string type (InspectorCaracal)
- [Fix][pull3682]: Allow in-game help searching for commands natively starting
with `*` (which is the Lunr search wildcard) (count-infinity)
- [Fix][pull3684]: Web client stopped auto-focusing the input box after opening
settings (count-infinity)
- [Fix][pull3684]: Web client stopped auto-focusing the input box after opening settings (count-infinity)
- [Fix][pull3689]: Partial matching fix in default search, makes sure e.g. `b sw` uniquely
finds `big sword` even if another type of sword is around (InspectorCaracal)
- [Fix][pull3690]: In searches, allow special 'here' and 'me' keywords only be valid queries
@ -37,7 +38,6 @@ drop 3.10 support as part of next major release.
used as the task's category (Griatch)
- [Docs]: Fixes from InspectorCaracal, Griatch, ChrisLR
[pull3633]: https://github.com/evennia/evennia/pull/3633
[pull3677]: https://github.com/evennia/evennia/pull/3677
[pull3682]: https://github.com/evennia/evennia/pull/3682

View file

@ -3,8 +3,8 @@
# when people upgrade outside regular channels). This file only supports lines of
# `value = number` and only specific names supported by the handler.
PYTHON_MIN = 3.10
PYTHON_MIN = 3.11
PYTHON_MAX_TESTED = 3.13.100
TWISTED_MIN = 24.11
DJANGO_MIN = 4.0.2
DJANGO_MAX_TESTED = 4.2.100
DJANGO_MAX_TESTED = 5.1.100

View file

@ -8,7 +8,7 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.files.base import ContentFile
from django.test import TestCase, override_settings
from django.utils.timezone import is_aware, utc
from django.utils.timezone import is_aware
_SKIP = False
try:
@ -533,7 +533,7 @@ class S3Boto3StorageTests(S3Boto3TestCase):
def _test_storage_mtime(self, use_tz):
obj = self.storage.bucket.Object.return_value
obj.last_modified = datetime.datetime.now(utc)
obj.last_modified = datetime.datetime.now(datetime.timezone.utc)
name = "file.txt"
self.assertFalse(

View file

@ -1,15 +1,15 @@
This directory contains Evennia's log files. The existence of this README.md file is also necessary
to correctly include the log directory in git (since log files are ignored by git and you can't
commit an empty directory).
commit an empty directory).
- `server.log` - log file from the game Server.
- `portal.log` - log file from Portal proxy (internet facing)
`server.log` - log file from the game Server.
`portal.log` - log file from Portal proxy (internet facing)
Usually these logs are viewed together with `evennia -l`. They are also rotated every week so as not
to be too big. Older log names will have a name appended by `_month_date`.
- `lockwarnings.log` - warnings from the lock system.
- `http_requests.log` - this will generally be empty unless turning on debugging inside the server.
to be too big. Older log names will have a name appended by `_month_date`.
- `channel_<channelname>.log` - these are channel logs for the in-game channels They are also used
by the `/history` flag in-game to get the latest message history.
`lockwarnings.log` - warnings from the lock system.
`http_requests.log` - this will generally be empty unless turning on debugging inside the server.
`channel_<channelname>.log` - these are channel logs for the in-game channels They are also used
by the `/history` flag in-game to get the latest message history.

View file

@ -1484,69 +1484,78 @@ def create_superuser():
def check_database(always_return=False):
"""
Check so the database exists.
Check if the database exists and has basic tables. This is only run by the launcher.
Args:
always_return (bool, optional): If set, will always return True/False
also on critical errors. No output will be printed.
always_return (bool, optional): If True, will not raise exceptions on errors.
Returns:
exists (bool): `True` if the database exists, otherwise `False`.
exists (bool): `True` if database exists and seems set up, `False` otherwise.
If `always_return` is `False`, this will raise exceptions instead of
returning `False`.
"""
# Check so a database exists and is accessible
# Check if database exists
from django.conf import settings
from django.db import connection
tables = connection.introspection.get_table_list(connection.cursor())
if not tables or not isinstance(tables[0], str): # django 1.8+
tables = [tableinfo.name for tableinfo in tables]
if tables and "accounts_accountdb" in tables:
# database exists and seems set up. Initialize evennia.
evennia._init()
# Try to get Account#1
from evennia.accounts.models import AccountDB
tables_to_check = [
"accounts_accountdb", # base account table
"objects_objectdb", # base object table
"scripts_scriptdb", # base script table
"typeclasses_tag", # base tag table
]
try:
AccountDB.objects.get(id=1)
except (django.db.utils.OperationalError, ProgrammingError) as e:
if always_return:
return False
print(ERROR_DATABASE.format(traceback=e))
sys.exit()
except AccountDB.DoesNotExist:
# no superuser yet. We need to create it.
with connection.cursor() as cursor:
# Get all table names in the database
if connection.vendor == "postgresql":
cursor.execute(
"""
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
"""
)
elif connection.vendor == "mysql":
cursor.execute(
"""
SELECT table_name FROM information_schema.tables
WHERE table_schema = %s
""",
[settings.DATABASES["default"]["NAME"]],
)
elif connection.vendor == "sqlite":
cursor.execute(
"""
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
"""
)
else:
if not always_return:
raise Exception(f"Unsupported database vendor: {connection.vendor}")
return False
other_superuser = AccountDB.objects.filter(is_superuser=True)
if other_superuser:
# Another superuser was found, but not with id=1. This may
# happen if using flush (the auto-id starts at a higher
# value). Wwe copy this superuser into id=1. To do
# this we must deepcopy it, delete it then save the copy
# with the new id. This allows us to avoid the UNIQUE
# constraint on usernames.
other = other_superuser[0]
other_id = other.id
other_key = other.username
print(WARNING_MOVING_SUPERUSER.format(other_key=other_key, other_id=other_id))
res = ""
while res.upper() != "Y":
# ask for permission
res = eval(input("Continue [Y]/N: "))
if res.upper() == "N":
sys.exit()
elif not res:
break
# continue with the
from copy import deepcopy
existing_tables = {row[0].lower() for row in cursor.fetchall()}
new = deepcopy(other)
other.delete()
new.id = 1
new.save()
else:
create_superuser()
check_database(always_return=always_return)
return True
# Check if essential tables exist
missing_tables = [table for table in tables_to_check if table not in existing_tables]
if missing_tables:
if always_return:
return False
raise Exception(
f"Database tables missing: {', '.join(missing_tables)}. "
"Did you remember to run migrations?"
)
return True
except Exception as exc:
if not always_return:
raise
import traceback
traceback.print_exc()
return False
def getenv():

View file

@ -0,0 +1,27 @@
# Generated by Django 5.1.6 on 2025-03-01 21:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("typeclasses", "0016_alter_attribute_id_alter_tag_id"),
]
operations = [
# First create the index with the old name that Django expects
migrations.AddIndex(
model_name="tag",
index=models.Index(
fields=["db_key", "db_category", "db_tagtype", "db_model"],
name="typeclasses_tag_db_key_db_category_db_tagtype_db_model_idx",
),
),
# Then rename it to the new name
migrations.RenameIndex(
model_name="tag",
new_name="typeclasses_db_key_be0c81_idx",
old_fields=("db_key", "db_category", "db_tagtype", "db_model"),
),
]

View file

@ -14,7 +14,6 @@ from collections import defaultdict
from django.conf import settings
from django.db import models
from evennia.locks.lockfuncs import perm as perm_lockfunc
from evennia.utils.utils import make_iter, to_str
@ -79,9 +78,10 @@ class Tag(models.Model):
class Meta:
"Define Django meta options"
verbose_name = "Tag"
unique_together = (("db_key", "db_category", "db_tagtype", "db_model"),)
index_together = (("db_key", "db_category", "db_tagtype", "db_model"),)
indexes = [models.Index(fields=["db_key", "db_category", "db_tagtype", "db_model"])]
def __lt__(self, other):
return str(self) < str(other)

View file

@ -14,8 +14,7 @@ objects already existing in the database.
"""
from django.contrib.contenttypes.models import ContentType
from django.db.utils import OperationalError, ProgrammingError
from django.utils.functional import SimpleLazyObject
# limit symbol import from API
__all__ = (
@ -29,232 +28,254 @@ __all__ = (
_GA = object.__getattribute__
# import objects this way to avoid circular import problems
try:
ObjectDB = ContentType.objects.get(app_label="objects", model="objectdb").model_class()
ScriptDB = ContentType.objects.get(app_label="scripts", model="scriptdb").model_class()
AccountDB = ContentType.objects.get(app_label="accounts", model="accountdb").model_class()
Msg = ContentType.objects.get(app_label="comms", model="msg").model_class()
ChannelDB = ContentType.objects.get(app_label="comms", model="channeldb").model_class()
HelpEntry = ContentType.objects.get(app_label="help", model="helpentry").model_class()
Tag = ContentType.objects.get(app_label="typeclasses", model="tag").model_class()
except (OperationalError, ProgrammingError):
# this is a fallback used during tests/doc building
print("Database not available yet - using temporary fallback for create managers.")
from evennia.accounts.models import AccountDB
from evennia.comms.models import ChannelDB, Msg
from evennia.help.models import HelpEntry
from evennia.objects.models import ObjectDB
from evennia.scripts.models import ScriptDB
from evennia.typeclasses.tags import Tag # noqa
#
# Game Object creation
#
# Create a new in-game object.
#
# Keyword Args:
# typeclass (class or str): Class or python path to a typeclass.
# key (str): Name of the new object. If not set, a name of
# `#dbref` will be set.
# location (Object or str): Obj or #dbref to use as the location of the new object.
# home (Object or str): Obj or #dbref to use as the object's home location.
# permissions (list): A list of permission strings or tuples (permstring, category).
# locks (str): one or more lockstrings, separated by semicolons.
# aliases (list): A list of alternative keys or tuples (aliasstring, category).
# tags (list): List of tag keys or tuples (tagkey, category) or (tagkey, category, data).
# destination (Object or str): Obj or #dbref to use as an Exit's target.
# report_to (Object): The object to return error messages to.
# nohome (bool): This allows the creation of objects without a
# default home location; only used when creating the default
# location itself or during unittests.
# attributes (list): Tuples on the form (key, value) or (key, value, category),
# (key, value, lockstring) or (key, value, lockstring, default_access).
# to set as Attributes on the new object.
# nattributes (list): Non-persistent tuples on the form (key, value). Note that
# adding this rarely makes sense since this data will not survive a reload.
#
# Returns:
# object (Object): A newly created object of the given typeclass.
#
# Raises:
# ObjectDB.DoesNotExist: If trying to create an Object with
# `location` or `home` that can't be found.
#
# Lazy-loaded model classes
def _get_objectdb():
from django.contrib.contenttypes.models import ContentType
create_object = ObjectDB.objects.create_object
# alias for create_object
return ContentType.objects.get(app_label="objects", model="objectdb").model_class()
def _get_scriptdb():
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get(app_label="scripts", model="scriptdb").model_class()
def _get_accountdb():
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get(app_label="accounts", model="accountdb").model_class()
def _get_msg():
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get(app_label="comms", model="msg").model_class()
def _get_channeldb():
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get(app_label="comms", model="channeldb").model_class()
def _get_helpentry():
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get(app_label="help", model="helpentry").model_class()
def _get_tag():
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get(app_label="typeclasses", model="tag").model_class()
# Lazy model instances
ObjectDB = SimpleLazyObject(_get_objectdb)
ScriptDB = SimpleLazyObject(_get_scriptdb)
AccountDB = SimpleLazyObject(_get_accountdb)
Msg = SimpleLazyObject(_get_msg)
ChannelDB = SimpleLazyObject(_get_channeldb)
HelpEntry = SimpleLazyObject(_get_helpentry)
Tag = SimpleLazyObject(_get_tag)
def create_object(*args, **kwargs):
"""
Create a new in-game object.
Keyword Args:
typeclass (class or str): Class or python path to a typeclass.
key (str): Name of the new object. If not set, a name of
`#dbref` will be set.
location (Object or str): Obj or #dbref to use as the location of the new object.
home (Object or str): Obj or #dbref to use as the object's home location.
permissions (list): A list of permission strings or tuples (permstring, category).
locks (str): one or more lockstrings, separated by semicolons.
aliases (list): A list of alternative keys or tuples (aliasstring, category).
tags (list): List of tag keys or tuples (tagkey, category) or (tagkey, category, data).
destination (Object or str): Obj or #dbref to use as an Exit's target.
report_to (Object): The object to return error messages to.
nohome (bool): This allows the creation of objects without a
default home location; only used when creating the default
location itself or during unittests.
attributes (list): Tuples on the form (key, value) or (key, value, category),
(key, value, lockstring) or (key, value, lockstring, default_access).
to set as Attributes on the new object.
nattributes (list): Non-persistent tuples on the form (key, value). Note that
adding this rarely makes sense since this data will not survive a reload.
Returns:
object (Object): A newly created object of the given typeclass.
Raises:
ObjectDB.DoesNotExist: If trying to create an Object with
`location` or `home` that can't be found.
"""
return ObjectDB.objects.create_object(*args, **kwargs)
def create_script(*args, **kwargs):
"""
Create a new script. All scripts are a combination of a database
object that communicates with the database, and an typeclass that
'decorates' the database object into being different types of
scripts. It's behaviour is similar to the game objects except
scripts has a time component and are more limited in scope.
Keyword Args:
typeclass (class or str): Class or python path to a typeclass.
key (str): Name of the new object. If not set, a name of
#dbref will be set.
obj (Object): The entity on which this Script sits. If this
is `None`, we are creating a "global" script.
account (Account): The account on which this Script sits. It is
exclusiv to `obj`.
locks (str): one or more lockstrings, separated by semicolons.
interval (int): The triggering interval for this Script, in
seconds. If unset, the Script will not have a timing
component.
start_delay (bool): If `True`, will wait `interval` seconds
before triggering the first time.
repeats (int): The number of times to trigger before stopping.
If unset, will repeat indefinitely.
persistent (bool): If this Script survives a server shutdown
or not (all Scripts will survive a reload).
autostart (bool): If this Script will start immediately when
created or if the `start` method must be called explicitly.
report_to (Object): The object to return error messages to.
desc (str): Optional description of script
tags (list): List of tags or tuples (tag, category).
attributes (list): List if tuples (key, value) or (key, value, category)
(key, value, lockstring) or (key, value, lockstring, default_access).
Returns:
script (obj): An instance of the script created
See evennia.scripts.manager for methods to manipulate existing
scripts in the database.
"""
return ScriptDB.objects.create_script(*args, **kwargs)
def create_help_entry(*args, **kwargs):
"""
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.
Args:
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.
"""
return HelpEntry.objects.create_help(*args, **kwargs)
def create_message(*args, **kwargs):
"""
Create a new communication Msg. Msgs represent a unit of
database-persistent communication between entites.
Args:
senderobj (Object, Account, Script, str or list): The entity (or
entities) sending the Msg. If a `str`, this is the id-string
for an external sender type.
message (str): Text with the message. Eventual headers, titles
etc should all be included in this text string. Formatting
will be retained.
receivers (Object, Account, Script, str or list): An Account/Object to send
to, or a list of them. If a string, it's an identifier for an external
receiver.
locks (str): Lock definition string.
tags (list): A list of tags or tuples `(tag, category)`.
header (str): Mime-type or other optional information for the message
Notes:
The Comm system is created to be very open-ended, so it's fully
possible to let a message both go several receivers at the same time,
it's up to the command definitions to limit this as desired.
"""
return Msg.objects.create_message(*args, **kwargs)
def create_channel(*args, **kwargs):
"""
Create A communication Channel. A Channel serves as a central hub
for distributing Msgs to groups of people without specifying the
receivers explicitly. Instead accounts may 'connect' to the channel
and follow the flow of messages. By default the channel allows
access to all old messages, but this can be turned off with the
keep_log switch.
Args:
key (str): This must be unique.
Keyword Args:
aliases (list of str): List of alternative (likely shorter) keynames.
desc (str): A description of the channel, for use in listings.
locks (str): Lockstring.
keep_log (bool): Log channel throughput.
typeclass (str or class): The typeclass of the Channel (not
often used).
tags (list): A list of tags or tuples `(tag[,category[,data]])`.
attrs (list): List of attributes on form `(name, value[,category[,lockstring]])`
Returns:
channel (Channel): A newly created channel.
"""
return ChannelDB.objects.create_channel(*args, **kwargs)
def create_account(*args, **kwargs):
"""
This creates a new account.
Args:
key (str): The account's name. This should be unique.
email (str or None): Email on valid addr@addr.domain form. If
the empty string, will be set to None.
password (str): Password in cleartext.
Keyword Args:
typeclass (str): The typeclass to use for the account.
is_superuser (bool): Whether or not this account is to be a superuser
locks (str): Lockstring.
permission (list): List of permission strings.
tags (list): List of Tags on form `(key, category[, data])`
attributes (list): List of Attributes on form
`(key, value [, category, [,lockstring [, default_pass]]])`
report_to (Object): An object with a msg() method to report
errors to. If not given, errors will be logged.
Returns:
Account: The newly created Account.
Raises:
ValueError: If `key` already exists in database.
Notes:
Usually only the server admin should need to be superuser, all
other access levels can be handled with more fine-grained
permissions or groups. A superuser bypasses all lock checking
operations and is thus not suitable for play-testing the game.
"""
return AccountDB.objects.create_account(*args, **kwargs)
# Aliases for the creation functions
object = create_object
#
# Script creation
# Create a new script. All scripts are a combination of a database
# object that communicates with the database, and an typeclass that
# 'decorates' the database object into being different types of
# scripts. It's behaviour is similar to the game objects except
# scripts has a time component and are more limited in scope.
#
# Keyword Args:
# typeclass (class or str): Class or python path to a typeclass.
# key (str): Name of the new object. If not set, a name of
# #dbref will be set.
# obj (Object): The entity on which this Script sits. If this
# is `None`, we are creating a "global" script.
# account (Account): The account on which this Script sits. It is
# exclusiv to `obj`.
# locks (str): one or more lockstrings, separated by semicolons.
# interval (int): The triggering interval for this Script, in
# seconds. If unset, the Script will not have a timing
# component.
# start_delay (bool): If `True`, will wait `interval` seconds
# before triggering the first time.
# repeats (int): The number of times to trigger before stopping.
# If unset, will repeat indefinitely.
# persistent (bool): If this Script survives a server shutdown
# or not (all Scripts will survive a reload).
# autostart (bool): If this Script will start immediately when
# created or if the `start` method must be called explicitly.
# report_to (Object): The object to return error messages to.
# desc (str): Optional description of script
# tags (list): List of tags or tuples (tag, category).
# attributes (list): List if tuples (key, value) or (key, value, category)
# (key, value, lockstring) or (key, value, lockstring, default_access).
#
# Returns:
# script (obj): An instance of the script created
#
# See evennia.scripts.manager for methods to manipulate existing
# scripts in the database.
create_script = ScriptDB.objects.create_script
# alias
script = create_script
#
# Help entry creation
#
# """
# 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.
#
# Args:
# 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.
#
create_help_entry = HelpEntry.objects.create_help
# alias
help_entry = create_help_entry
#
# Comm system methods
#
# Create a new communication Msg. Msgs represent a unit of
# database-persistent communication between entites.
#
# Args:
# senderobj (Object, Account, Script, str or list): The entity (or
# entities) sending the Msg. If a `str`, this is the id-string
# for an external sender type.
# message (str): Text with the message. Eventual headers, titles
# etc should all be included in this text string. Formatting
# will be retained.
# receivers (Object, Account, Script, str or list): An Account/Object to send
# to, or a list of them. If a string, it's an identifier for an external
# receiver.
# locks (str): Lock definition string.
# tags (list): A list of tags or tuples `(tag, category)`.
# header (str): Mime-type or other optional information for the message
#
# Notes:
# The Comm system is created to be very open-ended, so it's fully
# possible to let a message both go several receivers at the same time,
# it's up to the command definitions to limit this as desired.
#
create_message = Msg.objects.create_message
message = create_message
create_msg = create_message
# Create A communication Channel. A Channel serves as a central hub
# for distributing Msgs to groups of people without specifying the
# receivers explicitly. Instead accounts may 'connect' to the channel
# and follow the flow of messages. By default the channel allows
# access to all old messages, but this can be turned off with the
# keep_log switch.
#
# Args:
# key (str): This must be unique.
#
# Keyword Args:
# aliases (list of str): List of alternative (likely shorter) keynames.
# desc (str): A description of the channel, for use in listings.
# locks (str): Lockstring.
# keep_log (bool): Log channel throughput.
# typeclass (str or class): The typeclass of the Channel (not
# often used).
# tags (list): A list of tags or tuples `(tag, category)`.
#
# Returns:
# channel (Channel): A newly created channel.
#
create_channel = ChannelDB.objects.create_channel
channel = create_channel
#
# Account creation methods
#
# This creates a new account.
#
# Args:
# key (str): The account's name. This should be unique.
# email (str or None): Email on valid addr@addr.domain form. If
# the empty string, will be set to None.
# password (str): Password in cleartext.
#
# Keyword Args:
# typeclass (str): The typeclass to use for the account.
# is_superuser (bool): Wether or not this account is to be a superuser
# locks (str): Lockstring.
# permission (list): List of permission strings.
# tags (list): List of Tags on form `(key, category[, data])`
# attributes (list): List of Attributes on form
# `(key, value [, category, [,lockstring [, default_pass]]])`
# report_to (Object): An object with a msg() method to report
# errors to. If not given, errors will be logged.
#
# Returns:
# Account: The newly created Account.
# Raises:
# ValueError: If `key` already exists in database.
#
#
# Notes:
# Usually only the server admin should need to be superuser, all
# other access levels can be handled with more fine-grained
# permissions or groups. A superuser bypasses all lock checking
# operations and is thus not suitable for play-testing the game.
create_account = AccountDB.objects.create_account
# alias
account = create_account

View file

@ -21,13 +21,9 @@ Example: To reach the search method 'get_object_with_account'
> from evennia.objects.models import ObjectDB
> match = Object.objects.get_object_with_account(...)
"""
# Import the manager methods to be wrapped
from django.contrib.contenttypes.models import ContentType
from django.db.utils import OperationalError, ProgrammingError
from django.utils.functional import SimpleLazyObject
# limit symbol import from API
__all__ = (
@ -45,180 +41,265 @@ __all__ = (
)
# import objects this way to avoid circular import problems
try:
ObjectDB = ContentType.objects.get(app_label="objects", model="objectdb").model_class()
AccountDB = ContentType.objects.get(app_label="accounts", model="accountdb").model_class()
ScriptDB = ContentType.objects.get(app_label="scripts", model="scriptdb").model_class()
Msg = ContentType.objects.get(app_label="comms", model="msg").model_class()
ChannelDB = ContentType.objects.get(app_label="comms", model="channeldb").model_class()
HelpEntry = ContentType.objects.get(app_label="help", model="helpentry").model_class()
Tag = ContentType.objects.get(app_label="typeclasses", model="tag").model_class()
except (OperationalError, ProgrammingError):
# this is a fallback used during tests/doc building
print("Database not available yet - using temporary fallback for search managers.")
from evennia.accounts.models import AccountDB
from evennia.comms.models import ChannelDB, Msg
from evennia.help.models import HelpEntry
from evennia.objects.models import ObjectDB
from evennia.scripts.models import ScriptDB
from evennia.typeclasses.tags import Tag # noqa
# Lazy-loaded model classes
def _get_objectdb():
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get(app_label="objects", model="objectdb").model_class()
def _get_accountdb():
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get(app_label="accounts", model="accountdb").model_class()
def _get_scriptdb():
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get(app_label="scripts", model="scriptdb").model_class()
def _get_msg():
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get(app_label="comms", model="msg").model_class()
def _get_channeldb():
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get(app_label="comms", model="channeldb").model_class()
def _get_helpentry():
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get(app_label="help", model="helpentry").model_class()
def _get_tag():
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get(app_label="typeclasses", model="tag").model_class()
# Lazy model instances
ObjectDB = SimpleLazyObject(_get_objectdb)
AccountDB = SimpleLazyObject(_get_accountdb)
ScriptDB = SimpleLazyObject(_get_scriptdb)
Msg = SimpleLazyObject(_get_msg)
ChannelDB = SimpleLazyObject(_get_channeldb)
HelpEntry = SimpleLazyObject(_get_helpentry)
Tag = SimpleLazyObject(_get_tag)
# -------------------------------------------------------------------
# Search manager-wrappers
# -------------------------------------------------------------------
#
# Search objects as a character
#
# NOTE: A more powerful wrapper of this method
# is reachable from within each command class
# by using self.caller.search()!
#
# def object_search(self, ostring=None,
# attribute_name=None,
# typeclass=None,
# candidates=None,
# exact=True):
#
# Search globally or in a list of candidates and return results.
# The result is always a list of Objects (or the empty list)
#
# Arguments:
# ostring: (str) The string to compare names against. By default (if
# not attribute_name is set), this will search object.key
# and object.aliases in order. Can also be on the form #dbref,
# which will, if exact=True be matched against primary key.
# attribute_name: (str): Use this named ObjectAttribute to match ostring
# against, instead of the defaults.
# typeclass (str or TypeClass): restrict matches to objects having
# this typeclass. This will help speed up global searches.
# candidates (list obj ObjectDBs): If supplied, search will only be
# performed among the candidates in this list. A common list
# of candidates is the contents of the current location.
# exact (bool): Match names/aliases exactly or partially. Partial
# matching matches the beginning of words in the names/aliases,
# using a matching routine to separate multiple matches in
# names with multiple components (so "bi sw" will match
# "Big sword"). Since this is more expensive than exact
# matching, it is recommended to be used together with
# the objlist keyword to limit the number of possibilities.
# This keyword has no meaning if attribute_name is set.
#
# Returns:
# A list of matching objects (or a list with one unique match)
# def object_search(self, ostring, caller=None,
# candidates=None,
# attribute_name=None):
#
search_object = ObjectDB.objects.search_object
def search_object(*args, **kwargs):
"""
Search for objects in the database.
Args:
key (str or int): Object key or dbref to search for. This can also
be a list of keys/dbrefs. `None` (default) returns all objects.
exact (bool): Only valid for string keys. If True, requires exact
key match, otherwise also match key with case-insensitive and
partial matching. Default is True.
candidates (list): Only search among these object candidates,
if given. Default is to search all objects.
attribute_name (str): If set, search by objects with this attribute_name
defined on them, with the value specified by `attribute_value`.
attribute_value (any): What value the given attribute_name must have.
location (Object): Filter by objects at this location.
typeclass (str or TypeClass): Filter by objects having this typeclass.
This can also be a list of typeclasses.
tags (str or list): Filter by objects having one or more Tags.
This can be a single tag key, a list of tag keys, or a list of
tuples (tag_key, tag_category).
nofetch (bool): Don't fetch typeclass and perms data from db.
This is faster but gives less info.
Returns:
matches (list): List of Objects matching the search criteria.
"""
return ObjectDB.objects.search_object(*args, **kwargs)
search_objects = search_object
object_search = search_object
objects = search_objects
#
# Search for accounts
#
# account_search(self, ostring)
# Searches for a particular account by name or
# database id.
#
# ostring = a string or database id.
#
def search_account(*args, **kwargs):
"""
Search for accounts in the database.
Args:
key (str or int): Account key or dbref to search for. This can also
be a list of keys/dbrefs. `None` (default) returns all accounts.
exact (bool): Only valid for string keys. If True, requires exact
key match, otherwise also match key with case-insensitive and
partial matching. Default is True.
candidates (list): Only search among these account candidates,
if given. Default is to search all accounts.
attribute_name (str): If set, search by accounts with this attribute_name
defined on them, with the value specified by `attribute_value`.
attribute_value (any): What value the given attribute_name must have.
tags (str or list): Filter by accounts having one or more Tags.
This can be a single tag key, a list of tag keys, or a list of
tuples (tag_key, tag_category).
nofetch (bool): Don't fetch typeclass and perms data from db.
This is faster but gives less info.
Returns:
matches (list): List of Accounts matching the search criteria.
"""
return AccountDB.objects.search_account(*args, **kwargs)
search_account = AccountDB.objects.search_account
search_accounts = search_account
account_search = search_account
accounts = search_accounts
#
# Searching for scripts
#
# script_search(self, ostring, obj=None, only_timed=False)
#
# Search for a particular script.
#
# ostring - search criterion - a script ID or key
# obj - limit search to scripts defined on this object
# only_timed - limit search only to scripts that run
# on a timer.
#
search_script = ScriptDB.objects.search_script
def search_script(*args, **kwargs):
"""
Search for scripts in the database.
Args:
key (str or int): Script key or dbref to search for. This can also
be a list of keys/dbrefs. `None` (default) returns all scripts.
exact (bool): Only valid for string keys. If True, requires exact
key match, otherwise also match key with case-insensitive and
partial matching. Default is True.
candidates (list): Only search among these script candidates,
if given. Default is to search all scripts.
attribute_name (str): If set, search by scripts with this attribute_name
defined on them, with the value specified by `attribute_value`.
attribute_value (any): What value the given attribute_name must have.
obj (Object): Filter by scripts defined on this object.
account (Account): Filter by scripts defined on this account.
typeclass (str or TypeClass): Filter by scripts having this typeclass.
This can also be a list of typeclasses.
tags (str or list): Filter by scripts having one or more Tags.
This can be a single tag key, a list of tag keys, or a list of
tuples (tag_key, tag_category).
nofetch (bool): Don't fetch typeclass and perms data from db.
This is faster but gives less info.
Returns:
matches (list): List of Scripts matching the search criteria.
"""
return ScriptDB.objects.search_script(*args, **kwargs)
search_scripts = search_script
script_search = search_script
scripts = search_scripts
#
# Searching for communication messages
#
#
# message_search(self, sender=None, receiver=None, channel=None, freetext=None)
#
# Search the message database for particular messages. At least one
# of the arguments must be given to do a search.
#
# sender - get messages sent by a particular account
# receiver - get messages received by a certain account
# channel - get messages sent to a particular channel
# freetext - Search for a text string in a message.
# NOTE: This can potentially be slow, so make sure to supply
# one of the other arguments to limit the search.
#
search_message = Msg.objects.search_message
def search_message(*args, **kwargs):
"""
Search for messages in the database.
Args:
sender (Object, Account or str): Filter by messages sent by this entity.
If a string, this is an external sender name.
receiver (Object, Account or str): Filter by messages received by this entity.
If a string, this is an external receiver name.
channel (Channel): Filter by messages sent to this channel.
date (datetime): Filter by messages sent on this date.
type (str): Filter by messages of this type.
tags (str or list): Filter by messages having one or more Tags.
This can be a single tag key, a list of tag keys, or a list of
tuples (tag_key, tag_category).
exclude_tags (str or list): Exclude messages with these tags.
search_text (str): Search for text in message content.
exact (bool): If True, require exact text match. Default False.
Returns:
matches (list): List of Messages matching the search criteria.
"""
return Msg.objects.search_message(*args, **kwargs)
search_messages = search_message
message_search = search_message
messages = search_messages
#
# Search for Communication Channels
#
# channel_search(self, ostring)
#
# Search the channel database for a particular channel.
#
# ostring - the key or database id of the channel.
# exact - requires an exact ostring match (not case sensitive)
#
search_channel = ChannelDB.objects.search_channel
def search_channel(*args, **kwargs):
"""
Search for channels in the database.
Args:
key (str or int): Channel key or dbref to search for. This can also
be a list of keys/dbrefs. `None` (default) returns all channels.
exact (bool): Only valid for string keys. If True, requires exact
key match, otherwise also match key with case-insensitive and
partial matching. Default is True.
candidates (list): Only search among these channel candidates,
if given. Default is to search all channels.
attribute_name (str): If set, search by channels with this attribute_name
defined on them, with the value specified by `attribute_value`.
attribute_value (any): What value the given attribute_name must have.
typeclass (str or TypeClass): Filter by channels having this typeclass.
This can also be a list of typeclasses.
tags (str or list): Filter by channels having one or more Tags.
This can be a single tag key, a list of tag keys, or a list of
tuples (tag_key, tag_category).
nofetch (bool): Don't fetch typeclass and perms data from db.
This is faster but gives less info.
Returns:
matches (list): List of Channels matching the search criteria.
"""
return ChannelDB.objects.search_channel(*args, **kwargs)
search_channels = search_channel
channel_search = search_channel
channels = search_channels
#
# Find help entry objects.
#
# search_help(self, ostring, help_category=None)
#
# Retrieve a search entry object.
#
# ostring - the help topic to look for
# category - limit the search to a particular help topic
#
search_help = HelpEntry.objects.search_help
def search_help(*args, **kwargs):
"""
Search for help entries in the database.
Args:
key (str or int): Help entry key or dbref to search for. This can also
be a list of keys/dbrefs. `None` (default) returns all help entries.
exact (bool): Only valid for string keys. If True, requires exact
key match, otherwise also match key with case-insensitive and
partial matching. Default is True.
category (str): Filter by help entries in this category.
tags (str or list): Filter by help entries having one or more Tags.
This can be a single tag key, a list of tag keys, or a list of
tuples (tag_key, tag_category).
locks (str): Filter by help entries with these locks.
Returns:
matches (list): List of HelpEntries matching the search criteria.
"""
return HelpEntry.objects.search_help(*args, **kwargs)
search_help_entry = search_help
search_help_entries = search_help
help_entry_search = search_help
help_entries = search_help
# Locate Attributes
# search_object_attribute(key, category, value, strvalue) (also search_attribute works)
# search_account_attribute(key, category, value, strvalue) (also search_attribute works)
# search_script_attribute(key, category, value, strvalue) (also search_attribute works)
# search_channel_attribute(key, category, value, strvalue) (also search_attribute works)
# Note that these return the object attached to the Attribute,
# not the attribute object itself (this is usually what you want)
def search_object_attribute(
key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs
):
"""
Search for objects by their attributes.
"""
return ObjectDB.objects.get_by_attribute(
key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs
)
@ -227,6 +308,9 @@ def search_object_attribute(
def search_account_attribute(
key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs
):
"""
Search for accounts by their attributes.
"""
return AccountDB.objects.get_by_attribute(
key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs
)
@ -235,6 +319,9 @@ def search_account_attribute(
def search_script_attribute(
key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs
):
"""
Search for scripts by their attributes.
"""
return ScriptDB.objects.get_by_attribute(
key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs
)
@ -243,23 +330,20 @@ def search_script_attribute(
def search_channel_attribute(
key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs
):
"""
Search for channels by their attributes.
"""
return ChannelDB.objects.get_by_attribute(
key=key, category=category, value=value, strvalue=strvalue, attrtype=attrtype, **kwargs
)
# search for attribute objects
search_attribute_object = ObjectDB.objects.get_attribute
# Locate Tags
# search_object_tag(key=None, category=None) (also search_tag works)
# search_account_tag(key=None, category=None)
# search_script_tag(key=None, category=None)
# search_channel_tag(key=None, category=None)
# Note that this returns the object attached to the tag, not the tag
# object itself (this is usually what you want)
# Replace direct assignments with functions
def search_attribute_object(*args, **kwargs):
"""
Search for attribute objects.
"""
return ObjectDB.objects.get_attribute(*args, **kwargs)
def search_object_by_tag(key=None, category=None, tagtype=None, **kwargs):
@ -281,7 +365,6 @@ def search_object_by_tag(key=None, category=None, tagtype=None, **kwargs):
matches (list): List of Objects with tags matching
the search criteria, or an empty list if no
matches were found.
"""
return ObjectDB.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs)
@ -292,23 +375,6 @@ search_tag = search_object_by_tag # this is the most common case
def search_account_tag(key=None, category=None, tagtype=None, **kwargs):
"""
Find account based on tag or category.
Args:
key (str, optional): The tag key to search for.
category (str, optional): The category of tag
to search for. If not set, uncategorized
tags will be searched.
tagtype (str, optional): 'type' of Tag, by default
this is either `None` (a normal Tag), `alias` or
`permission`. This always apply to all queried tags.
kwargs (any): Other optional parameter that may be supported
by the manager method.
Returns:
matches (list): List of Accounts with tags matching
the search criteria, or an empty list if no
matches were found.
"""
return AccountDB.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs)
@ -316,23 +382,6 @@ def search_account_tag(key=None, category=None, tagtype=None, **kwargs):
def search_script_tag(key=None, category=None, tagtype=None, **kwargs):
"""
Find script based on tag or category.
Args:
key (str, optional): The tag key to search for.
category (str, optional): The category of tag
to search for. If not set, uncategorized
tags will be searched.
tagtype (str, optional): 'type' of Tag, by default
this is either `None` (a normal Tag), `alias` or
`permission`. This always apply to all queried tags.
kwargs (any): Other optional parameter that may be supported
by the manager method.
Returns:
matches (list): List of Scripts with tags matching
the search criteria, or an empty list if no
matches were found.
"""
return ScriptDB.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs)
@ -340,35 +389,16 @@ def search_script_tag(key=None, category=None, tagtype=None, **kwargs):
def search_channel_tag(key=None, category=None, tagtype=None, **kwargs):
"""
Find channel based on tag or category.
Args:
key (str, optional): The tag key to search for.
category (str, optional): The category of tag
to search for. If not set, uncategorized
tags will be searched.
tagtype (str, optional): 'type' of Tag, by default
this is either `None` (a normal Tag), `alias` or
`permission`. This always apply to all queried tags.
kwargs (any): Other optional parameter that may be supported
by the manager method.
Returns:
matches (list): List of Channels with tags matching
the search criteria, or an empty list if no
matches were found.
"""
return ChannelDB.objects.get_by_tag(key=key, category=category, tagtype=tagtype, **kwargs)
# search for tag objects (not the objects they are attached to
search_tag_object = ObjectDB.objects.get_tag
# Locate Objects by Typeclass
# search_objects_by_typeclass(typeclass="", include_children=False, include_parents=False) (also search_typeclass works)
# This returns the objects of the given typeclass
# Replace direct assignment with function
def search_tag_object(*args, **kwargs):
"""
Search for tag objects.
"""
return ObjectDB.objects.get_tag(*args, **kwargs)
def search_objects_by_typeclass(typeclass, include_children=False, include_parents=False):

View file

@ -5,86 +5,98 @@ folder and edit it to add/remove links to the menu.
{% endcomment %}
{% load static %}
<nav class="navbar navbar-dark font-weight-bold navbar-expand-md">
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#menu-content" aria-controls="menu-content" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#menu-content"
aria-controls="menu-content" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="/">
<div class="media">
<img class="d-flex navbar-brand-logo mx-3" src="{% static "website/images/evennia_logo.png" %}" alt="{{game_name}} logo" />
<div class="media-body">
{{ game_name }}<br />
<small>{{game_slogan}}</small>
</div>
<a class="navbar-brand" href="/">
<div class="media">
<img class="d-flex navbar-brand-logo mx-3" src="{% static "website/images/evennia_logo.png" %}"
alt="{{game_name}} logo" />
<div class="media-body">
{{ game_name }}<br />
<small>{{game_slogan}}</small>
</div>
</a>
<div class="collapse navbar-collapse" id="menu-content">
<ul class="navbar-nav">
{% block navbar_left %}
<li>
<a class="nav-link" href="{% url 'index' %}">Home</a>
</li>
<!-- game views -->
<li><a class="nav-link" href="{% url 'characters' %}">Characters</a></li>
<li><a class="nav-link" href="{% url 'channels' %}">Channels</a></li>
<li><a class="nav-link" href="{% url 'help' %}">Help</a></li>
<!-- end game views -->
{% if webclient_enabled %}
<li><a class="nav-link" href="{% url 'webclient:index' %}">Play Online</a></li>
{% endif %}
{% if user.is_staff %}
<li><a class="nav-link" href="{% url 'admin:index' %}">Admin</a></li>
{% if rest_api_enabled %}
<li><a class="nav-link" href="/api">API</a></li>
{% endif %}
{% endif %}
{% endblock %}
</ul>
<ul class="nav navbar-nav ml-auto w-120 justify-content-end">
{% block navbar_right %}
{% endblock %}
{% block navbar_user %}
{% if account %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" id="user_options" aria-expanded="false">
{% if puppet %}
Welcome, {{ puppet }}! <span class="text-muted">({{ account.username }})</span> <span class="caret"></span>
{% else %}
Logged in as {{ account.username }} <span class="caret"></span>
{% endif %}
</a>
<div class="dropdown-menu" aria-labelledby="user_options">
<a class="dropdown-item" href="{% url 'character-create' %}">Create Character</a>
<a class="dropdown-item" href="{% url 'character-manage' %}">Manage Characters</a>
<div class="dropdown-divider"></div>
{% for character in account.characters|slice:"10" %}
<a class="dropdown-item" href="{{ character.web_get_puppet_url }}?next={{ request.path }}">{{ character }}</a>
{% empty %}
<a class="dropdown-item" href="#">No characters found!</a>
{% endfor %}
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'password_change' %}">Change Password</a>
<a class="dropdown-item" href="{% url 'logout' %}">Log Out</a>
</div>
</li>
<li>
<a class="nav-link" href="{% url 'logout' %}">Log Out</a>
</li>
{% else %}
<li>
<a class="nav-link" href="{% url 'login' %}?next={{ request.path }}">Log In</a>
</li>
{% if register_enabled %}
<li>
<a class="nav-link" href="{% url 'register' %}">Register</a>
</li>
{% endif %}
{% endif %}
{% endblock %}
</ul>
</div>
</a>
<div class="collapse navbar-collapse" id="menu-content">
<ul class="navbar-nav">
{% block navbar_left %}
<li>
<a class="nav-link" href="{% url 'index' %}">Home</a>
</li>
<!-- game views -->
<li><a class="nav-link" href="{% url 'characters' %}">Characters</a></li>
<li><a class="nav-link" href="{% url 'channels' %}">Channels</a></li>
<li><a class="nav-link" href="{% url 'help' %}">Help</a></li>
<!-- end game views -->
{% if webclient_enabled %}
<li><a class="nav-link" href="{% url 'webclient:index' %}">Play Online</a></li>
{% endif %}
{% if user.is_staff %}
<li><a class="nav-link" href="{% url 'admin:index' %}">Admin</a></li>
{% if rest_api_enabled %}
<li><a class="nav-link" href="/api">API</a></li>
{% endif %}
{% endif %}
{% endblock %}
</ul>
<ul class="nav navbar-nav ml-auto w-120 justify-content-end">
{% block navbar_right %}
{% endblock %}
{% block navbar_user %}
{% if account %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" id="user_options" aria-expanded="false">
{% if puppet %}
Welcome, {{ puppet }}! <span class="text-muted">({{ account.username }})</span> <span class="caret"></span>
{% else %}
Logged in as {{ account.username }} <span class="caret"></span>
{% endif %}
</a>
<div class="dropdown-menu" aria-labelledby="user_options">
<a class="dropdown-item" href="{% url 'character-create' %}">Create Character</a>
<a class="dropdown-item" href="{% url 'character-manage' %}">Manage Characters</a>
<div class="dropdown-divider"></div>
{% for character in account.characters|slice:"10" %}
<a class="dropdown-item" href="{{ character.web_get_puppet_url }}?next={{ request.path }}">{{ character }}</a>
{% empty %}
<a class="dropdown-item" href="#">No characters found!</a>
{% endfor %}
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'password_change' %}">Change Password</a>
<form method="post" action="{% url 'logout' %}" style="display:inline;">
{% csrf_token %}
<button type="submit" class="dropdown-item">Log Out</button>
</form>
</div>
</li>
<li>
<form method="post" action="{% url 'logout' %}" style="display:inline;">
{% csrf_token %}
<button type="submit" class="nav-link btn btn-link">Log Out</button>
</form>
</li>
{% else %}
<li>
<form method="post" action="{% url 'login' %}" style="display:inline;">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">
<button type="submit" class="nav-link btn btn-link">Log In</button>
</form>
</li>
{% if register_enabled %}
<li>
<a class="nav-link" href="{% url 'register' %}">Register</a>
</li>
{% endif %}
{% endif %}
{% endblock %}
</ul>
</div>
</nav>

View file

@ -97,6 +97,20 @@ class LoginTest(EvenniaWebTest):
class LogoutTest(EvenniaWebTest):
url_name = "logout"
def test_get(self):
"""Since Django 5.0, logout is no longer supported with GET requests"""
pass
def test_post(self):
"""Do the logout test with a POST request"""
response = self.client.post(reverse(self.url_name), follow=True)
self.assertEqual(response.status_code, 200)
def test_get_authenticated(self):
"""Do the logout test with a POST instead of GET"""
response = self.client.post(reverse(self.url_name), follow=True)
self.assertEqual(response.status_code, 200)
class PasswordResetTest(EvenniaWebTest):
url_name = "password_change"

View file

@ -64,7 +64,7 @@ classifiers = [
dependencies = [
# core dependencies
"legacy-cgi;python_version >= '3.13'",
"django >= 4.2, < 4.3",
"django >= 5.1, < 5.2",
"twisted >= 24.11.0, < 25",
"pytz >= 2022.6",
"djangorestframework >= 3.14, < 3.15",