Merge pull request #908 from normanjaeckel/MotionAttachment
Insert new feature: attachments for motions.
This commit is contained in:
commit
563f40a889
@ -14,6 +14,7 @@ from django import forms
|
|||||||
from django.utils.translation import ugettext_lazy
|
from django.utils.translation import ugettext_lazy
|
||||||
|
|
||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
|
from openslides.mediafile.models import Mediafile
|
||||||
from openslides.utils.forms import (CleanHtmlFormMixin, CssClassMixin,
|
from openslides.utils.forms import (CleanHtmlFormMixin, CssClassMixin,
|
||||||
LocalizedModelChoiceField)
|
LocalizedModelChoiceField)
|
||||||
from openslides.utils.person import MultiplePersonFormField, PersonFormField
|
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.
|
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:
|
class Meta:
|
||||||
model = Motion
|
model = Motion
|
||||||
fields = ()
|
fields = ()
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
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.motion = kwargs.get('instance', None)
|
||||||
self.initial = kwargs.setdefault('initial', {})
|
self.initial = kwargs.setdefault('initial', {})
|
||||||
@ -63,6 +73,7 @@ class BaseMotionForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
|
|||||||
self.initial['title'] = last_version.title
|
self.initial['title'] = last_version.title
|
||||||
self.initial['text'] = last_version.text
|
self.initial['text'] = last_version.text
|
||||||
self.initial['reason'] = last_version.reason
|
self.initial['reason'] = last_version.reason
|
||||||
|
self.initial['attachments'] = self.motion.attachments.all()
|
||||||
else:
|
else:
|
||||||
self.initial['text'] = config['motion_preamble']
|
self.initial['text'] = config['motion_preamble']
|
||||||
super(BaseMotionForm, self).__init__(*args, **kwargs)
|
super(BaseMotionForm, self).__init__(*args, **kwargs)
|
||||||
|
@ -21,6 +21,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 openslides.config.api import config
|
from openslides.config.api import config
|
||||||
|
from openslides.mediafile.models import Mediafile
|
||||||
from openslides.poll.models import (BaseOption, BasePoll, BaseVote,
|
from openslides.poll.models import (BaseOption, BasePoll, BaseVote,
|
||||||
CountInvalid, CountVotesCast)
|
CountInvalid, CountVotesCast)
|
||||||
from openslides.projector.models import RelatedModelMixin, SlideMixin
|
from openslides.projector.models import RelatedModelMixin, SlideMixin
|
||||||
@ -78,6 +79,11 @@ class Motion(SlideMixin, models.Model):
|
|||||||
ForeignKey to one category of motions.
|
ForeignKey to one category of motions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
attachments = models.ManyToManyField(Mediafile)
|
||||||
|
"""
|
||||||
|
Many to many relation to mediafile objects.
|
||||||
|
"""
|
||||||
|
|
||||||
# TODO: proposal
|
# TODO: proposal
|
||||||
#master = models.ForeignKey('self', null=True, blank=True)
|
#master = models.ForeignKey('self', null=True, blank=True)
|
||||||
|
|
||||||
|
@ -89,6 +89,17 @@
|
|||||||
{{ reason|safe|default:'–' }}
|
{{ reason|safe|default:'–' }}
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
<!-- Attachments -->
|
||||||
|
{% with attachments=motion.attachments.all %}
|
||||||
|
{% if attachments %}
|
||||||
|
<h4>{% trans "Attachments" %}:</h4>
|
||||||
|
{% for attachment in attachments %}
|
||||||
|
<p><a href="{{ attachment.mediafile.url }}">{{ attachment }}</a></p>
|
||||||
|
{% endfor %}
|
||||||
|
<br>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
<!-- Version history -->
|
<!-- Version history -->
|
||||||
{% with versions=motion.versions.all %}
|
{% with versions=motion.versions.all %}
|
||||||
{% if versions|length > 1 %}
|
{% if versions|length > 1 %}
|
||||||
|
@ -126,6 +126,10 @@ class MotionEditMixin(object):
|
|||||||
[MotionSupporter(motion=self.object, person=person)
|
[MotionSupporter(motion=self.object, person=person)
|
||||||
for person in form.cleaned_data['supporter']])
|
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
|
# 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.
|
# the model, because bulk_create does not call the save method.
|
||||||
active_slide = get_active_slide()
|
active_slide = get_active_slide()
|
||||||
|
@ -43,9 +43,8 @@ class MediafileTest(TestCase):
|
|||||||
self.normal_user.reset_password('default')
|
self.normal_user.reset_password('default')
|
||||||
|
|
||||||
# Setup a mediafile object
|
# Setup a mediafile object
|
||||||
self.tmp_dir = os.path.realpath(os.path.dirname(__file__))
|
self.tmp_dir = settings.MEDIA_ROOT
|
||||||
settings.MEDIA_ROOT = self.tmp_dir
|
tmpfile_no, mediafile_path = tempfile.mkstemp(prefix='tmp_openslides_test_', dir=self.tmp_dir)
|
||||||
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)
|
self.object = Mediafile.objects.create(title='Title File 1', mediafile=mediafile_path, uploader=self.vip_user)
|
||||||
os.close(tmpfile_no)
|
os.close(tmpfile_no)
|
||||||
|
|
||||||
@ -155,7 +154,7 @@ class MediafileTest(TestCase):
|
|||||||
|
|
||||||
def test_edit_mediafile_post_request(self):
|
def test_edit_mediafile_post_request(self):
|
||||||
# Test only one user
|
# 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)
|
os.close(tmpfile_no)
|
||||||
object_2 = Mediafile.objects.create(title='Title File 2', mediafile=mediafile_2_path, uploader=self.vip_user)
|
object_2 = Mediafile.objects.create(title='Title File 2', mediafile=mediafile_2_path, uploader=self.vip_user)
|
||||||
client_1 = self.login_clients()['client_manager']
|
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)
|
self.assertRedirects(response, expected_url='/login/?next=/mediafile/2/del/', status_code=302, target_status_code=200)
|
||||||
|
|
||||||
def test_delete_mediafile_post_request(self):
|
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)
|
os.close(tmpfile_no)
|
||||||
object_3 = Mediafile.objects.create(title='Title File 3', mediafile=mediafile_3_path)
|
object_3 = Mediafile.objects.create(title='Title File 3', mediafile=mediafile_3_path)
|
||||||
client_1 = self.login_clients()['client_manager']
|
client_1 = self.login_clients()['client_manager']
|
||||||
@ -194,7 +193,7 @@ class MediafileTest(TestCase):
|
|||||||
self.assertFalse(os.path.exists(object_3.mediafile.path))
|
self.assertFalse(os.path.exists(object_3.mediafile.path))
|
||||||
|
|
||||||
def test_filesize(self):
|
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)
|
os.close(tmpfile_no)
|
||||||
object_4 = Mediafile.objects.create(title='Title File 4', mediafile=mediafile_4_path)
|
object_4 = Mediafile.objects.create(title='Title File 4', mediafile=mediafile_4_path)
|
||||||
self.assertEqual(object_4.get_filesize(), '< 1 kB')
|
self.assertEqual(object_4.get_filesize(), '< 1 kB')
|
||||||
|
@ -8,9 +8,14 @@
|
|||||||
:license: GNU GPL, see LICENSE for more details.
|
:license: GNU GPL, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.test.client import Client
|
from django.test.client import Client
|
||||||
|
|
||||||
from openslides.config.api import config
|
from openslides.config.api import config
|
||||||
|
from openslides.mediafile.models import Mediafile
|
||||||
from openslides.motion.models import Category, Motion, MotionLog, State
|
from openslides.motion.models import Category, Motion, MotionLog, State
|
||||||
from openslides.participant.models import Group, User
|
from openslides.participant.models import Group, User
|
||||||
from openslides.utils.test import TestCase
|
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/2/', self.admin_client, 200)
|
||||||
self.check_url('/motion/500/', self.admin_client, 404)
|
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, '<h4>Attachments:</h4>')
|
||||||
|
self.assertContains(response, 'TestFile_Neiri4xai4ueseGohzid')
|
||||||
|
|
||||||
|
# Test disappearance
|
||||||
|
attachment.mediafile.delete()
|
||||||
|
attachment.delete()
|
||||||
|
response = self.registered_client.get('/motion/1/')
|
||||||
|
self.assertNotContains(response, '<h4>Attachments:</h4>')
|
||||||
|
self.assertNotContains(response, 'TestFile_Neiri4xai4ueseGohzid')
|
||||||
|
|
||||||
|
|
||||||
class TestMotionDetailVersionView(MotionViewTestCase):
|
class TestMotionDetailVersionView(MotionViewTestCase):
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
@ -363,6 +387,15 @@ class TestMotionUpdateView(MotionViewTestCase):
|
|||||||
'workflow': 2})
|
'workflow': 2})
|
||||||
self.assertEqual(MotionLog.objects.get(pk=5).message_list, ['Motion version', ' 2 ', 'updated'])
|
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, '<option value="1" selected="selected">%s</option>' % attachment.title)
|
||||||
|
self.motion1.attachments.clear()
|
||||||
|
response = self.admin_client.get(self.url)
|
||||||
|
self.assertNotContains(response, '<option value="1" selected="selected">%s</option>' % attachment.title)
|
||||||
|
|
||||||
|
|
||||||
class TestMotionDeleteView(MotionViewTestCase):
|
class TestMotionDeleteView(MotionViewTestCase):
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from openslides.global_settings import * # noqa
|
from openslides.global_settings import * # noqa
|
||||||
|
|
||||||
# Use 'DEBUG = True' to get more details for server errors
|
# 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.
|
# Absolute path to the directory that holds media.
|
||||||
# Example: "/home/media/media.lawrence.com/"
|
# Example: "/home/media/media.lawrence.com/"
|
||||||
MEDIA_ROOT = ''
|
MEDIA_ROOT = os.path.realpath(os.path.dirname(__file__))
|
||||||
|
|
||||||
# Use RAM storage for whoosh index
|
# Use RAM storage for whoosh index
|
||||||
HAYSTACK_CONNECTIONS['default']['STORAGE'] = 'ram'
|
HAYSTACK_CONNECTIONS['default']['STORAGE'] = 'ram'
|
||||||
|
Loading…
Reference in New Issue
Block a user