Added docstrings. Small changes.
This commit is contained in:
parent
368873e738
commit
7cd70a568c
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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'))
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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',
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
if not skip_autoupdate:
|
||||||
inform_changed_data(self.get_root_rest_element(), information=information)
|
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 not skip_autoupdate:
|
||||||
if self != self.get_root_rest_element():
|
if self != self.get_root_rest_element():
|
||||||
# The deletion of a included element is a change of the master
|
# The deletion of a included element is a change of the root element.
|
||||||
# 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
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user