New Agenda Item

Changed agenda item, so it can not be manualy created, but is always created
when a custom slide, motion or assignment is created.
This commit is contained in:
Oskar Hahn 2015-10-24 19:02:43 +02:00
parent d3a6c05a68
commit 12a08b9732
44 changed files with 426 additions and 380 deletions

View File

@ -8,6 +8,7 @@ cache:
python: python:
- "3.3" - "3.3"
- "3.4" - "3.4"
- "3.5"
install: install:
- "pip install --upgrade --requirement requirements.txt" - "pip install --upgrade --requirement requirements.txt"
- "npm install" - "npm install"
@ -18,7 +19,7 @@ script:
- "isort --check-only --recursive openslides tests" - "isort --check-only --recursive openslides tests"
- "DJANGO_SETTINGS_MODULE='tests.settings' coverage run ./manage.py test tests.unit" - "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" - "DJANGO_SETTINGS_MODULE='tests.settings' coverage run ./manage.py test tests.integration"
- "coverage report --fail-under=50" - "coverage report --fail-under=50"

View File

@ -14,15 +14,23 @@ class AgendaAppConfig(AppConfig):
from . import projector # noqa from . import projector # noqa
# Import all required stuff. # 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.core.signals import config_signal
from openslides.utils.rest_api import router 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 from .views import ItemViewSet
# Connect signals. # Connect signals.
config_signal.connect(setup_agenda_config, dispatch_uid='setup_agenda_config') 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. # Register viewsets.
router.register('agenda/item', ItemViewSet) router.register('agenda/item', ItemViewSet)

View File

@ -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',
),
]

View File

