Made all unit tests pass

This commit is contained in:
Griatch 2019-01-09 22:08:48 +01:00
parent a74029e3a9
commit ba61ed3d7e
10 changed files with 148 additions and 110 deletions

View file

@ -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

View file

@ -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',

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -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

View file

@ -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'

View file

@ -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
pass

View file

@ -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