diff --git a/openslides/motion/forms.py b/openslides/motion/forms.py index 80be63255..21f90cfc2 100644 --- a/openslides/motion/forms.py +++ b/openslides/motion/forms.py @@ -14,6 +14,7 @@ from django import forms from django.utils.translation import ugettext_lazy from openslides.config.api import config +from openslides.mediafile.models import Mediafile from openslides.utils.forms import (CleanHtmlFormMixin, CssClassMixin, LocalizedModelChoiceField) from openslides.utils.person import MultiplePersonFormField, PersonFormField @@ -48,13 +49,22 @@ class BaseMotionForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm): Reason of the motion. will be saved in a MotionVersion object. """ + attachments = forms.ModelMultipleChoiceField( + queryset=Mediafile.objects.all(), + required=False, + label=ugettext_lazy('Attachments')) + """ + Attachments of the motion. + """ + class Meta: model = Motion fields = () def __init__(self, *args, **kwargs): """ - Fill the FormFields releated to the version data with initial data. + Fill the FormFields related to the version data with initial data. + Fill also the initial data for attachments. """ self.motion = kwargs.get('instance', None) self.initial = kwargs.setdefault('initial', {}) @@ -63,6 +73,7 @@ class BaseMotionForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm): self.initial['title'] = last_version.title self.initial['text'] = last_version.text self.initial['reason'] = last_version.reason + self.initial['attachments'] = self.motion.attachments.all() else: self.initial['text'] = config['motion_preamble'] super(BaseMotionForm, self).__init__(*args, **kwargs) diff --git a/openslides/motion/models.py b/openslides/motion/models.py index c0d9a01b4..5c281d625 100644 --- a/openslides/motion/models.py +++ b/openslides/motion/models.py @@ -21,6 +21,7 @@ from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy, ugettext_noop from openslides.config.api import config +from openslides.mediafile.models import Mediafile from openslides.poll.models import (BaseOption, BasePoll, BaseVote, CountInvalid, CountVotesCast) from openslides.projector.models import RelatedModelMixin, SlideMixin @@ -78,6 +79,11 @@ class Motion(SlideMixin, models.Model): ForeignKey to one category of motions. """ + attachments = models.ManyToManyField(Mediafile) + """ + Many to many relation to mediafile objects. + """ + # TODO: proposal #master = models.ForeignKey('self', null=True, blank=True) diff --git a/openslides/motion/templates/motion/motion_detail.html b/openslides/motion/templates/motion/motion_detail.html index ad51aac62..680e8663c 100644 --- a/openslides/motion/templates/motion/motion_detail.html +++ b/openslides/motion/templates/motion/motion_detail.html @@ -89,6 +89,17 @@ {{ reason|safe|default:'–' }}
+ + {% with attachments=motion.attachments.all %} + {% if attachments %} +

{% trans "Attachments" %}:

+ {% for attachment in attachments %} +

{{ attachment }}