@ -5,13 +5,11 @@ from django.conf import settings
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models, transaction from django.db import models, transaction
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.core.config import config from openslides.core.config import config
from openslides.core.models import Tag
from openslides.core.projector import Countdown from openslides.core.projector import Countdown
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.models import RESTModelMixin from openslides.utils.models import RESTModelMixin
@ -19,6 +17,45 @@ from openslides.utils.utils import to_roman
class ItemManager(models.Manager): 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): def get_tree(self, only_agenda_items=False, include_content=False):
""" """
Generator that yields dictonaries. Each dictonary has two keys, id 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' If include_content is True, the yielded dictonaries have no key 'id'
but a key 'item' with the entire object. but a key 'item' with the entire object.
""" """
item_queryset = self.order_by('weight') root_items, item_children = self.get_root_and_children(only_agenda_items=only_agenda_items)
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)
def get_children(items): def get_children(items):
""" """
@ -50,7 +79,7 @@ class ItemManager(models.Manager):
else: else:
yield dict(id=item.pk, children=get_children(item_children[item.pk])) 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 @transaction.atomic
def set_tree(self, tree): def set_tree(self, tree):
@ -101,27 +130,17 @@ class Item(RESTModelMixin, models.Model):
slide_callback_name = 'agenda' slide_callback_name = 'agenda'
AGENDA_ITEM = 1 AGENDA_ITEM = 1
ORGANIZATIONAL_ITEM = 2 HIDDEN_ITEM = 2
ITEM_TYPE = ( ITEM_TYPE = (
(AGENDA_ITEM, ugettext_lazy('Agenda item')), (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")) item_number = models.CharField(blank=True, max_length=255, verbose_name=ugettext_lazy("Number"))
""" """
Number of agenda item. 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")) 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. 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. 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. Type of the agenda item.
@ -175,32 +197,15 @@ class Item(RESTModelMixin, models.Model):
True, if the list of speakers is closed. True, if the list of speakers is closed.
""" """
tags = models.ManyToManyField(Tag, blank=True)
"""
Tags to categorise agenda items.
"""
class Meta: class Meta:
permissions = ( permissions = (
('can_see', ugettext_noop("Can see agenda")), ('can_see', ugettext_noop("Can see agenda")),
('can_manage', ugettext_noop("Can manage 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): def __str__(self):
return self.get_title() return self.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()
def delete(self, with_children=False): def delete(self, with_children=False):
""" """
@ -216,91 +221,27 @@ class Item(RESTModelMixin, models.Model):
child.save() child.save()
super().delete() 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: try:
return self.content_object.get_agenda_title_supplement() title = self.content_object.get_agenda_title()
except AttributeError: 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 Returns True if the type of this object itself is a hidden item or any
dictionary contains a prefix, the speaker and its type. Types of its ancestors has such a type.
are old_speaker, actual_speaker and coming_speaker.
Attention! This executes one query for each ancestor of the item.
""" """
list_of_speakers = [] return (self.type == self.HIDDEN_ITEM or
(self.parent is not None and self.parent.is_hidden()))
# 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
def get_next_speaker(self): def get_next_speaker(self):
""" """
@ -421,11 +362,12 @@ class Speaker(RESTModelMixin, models.Model):
speaking, end his speech. speaking, end his speech.
""" """
try: 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: except Speaker.DoesNotExist:
pass pass
else: else:
actual_speaker.end_speech() current_speaker.end_speech()
self.weight = None self.weight = None
self.begin_time = datetime.now() self.begin_time = datetime.now()
self.save() self.save()

View File

@ -1,7 +1,6 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from openslides.core.exceptions import ProjectorException from openslides.core.exceptions import ProjectorException
from openslides.core.views import TagViewSet
from openslides.utils.projector import ProjectorElement, ProjectorRequirement from openslides.utils.projector import ProjectorElement, ProjectorRequirement
from .models import Item from .models import Item
@ -80,8 +79,3 @@ class ItemDetailSlide(ProjectorElement):
view_class=speaker.user.get_view_class(), view_class=speaker.user.get_view_class(),
view_action='retrieve', view_action='retrieve',
pk=str(speaker.user_id)) pk=str(speaker.user_id))
for tag in item.tags.all():
yield ProjectorRequirement(
view_class=TagViewSet,
view_action='retrieve',
pk=str(tag.pk))

View File

@ -1,7 +1,6 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from openslides.utils.rest_api import ( from openslides.utils.rest_api import (
CharField,
ModelSerializer, ModelSerializer,
RelatedField, RelatedField,
get_collection_and_id_from_url, get_collection_and_id_from_url,
@ -45,10 +44,7 @@ class ItemSerializer(ModelSerializer):
""" """
Serializer for agenda.models.Item objects. 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) content_object = RelatedItemRelatedField(read_only=True)
item_no = CharField(read_only=True)
speakers = SpeakerSerializer(many=True, read_only=True) speakers = SpeakerSerializer(many=True, read_only=True)
class Meta: class Meta:
@ -56,11 +52,7 @@ class ItemSerializer(ModelSerializer):
fields = ( fields = (
'id', 'id',
'item_number', 'item_number',
'item_no',
'title', 'title',
'get_title',
'get_title_supplement',
'text',
'comment', 'comment',
'closed', 'closed',
'type', 'type',
@ -68,6 +60,5 @@ class ItemSerializer(ModelSerializer):
'speakers', 'speakers',
'speaker_list_closed', 'speaker_list_closed',
'content_object', 'content_object',
'tags',
'weight', 'weight',
'parent',) 'parent',)

View File

@ -72,15 +72,24 @@ def setup_agenda_config(sender, **kwargs):
group=ugettext_lazy('Agenda')) 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 Receiver function to create agenda items. It is connected to the signal
be deleted. It is connected to the signal django.db.models.signals.post_save during app loading.
django.db.models.signals.pre_delete 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'): if hasattr(instance, 'get_agenda_title'):
for item in Item.objects.filter(content_type=ContentType.objects.get_for_model(sender), object_id=instance.pk): content_type = ContentType.objects.get_for_model(instance)
item.title = '< Item for deleted (%s) >' % instance.get_agenda_title() try:
item.content_type = None Item.objects.get(object_id=instance.pk, content_type=content_type).delete()
item.object_id = None except Item.DoesNotExist:
item.save() # Item does not exist so we do not have to delete it.
pass

View File

@ -33,6 +33,27 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
methods: { methods: {
getResourceName: function () { getResourceName: function () {
return name; 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: { relations: {
@ -97,6 +118,7 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
}); });
return getChildren(parentItems); return getChildren(parentItems);
}, },
// Returns a list of all items as a flat tree the attribute parentCount // Returns a list of all items as a flat tree the attribute parentCount
getFlatTree: function(items) { getFlatTree: function(items) {
var tree = this.getTree(items); var tree = this.getTree(items);

View File

@ -99,6 +99,7 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
// Bind agenda tree to the scope // Bind agenda tree to the scope
$scope.$watch(function () { $scope.$watch(function () {
return Agenda.lastModified(); return Agenda.lastModified();
}, function () { }, function () {
$scope.items = AgendaTree.getFlatTree(Agenda.getAll()); $scope.items = AgendaTree.getFlatTree(Agenda.getAll());
}); });

View File

@ -103,7 +103,7 @@
<!-- agenda data columns --> <!-- agenda data columns -->
<td> <td>
<span ng-repeat="n in [].constructor(item.parentCount) track by $index">&ndash;</span> <span ng-repeat="n in [].constructor(item.parentCount) track by $index">&ndash;</span>
{{ item.item_number }} {{ item.title }} {{ item.item_number }} {{ item.getTitle() }}
<div ng-if="item.comment"> <div ng-if="item.comment">
<small><i class="fa fa-info-circle"></i> {{ item.comment }}</small> <small><i class="fa fa-info-circle"></i> {{ item.comment }}</small>
</div> </div>

View File

@ -1,16 +0,0 @@
{% load i18n %}
{% load highlight %}
{% if perms.agenda.can_see and result.object.type == result.object.AGENDA_ITEM %}
<li>
<a href="{{ result.object.get_absolute_url }}">{{ result.object }}</a><br>
<span class="app">{% trans "Agenda" %}</a></span><br>
{% highlight result.text with request.GET.q %}
</li>
{% elif perms.agenda.can_see_orga_items and result.object.type == result.object.ORGANIZATIONAL_ITEM %}
<li>
<a href="{{ result.object.get_absolute_url }}"><i>[{{ result.object }}]</i></a><br>
<span class="app">{% trans "Agenda" %} ({% trans "Organizational item" %})</a></span><br>
{% highlight result.text with request.GET.q %}
</li>
{% endif %}

View File

@ -1,3 +0,0 @@
{{ object.title }}
{{ object.text }}
{{ object.tags.all }}

View File

@ -8,8 +8,11 @@ from reportlab.platypus import Paragraph
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.pdf import stylesheet from openslides.utils.pdf import stylesheet
from openslides.utils.rest_api import ( from openslides.utils.rest_api import (
ModelViewSet, GenericViewSet,
ListModelMixin,
Response, Response,
RetrieveModelMixin,
UpdateModelMixin,
ValidationError, ValidationError,
detail_route, detail_route,
list_route, list_route,
@ -22,7 +25,7 @@ from .serializers import ItemSerializer
# Viewsets for the REST API # Viewsets for the REST API
class ItemViewSet(ModelViewSet): class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
""" """
API endpoint for agenda items. API endpoint for agenda items.
@ -40,9 +43,9 @@ class ItemViewSet(ModelViewSet):
result = self.request.user.has_perm('agenda.can_see') result = self.request.user.has_perm('agenda.can_see')
# For manage_speaker and tree requests the rest of the check is # For manage_speaker and tree requests the rest of the check is
# done in the specific method. See below. # 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 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')) self.request.user.has_perm('agenda.can_manage'))
elif self.action == 'speak': elif self.action == 'speak':
result = (self.request.user.has_perm('agenda.can_see') and 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 Checks if the requesting user has permission to see also an
organizational item if it is one. 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) self.permission_denied(request)
def get_queryset(self): def get_queryset(self):
@ -64,9 +67,10 @@ class ItemViewSet(ModelViewSet):
Filters organizational items if the user has no permission to see them. Filters organizational items if the user has no permission to see them.
""" """
queryset = super().get_queryset() queryset = super().get_queryset()
if not self.request.user.has_perm('agenda.can_see_orga_items'): if self.request.user.has_perm('agenda.can_see_hidden_items'):
queryset = queryset.exclude(type__exact=Item.ORGANIZATIONAL_ITEM) return queryset
return queryset else:
return Item.objects.get_only_agenda_items(queryset)
@detail_route(methods=['POST', 'DELETE']) @detail_route(methods=['POST', 'DELETE'])
def manage_speaker(self, request, pk=None): def manage_speaker(self, request, pk=None):
@ -197,7 +201,7 @@ class ItemViewSet(ModelViewSet):
""" """
if request.method == 'PUT': if request.method == 'PUT':
if not (request.user.has_perm('agenda.can_manage') and 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) self.permission_denied(request)
try: try:
tree = request.data['tree'] tree = request.data['tree']
@ -242,7 +246,7 @@ class AgendaPDF(PDFView):
if ancestors: if ancestors:
space = "&nbsp;" * 6 * ancestors space = "&nbsp;" * 6 * ancestors
story.append(Paragraph( story.append(Paragraph(
"%s%s" % (space, escape(item.get_title())), "%s%s" % (space, escape(item.title)),
stylesheet['Subitem'])) stylesheet['Subitem']))
else: else:
story.append(Paragraph(escape(item.get_title()), stylesheet['Item'])) story.append(Paragraph(escape(item.title), stylesheet['Item']))

View File

@ -1,5 +1,5 @@
from django.conf import settings 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.db import models
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -116,11 +116,6 @@ class Assignment(RESTModelMixin, models.Model):
Tags for the assignment. Tags for the assignment.
""" """
items = GenericRelation(Item)
"""
Agenda items for this assignment.
"""
class Meta: class Meta:
permissions = ( permissions = (
('can_see', ugettext_noop('Can see elections')), ('can_see', ugettext_noop('Can see elections')),
@ -308,8 +303,20 @@ class Assignment(RESTModelMixin, models.Model):
def get_agenda_title(self): def get_agenda_title(self):
return str(self) return str(self)
def get_agenda_title_supplement(self): @property
return '(%s)' % _('Assignment') 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): class AssignmentVote(RESTModelMixin, BaseVote):

View File

@ -170,6 +170,7 @@ class AssignmentFullSerializer(ModelSerializer):
'assignment_related_users', 'assignment_related_users',
'poll_description_default', 'poll_description_default',
'polls', 'polls',
'agenda_item_id',
'tags',) 'tags',)
@ -191,4 +192,5 @@ class AssignmentShortSerializer(AssignmentFullSerializer):
'assignment_related_users', 'assignment_related_users',
'poll_description_default', 'poll_description_default',
'polls', 'polls',
'agenda_item_id',
'tags',) 'tags',)

View File

@ -4,18 +4,34 @@
angular.module('OpenSlidesApp.assignments', []) angular.module('OpenSlidesApp.assignments', [])
.factory('Assignment', ['DS', 'jsDataModel', function(DS, jsDataModel) { .factory('Assignment', [
var name = 'assignments/assignment'; 'DS',
return DS.defineResource({ 'jsDataModel',
name: name, function(DS, jsDataModel) {
useClass: jsDataModel, var name = 'assignments/assignment';
methods: { return DS.defineResource({
getResourceName: function () { name: name,
return 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) {}]); .run(['Assignment', function(Assignment) {}]);

View File

@ -79,6 +79,7 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
.controller('AssignmentDetailCtrl', function($scope, Assignment, assignment) { .controller('AssignmentDetailCtrl', function($scope, Assignment, assignment) {
Assignment.bindOne(assignment.id, $scope, 'assignment'); Assignment.bindOne(assignment.id, $scope, 'assignment');
Assignment.loadRelations(assignment);
}) })
.controller('AssignmentCreateCtrl', function($scope, $state, Assignment) { .controller('AssignmentCreateCtrl', function($scope, $state, Assignment) {

View File

@ -24,6 +24,8 @@
</a> </a>
</div> </div>
{{ assignment.agenda_item }}
<h3 translate>Description</h3> <h3 translate>Description</h3>
<div class="white-space-pre-line">{{ assignment.description }}</div> <div class="white-space-pre-line">{{ assignment.description }}</div>

View File

@ -1,10 +0,0 @@
{% load i18n %}
{% load highlight %}
{% if perms.assignments.can_see %}
<li>
<a href="{{ result.object.get_absolute_url }}">{{ result.object }}</a><br>
<span class="app">{% trans "Election" %}</a></span><br>
{% highlight result.text with request.GET.q %}
</li>
{% endif %}

View File

@ -1,4 +0,0 @@
{{ object.title }}
{{ object.description }}
{{ object.candidates }}
{{ object.tags.all }}

View File

@ -1,4 +1,5 @@
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop from django.utils.translation import ugettext_lazy, ugettext_noop
@ -135,6 +136,26 @@ class CustomSlide(RESTModelMixin, models.Model):
def __str__(self): def __str__(self):
return self.title 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): class Tag(RESTModelMixin, models.Model):
""" """

View File

@ -39,7 +39,7 @@ class CustomSlideSerializer(ModelSerializer):
""" """
class Meta: class Meta:
model = CustomSlide model = CustomSlide
fields = ('id', 'title', 'text', 'weight', ) fields = ('id', 'title', 'text', 'weight', 'agenda_item_id')
class TagSerializer(ModelSerializer): class TagSerializer(ModelSerializer):

View File

@ -140,18 +140,33 @@ angular.module('OpenSlidesApp.core', [
return BaseModel; return BaseModel;
}]) }])
.factory('Customslide', ['DS', 'jsDataModel', function(DS, jsDataModel) { .factory('Customslide', [
var name = 'core/customslide'; 'DS',
return DS.defineResource({ 'jsDataModel',
name: name, function(DS, jsDataModel) {
useClass: jsDataModel, var name = 'core/customslide';
methods: { return DS.defineResource({
getResourceName: function () { name: name,
return 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) { .factory('Tag', ['DS', function(DS) {
return DS.defineResource({ return DS.defineResource({

View File

@ -646,6 +646,7 @@ angular.module('OpenSlidesApp.core.site', [
.controller('CustomslideDetailCtrl', function($scope, Customslide, customslide) { .controller('CustomslideDetailCtrl', function($scope, Customslide, customslide) {
Customslide.bindOne(customslide.id, $scope, 'customslide'); Customslide.bindOne(customslide.id, $scope, 'customslide');
Customslide.loadRelations(customslide);
}) })
.controller('CustomslideCreateCtrl', function($scope, $state, Customslide) { .controller('CustomslideCreateCtrl', function($scope, $state, Customslide) {

View File

@ -18,5 +18,7 @@
</a> </a>
</div> </div>
{{ customslide.agenda_item }}
<div class="white-space-pre-line">{{ customslide.text }}</div> <div class="white-space-pre-line">{{ customslide.text }}</div>

View File

@ -82,7 +82,6 @@ INSTALLED_APPS = (
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.humanize', 'django.contrib.humanize',
'haystack', # full-text-search
'rest_framework', 'rest_framework',
'openslides.poll', # TODO: try to remove this line 'openslides.poll', # TODO: try to remove this line
'openslides.agenda', 'openslides.agenda',

View File

@ -1,4 +1,5 @@
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.db.models import Max from django.db.models import Max
from django.utils import formats 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 django.utils.translation import ugettext_lazy, ugettext_noop
from jsonfield import JSONField from jsonfield import JSONField
from openslides.agenda.models import Item
from openslides.core.config import config from openslides.core.config import config
from openslides.core.models import Tag from openslides.core.models import Tag
from openslides.mediafiles.models import Mediafile from openslides.mediafiles.models import Mediafile
@ -447,13 +449,23 @@ class Motion(RESTModelMixin, models.Model):
""" """
Return a title for the agenda. Return a title for the agenda.
""" """
# There has to be a function with the same return value in javascript.
return str(self) 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): def get_allowed_actions(self, person):
""" """

View File

@ -152,7 +152,7 @@ def motion_to_pdf(pdf, motion):
def convert_html_to_reportlab(pdf, text): def convert_html_to_reportlab(pdf, text):
# parsing and replacing not supported html tags for reportlab... # parsing and replacing not supported html tags for reportlab...
soup = BeautifulSoup(text) soup = BeautifulSoup(text, "html5lib")
# read all list elements... # read all list elements...
for element in soup.find_all('li'): for element in soup.find_all('li'):
# ... and replace ul list elements with <para><bullet>&bull;</bullet>...<para> # ... and replace ul list elements with <para><bullet>&bull;</bullet>...<para>

View File

@ -204,6 +204,7 @@ class MotionSerializer(ModelSerializer):
'tags', 'tags',
'attachments', 'attachments',
'polls', 'polls',
'agenda_item_id',
'log_messages',) 'log_messages',)
read_only_fields = ('parent',) # Some other fields are also read_only. See definitions above. read_only_fields = ('parent',) # Some other fields are also read_only. See definitions above.

View File

@ -2,63 +2,79 @@
angular.module('OpenSlidesApp.motions', []) angular.module('OpenSlidesApp.motions', [])
.factory('Motion', ['DS', 'jsDataModel', function(DS, jsDataModel) { .factory('Motion', [
var name = 'motions/motion' 'DS',
return DS.defineResource({ 'jsDataModel',
name: name, function(DS, jsDataModel) {
useClass: jsDataModel, var name = 'motions/motion'
methods: { return DS.defineResource({
getResourceName: function () { name: name,
return name; useClass: jsDataModel,
}, agendaSupplement: '(Motion)',
getVersion: function(versionId) { methods: {
versionId = versionId || this.active_version; getResourceName: function () {
var index; return name;
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',
}, },
}, getVersion: function (versionId) {
hasMany: { versionId = versionId || this.active_version;
'core/tag': { var index;
localField: 'tags', if (versionId == -1) {
localKeys: 'tags_id', index = this.versions.length - 1;
}, } else {
'users/user': [ index = _.findIndex(this.versions, function (element) {
{ return element.id == versionId
localField: 'submitters', });
localKeys: 'submitters_id',
},
{
localField: 'supporters',
localKeys: 'supporters_id',
} }
], 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) { .factory('Category', ['DS', function(DS) {
return DS.defineResource({ return DS.defineResource({
@ -240,6 +256,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
Motion.bindOne(motion.id, $scope, 'motion'); Motion.bindOne(motion.id, $scope, 'motion');
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
Motion.loadRelations(motion);
}) })
.controller('MotionCreateCtrl', .controller('MotionCreateCtrl',

View File

@ -7,6 +7,8 @@
</h1> </h1>
{{ motion.tags }} {{ motion.tags }}
{{ motion.agenda_item }}
<div id="submenu"> <div id="submenu">
<a ui-sref="motions.motion.list" class="btn btn-sm btn-default"> <a ui-sref="motions.motion.list" class="btn btn-sm btn-default">
<i class="fa fa-angle-double-left fa-lg"></i> <i class="fa fa-angle-double-left fa-lg"></i>

View File

@ -1,8 +0,0 @@
{{ object.identifier }}
{{ object.title }}
{{ object.text }}
{{ object.reason }}
{{ object.submitters.all }}
{{ object.supporters.all }}
{{ object.category }}
{{ object.tags.all }}

View File

@ -1,10 +0,0 @@
{% load i18n %}
{% load highlight %}
{% if perms.motions.can_see %}
<li>
<a href="{{ result.object.get_absolute_url }}">{{ result.object }}</a><br>
<span class="app">{% trans "Motion" %}</a></span><br>
{% highlight result.text with request.GET.q %}
</li>
{% endif %}

View File

@ -105,7 +105,7 @@ def create_builtin_groups_and_admin(**kwargs):
'agenda.can_be_speaker', 'agenda.can_be_speaker',
'agenda.can_manage', 'agenda.can_manage',
'agenda.can_see', 'agenda.can_see',
'agenda.can_see_orga_items', 'agenda.can_see_hidden_items',
'assignments.can_manage', 'assignments.can_manage',
'assignments.can_nominate_other', 'assignments.can_nominate_other',
'assignments.can_nominate_self', 'assignments.can_nominate_self',
@ -141,7 +141,7 @@ def create_builtin_groups_and_admin(**kwargs):
# Anonymous (pk 1) and Registered (pk 2) # Anonymous (pk 1) and Registered (pk 2)
base_permissions = ( base_permissions = (
permission_dict['agenda.can_see'], permission_dict['agenda.can_see'],
permission_dict['agenda.can_see_orga_items'], permission_dict['agenda.can_see_hidden_items'],
permission_dict['assignments.can_see'], permission_dict['assignments.can_see'],
permission_dict['core.can_see_dashboard'], permission_dict['core.can_see_dashboard'],
permission_dict['core.can_see_projector'], permission_dict['core.can_see_projector'],

View File

@ -1,4 +0,0 @@
{{ object.django_user }}
{{ object.structure_level }}
{{ object.committee }}
{{ object.about_me }}

View File

@ -1,10 +0,0 @@
{% load i18n %}
{% load highlight %}
{% if perms.users.can_see %}
<li>
<a href="{{ result.object.get_absolute_url }}">{{ result.object }}</a><br>
<span class="app">{% trans "User" %}</a></span><br>
{% highlight result.text with request.GET.q %}
</li>
{% endif %}

View File

@ -5,7 +5,12 @@ from urllib.parse import urlparse
from rest_framework import status # noqa from rest_framework import status # noqa
from rest_framework.decorators import detail_route, list_route # noqa from rest_framework.decorators import detail_route, list_route # noqa
from rest_framework.metadata import SimpleMetadata # noqa from rest_framework.metadata import SimpleMetadata # noqa
from rest_framework.mixins import DestroyModelMixin, UpdateModelMixin # noqa from rest_framework.mixins import ( # noqa
DestroyModelMixin,
ListModelMixin,
RetrieveModelMixin,
UpdateModelMixin,
)
from rest_framework.response import Response # noqa from rest_framework.response import Response # noqa
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from rest_framework.serializers import ModelSerializer as _ModelSerializer from rest_framework.serializers import ModelSerializer as _ModelSerializer

View File

@ -1,4 +1,3 @@
from django.core.management import call_command
from django.test import TestCase as _TestCase from django.test import TestCase as _TestCase
from django.test.runner import DiscoverRunner from django.test.runner import DiscoverRunner
@ -40,6 +39,4 @@ class TestCase(_TestCase):
except AttributeError: except AttributeError:
# The cache has only to be deleted if it exists. # The cache has only to be deleted if it exists.
pass pass
# Clear the whoosh search index
call_command('clear_index', interactive=False, verbosity=0)
return return_value return return_value

View File

@ -3,11 +3,11 @@ Django>=1.7.1,<1.9
beautifulsoup4>=4.1,<4.5 beautifulsoup4>=4.1,<4.5
django-haystack>=2.1,<2.5 django-haystack>=2.1,<2.5
djangorestframework>=3.2.0,<3.3.0 djangorestframework>=3.2.0,<3.3.0
html5lib>=0.9,<1.0
jsonfield>=0.9.19,<1.1 jsonfield>=0.9.19,<1.1
natsort>=3.2,<4.1 natsort>=3.2,<4.1
reportlab>=3.0,<3.3 reportlab>=3.0,<3.3
roman>=2.0,<2.1 roman>=2.0,<2.1
setuptools>=2.2,<19.0 setuptools>=2.2,<19.0
sockjs-tornado>=1.0,<1.1 sockjs-tornado>=1.0,<1.1
whoosh>=2.5.6,<2.8

View File

@ -0,0 +1,15 @@
from openslides.agenda.models import Item
from openslides.core.models import CustomSlide
from openslides.utils.test import TestCase
class TestItemManager(TestCase):
def test_get_root_and_children_db_queries(self):
"""
Test that get_root_and_children needs only one db query.
"""
for i in range(10):
CustomSlide.objects.create(title='item{}'.format(i))
with self.assertNumQueries(1):
Item.objects.get_root_and_children()

View File

@ -3,14 +3,17 @@ import json
from rest_framework.test import APIClient from rest_framework.test import APIClient
from openslides.agenda.models import Item from openslides.agenda.models import Item
from openslides.core.models import CustomSlide
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
class AgendaTreeTest(TestCase): class AgendaTreeTest(TestCase):
def setUp(self): def setUp(self):
Item.objects.create(title='item1') CustomSlide.objects.create(title='item1')
item2 = Item.objects.create(title='item2') item2 = CustomSlide.objects.create(title='item2').agenda_item
Item.objects.create(title='item2a', parent=item2) item3 = CustomSlide.objects.create(title='item2a').agenda_item
item3.parent = item2
item3.save()
self.client = APIClient() self.client = APIClient()
self.client.login(username='admin', password='admin') self.client.login(username='admin', password='admin')
@ -87,7 +90,7 @@ class TestAgendaPDF(TestCase):
""" """
Tests that a requst on the pdf-page returns with statuscode 200. Tests that a requst on the pdf-page returns with statuscode 200.
""" """
Item.objects.create(title='item1') CustomSlide.objects.create(title='item1')
self.client.login(username='admin', password='admin') self.client.login(username='admin', password='admin')
response = self.client.get('/agenda/print/') response = self.client.get('/agenda/print/')

View File

@ -2,9 +2,9 @@ from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from rest_framework.test import APIClient from rest_framework.test import APIClient
from openslides.agenda.models import Item, Speaker from openslides.agenda.models import Speaker
from openslides.core.config import config from openslides.core.config import config
from openslides.core.models import Projector from openslides.core.models import CustomSlide, Projector
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
@ -15,7 +15,8 @@ class ManageSpeaker(TestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
self.client.login(username='admin', password='admin') self.client.login(username='admin', password='admin')
self.item = Item.objects.create(title='test_title_aZaedij4gohn5eeQu8fe')
self.item = CustomSlide.objects.create(title='test_title_aZaedij4gohn5eeQu8fe').agenda_item
self.user = get_user_model().objects.create_user( self.user = get_user_model().objects.create_user(
username='test_user_jooSaex1bo5ooPhuphae', username='test_user_jooSaex1bo5ooPhuphae',
password='test_password_e6paev4zeeh9n') password='test_password_e6paev4zeeh9n')
@ -132,7 +133,7 @@ class Speak(TestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
self.client.login(username='admin', password='admin') self.client.login(username='admin', password='admin')
self.item = Item.objects.create(title='test_title_KooDueco3zaiGhiraiho') self.item = CustomSlide.objects.create(title='test_title_KooDueco3zaiGhiraiho').agenda_item
self.user = get_user_model().objects.create_user( self.user = get_user_model().objects.create_user(
username='test_user_Aigh4vohb3seecha4aa4', username='test_user_Aigh4vohb3seecha4aa4',
password='test_password_eneupeeVo5deilixoo8j') password='test_password_eneupeeVo5deilixoo8j')

View File

@ -4,57 +4,31 @@ from unittest.mock import patch
from openslides.agenda.models import Item from openslides.agenda.models import Item
class ItemTitle(TestCase): class TestItemTitle(TestCase):
def test_get_title_without_item_no(self):
item = Item(title='test_title')
self.assertEqual(
item.get_title(),
'test_title')
@patch('openslides.agenda.models.Item.item_no', '5')
def test_get_title_with_item_no(self):
item = Item(title='test_title')
self.assertEqual(
item.get_title(),
'5 test_title')
@patch('openslides.agenda.models.Item.content_object') @patch('openslides.agenda.models.Item.content_object')
def test_get_title_from_related(self, content_object): def test_title_from_content_object(self, content_object):
item = Item(title='test_title') item = Item()
content_object.get_agenda_title.return_value = 'related_title' content_object.get_agenda_title.return_value = 'related_title'
self.assertEqual( self.assertEqual(
item.get_title(), item.title,
'related_title') 'related_title')
@patch('openslides.agenda.models.Item.item_no', '5')
@patch('openslides.agenda.models.Item.content_object') @patch('openslides.agenda.models.Item.content_object')
def test_get_title_invalid_related(self, content_object): def test_title_with_item_no(self, content_object):
item = Item(title='test_title') item = Item()
content_object.get_agenda_title.return_value = 'related_title'
self.assertEqual(
item.title,
'5 related_title')
@patch('openslides.agenda.models.Item.content_object')
def test_title_invalid_related(self, content_object):
item = Item()
content_object.get_agenda_title.return_value = 'related_title' content_object.get_agenda_title.return_value = 'related_title'
del content_object.get_agenda_title del content_object.get_agenda_title
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
item.get_title() item.title
def test_title_supplement_without_related(self):
item = Item()
self.assertEqual(
item.get_title_supplement(),
'')
@patch('openslides.agenda.models.Item.content_object')
def test_title_supplement_with_related(self, content_object):
item = Item()
content_object.get_agenda_title_supplement.return_value = 'related_title_supplement'
self.assertEqual(
item.get_title_supplement(),
'related_title_supplement')
@patch('openslides.agenda.models.Item.content_object')
def test_title_supplement_invalid_related(self, content_object):
item = Item()
del content_object.get_agenda_title_supplement
with self.assertRaises(NotImplementedError):
item.get_title_supplement()

View File

@ -308,8 +308,8 @@ class UserManagerCreateOrResetAdminUser(TestCase):
manager.create_or_reset_admin_user() manager.create_or_reset_admin_user()
mock_group.objects.get.assert_called_once(pk=2) mock_group.objects.get.assert_called_once_with(pk=4)
admin_user.groups.add.assert_called_once('mock_staff') admin_user.groups.add.assert_called_once_with('mock_staff')
def test_password_set_to_admin(self, mock_group): def test_password_set_to_admin(self, mock_group):
""" """