diff --git a/.travis.yml b/.travis.yml
index 142b08c05..81e94e68b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,6 +8,7 @@ cache:
python:
- "3.3"
- "3.4"
+ - "3.5"
install:
- "pip install --upgrade --requirement requirements.txt"
- "npm install"
@@ -18,7 +19,7 @@ script:
- "isort --check-only --recursive openslides tests"
- "DJANGO_SETTINGS_MODULE='tests.settings' coverage run ./manage.py test tests.unit"
- - "coverage report --fail-under=43"
+ - "coverage report --fail-under=40"
- "DJANGO_SETTINGS_MODULE='tests.settings' coverage run ./manage.py test tests.integration"
- "coverage report --fail-under=50"
diff --git a/openslides/agenda/apps.py b/openslides/agenda/apps.py
index 8c7813be9..6e0f2c913 100644
--- a/openslides/agenda/apps.py
+++ b/openslides/agenda/apps.py
@@ -14,15 +14,23 @@ class AgendaAppConfig(AppConfig):
from . import projector # noqa
# Import all required stuff.
- from django.db.models.signals import pre_delete
+ from django.db.models.signals import pre_delete, post_save
from openslides.core.signals import config_signal
from openslides.utils.rest_api import router
- from .signals import setup_agenda_config, listen_to_related_object_delete_signal
+ from .signals import (
+ setup_agenda_config,
+ listen_to_related_object_post_delete,
+ listen_to_related_object_post_save)
from .views import ItemViewSet
# Connect signals.
config_signal.connect(setup_agenda_config, dispatch_uid='setup_agenda_config')
- pre_delete.connect(listen_to_related_object_delete_signal, dispatch_uid='agenda_listen_to_related_object_delete_signal')
+ post_save.connect(
+ listen_to_related_object_post_save,
+ dispatch_uid='listen_to_related_object_post_save')
+ pre_delete.connect(
+ listen_to_related_object_post_delete,
+ dispatch_uid='listen_to_related_object_post_delete')
# Register viewsets.
router.register('agenda/item', ItemViewSet)
diff --git a/openslides/agenda/migrations/0004_auto_20151027_1423.py b/openslides/agenda/migrations/0004_auto_20151027_1423.py
new file mode 100644
index 000000000..7369d4513
--- /dev/null
+++ b/openslides/agenda/migrations/0004_auto_20151027_1423.py
@@ -0,0 +1,46 @@
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('agenda', '0003_auto_20150904_1732'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='item',
+ options={
+ 'permissions': (
+ ('can_see', 'Can see agenda'),
+ ('can_manage', 'Can manage agenda'),
+ ('can_see_hidden_items',
+ 'Can see hidden items and time scheduling of agenda'))},
+ ),
+ migrations.AlterField(
+ model_name='item',
+ name='type',
+ field=models.IntegerField(
+ choices=[(1, 'Agenda item'), (2, 'Hidden item')],
+ verbose_name='Type',
+ default=1),
+ ),
+ migrations.AlterUniqueTogether(
+ name='item',
+ unique_together=set([('content_type', 'object_id')]),
+ ),
+ migrations.RemoveField(
+ model_name='item',
+ name='tags',
+ ),
+ migrations.RemoveField(
+ model_name='item',
+ name='text',
+ ),
+ migrations.RemoveField(
+ model_name='item',
+ name='title',
+ ),
+ ]
diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py
index b6f6bff17..48e26a021 100644
--- a/openslides/agenda/models.py
+++ b/openslides/agenda/models.py
@@ -5,13 +5,11 @@ from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.core.config import config
-from openslides.core.models import Tag
from openslides.core.projector import Countdown
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.models import RESTModelMixin
@@ -19,6 +17,45 @@ from openslides.utils.utils import to_roman
class ItemManager(models.Manager):
+ def get_only_agenda_items(self, queryset=None):
+ """
+ Generator, which yields only agenda items. Skips hidden items.
+ """
+ if queryset is None:
+ queryset = self.all()
+
+ # Do not execute item.is_hidden() because this would create a lot of db queries
+ root_items, item_children = self.get_root_and_children(only_agenda_items=True)
+
+ def yield_items(items):
+ """
+ Generator that yields a list of items and their children.
+ """
+ for item in items:
+ yield item
+ yield from yield_items(item_children[item.pk])
+ yield from yield_items(root_items)
+
+ def get_root_and_children(self, queryset=None, only_agenda_items=False):
+ """
+ Returns a list with all root items and a dictonary where the key is an
+ item pk and the value is a list with all children of the item.
+ """
+ if queryset is None:
+ queryset = self.order_by('weight')
+
+ item_children = defaultdict(list)
+ root_items = []
+ for item in queryset:
+ if only_agenda_items and item.type == item.HIDDEN_ITEM:
+ continue
+ if item.parent_id is not None:
+ item_children[item.parent_id].append(item)
+ else:
+ root_items.append(item)
+
+ return root_items, item_children
+
def get_tree(self, only_agenda_items=False, include_content=False):
"""
Generator that yields dictonaries. Each dictonary has two keys, id
@@ -30,15 +67,7 @@ class ItemManager(models.Manager):
If include_content is True, the yielded dictonaries have no key 'id'
but a key 'item' with the entire object.
"""
- item_queryset = self.order_by('weight')
- if only_agenda_items:
- item_queryset = item_queryset.filter(type__exact=Item.AGENDA_ITEM)
-
- # Index the items to get the children for each item
- item_children = defaultdict(list)
- for item in item_queryset:
- if item.parent:
- item_children[item.parent_id].append(item)
+ root_items, item_children = self.get_root_and_children(only_agenda_items=only_agenda_items)
def get_children(items):
"""
@@ -50,7 +79,7 @@ class ItemManager(models.Manager):
else:
yield dict(id=item.pk, children=get_children(item_children[item.pk]))
- yield from get_children(filter(lambda item: item.parent is None, item_queryset))
+ yield from get_children(root_items)
@transaction.atomic
def set_tree(self, tree):
@@ -101,27 +130,17 @@ class Item(RESTModelMixin, models.Model):
slide_callback_name = 'agenda'
AGENDA_ITEM = 1
- ORGANIZATIONAL_ITEM = 2
+ HIDDEN_ITEM = 2
ITEM_TYPE = (
(AGENDA_ITEM, ugettext_lazy('Agenda item')),
- (ORGANIZATIONAL_ITEM, ugettext_lazy('Organizational item')))
+ (HIDDEN_ITEM, ugettext_lazy('Hidden item')))
item_number = models.CharField(blank=True, max_length=255, verbose_name=ugettext_lazy("Number"))
"""
Number of agenda item.
"""
- title = models.CharField(null=True, max_length=255, verbose_name=ugettext_lazy("Title"))
- """
- Title of the agenda item.
- """
-
- text = models.TextField(null=True, blank=True, verbose_name=ugettext_lazy("Text"))
- """
- The optional text of the agenda item.
- """
-
comment = models.TextField(null=True, blank=True, verbose_name=ugettext_lazy("Comment"))
"""
Optional comment to the agenda item. Will not be shoun to normal users.
@@ -132,7 +151,10 @@ class Item(RESTModelMixin, models.Model):
Flag, if the item is finished.
"""
- type = models.IntegerField(choices=ITEM_TYPE, default=AGENDA_ITEM, verbose_name=ugettext_lazy("Type"))
+ type = models.IntegerField(
+ choices=ITEM_TYPE,
+ default=AGENDA_ITEM,
+ verbose_name=ugettext_lazy("Type"))
"""
Type of the agenda item.
@@ -175,32 +197,15 @@ class Item(RESTModelMixin, models.Model):
True, if the list of speakers is closed.
"""
- tags = models.ManyToManyField(Tag, blank=True)
- """
- Tags to categorise agenda items.
- """
-
class Meta:
permissions = (
('can_see', ugettext_noop("Can see agenda")),
('can_manage', ugettext_noop("Can manage agenda")),
- ('can_see_orga_items', ugettext_noop("Can see orga items and time scheduling of agenda")))
+ ('can_see_hidden_items', ugettext_noop("Can see hidden items and time scheduling of agenda")))
+ unique_together = ('content_type', 'object_id')
def __str__(self):
- return self.get_title()
-
- def clean(self):
- """
- Ensures that the children of orga items are only orga items.
- """
- if (self.type == self.AGENDA_ITEM and
- self.parent is not None and
- self.parent.type == self.ORGANIZATIONAL_ITEM):
- raise ValidationError(_('Agenda items can not be child elements of an organizational item.'))
- if (self.type == self.ORGANIZATIONAL_ITEM and
- self.children.filter(type=self.AGENDA_ITEM).exists()):
- raise ValidationError(_('Organizational items can not have agenda items as child elements.'))
- return super().clean()
+ return self.title
def delete(self, with_children=False):
"""
@@ -216,91 +221,27 @@ class Item(RESTModelMixin, models.Model):
child.save()
super().delete()
- def get_title(self):
+ @property
+ def title(self):
"""
- Return the title of this item.
+ Return get_agenda_title() from the content_object.
"""
- if not self.content_object:
- agenda_title = self.title or ""
- else:
- try:
- agenda_title = self.content_object.get_agenda_title()
- except AttributeError:
- raise NotImplementedError('You have to provide a get_agenda_title '
- 'method on your related model.')
- return '%s %s' % (self.item_no, agenda_title) if self.item_no else agenda_title
-
- def get_title_supplement(self):
- """
- Return a supplement for the title.
- """
- if not self.content_object:
- return ''
try:
- return self.content_object.get_agenda_title_supplement()
+ title = self.content_object.get_agenda_title()
except AttributeError:
- raise NotImplementedError('You have to provide a get_agenda_title_supplement method on your related model.')
+ raise NotImplementedError('You have to provide a get_agenda_title '
+ 'method on your related model.')
+ return '%s %s' % (self.item_no, title) if self.item_no else title
- def get_list_of_speakers(self, old_speakers_count=None, coming_speakers_count=None):
+ def is_hidden(self):
"""
- Returns the list of speakers as a list of dictionaries. Each
- dictionary contains a prefix, the speaker and its type. Types
- are old_speaker, actual_speaker and coming_speaker.
+ Returns True if the type of this object itself is a hidden item or any
+ of its ancestors has such a type.
+
+ Attention! This executes one query for each ancestor of the item.
"""
- list_of_speakers = []
-
- # Parse old speakers
- old_speakers = self.speakers.exclude(begin_time=None).exclude(end_time=None).order_by('end_time')
- if old_speakers_count is None:
- old_speakers_count = old_speakers.count()
- last_old_speakers_count = max(0, old_speakers.count() - old_speakers_count)
- old_speakers = old_speakers[last_old_speakers_count:]
- for number, speaker in enumerate(old_speakers):
- prefix = old_speakers_count - number
- speaker_dict = {
- 'prefix': '-%d' % prefix,
- 'speaker': speaker,
- 'type': 'old_speaker',
- 'first_in_group': False,
- 'last_in_group': False}
- if number == 0:
- speaker_dict['first_in_group'] = True
- if number == old_speakers_count - 1:
- speaker_dict['last_in_group'] = True
- list_of_speakers.append(speaker_dict)
-
- # Parse actual speaker
- try:
- actual_speaker = self.speakers.filter(end_time=None).exclude(begin_time=None).get()
- except Speaker.DoesNotExist:
- pass
- else:
- list_of_speakers.append({
- 'prefix': '0',
- 'speaker': actual_speaker,
- 'type': 'actual_speaker',
- 'first_in_group': True,
- 'last_in_group': True})
-
- # Parse coming speakers
- coming_speakers = self.speakers.filter(begin_time=None).order_by('weight')
- if coming_speakers_count is None:
- coming_speakers_count = coming_speakers.count()
- coming_speakers = coming_speakers[:max(0, coming_speakers_count)]
- for number, speaker in enumerate(coming_speakers):
- speaker_dict = {
- 'prefix': number + 1,
- 'speaker': speaker,
- 'type': 'coming_speaker',
- 'first_in_group': False,
- 'last_in_group': False}
- if number == 0:
- speaker_dict['first_in_group'] = True
- if number == coming_speakers_count - 1:
- speaker_dict['last_in_group'] = True
- list_of_speakers.append(speaker_dict)
-
- return list_of_speakers
+ return (self.type == self.HIDDEN_ITEM or
+ (self.parent is not None and self.parent.is_hidden()))
def get_next_speaker(self):
"""
@@ -421,11 +362,12 @@ class Speaker(RESTModelMixin, models.Model):
speaking, end his speech.
"""
try:
- actual_speaker = Speaker.objects.filter(item=self.item, end_time=None).exclude(begin_time=None).get()
+ current_speaker = (Speaker.objects.filter(item=self.item, end_time=None)
+ .exclude(begin_time=None).get())
except Speaker.DoesNotExist:
pass
else:
- actual_speaker.end_speech()
+ current_speaker.end_speech()
self.weight = None
self.begin_time = datetime.now()
self.save()
diff --git a/openslides/agenda/projector.py b/openslides/agenda/projector.py
index c55356737..2cc9c7d68 100644
--- a/openslides/agenda/projector.py
+++ b/openslides/agenda/projector.py
@@ -1,7 +1,6 @@
from django.utils.translation import ugettext as _
from openslides.core.exceptions import ProjectorException
-from openslides.core.views import TagViewSet
from openslides.utils.projector import ProjectorElement, ProjectorRequirement
from .models import Item
@@ -80,8 +79,3 @@ class ItemDetailSlide(ProjectorElement):
view_class=speaker.user.get_view_class(),
view_action='retrieve',
pk=str(speaker.user_id))
- for tag in item.tags.all():
- yield ProjectorRequirement(
- view_class=TagViewSet,
- view_action='retrieve',
- pk=str(tag.pk))
diff --git a/openslides/agenda/serializers.py b/openslides/agenda/serializers.py
index 919be9416..23d916dbd 100644
--- a/openslides/agenda/serializers.py
+++ b/openslides/agenda/serializers.py
@@ -1,7 +1,6 @@
from django.core.urlresolvers import reverse
from openslides.utils.rest_api import (
- CharField,
ModelSerializer,
RelatedField,
get_collection_and_id_from_url,
@@ -45,10 +44,7 @@ class ItemSerializer(ModelSerializer):
"""
Serializer for agenda.models.Item objects.
"""
- get_title = CharField(read_only=True)
- get_title_supplement = CharField(read_only=True)
content_object = RelatedItemRelatedField(read_only=True)
- item_no = CharField(read_only=True)
speakers = SpeakerSerializer(many=True, read_only=True)
class Meta:
@@ -56,11 +52,7 @@ class ItemSerializer(ModelSerializer):
fields = (
'id',
'item_number',
- 'item_no',
'title',
- 'get_title',
- 'get_title_supplement',
- 'text',
'comment',
'closed',
'type',
@@ -68,6 +60,5 @@ class ItemSerializer(ModelSerializer):
'speakers',
'speaker_list_closed',
'content_object',
- 'tags',
'weight',
'parent',)
diff --git a/openslides/agenda/signals.py b/openslides/agenda/signals.py
index cdf07efbd..6aa89c440 100644
--- a/openslides/agenda/signals.py
+++ b/openslides/agenda/signals.py
@@ -72,15 +72,24 @@ def setup_agenda_config(sender, **kwargs):
group=ugettext_lazy('Agenda'))
-def listen_to_related_object_delete_signal(sender, instance, **kwargs):
+def listen_to_related_object_post_save(sender, instance, created, **kwargs):
"""
- Receiver function to change agenda items of a related item that is to
- be deleted. It is connected to the signal
- django.db.models.signals.pre_delete during app loading.
+ Receiver function to create agenda items. It is connected to the signal
+ django.db.models.signals.post_save during app loading.
+ """
+ if created and hasattr(instance, 'get_agenda_title'):
+ Item.objects.create(content_object=instance)
+
+
+def listen_to_related_object_post_delete(sender, instance, **kwargs):
+ """
+ Receiver function to delete agenda items. It is connected to the signal
+ django.db.models.signals.post_delete during app loading.
"""
if hasattr(instance, 'get_agenda_title'):
- for item in Item.objects.filter(content_type=ContentType.objects.get_for_model(sender), object_id=instance.pk):
- item.title = '< Item for deleted (%s) >' % instance.get_agenda_title()
- item.content_type = None
- item.object_id = None
- item.save()
+ content_type = ContentType.objects.get_for_model(instance)
+ try:
+ Item.objects.get(object_id=instance.pk, content_type=content_type).delete()
+ except Item.DoesNotExist:
+ # Item does not exist so we do not have to delete it.
+ pass
diff --git a/openslides/agenda/static/js/agenda/base.js b/openslides/agenda/static/js/agenda/base.js
index 89c40a96e..87e7cf4cc 100644
--- a/openslides/agenda/static/js/agenda/base.js
+++ b/openslides/agenda/static/js/agenda/base.js
@@ -33,6 +33,27 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
methods: {
getResourceName: function () {
return name;
+ },
+ getContentObject: function () {
+ return DS.get(this.content_object.collection, this.content_object.id);
+ },
+ getContentResource: function () {
+ return DS.definitions[this.content_object.collection];
+ },
+ getTitle: function () {
+ var title;
+ try {
+ title = this.getContentObject().getAgendaTitle();
+ } catch (e) {
+ // Only use this.title when the content object is not
+ // in the DS store.
+ title = this.title;
+ }
+ return _.trim(
+ title + ' ' + (
+ this.getContentResource().agendaSupplement || ''
+ )
+ );
}
},
relations: {
@@ -97,6 +118,7 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
});
return getChildren(parentItems);
},
+
// Returns a list of all items as a flat tree the attribute parentCount
getFlatTree: function(items) {
var tree = this.getTree(items);
diff --git a/openslides/agenda/static/js/agenda/site.js b/openslides/agenda/static/js/agenda/site.js
index a62cd102f..533bf6f12 100644
--- a/openslides/agenda/static/js/agenda/site.js
+++ b/openslides/agenda/static/js/agenda/site.js
@@ -99,6 +99,7 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
// Bind agenda tree to the scope
$scope.$watch(function () {
return Agenda.lastModified();
+
}, function () {
$scope.items = AgendaTree.getFlatTree(Agenda.getAll());
});
diff --git a/openslides/agenda/static/templates/agenda/item-list.html b/openslides/agenda/static/templates/agenda/item-list.html
index 555181c74..8c7b2aee8 100644
--- a/openslides/agenda/static/templates/agenda/item-list.html
+++ b/openslides/agenda/static/templates/agenda/item-list.html
@@ -103,7 +103,7 @@
–
- {{ item.item_number }} {{ item.title }}
+ {{ item.item_number }} {{ item.getTitle() }}
{{ item.comment }}
diff --git a/openslides/agenda/templates/search/agenda-results.html b/openslides/agenda/templates/search/agenda-results.html
deleted file mode 100644
index 808e38217..000000000
--- a/openslides/agenda/templates/search/agenda-results.html
+++ /dev/null
@@ -1,16 +0,0 @@
-{% load i18n %}
-{% load highlight %}
-
-{% if perms.agenda.can_see and result.object.type == result.object.AGENDA_ITEM %}
-
- {{ result.object }}
- {% trans "Agenda" %}
- {% highlight result.text with request.GET.q %}
-
-{% elif perms.agenda.can_see_orga_items and result.object.type == result.object.ORGANIZATIONAL_ITEM %}
-
- [{{ result.object }}]
- {% trans "Agenda" %} ({% trans "Organizational item" %})
- {% highlight result.text with request.GET.q %}
-
-{% endif %}
diff --git a/openslides/agenda/templates/search/indexes/agenda/item_text.txt b/openslides/agenda/templates/search/indexes/agenda/item_text.txt
deleted file mode 100644
index b7c4eae96..000000000
--- a/openslides/agenda/templates/search/indexes/agenda/item_text.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-{{ object.title }}
-{{ object.text }}
-{{ object.tags.all }}
diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py
index 562b8542d..7b3b7c000 100644
--- a/openslides/agenda/views.py
+++ b/openslides/agenda/views.py
@@ -8,8 +8,11 @@ from reportlab.platypus import Paragraph
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.pdf import stylesheet
from openslides.utils.rest_api import (
- ModelViewSet,
+ GenericViewSet,
+ ListModelMixin,
Response,
+ RetrieveModelMixin,
+ UpdateModelMixin,
ValidationError,
detail_route,
list_route,
@@ -22,7 +25,7 @@ from .serializers import ItemSerializer
# Viewsets for the REST API
-class ItemViewSet(ModelViewSet):
+class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
"""
API endpoint for agenda items.
@@ -40,9 +43,9 @@ class ItemViewSet(ModelViewSet):
result = self.request.user.has_perm('agenda.can_see')
# For manage_speaker and tree requests the rest of the check is
# done in the specific method. See below.
- elif self.action in ('create', 'partial_update', 'update', 'destroy'):
+ elif self.action in ('partial_update', 'update'):
result = (self.request.user.has_perm('agenda.can_see') and
- self.request.user.has_perm('agenda.can_see_orga_items') and
+ self.request.user.has_perm('agenda.can_see_hidden_items') and
self.request.user.has_perm('agenda.can_manage'))
elif self.action == 'speak':
result = (self.request.user.has_perm('agenda.can_see') and
@@ -56,7 +59,7 @@ class ItemViewSet(ModelViewSet):
Checks if the requesting user has permission to see also an
organizational item if it is one.
"""
- if obj.type == obj.ORGANIZATIONAL_ITEM and not request.user.has_perm('agenda.can_see_orga_items'):
+ if obj.is_hidden() and not request.user.has_perm('agenda.can_see_hidden_items'):
self.permission_denied(request)
def get_queryset(self):
@@ -64,9 +67,10 @@ class ItemViewSet(ModelViewSet):
Filters organizational items if the user has no permission to see them.
"""
queryset = super().get_queryset()
- if not self.request.user.has_perm('agenda.can_see_orga_items'):
- queryset = queryset.exclude(type__exact=Item.ORGANIZATIONAL_ITEM)
- return queryset
+ if self.request.user.has_perm('agenda.can_see_hidden_items'):
+ return queryset
+ else:
+ return Item.objects.get_only_agenda_items(queryset)
@detail_route(methods=['POST', 'DELETE'])
def manage_speaker(self, request, pk=None):
@@ -197,7 +201,7 @@ class ItemViewSet(ModelViewSet):
"""
if request.method == 'PUT':
if not (request.user.has_perm('agenda.can_manage') and
- request.user.has_perm('agenda.can_see_orga_items')):
+ request.user.has_perm('agenda.can_see_hidden_items')):
self.permission_denied(request)
try:
tree = request.data['tree']
@@ -242,7 +246,7 @@ class AgendaPDF(PDFView):
if ancestors:
space = " " * 6 * ancestors
story.append(Paragraph(
- "%s%s" % (space, escape(item.get_title())),
+ "%s%s" % (space, escape(item.title)),
stylesheet['Subitem']))
else:
- story.append(Paragraph(escape(item.get_title()), stylesheet['Item']))
+ story.append(Paragraph(escape(item.title), stylesheet['Item']))
diff --git a/openslides/assignments/models.py b/openslides/assignments/models.py
index 83d4ab9b3..2ebfe3db2 100644
--- a/openslides/assignments/models.py
+++ b/openslides/assignments/models.py
@@ -1,5 +1,5 @@
from django.conf import settings
-from django.contrib.contenttypes.fields import GenericRelation
+from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext as _
@@ -116,11 +116,6 @@ class Assignment(RESTModelMixin, models.Model):
Tags for the assignment.
"""
- items = GenericRelation(Item)
- """
- Agenda items for this assignment.
- """
-
class Meta:
permissions = (
('can_see', ugettext_noop('Can see elections')),
@@ -308,8 +303,20 @@ class Assignment(RESTModelMixin, models.Model):
def get_agenda_title(self):
return str(self)
- def get_agenda_title_supplement(self):
- return '(%s)' % _('Assignment')
+ @property
+ def agenda_item(self):
+ """
+ Returns the related agenda item.
+ """
+ content_type = ContentType.objects.get_for_model(self)
+ return Item.objects.get(object_id=self.pk, content_type=content_type)
+
+ @property
+ def agenda_item_id(self):
+ """
+ Returns the id of the agenda item object related to this object.
+ """
+ return self.agenda_item.pk
class AssignmentVote(RESTModelMixin, BaseVote):
diff --git a/openslides/assignments/serializers.py b/openslides/assignments/serializers.py
index 3effc730b..1ba376353 100644
--- a/openslides/assignments/serializers.py
+++ b/openslides/assignments/serializers.py
@@ -170,6 +170,7 @@ class AssignmentFullSerializer(ModelSerializer):
'assignment_related_users',
'poll_description_default',
'polls',
+ 'agenda_item_id',
'tags',)
@@ -191,4 +192,5 @@ class AssignmentShortSerializer(AssignmentFullSerializer):
'assignment_related_users',
'poll_description_default',
'polls',
+ 'agenda_item_id',
'tags',)
diff --git a/openslides/assignments/static/js/assignments/base.js b/openslides/assignments/static/js/assignments/base.js
index 8aac18039..e66d21320 100644
--- a/openslides/assignments/static/js/assignments/base.js
+++ b/openslides/assignments/static/js/assignments/base.js
@@ -4,18 +4,34 @@
angular.module('OpenSlidesApp.assignments', [])
-.factory('Assignment', ['DS', 'jsDataModel', function(DS, jsDataModel) {
- var name = 'assignments/assignment';
- return DS.defineResource({
- name: name,
- useClass: jsDataModel,
- methods: {
- getResourceName: function () {
- return name;
+.factory('Assignment', [
+ 'DS',
+ 'jsDataModel',
+ function(DS, jsDataModel) {
+ var name = 'assignments/assignment';
+ return DS.defineResource({
+ name: name,
+ useClass: jsDataModel,
+ agendaSupplement: '(Assignment)',
+ methods: {
+ getResourceName: function () {
+ return name;
+ },
+ getAgendaTitle: function () {
+ return this.title;
+ }
+ },
+ relations: {
+ belongsTo: {
+ 'agenda/item': {
+ localKey: 'agenda_item_id',
+ localField: 'agenda_item',
+ }
+ }
}
- }
- });
-}])
+ });
+ }
+])
.run(['Assignment', function(Assignment) {}]);
diff --git a/openslides/assignments/static/js/assignments/site.js b/openslides/assignments/static/js/assignments/site.js
index 57361a927..a7e35c065 100644
--- a/openslides/assignments/static/js/assignments/site.js
+++ b/openslides/assignments/static/js/assignments/site.js
@@ -79,6 +79,7 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
.controller('AssignmentDetailCtrl', function($scope, Assignment, assignment) {
Assignment.bindOne(assignment.id, $scope, 'assignment');
+ Assignment.loadRelations(assignment);
})
.controller('AssignmentCreateCtrl', function($scope, $state, Assignment) {
diff --git a/openslides/assignments/static/templates/assignments/assignment-detail.html b/openslides/assignments/static/templates/assignments/assignment-detail.html
index f0fc71128..35224dbe2 100644
--- a/openslides/assignments/static/templates/assignments/assignment-detail.html
+++ b/openslides/assignments/static/templates/assignments/assignment-detail.html
@@ -24,6 +24,8 @@
+{{ assignment.agenda_item }}
+
Description
{{ assignment.description }}
diff --git a/openslides/assignments/templates/search/assignment-results.html b/openslides/assignments/templates/search/assignment-results.html
deleted file mode 100644
index d118d1817..000000000
--- a/openslides/assignments/templates/search/assignment-results.html
+++ /dev/null
@@ -1,10 +0,0 @@
-{% load i18n %}
-{% load highlight %}
-
-{% if perms.assignments.can_see %}
-
- {{ result.object }}
- {% trans "Election" %}
- {% highlight result.text with request.GET.q %}
-
-{% endif %}
diff --git a/openslides/assignments/templates/search/indexes/assignments/assignment_text.txt b/openslides/assignments/templates/search/indexes/assignments/assignment_text.txt
deleted file mode 100644
index de6268129..000000000
--- a/openslides/assignments/templates/search/indexes/assignments/assignment_text.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-{{ object.title }}
-{{ object.description }}
-{{ object.candidates }}
-{{ object.tags.all }}
diff --git a/openslides/core/models.py b/openslides/core/models.py
index 28089de44..9f909cb7b 100644
--- a/openslides/core/models.py
+++ b/openslides/core/models.py
@@ -1,4 +1,5 @@
from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop
@@ -135,6 +136,26 @@ class CustomSlide(RESTModelMixin, models.Model):
def __str__(self):
return self.title
+ @property
+ def agenda_item(self):
+ """
+ Returns the related agenda item.
+ """
+ # TODO: Move the agenda app in the core app to fix circular dependencies
+ from openslides.agenda.models import Item
+ content_type = ContentType.objects.get_for_model(self)
+ return Item.objects.get(object_id=self.pk, content_type=content_type)
+
+ @property
+ def agenda_item_id(self):
+ """
+ Returns the id of the agenda item object related to this object.
+ """
+ return self.agenda_item.pk
+
+ def get_agenda_title(self):
+ return self.title
+
class Tag(RESTModelMixin, models.Model):
"""
diff --git a/openslides/core/serializers.py b/openslides/core/serializers.py
index 4dab834a7..2622fe948 100644
--- a/openslides/core/serializers.py
+++ b/openslides/core/serializers.py
@@ -39,7 +39,7 @@ class CustomSlideSerializer(ModelSerializer):
"""
class Meta:
model = CustomSlide
- fields = ('id', 'title', 'text', 'weight', )
+ fields = ('id', 'title', 'text', 'weight', 'agenda_item_id')
class TagSerializer(ModelSerializer):
diff --git a/openslides/core/static/js/core/base.js b/openslides/core/static/js/core/base.js
index 7a092c0e2..84d6771f4 100644
--- a/openslides/core/static/js/core/base.js
+++ b/openslides/core/static/js/core/base.js
@@ -140,18 +140,33 @@ angular.module('OpenSlidesApp.core', [
return BaseModel;
}])
-.factory('Customslide', ['DS', 'jsDataModel', function(DS, jsDataModel) {
- var name = 'core/customslide';
- return DS.defineResource({
- name: name,
- useClass: jsDataModel,
- methods: {
- getResourceName: function () {
- return name;
+.factory('Customslide', [
+ 'DS',
+ 'jsDataModel',
+ function(DS, jsDataModel) {
+ var name = 'core/customslide';
+ return DS.defineResource({
+ name: name,
+ useClass: jsDataModel,
+ methods: {
+ getResourceName: function () {
+ return name;
+ },
+ getAgendaTitle: function () {
+ return this.title;
+ }
},
- },
- });
-}])
+ relations: {
+ belongsTo: {
+ 'agenda/item': {
+ localKey: 'agenda_item_id',
+ localField: 'agenda_item',
+ }
+ }
+ }
+ });
+ }
+])
.factory('Tag', ['DS', function(DS) {
return DS.defineResource({
diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js
index 2d9e214f6..4aba97a13 100644
--- a/openslides/core/static/js/core/site.js
+++ b/openslides/core/static/js/core/site.js
@@ -646,6 +646,7 @@ angular.module('OpenSlidesApp.core.site', [
.controller('CustomslideDetailCtrl', function($scope, Customslide, customslide) {
Customslide.bindOne(customslide.id, $scope, 'customslide');
+ Customslide.loadRelations(customslide);
})
.controller('CustomslideCreateCtrl', function($scope, $state, Customslide) {
diff --git a/openslides/core/static/templates/core/customslide-detail.html b/openslides/core/static/templates/core/customslide-detail.html
index 2019a7f5e..4c8e2f58a 100644
--- a/openslides/core/static/templates/core/customslide-detail.html
+++ b/openslides/core/static/templates/core/customslide-detail.html
@@ -18,5 +18,7 @@
+{{ customslide.agenda_item }}
+
{{ customslide.text }}
diff --git a/openslides/global_settings.py b/openslides/global_settings.py
index 161b62f11..f7e609eb1 100644
--- a/openslides/global_settings.py
+++ b/openslides/global_settings.py
@@ -82,7 +82,6 @@ INSTALLED_APPS = (
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
- 'haystack', # full-text-search
'rest_framework',
'openslides.poll', # TODO: try to remove this line
'openslides.agenda',
diff --git a/openslides/motions/models.py b/openslides/motions/models.py
index 3948fc577..fdb36aebd 100644
--- a/openslides/motions/models.py
+++ b/openslides/motions/models.py
@@ -1,4 +1,5 @@
from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import Max
from django.utils import formats
@@ -6,6 +7,7 @@ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop
from jsonfield import JSONField
+from openslides.agenda.models import Item
from openslides.core.config import config
from openslides.core.models import Tag
from openslides.mediafiles.models import Mediafile
@@ -447,13 +449,23 @@ class Motion(RESTModelMixin, models.Model):
"""
Return a title for the agenda.
"""
+ # There has to be a function with the same return value in javascript.
return str(self)
- def get_agenda_title_supplement(self):
+ @property
+ def agenda_item(self):
"""
- Returns the supplement to the title for the agenda item.
+ Returns the related agenda item.
"""
- return '(%s)' % _('Motion')
+ content_type = ContentType.objects.get_for_model(self)
+ return Item.objects.get(object_id=self.pk, content_type=content_type)
+
+ @property
+ def agenda_item_id(self):
+ """
+ Returns the id of the agenda item object related to this object.
+ """
+ return self.agenda_item.pk
def get_allowed_actions(self, person):
"""
diff --git a/openslides/motions/pdf.py b/openslides/motions/pdf.py
index eb1797c1b..5d4f12e77 100644
--- a/openslides/motions/pdf.py
+++ b/openslides/motions/pdf.py
@@ -152,7 +152,7 @@ def motion_to_pdf(pdf, motion):
def convert_html_to_reportlab(pdf, text):
# parsing and replacing not supported html tags for reportlab...
- soup = BeautifulSoup(text)
+ soup = BeautifulSoup(text, "html5lib")
# read all list elements...
for element in soup.find_all('li'):
# ... and replace ul list elements with •...
diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py
index 084428e48..f86689280 100644
--- a/openslides/motions/serializers.py
+++ b/openslides/motions/serializers.py
@@ -204,6 +204,7 @@ class MotionSerializer(ModelSerializer):
'tags',
'attachments',
'polls',
+ 'agenda_item_id',
'log_messages',)
read_only_fields = ('parent',) # Some other fields are also read_only. See definitions above.
diff --git a/openslides/motions/static/js/motions/motions.js b/openslides/motions/static/js/motions/motions.js
index a9c612596..c08bc8cd3 100644
--- a/openslides/motions/static/js/motions/motions.js
+++ b/openslides/motions/static/js/motions/motions.js
@@ -2,63 +2,79 @@
angular.module('OpenSlidesApp.motions', [])
-.factory('Motion', ['DS', 'jsDataModel', function(DS, jsDataModel) {
- var name = 'motions/motion'
- return DS.defineResource({
- name: name,
- useClass: jsDataModel,
- methods: {
- getResourceName: function () {
- return name;
- },
- getVersion: function(versionId) {
- versionId = versionId || this.active_version;
- var index;
- if (versionId == -1) {
- index = this.versions.length - 1;
- } else {
- index = _.findIndex(this.versions, function(element) {
- return element.id == versionId
- });
- }
- return this.versions[index];
- },
- getTitle: function(versionId) {
- return this.getVersion(versionId).title;
- },
- getText: function(versionId) {
- return this.getVersion(versionId).text;
- },
- getReason: function(versionId) {
- return this.getVersion(versionId).reason;
- }
- },
- relations: {
- belongsTo: {
- 'motions/category': {
- localField: 'category',
- localKey: 'category_id',
+.factory('Motion', [
+ 'DS',
+ 'jsDataModel',
+ function(DS, jsDataModel) {
+ var name = 'motions/motion'
+ return DS.defineResource({
+ name: name,
+ useClass: jsDataModel,
+ agendaSupplement: '(Motion)',
+ methods: {
+ getResourceName: function () {
+ return name;
},
- },
- hasMany: {
- 'core/tag': {
- localField: 'tags',
- localKeys: 'tags_id',
- },
- 'users/user': [
- {
- localField: 'submitters',
- localKeys: 'submitters_id',
- },
- {
- localField: 'supporters',
- localKeys: 'supporters_id',
+ getVersion: function (versionId) {
+ versionId = versionId || this.active_version;
+ var index;
+ if (versionId == -1) {
+ index = this.versions.length - 1;
+ } else {
+ index = _.findIndex(this.versions, function (element) {
+ return element.id == versionId
+ });
}
- ],
+ return this.versions[index];
+ },
+ getTitle: function (versionId) {
+ return this.getVersion(versionId).title;
+ },
+ getText: function (versionId) {
+ return this.getVersion(versionId).text;
+ },
+ getReason: function (versionId) {
+ return this.getVersion(versionId).reason;
+ },
+ getAgendaTitle: function () {
+ var value = '';
+ if (this.identifier) {
+ value = this.identifier + ' | ';
+ }
+ return value + this.getTitle();
+ }
+ },
+ relations: {
+ belongsTo: {
+ 'motions/category': {
+ localField: 'category',
+ localKey: 'category_id',
+ },
+ 'agenda/item': {
+ localKey: 'agenda_item_id',
+ localField: 'agenda_item',
+ }
+ },
+ hasMany: {
+ 'core/tag': {
+ localField: 'tags',
+ localKeys: 'tags_id',
+ },
+ 'users/user': [
+ {
+ localField: 'submitters',
+ localKeys: 'submitters_id',
+ },
+ {
+ localField: 'supporters',
+ localKeys: 'supporters_id',
+ }
+ ],
+ }
}
- }
- });
-}])
+ });
+ }
+])
.factory('Category', ['DS', function(DS) {
return DS.defineResource({
@@ -240,6 +256,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
Motion.bindOne(motion.id, $scope, 'motion');
Category.bindAll({}, $scope, 'categories');
User.bindAll({}, $scope, 'users');
+ Motion.loadRelations(motion);
})
.controller('MotionCreateCtrl',
diff --git a/openslides/motions/static/templates/motions/motion-detail.html b/openslides/motions/static/templates/motions/motion-detail.html
index 7cbd0b459..c4a2c8183 100644
--- a/openslides/motions/static/templates/motions/motion-detail.html
+++ b/openslides/motions/static/templates/motions/motion-detail.html
@@ -7,6 +7,8 @@
{{ motion.tags }}
+{{ motion.agenda_item }}
+
|