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 }} +