+ {% endfor %} +
+ {% endif %} + {% endwith %} + {% with versions=motion.versions.all %} {% if versions|length > 1 %} diff --git a/openslides/motion/views.py b/openslides/motion/views.py index d803fe53f..9af173707 100644 --- a/openslides/motion/views.py +++ b/openslides/motion/views.py @@ -126,6 +126,10 @@ class MotionEditMixin(object): [MotionSupporter(motion=self.object, person=person) for person in form.cleaned_data['supporter']]) + # Save the attachments + self.object.attachments.clear() + self.object.attachments.add(*form.cleaned_data['attachments']) + # Update the projector if the motion is on it. This can not be done in # the model, because bulk_create does not call the save method. active_slide = get_active_slide() diff --git a/tests/mediafile/tests.py b/tests/mediafile/tests.py index 344246ac1..2987c5484 100644 --- a/tests/mediafile/tests.py +++ b/tests/mediafile/tests.py @@ -43,9 +43,8 @@ class MediafileTest(TestCase): self.normal_user.reset_password('default') # Setup a mediafile object - self.tmp_dir = os.path.realpath(os.path.dirname(__file__)) - settings.MEDIA_ROOT = self.tmp_dir - tmpfile_no, mediafile_path = tempfile.mkstemp(prefix='tmp_openslides_test', dir=self.tmp_dir) + self.tmp_dir = settings.MEDIA_ROOT + tmpfile_no, mediafile_path = tempfile.mkstemp(prefix='tmp_openslides_test_', dir=self.tmp_dir) self.object = Mediafile.objects.create(title='Title File 1', mediafile=mediafile_path, uploader=self.vip_user) os.close(tmpfile_no) @@ -155,7 +154,7 @@ class MediafileTest(TestCase): def test_edit_mediafile_post_request(self): # Test only one user - tmpfile_no, mediafile_2_path = tempfile.mkstemp(prefix='tmp_openslides_test', dir=self.tmp_dir) + tmpfile_no, mediafile_2_path = tempfile.mkstemp(prefix='tmp_openslides_test_', dir=self.tmp_dir) os.close(tmpfile_no) object_2 = Mediafile.objects.create(title='Title File 2', mediafile=mediafile_2_path, uploader=self.vip_user) client_1 = self.login_clients()['client_manager'] @@ -185,7 +184,7 @@ class MediafileTest(TestCase): self.assertRedirects(response, expected_url='/login/?next=/mediafile/2/del/', status_code=302, target_status_code=200) def test_delete_mediafile_post_request(self): - tmpfile_no, mediafile_3_path = tempfile.mkstemp(prefix='tmp_openslides_test', dir=self.tmp_dir) + tmpfile_no, mediafile_3_path = tempfile.mkstemp(prefix='tmp_openslides_test_', dir=self.tmp_dir) os.close(tmpfile_no) object_3 = Mediafile.objects.create(title='Title File 3', mediafile=mediafile_3_path) client_1 = self.login_clients()['client_manager'] @@ -194,7 +193,7 @@ class MediafileTest(TestCase): self.assertFalse(os.path.exists(object_3.mediafile.path)) def test_filesize(self): - tmpfile_no, mediafile_4_path = tempfile.mkstemp(prefix='tmp_openslides_test', dir=self.tmp_dir) + tmpfile_no, mediafile_4_path = tempfile.mkstemp(prefix='tmp_openslides_test_', dir=self.tmp_dir) os.close(tmpfile_no) object_4 = Mediafile.objects.create(title='Title File 4', mediafile=mediafile_4_path) self.assertEqual(object_4.get_filesize(), '< 1 kB') diff --git a/tests/motion/test_views.py b/tests/motion/test_views.py index b75eff531..2093f46d2 100644 --- a/tests/motion/test_views.py +++ b/tests/motion/test_views.py @@ -8,9 +8,14 @@ :license: GNU GPL, see LICENSE for more details. """ +import os +import tempfile + +from django.conf import settings from django.test.client import Client from openslides.config.api import config +from openslides.mediafile.models import Mediafile from openslides.motion.models import Category, Motion, MotionLog, State from openslides.participant.models import Group, User from openslides.utils.test import TestCase @@ -64,6 +69,25 @@ class TestMotionDetailView(MotionViewTestCase): self.check_url('/motion/2/', self.admin_client, 200) self.check_url('/motion/500/', self.admin_client, 404) + def test_attachment(self): + # Preparation + tmpfile_no, attachment_path = tempfile.mkstemp(prefix='tmp_openslides_test_', dir=settings.MEDIA_ROOT) + os.close(tmpfile_no) + attachment = Mediafile.objects.create(title='TestFile_Neiri4xai4ueseGohzid', mediafile=attachment_path) + self.motion1.attachments.add(attachment) + + # Test appearance + response = self.registered_client.get('/motion/1/') + self.assertContains(response, '

Attachments:

') + self.assertContains(response, 'TestFile_Neiri4xai4ueseGohzid') + + # Test disappearance + attachment.mediafile.delete() + attachment.delete() + response = self.registered_client.get('/motion/1/') + self.assertNotContains(response, '

Attachments:

') + self.assertNotContains(response, 'TestFile_Neiri4xai4ueseGohzid') + class TestMotionDetailVersionView(MotionViewTestCase): def test_get(self): @@ -363,6 +387,15 @@ class TestMotionUpdateView(MotionViewTestCase): 'workflow': 2}) self.assertEqual(MotionLog.objects.get(pk=5).message_list, ['Motion version', ' 2 ', 'updated']) + def test_attachment_initial(self): + attachment = Mediafile.objects.create(title='test_title_iech1maatahShiecohca') + self.motion1.attachments.add(attachment) + response = self.admin_client.get(self.url) + self.assertContains(response, '' % attachment.title) + self.motion1.attachments.clear() + response = self.admin_client.get(self.url) + self.assertNotContains(response, '' % attachment.title) + class TestMotionDeleteView(MotionViewTestCase): def test_get(self): diff --git a/tests/settings.py b/tests/settings.py index fcd226c16..f5f4d5657 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import os + from openslides.global_settings import * # noqa # Use 'DEBUG = True' to get more details for server errors @@ -36,7 +38,7 @@ INSTALLED_APPS += INSTALLED_PLUGINS # Absolute path to the directory that holds media. # Example: "/home/media/media.lawrence.com/" -MEDIA_ROOT = '' +MEDIA_ROOT = os.path.realpath(os.path.dirname(__file__)) # Use RAM storage for whoosh index HAYSTACK_CONNECTIONS['default']['STORAGE'] = 'ram'