Code documentation improvements

This commit is contained in:
Raphael Michel
2015-01-07 18:28:05 +01:00
parent 48109720cc
commit a5a976b16a
16 changed files with 136 additions and 64 deletions

View File

@@ -15,8 +15,9 @@ class EventRelatedCache:
main purpose of this is to be able to flush all cached data related
to this event at once.
The object is stateless, all state is in the cache, so you can
instantiate it as many times as you want.
The EventRelatedCache instance itself is stateless, all state is
stored in the cache backend, so you can instantiate this class as many
times as you want.
"""
def __init__(self, event: Event, cache: str='default'):

View File

@@ -4,6 +4,9 @@ from versions.models import Versionable
class VersionedBaseModelForm(BaseModelForm):
"""
This is a helperclass to construct VersionedModelForm
"""
def save(self, commit=True):
if self.instance.pk is not None and isinstance(self.instance, Versionable):
if self.has_changed():
@@ -12,4 +15,11 @@ class VersionedBaseModelForm(BaseModelForm):
class VersionedModelForm(six.with_metaclass(ModelFormMetaclass, VersionedBaseModelForm)):
"""
This is a modified version of Django's ModelForm which differs from ModelForm in
only one way: It executes the .clone() method of an object before saving it back to
the database, if the model is a sub-class of versions.models.Versionable. You can
safely use this as a base class for all your model forms, it will work out correctly
with both versioned and non-versioned models.
"""
pass

View File

@@ -349,7 +349,8 @@ class EventPermission(Versionable):
class ItemCategory(Versionable):
"""
Items can be sorted into categories
Items can be sorted into categories, which only have a name and a
configurable order
"""
event = VersionedForeignKey(
Event,
@@ -385,9 +386,8 @@ class ItemCategory(Versionable):
class Property(Versionable):
"""
A property is a modifier which can be applied to an
Item. For example 'Size' would be a property associated
with the item 'T-Shirt'.
A property is a modifier which can be applied to an Item. For example
'Size' would be a property associated with the item 'T-Shirt'.
"""
event = VersionedForeignKey(

View File

@@ -1,7 +1,7 @@
try: # NOQA
from enum import Enum
except ImportError: # NOQA
from flufl.enum import Enum
from flufl.enum import Enum # remove this dependency when support for python <=3.3 is dropped
from django.apps import apps
@@ -10,7 +10,10 @@ class PluginType(Enum):
RESTRICTION = 1
def get_all_plugins() -> "class":
def get_all_plugins() -> "List[class]":
"""
Returns the TixlPluginMeta classes of all plugins found in the installed Django apps.
"""
plugins = []
for app in apps.get_app_configs():
if hasattr(app, 'TixlPluginMeta'):

View File

@@ -4,6 +4,11 @@ from django.dispatch.dispatcher import NO_RECEIVERS
class EventPluginSignal(django.dispatch.Signal):
"""
This is an extension to Django's built-in signals which differs in a way that it sends
out it's events only to receivers which belong to plugins that are enabled for the given
Event.
"""
def send(self, sender, **named):
"""
@@ -34,6 +39,11 @@ class EventPluginSignal(django.dispatch.Signal):
responses.append((receiver, response))
return responses
"""
This signal is sent out every time some component of tixl wants to know whether a specific
item or variation is available for sell. The item will only be sold, if all (active) receivers
return a positive result (see plugin API documentation for details).
"""
determine_availability = EventPluginSignal(
providing_args=["item", "variations", "context", "cache"]
)

View File

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -3,6 +3,9 @@ from django.core.urlresolvers import resolve
def contextprocessor(request):
"""
Adds data to all template contexts
"""
ctx = {
'url_name': resolve(request.path_info).url_name,
'settings': settings,

View File

@@ -13,8 +13,7 @@ from tixlbase.models import Event
class PermissionMiddleware:
"""
This middleware enforces all requests to the control app
to require login.
This middleware enforces all requests to the control app to require login.
Additionally, it enforces all requests to "control:event." URLs
to be for an event the user has basic access to.
"""
@@ -56,4 +55,5 @@ class PermissionMiddleware:
organizer__slug=url.kwargs['organizer'],
)
except Event.DoesNotExist:
return HttpResponseNotFound(_("The selected event was not found or you have no permission to administrate it."))
return HttpResponseNotFound(_("The selected event was not found or you "
"have no permission to administrate it."))

