Added docstrings. Small changes.

This commit is contained in:
Norman Jäckel 2016-09-30 20:42:58 +02:00 committed by Oskar Hahn
parent 368873e738
commit 7cd70a568c
15 changed files with 108 additions and 56 deletions

View File

@ -42,6 +42,7 @@ Other:
- Fixed bug, that the last change of a config value was not send via autoupdate. - Fixed bug, that the last change of a config value was not send via autoupdate.
- Added template hooks for plugins (in item detail view and motion poll form). - Added template hooks for plugins (in item detail view and motion poll form).
- Used Django Channels instead of Tornado. Refactoring of the autoupdate process. - Used Django Channels instead of Tornado. Refactoring of the autoupdate process.
- Added new caching system with support for Redis.
Version 2.0 (2016-04-18) Version 2.0 (2016-04-18)

View File

@ -24,6 +24,11 @@ class ItemManager(models.Manager):
numbering. numbering.
""" """
def get_full_queryset(self): def get_full_queryset(self):
"""
Returns the normal queryset with all items. In the background all
speakers and related items (topics, motions, assignments) are
prefetched from the database.
"""
return self.get_queryset().prefetch_related('speakers', 'content_object') return self.get_queryset().prefetch_related('speakers', 'content_object')
def get_only_agenda_items(self): def get_only_agenda_items(self):

View File

