From ba61ed3d7e45f7b3f60c9900cb0e6a14894bd0f2 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 9 Jan 2019 22:08:48 +0100 Subject: [PATCH] Made all unit tests pass --- evennia/prototypes/spawner.py | 8 +-- evennia/prototypes/tests.py | 10 +-- evennia/server/evennia_launcher.py | 2 +- evennia/server/portal/tests.py | 14 ++-- evennia/typeclasses/models.py | 2 +- evennia/typeclasses/tests.py | 19 ++++++ evennia/utils/dbserialize.py | 7 +- evennia/utils/tests/test_evform.py | 5 +- evennia/web/website/forms.py | 90 ++++++++++++------------- evennia/web/website/views.py | 101 +++++++++++++++++------------ 10 files changed, 148 insertions(+), 110 deletions(-) diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index ea76443f56..ea5ce2e0cd 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -258,12 +258,12 @@ def prototype_from_object(obj): aliases = obj.aliases.get(return_list=True) if aliases: prot['aliases'] = aliases - tags = [(tag.db_key, tag.db_category, tag.db_data) - for tag in obj.tags.all(return_objs=True)] + tags = sorted([(tag.db_key, tag.db_category, tag.db_data) + for tag in obj.tags.all(return_objs=True)]) if tags: prot['tags'] = tags - attrs = [(attr.key, attr.value, attr.category, ';'.join(attr.locks.all())) - for attr in obj.attributes.all()] + attrs = sorted([(attr.key, attr.value, attr.category, ';'.join(attr.locks.all())) + for attr in obj.attributes.all()]) if attrs: prot['attrs'] = attrs diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index f407a42899..fc0ec2f8b1 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -122,9 +122,9 @@ class TestUtils(EvenniaTest): self.assertEqual(obj_prototype, {'aliases': ['foo'], - 'attrs': [('oldtest', 'to_keep', None, ''), - ('test', 'testval', None, ''), - ('desc', 'changed desc', None, '')], + 'attrs': [('desc', 'changed desc', None, ''), + ('oldtest', 'to_keep', None, ''), + ('test', 'testval', None, '')], 'key': 'Obj', 'home': '#1', 'location': '#1', @@ -213,9 +213,9 @@ class TestUtils(EvenniaTest): self.assertEqual(count, 1) new_prot = spawner.prototype_from_object(self.obj1) - self.assertEqual({'attrs': [('oldtest', 'to_keep', None, ''), - ('fooattr', 'fooattrval', None, ''), + self.assertEqual({'attrs': [('fooattr', 'fooattrval', None, ''), ('new', 'new_val', None, ''), + ('oldtest', 'to_keep', None, ''), ('test', 'testval_changed', None, '')], 'home': Something, 'key': 'Obj', diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index 62779f184e..af13541523 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -1401,7 +1401,7 @@ def create_superuser(): """ print( "\nCreate a superuser below. The superuser is Account #1, the 'owner' " - "account of the server.\n") + "account of the server. Email is optional and can be empty.\n") django.core.management.call_command("createsuperuser", interactive=True) diff --git a/evennia/server/portal/tests.py b/evennia/server/portal/tests.py index 791e5172a4..7ff64a6509 100644 --- a/evennia/server/portal/tests.py +++ b/evennia/server/portal/tests.py @@ -114,20 +114,20 @@ class TestTelnet(TwistedTestCase): self.assertEqual(self.proto.protocol_flags['SCREENWIDTH'], {0: DEFAULT_WIDTH}) self.assertEqual(self.proto.protocol_flags['SCREENHEIGHT'], {0: DEFAULT_HEIGHT}) self.proto.dataReceived(IAC + WILL + NAWS) - self.proto.dataReceived([IAC, SB, NAWS, '', 'x', '', 'd', IAC, SE]) - self.assertEqual(self.proto.protocol_flags['SCREENWIDTH'][0], 120) - self.assertEqual(self.proto.protocol_flags['SCREENHEIGHT'][0], 100) + self.proto.dataReceived(b"".join([IAC, SB, NAWS, b'', b'x', b'', b'd', IAC, SE])) + self.assertEqual(self.proto.protocol_flags['SCREENWIDTH'][0], 78) + self.assertEqual(self.proto.protocol_flags['SCREENHEIGHT'][0], 45) self.assertEqual(self.proto.handshakes, 6) # test ttype self.assertTrue(self.proto.protocol_flags["FORCEDENDLINE"]) self.assertFalse(self.proto.protocol_flags["TTYPE"]) self.assertTrue(self.proto.protocol_flags["ANSI"]) self.proto.dataReceived(IAC + WILL + TTYPE) - self.proto.dataReceived([IAC, SB, TTYPE, IS, "MUDLET", IAC, SE]) + self.proto.dataReceived(b"".join([IAC, SB, TTYPE, IS, b"MUDLET", IAC, SE])) self.assertTrue(self.proto.protocol_flags["XTERM256"]) self.assertEqual(self.proto.protocol_flags["CLIENTNAME"], "MUDLET") - self.proto.dataReceived([IAC, SB, TTYPE, IS, "XTERM", IAC, SE]) - self.proto.dataReceived([IAC, SB, TTYPE, IS, "MTTS 137", IAC, SE]) + self.proto.dataReceived(b"".join([IAC, SB, TTYPE, IS, b"XTERM", IAC, SE])) + self.proto.dataReceived(b"".join([IAC, SB, TTYPE, IS, b"MTTS 137", IAC, SE])) self.assertEqual(self.proto.handshakes, 5) # test mccp self.proto.dataReceived(IAC + DONT + MCCP) @@ -138,7 +138,7 @@ class TestTelnet(TwistedTestCase): self.assertEqual(self.proto.handshakes, 3) # test oob self.proto.dataReceived(IAC + DO + MSDP) - self.proto.dataReceived([IAC, SB, MSDP, MSDP_VAR, "LIST", MSDP_VAL, "COMMANDS", IAC, SE]) + self.proto.dataReceived(b"".join([IAC, SB, MSDP, MSDP_VAR, b"LIST", MSDP_VAL, b"COMMANDS", IAC, SE])) self.assertTrue(self.proto.protocol_flags['OOB']) self.assertEqual(self.proto.handshakes, 2) # test mxp diff --git a/evennia/typeclasses/models.py b/evennia/typeclasses/models.py index 86d83c2207..53ce7879f8 100644 --- a/evennia/typeclasses/models.py +++ b/evennia/typeclasses/models.py @@ -817,7 +817,7 @@ class TypedObject(SharedMemoryModel): kwargs={'pk': self.pk, 'slug': slugify(self.name)}) except: return '#' - + def web_get_puppet_url(self): """ Returns the URI path for a View that allows users to puppet a specific diff --git a/evennia/typeclasses/tests.py b/evennia/typeclasses/tests.py index 09df228dee..986cb1c025 100644 --- a/evennia/typeclasses/tests.py +++ b/evennia/typeclasses/tests.py @@ -10,6 +10,25 @@ from evennia.utils.test_resources import EvenniaTest # ------------------------------------------------------------ +class TestAttributes(EvenniaTest): + def test_attrhandler(self): + key = 'testattr' + value = 'test attr value ' + self.obj1.attributes.add(key, value) + self.assertEqual(self.obj1.attributes.get(key), value) + self.obj1.db.testattr = value + self.assertEqual(self.obj1.db.testattr, value) + + def test_weird_text_save(self): + "test 'weird' text type (different in py2 vs py3)" + from django.utils.safestring import SafeText + key = 'test attr 2' + value = SafeText('test attr value 2') + self.obj1.attributes.add(key, value) + self.assertEqual(self.obj1.attributes.get(key), value) + + + class TestTypedObjectManager(EvenniaTest): def _manager(self, methodname, *args, **kwargs): return list(getattr(self.obj1.__class__.objects, methodname)(*args, **kwargs)) diff --git a/evennia/utils/dbserialize.py b/evennia/utils/dbserialize.py index b5c8f6b65f..51ee0e04c4 100644 --- a/evennia/utils/dbserialize.py +++ b/evennia/utils/dbserialize.py @@ -29,6 +29,7 @@ except ImportError: from pickle import dumps, loads from django.core.exceptions import ObjectDoesNotExist from django.contrib.contenttypes.models import ContentType +from django.utils.safestring import SafeString, SafeBytes from evennia.utils.utils import to_str, uses_database, is_iter from evennia.utils import logger @@ -521,7 +522,7 @@ def to_pickle(data): def process_item(item): """Recursive processor and identification of data""" dtype = type(item) - if dtype in (str, int, float, bool): + if dtype in (str, int, float, bool, bytes, SafeString, SafeBytes): return item elif dtype == tuple: return tuple(process_item(val) for val in item) @@ -573,7 +574,7 @@ def from_pickle(data, db_obj=None): def process_item(item): """Recursive processor and identification of data""" dtype = type(item) - if dtype in (str, int, float, bool): + if dtype in (str, int, float, bool, bytes, SafeString, SafeBytes): return item elif _IS_PACKED_DBOBJ(item): # this must be checked before tuple @@ -602,7 +603,7 @@ def from_pickle(data, db_obj=None): def process_tree(item, parent): """Recursive processor, building a parent-tree from iterable data""" dtype = type(item) - if dtype in (str, int, float, bool): + if dtype in (str, int, float, bool, bytes, SafeString, SafeBytes): return item elif _IS_PACKED_DBOBJ(item): # this must be checked before tuple diff --git a/evennia/utils/tests/test_evform.py b/evennia/utils/tests/test_evform.py index 6f78bf70d8..4c09066f3c 100644 --- a/evennia/utils/tests/test_evform.py +++ b/evennia/utils/tests/test_evform.py @@ -10,13 +10,10 @@ class TestEvForm(TestCase): def test_form(self): self.maxDiff = None form1 = evform._test() - print("len(form1): {}".format(len(form1))) form2 = evform._test() - print("len(form2): {}".format(len(form2))) - self.assertEqual(form1, form2) - # self.assertEqual(form, "") + # self.assertEqual(form1, "") # '.------------------------------------------------.\n' # '| |\n' # '| Name: \x1b[0m\x1b[1m\x1b[32mTom\x1b[1m\x1b[32m \x1b' diff --git a/evennia/web/website/forms.py b/evennia/web/website/forms.py index 66c5224914..dc6f69dd71 100644 --- a/evennia/web/website/forms.py +++ b/evennia/web/website/forms.py @@ -10,24 +10,24 @@ class EvenniaForm(forms.Form): This is a stock Django form, but modified so that all values provided through it are escaped (sanitized). Validation is performed by the fields you define in the form. - + This has little to do with Evennia itself and is more general web security- related. - + https://www.owasp.org/index.php/Input_Validation_Cheat_Sheet#Goals_of_Input_Validation - + """ def clean(self): """ Django hook. Performed on form submission. - + Returns: cleaned (dict): Dictionary of key:value pairs submitted on the form. - + """ # Call parent function cleaned = super(EvenniaForm, self).clean() - + # Escape all values provided by user cleaned = {k:escape(v) for k,v in cleaned.items()} return cleaned @@ -35,123 +35,125 @@ class EvenniaForm(forms.Form): class AccountForm(UserCreationForm): """ This is a generic Django form tailored to the Account model. - + In this incarnation it does not allow getting/setting of attributes, only core User model fields (username, email, password). - + """ class Meta: """ This is a Django construct that provides additional configuration to the form. - + """ # The model/typeclass this form creates model = class_from_module(settings.BASE_ACCOUNT_TYPECLASS) - + # The fields to display on the form, in the given order fields = ("username", "email") - + # Any overrides of field classes field_classes = {'username': UsernameField} - + # Username is collected as part of the core UserCreationForm, so we just need # to add a field to (optionally) capture email. email = forms.EmailField(help_text="A valid email address. Optional; used for password resets.", required=False) - + class ObjectForm(EvenniaForm, ModelForm): """ This is a Django form for generic Evennia Objects that allows modification of attributes when called from a descendent of ObjectUpdate or ObjectCreate views. - + It defines no fields by default; you have to do that by extending this class and defining what fields you want to be recorded. See the CharacterForm for a simple example of how to do this. - + """ class Meta: """ This is a Django construct that provides additional configuration to the form. - + """ # The model/typeclass this form creates model = class_from_module(settings.BASE_OBJECT_TYPECLASS) - + # The fields to display on the form, in the given order fields = ("db_key",) - + # This lets us rename ugly db-specific keys to something more human labels = { 'db_key': 'Name', } - + class CharacterForm(ObjectForm): """ This is a Django form for Evennia Character objects. - + Since Evennia characters only have one attribute by default, this form only - defines a field for that single attribute. The names of fields you define should - correspond to their names as stored in the dbhandler; you can display + defines a field for that single attribute. The names of fields you define should + correspond to their names as stored in the dbhandler; you can display 'prettier' versions of the fieldname on the form using the 'label' kwarg. - + The basic field types are CharFields and IntegerFields, which let you enter - text and numbers respectively. IntegerFields have some neat validation tricks - they can do, like mandating values fall within a certain range. - + text and numbers respectively. IntegerFields have some neat validation tricks + they can do, like mandating values fall within a certain range. + For example, a complete "age" field (which stores its value to `character.db.age` might look like: - + age = forms.IntegerField( label="Your Age", - min_value=18, max_value=9000, + min_value=18, max_value=9000, help_text="Years since your birth.") - - Default input fields are generic single-line text boxes. You can control what - sort of input field users will see by specifying a "widget." An example of + + Default input fields are generic single-line text boxes. You can control what + sort of input field users will see by specifying a "widget." An example of this is used for the 'desc' field to show a Textarea box instead of a Textbox. - + For help in building out your form, please see: https://docs.djangoproject.com/en/1.11/topics/forms/#building-a-form-in-django - + For more information on fields and their capabilities, see: https://docs.djangoproject.com/en/1.11/ref/forms/fields/ - + For more on widgets, see: https://docs.djangoproject.com/en/1.11/ref/forms/widgets/ - + """ class Meta: """ This is a Django construct that provides additional configuration to the form. - + """ # Get the correct object model model = class_from_module(settings.BASE_CHARACTER_TYPECLASS) - + # Allow entry of the 'key' field fields = ("db_key",) - + # Rename 'key' to something more intelligible labels = { 'db_key': 'Name', } - + # Fields pertaining to configurable attributes on the Character object. - desc = forms.CharField(label='Description', max_length=2048, required=False, + desc = forms.CharField( + label='Description', max_length=2048, required=False, widget=forms.Textarea(attrs={'rows': 3}), help_text="A brief description of your character.") - + + class CharacterUpdateForm(CharacterForm): """ This is a Django form for updating Evennia Character objects. - + By default it is the same as the CharacterForm, but if there are circumstances in which you don't want to let players edit all the same attributes they had access to during creation, you can redefine this form with those fields you do wish to allow. - + """ - pass \ No newline at end of file + pass diff --git a/evennia/web/website/views.py b/evennia/web/website/views.py index 528bc0d0f0..d436f3f63e 100644 --- a/evennia/web/website/views.py +++ b/evennia/web/website/views.py @@ -1,24 +1,22 @@ +""" +This file contains the generic, assorted views that don't fall under one of the other applications. +Views are django's way of processing e.g. html templates on the fly. """ -This file contains the generic, assorted views that don't fall under one of -the other applications. Views are django's way of processing e.g. html -templates on the fly. -""" from collections import OrderedDict from django.contrib.admin.sites import site from django.conf import settings from django.contrib import messages -from django.contrib.auth import authenticate from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.admin.views.decorators import staff_member_required from django.core.exceptions import PermissionDenied from django.db.models.functions import Lower -from django.http import HttpResponseBadRequest, HttpResponseRedirect, Http404 +from django.http import HttpResponseBadRequest, HttpResponseRedirect from django.shortcuts import render -from django.urls import reverse, reverse_lazy -from django.views.generic import View, TemplateView, ListView, DetailView, FormView +from django.urls import reverse_lazy +from django.views.generic import TemplateView, ListView, DetailView from django.views.generic.base import RedirectView from django.views.generic.edit import CreateView, UpdateView, DeleteView @@ -26,15 +24,15 @@ from evennia import SESSION_HANDLER from evennia.help.models import HelpEntry from evennia.objects.models import ObjectDB from evennia.accounts.models import AccountDB -from evennia.utils import class_from_module, logger +from evennia.utils import class_from_module from evennia.utils.logger import tail_log_file -from evennia.web.website.forms import * +from evennia.web.website import forms as website_forms -from django.contrib.auth import login from django.utils.text import slugify _BASE_CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS + def _gamestats(): # Some misc. configurable stuff. # TODO: Move this to either SQL or settings.py based configuration. @@ -49,8 +47,10 @@ def _gamestats(): # nsess = len(AccountDB.objects.get_connected_accounts()) or "no one" nobjs = ObjectDB.objects.all().count() - nrooms = ObjectDB.objects.filter(db_location__isnull=True).exclude(db_typeclass_path=_BASE_CHAR_TYPECLASS).count() - nexits = ObjectDB.objects.filter(db_location__isnull=False, db_destination__isnull=False).count() + nrooms = ObjectDB.objects.filter( + db_location__isnull=True).exclude(db_typeclass_path=_BASE_CHAR_TYPECLASS).count() + nexits = ObjectDB.objects.filter( + db_location__isnull=False, db_destination__isnull=False).count() nchars = ObjectDB.objects.filter(db_typeclass_path=_BASE_CHAR_TYPECLASS).count() nothers = nobjs - nrooms - nchars - nexits @@ -99,6 +99,7 @@ def admin_wrapper(request): """ return staff_member_required(site.index)(request) + # # Class-based views # @@ -237,6 +238,7 @@ class EvenniaDeleteView(DeleteView, TypeclassMixin): # Makes sure the page has a sensible title. return 'Delete %s' % self.typeclass._meta.verbose_name.title() + # # Object views # @@ -336,8 +338,9 @@ class ObjectDetailView(EvenniaDetailView): # Check if this object was requested in a valid manner if slugify(obj.name) != self.kwargs.get(self.slug_url_kwarg): - raise HttpResponseBadRequest(u"No %(verbose_name)s found matching the query" % - {'verbose_name': queryset.model._meta.verbose_name}) + raise HttpResponseBadRequest( + u"No %(verbose_name)s found matching the query" % + {'verbose_name': queryset.model._meta.verbose_name}) # Check if the requestor account has permissions to access object account = self.request.user @@ -430,7 +433,8 @@ class ObjectUpdateView(LoginRequiredMixin, ObjectDetailView, EvenniaUpdateView): object detail page so the user can see their changes reflected. """ - if self.success_url: return self.success_url + if self.success_url: + return self.success_url return self.object.web_get_detail_url() def get_initial(self): @@ -448,10 +452,10 @@ class ObjectUpdateView(LoginRequiredMixin, ObjectDetailView, EvenniaUpdateView): obj = self.get_object() # Get attributes - data = {k:getattr(obj.db, k, '') for k in self.form_class.base_fields} + data = {k: getattr(obj.db, k, '') for k in self.form_class.base_fields} # Get model fields - data.update({k:getattr(obj, k, '') for k in self.form_class.Meta.fields}) + data.update({k: getattr(obj, k, '') for k in self.form_class.Meta.fields}) return data @@ -471,17 +475,18 @@ class ObjectUpdateView(LoginRequiredMixin, ObjectDetailView, EvenniaUpdateView): """ # Get the attributes after they've been cleaned and validated - data = {k:v for k,v in form.cleaned_data.items() if k not in self.form_class.Meta.fields} + data = {k: v for k, v in form.cleaned_data.items() if k not in self.form_class.Meta.fields} # Update the object attributes for key, value in data.items(): - setattr(self.object.db, key, value) + self.object.attributes.add(key, value) messages.success(self.request, "Successfully updated '%s' for %s." % (key, self.object)) # Do not return super().form_valid; we don't want to update the model # instance, just its attributes. return HttpResponseRedirect(self.get_success_url()) + # # Account views # @@ -496,7 +501,7 @@ class AccountMixin(TypeclassMixin): """ # -- Django constructs -- model = class_from_module(settings.BASE_ACCOUNT_TYPECLASS) - form_class = AccountForm + form_class = website_forms.AccountForm class AccountCreateView(AccountMixin, EvenniaCreateView): @@ -537,11 +542,13 @@ class AccountCreateView(AccountMixin, EvenniaCreateView): return self.form_invalid(form) # Inform user of success - messages.success(self.request, "Your account '%s' was successfully created! You may log in using it now." % account.name) + messages.success(self.request, "Your account '%s' was successfully created! " + "You may log in using it now." % account.name) # Redirect the user to the login page return HttpResponseRedirect(self.success_url) + # # Character views # @@ -556,7 +563,7 @@ class CharacterMixin(TypeclassMixin): """ # -- Django constructs -- model = class_from_module(settings.BASE_CHARACTER_TYPECLASS) - form_class = CharacterForm + form_class = website_forms.CharacterForm success_url = reverse_lazy('character-manage') def get_queryset(self): @@ -608,7 +615,8 @@ class CharacterListView(LoginRequiredMixin, CharacterMixin, ListView): # Return a queryset consisting of characters the user is allowed to # see. - ids = [obj.id for obj in self.typeclass.objects.all() if obj.access(account, self.access_type)] + ids = [obj.id for obj in self.typeclass.objects.all() + if obj.access(account, self.access_type)] return self.typeclass.objects.filter(id__in=ids).order_by(Lower('db_key')) @@ -637,7 +645,7 @@ class CharacterPuppetView(LoginRequiredMixin, CharacterMixin, RedirectView, Obje char = self.get_object() # Get the page the user came from - next = self.request.GET.get('next', self.success_url) + next_page = self.request.GET.get('next', self.success_url) if char: # If the account owns the char, store the ID of the char in the @@ -650,7 +658,7 @@ class CharacterPuppetView(LoginRequiredMixin, CharacterMixin, RedirectView, Obje self.request.session['puppet'] = None messages.error(self.request, "You cannot become '%s'." % char) - return next + return next_page class CharacterManageView(LoginRequiredMixin, CharacterMixin, ListView): @@ -674,7 +682,7 @@ class CharacterUpdateView(CharacterMixin, ObjectUpdateView): """ # -- Django constructs -- - form_class = CharacterUpdateForm + form_class = website_forms.CharacterUpdateForm template_name = 'website/character_form.html' @@ -705,7 +713,8 @@ class CharacterDetailView(CharacterMixin, ObjectDetailView): # Return a queryset consisting of characters the user is allowed to # see. - ids = [obj.id for obj in self.typeclass.objects.all() if obj.access(account, self.access_type)] + ids = [obj.id for obj in self.typeclass.objects.all() + if obj.access(account, self.access_type)] return self.typeclass.objects.filter(id__in=ids).order_by(Lower('db_key')) @@ -746,7 +755,6 @@ class CharacterCreateView(CharacterMixin, ObjectCreateView): self.attributes = {k: form.cleaned_data[k] for k in form.cleaned_data.keys()} charname = self.attributes.pop('db_key') description = self.attributes.pop('desc') - # Create a character character, errors = self.typeclass.create(charname, account, description=description) @@ -756,7 +764,7 @@ class CharacterCreateView(CharacterMixin, ObjectCreateView): if character: # Assign attributes from form - for key,value in self.attributes.items(): + for key, value in self.attributes.items(): setattr(character.db, key, value) # Return the user to the character management page, unless overridden @@ -768,6 +776,7 @@ class CharacterCreateView(CharacterMixin, ObjectCreateView): messages.error(self.request, "Your character could not be created.") return self.form_invalid(form) + # # Channel views # @@ -881,12 +890,14 @@ class ChannelDetailView(ChannelMixin, ObjectDetailView): context = super(ChannelDetailView, self).get_context_data(**kwargs) # Get the filename this Channel is recording to - filename = self.object.attributes.get("log_file", default="channel_%s.log" % self.object.key) + filename = self.object.attributes.get( + "log_file", default="channel_%s.log" % self.object.key) # Split log entries so we can filter by time bucket = [] for log in (x.strip() for x in tail_log_file(filename, 0, self.max_num_lines)): - if not log: continue + if not log: + continue time, msg = log.split(' [-] ') time_key = time.split(':')[0] @@ -904,7 +915,6 @@ class ChannelDetailView(ChannelMixin, ObjectDetailView): return context - def get_object(self, queryset=None): """ Override of Django hook that retrieves an object by slugified channel @@ -924,8 +934,9 @@ class ChannelDetailView(ChannelMixin, ObjectDetailView): # Check if this object was requested in a valid manner if not obj: - raise HttpResponseBadRequest(u"No %(verbose_name)s found matching the query" % - {'verbose_name': queryset.model._meta.verbose_name}) + raise HttpResponseBadRequest( + u"No %(verbose_name)s found matching the query" % + {'verbose_name': queryset.model._meta.verbose_name}) return obj @@ -976,6 +987,7 @@ class HelpMixin(TypeclassMixin): return filtered + class HelpListView(HelpMixin, ListView): """ Returns a list of help entries that can be viewed by a user, authenticated @@ -989,6 +1001,7 @@ class HelpListView(HelpMixin, ListView): # -- Evennia constructs -- page_title = "Help Index" + class HelpDetailView(HelpMixin, EvenniaDetailView): """ Returns the detail page for a given help entry. @@ -1012,7 +1025,8 @@ class HelpDetailView(HelpMixin, EvenniaDetailView): obj = self.get_object() # Get queryset and filter out non-related categories - queryset = self.get_queryset().filter(db_help_category=obj.db_help_category).order_by(Lower('db_key')) + queryset = self.get_queryset().filter( + db_help_category=obj.db_help_category).order_by(Lower('db_key')) context['topic_list'] = queryset # Find the index position of the given obj in the queryset @@ -1025,12 +1039,14 @@ class HelpDetailView(HelpMixin, EvenniaDetailView): try: assert i+1 <= len(objs) and objs[i+1] is not obj context['topic_next'] = objs[i+1] - except: context['topic_next'] = None + except: + context['topic_next'] = None try: assert i-1 >= 0 and objs[i-1] is not obj context['topic_previous'] = objs[i-1] - except: context['topic_previous'] = None + except: + context['topic_previous'] = None # Format the help entry using HTML instead of newlines text = obj.db_entrytext @@ -1057,11 +1073,14 @@ class HelpDetailView(HelpMixin, EvenniaDetailView): # Find the object in the queryset category = slugify(self.kwargs.get('category', '')) topic = slugify(self.kwargs.get('topic', '')) - obj = next((x for x in queryset if slugify(x.db_help_category)==category and slugify(x.db_key)==topic), None) + obj = next((x for x in queryset + if slugify(x.db_help_category) == category and + slugify(x.db_key) == topic), None) # Check if this object was requested in a valid manner if not obj: - raise HttpResponseBadRequest(u"No %(verbose_name)s found matching the query" % - {'verbose_name': queryset.model._meta.verbose_name}) + raise HttpResponseBadRequest( + u"No %(verbose_name)s found matching the query" % + {'verbose_name': queryset.model._meta.verbose_name}) return obj