Insert new feature: attachments for motions.

Fixed #522. Database struture changed.
This commit is contained in:
Norman Jäckel 2013-10-16 23:57:29 +02:00
parent 2f288f6cb1
commit 30371e964f
7 changed files with 74 additions and 8 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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 %}

View File

@ -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()

View File

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

View File

@ -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):

View File

@ -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'