@ -51,7 +51,15 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
class AssignmentManager(models.Manager): class AssignmentManager(models.Manager):
"""
Customized model manager to support our get_full_queryset method.
"""
def get_full_queryset(self): def get_full_queryset(self):
"""
Returns the normal queryset with all assignments. In the background
all related users (candidates), the related agenda item and all
polls are prefetched from the database.
"""
return self.get_queryset().prefetch_related( return self.get_queryset().prefetch_related(
'related_users', 'related_users',
'agenda_items', 'agenda_items',
@ -121,8 +129,8 @@ class Assignment(RESTModelMixin, models.Model):
Tags for the assignment. Tags for the assignment.
""" """
# In theory there could be one then more agenda_item. But support only one. # In theory there could be one then more agenda_item. But we support only
# See the property agenda_item. # one. See the property agenda_item.
agenda_items = GenericRelation(Item, related_name='assignments') agenda_items = GenericRelation(Item, related_name='assignments')
class Meta: class Meta:
@ -304,6 +312,8 @@ class Assignment(RESTModelMixin, models.Model):
""" """
Returns the related agenda item. Returns the related agenda item.
""" """
# We support only one agenda item so just return the first element of
# the queryset.
return self.agenda_items.all()[0] return self.agenda_items.all()[0]
@property @property

View File

@ -84,11 +84,13 @@ class ConfigAccessPermissions(BaseAccessPermissions):
Returns the serlialized config data. Returns the serlialized config data.
""" """
from .config import config from .config import config
from .models import ConfigStore
# Attention: The format of this response has to be the same as in # Attention: The format of this response has to be the same as in
# the retrieve method of ConfigViewSet. # the retrieve method of ConfigViewSet.
if isinstance(instance, dict): if isinstance(instance, ConfigStore):
# It happens, that the caching system already sends the correct dict result = {'key': instance.key, 'value': config[instance.key]}
# as instance. else:
return instance # It is possible, that the caching system already sends the correct data as "instance".
return {'key': instance.key, 'value': config[instance.key]} result = instance
return result

View File

@ -116,7 +116,7 @@ class Projector(RESTModelMixin, models.Model):
result[key]['error'] = str(e) result[key]['error'] = str(e)
return result return result
def get_all_requirements(self, on_slide=None): def get_all_requirements(self, on_slide=None): #TODO: Refactor or rename this.
""" """
Generator which returns all instances that are shown on this projector. Generator which returns all instances that are shown on this projector.
""" """

View File

@ -624,6 +624,7 @@ class ConfigViewSet(ViewSet):
raise Http404 raise Http404
if content is None: if content is None:
# If content is None, the user has no permissions to see the item. # If content is None, the user has no permissions to see the item.
# See ConfigAccessPermissions or rather its parent class.
self.permission_denied() self.permission_denied()
return Response(content) return Response(content)

View File

@ -29,8 +29,15 @@ from .exceptions import WorkflowError
class MotionManager(models.Manager): class MotionManager(models.Manager):
"""
Customized model manager to support our get_full_queryset method.
"""
def get_full_queryset(self): def get_full_queryset(self):
return (super().get_queryset() """
Returns the normal queryset with all motions. In the background we
join and prefetch all related models.
"""
return (self.get_queryset()
.select_related('active_version') .select_related('active_version')
.prefetch_related( .prefetch_related(
'versions', 'versions',
@ -45,7 +52,7 @@ class MotionManager(models.Manager):
class Motion(RESTModelMixin, models.Model): class Motion(RESTModelMixin, models.Model):
""" """
The Motion Class. Model for motions.
This class is the main entry point to all other classes related to a motion. This class is the main entry point to all other classes related to a motion.
""" """
@ -151,8 +158,8 @@ class Motion(RESTModelMixin, models.Model):
Configurable fields for comments. Contains a list of strings. Configurable fields for comments. Contains a list of strings.
""" """
# In theory there could be one then more agenda_item. But support only one. # In theory there could be one then more agenda_item. But we support only
# See the property agenda_item. # one. See the property agenda_item.
agenda_items = GenericRelation(Item, related_name='motions') agenda_items = GenericRelation(Item, related_name='motions')
class Meta: class Meta:
@ -540,6 +547,8 @@ class Motion(RESTModelMixin, models.Model):
""" """
Returns the related agenda item. Returns the related agenda item.
""" """
# We support only one agenda item so just return the first element of
# the queryset.
return self.agenda_items.all()[0] return self.agenda_items.all()[0]
@property @property
@ -961,7 +970,15 @@ class State(RESTModelMixin, models.Model):
class WorkflowManager(models.Manager): class WorkflowManager(models.Manager):
"""
Customized model manager to support our get_full_queryset method.
"""
def get_full_queryset(self): def get_full_queryset(self):
"""
Returns the normal queryset with all workflows. In the background
the first state is joined and all states and next states are
prefetched from the database.
"""
return (self.get_queryset() return (self.get_queryset()
.select_related('first_state') .select_related('first_state')
.prefetch_related('states', 'states__next_states')) .prefetch_related('states', 'states__next_states'))

View File

@ -8,9 +8,16 @@ from .access_permissions import TopicAccessPermissions
class TopicManager(models.Manager): class TopicManager(models.Manager):
def get_queryset(self): """
query = super().get_queryset().prefetch_related('attachments', 'agenda_items') Customized model manager to support our get_full_queryset method.
return query """
def get_full_queryset(self):
"""
Returns the normal queryset with all topics. In the background all
attachments and the related agenda item are prefetched from the
database.
"""
return self.get_queryset().prefetch_related('attachments', 'agenda_items')
class Topic(RESTModelMixin, models.Model): class Topic(RESTModelMixin, models.Model):
@ -18,14 +25,15 @@ class Topic(RESTModelMixin, models.Model):
Model for slides with custom content. Used to be called custom slide. Model for slides with custom content. Used to be called custom slide.
""" """
access_permissions = TopicAccessPermissions() access_permissions = TopicAccessPermissions()
objects = TopicManager() objects = TopicManager()
title = models.CharField(max_length=256) title = models.CharField(max_length=256)
text = models.TextField(blank=True) text = models.TextField(blank=True)
attachments = models.ManyToManyField(Mediafile, blank=True) attachments = models.ManyToManyField(Mediafile, blank=True)
# In theory there could be one then more agenda_item. But support only one. # In theory there could be one then more agenda_item. But we support only
# See the property agenda_item. # one. See the property agenda_item.
agenda_items = GenericRelation(Item, related_name='topics') agenda_items = GenericRelation(Item, related_name='topics')
class Meta: class Meta:
@ -39,6 +47,8 @@ class Topic(RESTModelMixin, models.Model):
""" """
Returns the related agenda item. Returns the related agenda item.
""" """
# We support only one agenda item so just return the first element of
# the queryset.
return self.agenda_items.all()[0] return self.agenda_items.all()[0]
@property @property

View File

@ -21,10 +21,13 @@ from .access_permissions import UserAccessPermissions
class UserManager(BaseUserManager): class UserManager(BaseUserManager):
""" """
Customized manager that creates new users only with a password and a Customized manager that creates new users only with a password and a
username. username. It also supports our get_full_queryset method.
""" """
def get_full_queryset(self): def get_full_queryset(self):
"""
Returns the normal queryset with all users. In the background all
groups are prefetched from the database.
"""
return self.get_queryset().prefetch_related('groups') return self.get_queryset().prefetch_related('groups')
def create_user(self, username, password, **kwargs): def create_user(self, username, password, **kwargs):

View File

@ -21,12 +21,12 @@ USERCANSEESERIALIZER_FIELDS = (
'number', 'number',
'about_me', 'about_me',
'groups', 'groups',
'is_present',
'is_committee', 'is_committee',
) )
USERCANSEEEXTRASERIALIZER_FIELDS = USERCANSEESERIALIZER_FIELDS + ( USERCANSEEEXTRASERIALIZER_FIELDS = USERCANSEESERIALIZER_FIELDS + (
'is_present',
'comment', 'comment',
'is_active', 'is_active',
) )

View File

@ -68,7 +68,7 @@ class BaseAccessPermissions(object, metaclass=SignalConnectMetaClass):
Hint: You should override this method if your get_serializer_class() Hint: You should override this method if your get_serializer_class()
method returns different serializers for different users or if you method returns different serializers for different users or if you
have access restrictions in your view or viewset in methods like have access restrictions in your view or viewset in methods like
retrieve(), list() or check_object_permissions(). retrieve() or list().
""" """
if self.check_permissions(user): if self.check_permissions(user):
data = full_data data = full_data
@ -78,8 +78,8 @@ class BaseAccessPermissions(object, metaclass=SignalConnectMetaClass):
def get_projector_data(self, full_data): def get_projector_data(self, full_data):
""" """
Returns the serialized data for the projector. Returns None if has no Returns the serialized data for the projector. Returns None if the
access to this specific data. Returns reduced data if the user has user has no access to this specific data. Returns reduced data if
limited access. Default: Returns full data. the user has limited access. Default: Returns full data.
""" """
return full_data return full_data

View File

@ -23,7 +23,7 @@ def get_logged_in_users():
return User.objects.exclude(session=None).filter(session__expire_date__gte=timezone.now()).distinct() return User.objects.exclude(session=None).filter(session__expire_date__gte=timezone.now()).distinct()
def get_projector_element_data(projector, on_slide=None): def get_projector_element_data(projector, on_slide=None): #TODO
""" """
Returns a list of dicts that are required for a specific projector. Returns a list of dicts that are required for a specific projector.
@ -81,7 +81,7 @@ def ws_add_projector(message, projector_id):
Group('projector-{}'.format(projector_id)).add(message.reply_channel) Group('projector-{}'.format(projector_id)).add(message.reply_channel)
# Send all elements that are on the projector. # Send all elements that are on the projector.
output = get_projector_element_data(projector) output = get_projector_element_data(projector) #TODO
# Send all config elements. # Send all config elements.
collection = Collection(config.get_collection_string()) collection = Collection(config.get_collection_string())
@ -102,7 +102,7 @@ def ws_disconnect_projector(message, projector_id):
Group('projector-{}'.format(projector_id)).discard(message.reply_channel) Group('projector-{}'.format(projector_id)).discard(message.reply_channel)
def send_data(message): def send_data(message): #TODO
""" """
Informs all users about changed data. Informs all users about changed data.
""" """
@ -174,9 +174,13 @@ def inform_changed_data(instance, information=None):
collection_element = CollectionElement.from_instance( collection_element = CollectionElement.from_instance(
root_instance, root_instance,
information=information) information=information)
# If currently there is an open database transaction, then the
# send_autoupdate function is only called, when the transaction is
# commited. If there is currently no transaction, then the function
# is called immediately.
transaction.on_commit(lambda: send_autoupdate(collection_element)) transaction.on_commit(lambda: send_autoupdate(collection_element))
def inform_deleted_data(collection_string, id, information=None): def inform_deleted_data(collection_string, id, information=None):
""" """
Informs the autoupdate system and the caching system about the deletion of Informs the autoupdate system and the caching system about the deletion of
@ -187,16 +191,10 @@ def inform_deleted_data(collection_string, id, information=None):
id=id, id=id,
deleted=True, deleted=True,
information=information) information=information)
# If currently there is an open database transaction, then the
# If currently there is an open database transaction, then the following # send_autoupdate function is only called, when the transaction is
# function is only called, when the transaction is commited. If there # commited. If there is currently no transaction, then the function
# is currently no transaction, then the function is called immediately. # is called immediately.
def send_autoupdate():
try:
Channel('autoupdate.send_data').send(collection_element.as_channels_message())
except ChannelLayer.ChannelFull:
pass
transaction.on_commit(lambda: send_autoupdate(collection_element)) transaction.on_commit(lambda: send_autoupdate(collection_element))

View File

@ -459,6 +459,9 @@ def get_collection_id_from_cache_key(cache_key):
def use_redis_cache(): def use_redis_cache():
"""
Returns True if Redis is used als caching backend.
"""
try: try:
from django_redis.cache import RedisCache from django_redis.cache import RedisCache
except ImportError: except ImportError:

View File

@ -57,38 +57,40 @@ class RESTModelMixin:
def save(self, skip_autoupdate=False, information=None, *args, **kwargs): def save(self, skip_autoupdate=False, information=None, *args, **kwargs):
""" """
Calls the django save-method and afterwards hits the autoupdate system. Calls Django's save() method and afterwards hits the autoupdate system.
If skip_autoupdate is set to True, then the autoupdate system is not If skip_autoupdate is set to True, then the autoupdate system is not
informed about the model changed. This also means, that the model cache informed about the model changed. This also means, that the model cache
is not updated. is not updated. You have to do it manually. Like this:
The optional argument information can be an object that is given to the TODO HELP ME
autoupdate system. It should be a dict.
The optional argument information can be a dictionary that is given to
the autoupdate system.
""" """
# TODO: Fix circular imports #TODO: Add example in docstring.
#TODO: Fix circular imports
from .autoupdate import inform_changed_data from .autoupdate import inform_changed_data
return_value = super().save(*args, **kwargs) return_value = super().save(*args, **kwargs)
inform_changed_data(self.get_root_rest_element(), information=information) if not skip_autoupdate:
inform_changed_data(self.get_root_rest_element(), information=information)
return return_value return return_value
def delete(self, skip_autoupdate=False, information=None, *args, **kwargs): def delete(self, skip_autoupdate=False, information=None, *args, **kwargs):
""" """
Calls the django delete-method and afterwards hits the autoupdate system. Calls Django's delete() method and afterwards hits the autoupdate system.
See the save method above. See the save method above.
""" """
# TODO: Fix circular imports #TODO: Fix circular imports
from .autoupdate import inform_changed_data, inform_deleted_data from .autoupdate import inform_changed_data, inform_deleted_data
# Django sets the pk of the instance to None after deleting it. But
# we need the pk to tell the autoupdate system which element was deleted.
instance_pk = self.pk instance_pk = self.pk
return_value = super().delete(*args, **kwargs) return_value = super().delete(*args, **kwargs)
if self != self.get_root_rest_element(): if not skip_autoupdate:
# The deletion of a included element is a change of the master if self != self.get_root_rest_element():
# element. # The deletion of a included element is a change of the root element.
# TODO: Does this work in any case with self.pk = None? #TODO: Does this work in any case with self.pk == None?
inform_changed_data(self.get_root_rest_element(), information=information) inform_changed_data(self.get_root_rest_element(), information=information)
else: else:
inform_deleted_data(self, information=information) inform_deleted_data(self.get_collection_string(), instance_pk, information=information)
return return_value return return_value

View File

@ -40,7 +40,7 @@ class RetrieveItem(TestCase):
permission = group.permissions.get(content_type__app_label=app_label, codename=codename) permission = group.permissions.get(content_type__app_label=app_label, codename=codename)
group.permissions.remove(permission) group.permissions.remove(permission)
response = self.client.get(reverse('item-detail', args=[self.item.pk])) response = self.client.get(reverse('item-detail', args=[self.item.pk]))
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, status.HTTP_403_PERMISSION_DENIED)
class TestDBQueries(TestCase): class TestDBQueries(TestCase):