View File

@@ -5,6 +5,10 @@ from tixlbase.models import EventPermission
def event_permission_required(permission):
"""
This view decorator rejects all requests with a 403 response which are not from
users having the given permission for the event the request is associated with.
"""
def decorator(function):
def wrapper(request, *args, **kw):
if not request.user.is_authenticated():
@@ -26,6 +30,10 @@ def event_permission_required(permission):
class EventPermissionRequiredMixin:
"""
This mixin is equivalent to the event_permission_required view decorator but
is in a form suitable for class-based views.
"""
permission = ''
@classmethod

View File

@@ -1,6 +1,10 @@
from tixlbase.signals import EventPluginSignal
"""
This signal is sent out to build configuration forms for all restriction formsets
(see plugin API documentation for details).
"""
restriction_formset = EventPluginSignal(
providing_args=["item"]
)

View File

@@ -22,21 +22,30 @@ urlpatterns += patterns(
url(r'^settings/plugins$', event.EventPlugins.as_view(), name='event.settings.plugins'),
url(r'^items/$', item.ItemList.as_view(), name='event.items'),
url(r'^items/(?P<item>[0-9a-f-]+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
url(r'^items/(?P<item>[0-9a-f-]+)/variations$', item.ItemVariations.as_view(), name='event.item.variations'),
url(r'^items/(?P<item>[0-9a-f-]+)/restrictions$', item.ItemRestrictions.as_view(), name='event.item.restrictions'),
url(r'^items/(?P<item>[0-9a-f-]+)/variations$', item.ItemVariations.as_view(),
name='event.item.variations'),
url(r'^items/(?P<item>[0-9a-f-]+)/restrictions$', item.ItemRestrictions.as_view(),
name='event.item.restrictions'),
url(r'^categories/$', item.CategoryList.as_view(), name='event.items.categories'),
url(r'^categories/(?P<category>[0-9a-f-]+)/delete$', item.CategoryDelete.as_view(), name='event.items.categories.delete'),
url(r'^categories/(?P<category>[0-9a-f-]+)/delete$', item.CategoryDelete.as_view(),
name='event.items.categories.delete'),
url(r'^categories/(?P<category>[0-9a-f-]+)/up$', item.category_move_up, name='event.items.categories.up'),
url(r'^categories/(?P<category>[0-9a-f-]+)/down$', item.category_move_down, name='event.items.categories.down'),
url(r'^categories/(?P<category>[0-9a-f-]+)/$', item.CategoryUpdate.as_view(), name='event.items.categories.edit'),
url(r'^categories/(?P<category>[0-9a-f-]+)/down$', item.category_move_down,
name='event.items.categories.down'),
url(r'^categories/(?P<category>[0-9a-f-]+)/$', item.CategoryUpdate.as_view(),
name='event.items.categories.edit'),
url(r'^categories/add$', item.CategoryCreate.as_view(), name='event.items.categories.add'),
url(r'^questions/$', item.QuestionList.as_view(), name='event.items.questions'),
url(r'^questions/(?P<question>[0-9a-f-]+)/delete$', item.QuestionDelete.as_view(), name='event.items.questions.delete'),
url(r'^questions/(?P<question>[0-9a-f-]+)/$', item.QuestionUpdate.as_view(), name='event.items.questions.edit'),
url(r'^questions/(?P<question>[0-9a-f-]+)/delete$', item.QuestionDelete.as_view(),
name='event.items.questions.delete'),
url(r'^questions/(?P<question>[0-9a-f-]+)/$', item.QuestionUpdate.as_view(),
name='event.items.questions.edit'),
url(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'),
url(r'^properties/$', item.PropertyList.as_view(), name='event.items.properties'),
url(r'^properties/(?P<property>[0-9a-f-]+)/$', item.PropertyUpdate.as_view(), name='event.items.properties.edit'),
url(r'^properties/(?P<property>[0-9a-f-]+)/delete$', item.PropertyDelete.as_view(), name='event.items.properties.delete'),
url(r'^properties/(?P<property>[0-9a-f-]+)/$', item.PropertyUpdate.as_view(),
name='event.items.properties.edit'),
url(r'^properties/(?P<property>[0-9a-f-]+)/delete$', item.PropertyDelete.as_view(),
name='event.items.properties.delete'),
url(r'^properties/add$', item.PropertyCreate.as_view(), name='event.items.properties.add'),
url(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'),
url(r'^quotas/(?P<quota>[0-9a-f-]+)/$', item.QuotaUpdate.as_view(), name='event.items.quotas.edit'),

View File

@@ -9,8 +9,8 @@ from django.contrib.auth import logout as auth_logout
class AuthenticationForm(BaseAuthenticationForm):
"""
The login form, providing an email and password field. The
form does already implement validation for correct user data.
The login form, providing an email and password field. The form already implements
validation for correct user data.
"""
email = forms.EmailField(label=_("E-mail address"), max_length=254)
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
@@ -68,5 +68,8 @@ def login(request):
def logout(request):
"""
Log the user out of the current session, then redirect to login page.
"""
auth_logout(request)
return redirect('control:auth.login')

View File

@@ -70,7 +70,7 @@ class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin
permission = 'can_change_settings'
template_name = 'tixlcontrol/event/plugins.html'
def get_object(self, queryset=None):
def get_object(self, queryset=None) -> Event:
return self.request.event
def get_context_data(self, *args, **kwargs) -> dict:

View File

@@ -13,6 +13,14 @@ from tixlbase.models import ItemVariation, PropertyValue, Item
class TolerantFormsetModelForm(VersionedModelForm):
"""
This is equivalent to a normal VersionedModelForm, but works around a problem that
arises when the form is used inside a FormSet with can_order=True and django-formset-js
enabled. In this configuration, even empty "extra" forms might have an ORDER value
sent and Django marks the form as empty and raises validation errors because the other
fields have not been filled.
"""
def has_changed(self) -> bool:
"""
Returns True if data differs from initial. Contrary to the default
@@ -99,6 +107,10 @@ class RestrictionInlineFormset(forms.BaseInlineFormSet):
class VariationsFieldRenderer(forms.widgets.CheckboxFieldRenderer):
"""
This is the default renderer for a VariationsField. Based on the choice input class
this renders a list or a matrix of checkboxes/radio buttons/...
"""
def __init__(self, name, value, attrs, choices):
self.name = name
@@ -207,10 +219,17 @@ class VariationsFieldRenderer(forms.widgets.CheckboxFieldRenderer):
class VariationsCheckboxRenderer(VariationsFieldRenderer):
"""
This is the same as VariationsFieldRenderer but with the choice input class
forced to checkboxes
"""
choice_input_class = forms.widgets.CheckboxChoiceInput
class VariationsSelectMultiple(forms.CheckboxSelectMultiple):
"""
This is the default widget for a VariationsField
"""
renderer = VariationsCheckboxRenderer
_empty_value = []
@@ -240,7 +259,7 @@ class VariationsField(forms.ModelMultipleChoiceField):
"""
We can't use a normal QuerySet as there theoretically might be
two types of variations: Some who already have a ItemVariation
object associated with tham and some who don't. We therefore use
object associated with them and some who don't. We therefore use
the item's ``get_all_variations`` method. In the first case, we
use the ItemVariation objects primary key as our choice, key,
in the latter case we use a string constructed from the values

View File

@@ -47,7 +47,7 @@ class CategoryDelete(EventPermissionRequiredMixin, DeleteView):
permission = 'can_change_items'
context_object_name = 'category'
def get_object(self, queryset=None):
def get_object(self, queryset=None) -> ItemCategory:
url = resolve(self.request.path_info)
return self.request.event.categories.current.get(
identity=url.kwargs['category']
@@ -62,7 +62,7 @@ class CategoryDelete(EventPermissionRequiredMixin, DeleteView):
self.object.delete()
return HttpResponseRedirect(success_url)
def get_success_url(self):
def get_success_url(self) -> str:
return reverse('control:event.items.categories', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
@@ -76,13 +76,13 @@ class CategoryUpdate(EventPermissionRequiredMixin, UpdateView):
permission = 'can_change_items'
context_object_name = 'category'
def get_object(self, queryset=None):
def get_object(self, queryset=None) -> ItemCategory:
url = resolve(self.request.path_info)
return self.request.event.categories.current.get(
identity=url.kwargs['category']
)
def get_success_url(self):
def get_success_url(self) -> str:
return reverse('control:event.items.categories', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
@@ -96,7 +96,7 @@ class CategoryCreate(EventPermissionRequiredMixin, CreateView):
permission = 'can_change_items'
context_object_name = 'category'
def get_success_url(self):
def get_success_url(self) -> str:
return reverse('control:event.items.categories', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
@@ -116,7 +116,12 @@ class CategoryList(ListView):
return self.request.event.categories.current.all()
def category_move(request, organizer, event, category, up=True):
def category_move(request, category, up=True):
"""
This is a helper function to avoid duplicating code in category_move_up and
category_move_down. It takes a category and a direction and then tries to bring
all categories for this event in a new order.
"""
category = request.event.categories.current.get(
identity=category
)
@@ -136,7 +141,7 @@ def category_move(request, organizer, event, category, up=True):
@event_permission_required("can_change_items")
def category_move_up(request, organizer, event, category):
category_move(request, organizer, event, category, up=True)
category_move(request, category, up=True)
return redirect(reverse('control:event.items.categories', kwargs={
'organizer': request.event.organizer.slug,
'event': request.event.slug,
@@ -145,7 +150,7 @@ def category_move_up(request, organizer, event, category):
@event_permission_required("can_change_items")
def category_move_down(request, organizer, event, category):
category_move(request, organizer, event, category, up=False)
category_move(request, category, up=False)
return redirect(reverse('control:event.items.categories', kwargs={
'organizer': request.event.organizer.slug,
'event': request.event.slug,
@@ -188,13 +193,13 @@ class PropertyUpdate(EventPermissionRequiredMixin, UpdateView):
permission = 'can_change_items'
context_object_name = 'property'
def get_object(self, queryset=None):
def get_object(self, queryset=None) -> Property:
url = resolve(self.request.path_info)
return self.request.event.properties.current.get(
identity=url.kwargs['property']
)
def get_success_url(self):
def get_success_url(self) -> str:
url = resolve(self.request.path_info)
return reverse('control:event.items.properties.edit', kwargs={
'organizer': self.request.event.organizer.slug,
@@ -214,7 +219,7 @@ class PropertyUpdate(EventPermissionRequiredMixin, UpdateView):
formset = formsetclass(**kwargs)
return formset
def get_context_data(self, *args, **kwargs):
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['formset'] = self.get_formset()
return context
@@ -249,7 +254,7 @@ class PropertyCreate(EventPermissionRequiredMixin, CreateView):
permission = 'can_change_items'
context_object_name = 'property'
def get_success_url(self):
def get_success_url(self) -> str:
return reverse('control:event.items.properties', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
@@ -265,7 +270,7 @@ class PropertyCreate(EventPermissionRequiredMixin, CreateView):
formset = formsetclass(**self.get_form_kwargs())
return formset
def get_context_data(self, *args, **kwargs):
def get_context_data(self, *args, **kwargs) -> dict:
self.object = None
context = super().get_context_data(*args, **kwargs)
context['formset'] = self.get_formset()
@@ -297,16 +302,16 @@ class PropertyDelete(EventPermissionRequiredMixin, DeleteView):
permission = 'can_change_items'
context_object_name = 'property'
def get_context_data(self, *args, **kwargs):
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['dependent'] = self.get_object().items.current.all()
context['possible'] = self.is_allowed()
return context
def is_allowed(self):
def is_allowed(self) -> bool:
return self.get_object().items.current.count() == 0
def get_object(self, queryset=None):
def get_object(self, queryset=None) -> Property:
if not hasattr(self, 'object') or not self.object:
url = resolve(self.request.path_info)
self.object = self.request.event.properties.current.get(
@@ -322,7 +327,7 @@ class PropertyDelete(EventPermissionRequiredMixin, DeleteView):
else:
return HttpResponseForbidden()
def get_success_url(self):
def get_success_url(self) -> str:
return reverse('control:event.items.properties', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
@@ -356,13 +361,13 @@ class QuestionDelete(EventPermissionRequiredMixin, DeleteView):
permission = 'can_change_items'
context_object_name = 'question'
def get_object(self, queryset=None):
def get_object(self, queryset=None) -> Question:
url = resolve(self.request.path_info)
return self.request.event.questions.current.get(
identity=url.kwargs['question']
)
def get_context_data(self, *args, **kwargs):
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['dependent'] = list(self.get_object().items.current.all())
return context
@@ -373,7 +378,7 @@ class QuestionDelete(EventPermissionRequiredMixin, DeleteView):
self.object.delete()
return HttpResponseRedirect(success_url)
def get_success_url(self):
def get_success_url(self) -> str:
return reverse('control:event.items.questions', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
@@ -387,13 +392,13 @@ class QuestionUpdate(EventPermissionRequiredMixin, UpdateView):
permission = 'can_change_items'
context_object_name = 'question'
def get_object(self, queryset=None):
def get_object(self, queryset=None) -> Question:
url = resolve(self.request.path_info)
return self.request.event.questions.current.get(
identity=url.kwargs['question']
)
def get_success_url(self):
def get_success_url(self) -> str:
return reverse('control:event.items.questions', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
@@ -407,7 +412,7 @@ class QuestionCreate(EventPermissionRequiredMixin, CreateView):
permission = 'can_change_items'
context_object_name = 'question'
def get_success_url(self):
def get_success_url(self) -> str:
return reverse('control:event.items.questions', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
@@ -447,7 +452,7 @@ class QuotaCreate(EventPermissionRequiredMixin, CreateView):
permission = 'can_change_items'
context_object_name = 'quota'
def get_success_url(self):
def get_success_url(self) -> str:
return reverse('control:event.items.quotas', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
@@ -465,13 +470,13 @@ class QuotaUpdate(EventPermissionRequiredMixin, UpdateView):
permission = 'can_change_items'
context_object_name = 'quota'
def get_object(self, queryset=None):
def get_object(self, queryset=None) -> Quota:
url = resolve(self.request.path_info)
return self.request.event.quotas.current.get(
identity=url.kwargs['quota']
)
def get_success_url(self):
def get_success_url(self) -> str:
return reverse('control:event.items.quotas', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
@@ -484,13 +489,13 @@ class QuotaDelete(EventPermissionRequiredMixin, DeleteView):
permission = 'can_change_items'
context_object_name = 'quota'
def get_object(self, queryset=None):
def get_object(self, queryset=None) -> Quota:
url = resolve(self.request.path_info)
return self.request.event.quotas.current.get(
identity=url.kwargs['quota']
)
def get_context_data(self, *args, **kwargs):
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['dependent'] = list(self.get_object().items.current.all())
return context
@@ -501,7 +506,7 @@ class QuotaDelete(EventPermissionRequiredMixin, DeleteView):
self.object.delete()
return HttpResponseRedirect(success_url)
def get_success_url(self):
def get_success_url(self) -> str:
return reverse('control:event.items.quotas', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
@@ -512,7 +517,7 @@ class ItemDetailMixin(SingleObjectMixin):
model = Item
context_object_name = 'item'
def get_object(self, queryset=None):
def get_object(self, queryset=None) -> Item:
if not hasattr(self, 'object') or not self.object:
url = resolve(self.request.path_info)
self.item = self.request.event.items.current.get(
@@ -551,7 +556,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
template_name = 'tixlcontrol/item/index.html'
permission = 'can_change_items'
def get_success_url(self):
def get_success_url(self) -> str:
return reverse('control:event.item', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
@@ -578,7 +583,7 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
super().__init__(*args, **kwargs)
self.item = None
def get_form(self, variation, data=None):
def get_form(self, variation, data=None) -> ItemVariationForm:
"""
Return the dict for one given variation. Variations are expected to be
dictionaries in the format of Item.get_all_variations()
@@ -603,7 +608,7 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
form.values = values
return form
def get_forms(self):
def get_forms(self) -> tuple:
"""
Returns one form per possible item variation. The forms are returned
twice: The first entry in the returned tuple contains a 1-, 2- or
@@ -705,7 +710,7 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
# TODO: Redirect to success message
return self.render_to_response(context)
def get_template_names(self):
def get_template_names(self) -> "List[str]":
if self.dimension == 0:
return ['tixlcontrol/item/variations_0d.html']
elif self.dimension == 1:
@@ -713,7 +718,7 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
elif self.dimension >= 2:
return ['tixlcontrol/item/variations_nd.html']
def get_context_data(self, **kwargs):
def get_context_data(self, **kwargs) -> dict:
context = super().get_context_data(**kwargs)
context['forms'] = self.forms
context['properties'] = self.properties
@@ -768,12 +773,12 @@ class ItemRestrictions(ItemDetailMixin, EventPermissionRequiredMixin, TemplateVi
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def get_context_data(self, *args, **kwargs):
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['formsets'] = self.formsets
return context
def get_success_url(self):
def get_success_url(self) -> str:
return reverse('control:event.item.restrictions', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,