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:
parent
d3a6c05a68
commit
12a08b9732
@ -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"
|
||||||
|
@ -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)
|
||||||
|
46
openslides/agenda/migrations/0004_auto_20151027_1423.py
Normal file
46
openslides/agenda/migrations/0004_auto_20151027_1423.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
@ -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()
|
||||||
|
@ -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))
|
|
||||||
|
@ -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',)
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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());
|
||||||
});
|
});
|
||||||
|
@ -103,7 +103,7 @@
|
|||||||
<!-- agenda data columns -->
|
<!-- agenda data columns -->
|
||||||
<td>
|
<td>
|
||||||
<span ng-repeat="n in [].constructor(item.parentCount) track by $index">–</span>
|
<span ng-repeat="n in [].constructor(item.parentCount) track by $index">–</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>
|
||||||
|
@ -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 %}
|
|
@ -1,3 +0,0 @@
|
|||||||
{{ object.title }}
|
|
||||||
{{ object.text }}
|
|
||||||
{{ object.tags.all }}
|
|
@ -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 = " " * 6 * ancestors
|
space = " " * 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']))
|
||||||
|
@ -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):
|
||||||
|
@ -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',)
|
||||||
|
@ -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) {}]);
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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 %}
|
|
@ -1,4 +0,0 @@
|
|||||||
{{ object.title }}
|
|
||||||
{{ object.description }}
|
|
||||||
{{ object.candidates }}
|
|
||||||
{{ object.tags.all }}
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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):
|
||||||
|
@ -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({
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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>•</bullet>...<para>
|
# ... and replace ul list elements with <para><bullet>•</bullet>...<para>
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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>
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
{{ object.identifier }}
|
|
||||||
{{ object.title }}
|
|
||||||
{{ object.text }}
|
|
||||||
{{ object.reason }}
|
|
||||||
{{ object.submitters.all }}
|
|
||||||
{{ object.supporters.all }}
|
|
||||||
{{ object.category }}
|
|
||||||
{{ object.tags.all }}
|
|
@ -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 %}
|
|
@ -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'],
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
{{ object.django_user }}
|
|
||||||
{{ object.structure_level }}
|
|
||||||
{{ object.committee }}
|
|
||||||
{{ object.about_me }}
|
|
@ -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 %}
|
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
|
15
tests/integration/agenda/test_models.py
Normal file
15
tests/integration/agenda/test_models.py
Normal 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()
|
@ -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/')
|
||||||
|
@ -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')
|
||||||
|
@ -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()
|
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user