From ad0e157bd185cf447381bc705d6fcb9f5d0140d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Sat, 16 Feb 2013 16:19:20 +0100 Subject: [PATCH] Insert new app to upload files via the frontend. Let tornado server media files. Insert icon-mediafile css class. Insert extra_stylfiles context variable. --- .gitignore | 9 +- openslides/global_settings.py | 17 +- openslides/main.py | 16 +- openslides/mediafile/__init__.py | 0 openslides/mediafile/forms.py | 50 +++++ openslides/mediafile/models.py | 85 ++++++++ .../mediafile/static/styles/mediafile.css | 12 ++ .../templates/mediafile/mediafile_form.html | 37 ++++ .../templates/mediafile/mediafile_list.html | 49 +++++ openslides/mediafile/urls.py | 40 ++++ openslides/mediafile/views.py | 96 +++++++++ openslides/participant/api.py | 8 +- openslides/participant/models.py | 2 +- openslides/participant/signals.py | 13 +- openslides/templates/base.html | 22 +- openslides/urls.py | 13 +- openslides/utils/person/forms.py | 4 +- openslides/utils/template.py | 12 +- openslides/utils/tornado_webserver.py | 7 +- openslides/utils/views.py | 12 +- tests/mediafile/__init__.py | 0 tests/mediafile/tests.py | 204 ++++++++++++++++++ 22 files changed, 651 insertions(+), 57 deletions(-) create mode 100644 openslides/mediafile/__init__.py create mode 100644 openslides/mediafile/forms.py create mode 100644 openslides/mediafile/models.py create mode 100644 openslides/mediafile/static/styles/mediafile.css create mode 100644 openslides/mediafile/templates/mediafile/mediafile_form.html create mode 100644 openslides/mediafile/templates/mediafile/mediafile_list.html create mode 100644 openslides/mediafile/urls.py create mode 100644 openslides/mediafile/views.py create mode 100644 tests/mediafile/__init__.py create mode 100644 tests/mediafile/tests.py diff --git a/.gitignore b/.gitignore index d6170257f..4c24d609b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,12 @@ -.venv/* +# General *.pyc *.swp *~ + +# Virtual Environment +.venv/* + +# Development settings and database settings.py database.sqlite !tests/settings.py @@ -14,6 +19,6 @@ dist/* .DS_Store versiontools* -# Unit test / coverage reports +# Unit test and coverage reports .coverage htmlcov diff --git a/openslides/global_settings.py b/openslides/global_settings.py index aec335377..c74bbd4ce 100644 --- a/openslides/global_settings.py +++ b/openslides/global_settings.py @@ -1,12 +1,12 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ - openslides.openslides_settings - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + openslides.global_settings + ~~~~~~~~~~~~~~~~~~~~~~~~~~ OpenSlides default settings. - :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ @@ -15,6 +15,7 @@ import sys from openslides.main import fs2unicode + SITE_ROOT = os.path.realpath(os.path.dirname(__file__)) AUTHENTICATION_BACKENDS = ( @@ -34,7 +35,6 @@ LANGUAGES = ( ('fr', ugettext('French')), ) - # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. USE_I18N = True @@ -47,18 +47,14 @@ LOCALE_PATHS = ( fs2unicode(os.path.join(SITE_ROOT, 'locale')), ) -# Absolute path to the directory that holds media. -# Example: "/home/media/media.lawrence.com/" -MEDIA_ROOT = fs2unicode(os.path.join(SITE_ROOT, './static/')) - # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash if there is a path component (optional in other cases). # Examples: "http://media.lawrence.com", "http://example.com/media/" -MEDIA_URL = '' +MEDIA_URL = '/media/' # Absolute path to the directory that holds static media from ``collectstatic`` # Example: "/home/media/static.lawrence.com/" -STATIC_ROOT = fs2unicode(os.path.join(SITE_ROOT, '../site-static')) +STATIC_ROOT = fs2unicode(os.path.join(SITE_ROOT, '../collected-site-static')) # URL that handles the media served from STATIC_ROOT. Make sure to use a # trailing slash if there is a path component (optional in other cases). @@ -119,6 +115,7 @@ INSTALLED_APPS = ( 'openslides.motion', 'openslides.assignment', 'openslides.participant', + 'openslides.mediafile', 'openslides.config', ) diff --git a/openslides/main.py b/openslides/main.py index b7107317d..4477b6a51 100644 --- a/openslides/main.py +++ b/openslides/main.py @@ -6,7 +6,7 @@ Main script to start and set up OpenSlides. - :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ @@ -64,6 +64,11 @@ INSTALLED_PLUGINS = ( ) INSTALLED_APPS += INSTALLED_PLUGINS + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = %(media_root_path)s + """ KEY_LENGTH = 30 @@ -195,14 +200,17 @@ def create_settings(settings_path, database_path=None): if database_path is _portable_db_path: database_path = get_portable_db_path() dbpath_value = 'openslides.main.get_portable_db_path()' + media_root_path_value = 'openslides.main.get_portable_media_root_path()' else: if database_path is None: database_path = get_user_data_path('openslides', 'database.sqlite') dbpath_value = repr(fs2unicode(database_path)) + media_root_path_value = repr(fs2unicode(get_user_data_path('openslides', 'media', ''))) settings_content = CONFIG_TEMPLATE % dict( default_key=base64.b64encode(os.urandom(KEY_LENGTH)), - dbpath=dbpath_value) + dbpath=dbpath_value, + media_root_path=media_root_path_value) if not os.path.exists(settings_module): os.makedirs(settings_module) @@ -370,6 +378,10 @@ def get_portable_db_path(): return get_portable_path('openslides', 'database.sqlite') +def get_portable_media_root_path(): + return get_portable_path('openslides', 'media', '') + + def win32_get_app_data_path(*args): shell32 = ctypes.WinDLL("shell32.dll") SHGetFolderPath = shell32.SHGetFolderPathW diff --git a/openslides/mediafile/__init__.py b/openslides/mediafile/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openslides/mediafile/forms.py b/openslides/mediafile/forms.py new file mode 100644 index 000000000..b9a94d0c0 --- /dev/null +++ b/openslides/mediafile/forms.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + openslides.mediafile.forms + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Forms for the mediafile app. + + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +from django.forms import ModelForm + +from openslides.utils.forms import CssClassMixin + +from .models import Mediafile + + +class MediafileNormalUserCreateForm(CssClassMixin, ModelForm): + """Form to create a media file. + + This form is only used by normal users, not by managers. + + """ + class Meta: + model = Mediafile + exclude = ('uploader',) + + +class MediafileUpdateForm(CssClassMixin, ModelForm): + """Form to edit mediafile entries. + + This form is only for managers to update the mediafile entry. + + """ + class Meta: + model = Mediafile + + def save(self, *args, **kwargs): + """Method to save the form. + + Here the overwrite is to delete old files. + + """ + if not self.instance.pk is None: + old_file = Mediafile.objects.get(pk=self.instance.pk).mediafile + if not old_file == self.instance.mediafile: + old_file.delete() + return super(MediafileUpdateForm, self).save(*args, **kwargs) diff --git a/openslides/mediafile/models.py b/openslides/mediafile/models.py new file mode 100644 index 000000000..f870e3100 --- /dev/null +++ b/openslides/mediafile/models.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + openslides.mediafile.models + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Models for the mediafile app. + + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +import mimetypes + +from django.db import models +from django.utils.translation import ugettext_noop + +from openslides.utils.person.models import PersonField + + +class Mediafile(models.Model): + """The Mediafile class + + Class for uploaded files which can be delivered under a certain url. + + """ + mediafile = models.FileField(upload_to='file') + """A FileField + + See https://docs.djangoproject.com/en/dev/ref/models/fields/#filefield + for more information. + + """ + title = models.CharField(max_length=255, unique=True) + """A string representing the title of the file.""" + + uploader = PersonField(blank=True) + """A person – the uploader of a file.""" + + timestamp = models.DateTimeField(auto_now_add=True) + """A DateTimeField to save the upload date and time.""" + + filetype = models.CharField(max_length=255, editable=False) + """A string used to show the type of the file.""" + + class Meta: + """Meta class for the mediafile model.""" + ordering = ['title'] + permissions = ( + ('can_see', ugettext_noop('Can see the list of files')), + ('can_upload', ugettext_noop('Can upload files')), + ('can_manage', ugettext_noop('Can manage files')),) + + def __unicode__(self): + """Method for representation.""" + return self.title + + def save(self, *args, **kwargs): + """Method to read filetype and then save to the database.""" + if self.mediafile: + self.filetype = mimetypes.guess_type(self.mediafile.path)[0] or ugettext_noop('unknown') + else: + self.filetype = ugettext_noop('unknown') + return super(Mediafile, self).save(*args, **kwargs) + + @models.permalink + def get_absolute_url(self, link='update'): + """Returns the URL to a mediafile. The link can be 'update' or 'delete'.""" + if link == 'update' or link == 'edit': # 'edit' ist only used until utils/views.py is fixed + return ('mediafile_update', [str(self.id)]) + if link == 'delete': + return ('mediafile_delete', [str(self.id)]) + + def get_filesize(self): + """Transforms Bytes to Kilobytes or Megabytes. Returns the size as string.""" + size = self.mediafile.size + if size < 1024: + return '< 1 kB' + if size >= 1024 * 1024: + mB = size / 1024 / 1024 + return '%d MB' % mB + else: + kB = size / 1024 + return '%d kB' % kB + # TODO: Read http://stackoverflow.com/a/1094933 and think about it. diff --git a/openslides/mediafile/static/styles/mediafile.css b/openslides/mediafile/static/styles/mediafile.css new file mode 100644 index 000000000..99f7d28cd --- /dev/null +++ b/openslides/mediafile/static/styles/mediafile.css @@ -0,0 +1,12 @@ +/* + * OpenSlides mediafile style + * + * :copyright: 2011–2013 by OpenSlides team, see AUTHORS. + * :license: GNU GPL, see LICENSE for more details. + */ + +/** Navigation icons (mapping to glyphicons-halflings) **/ + +.icon-mediafile { + background-position: -96px -24px; +} diff --git a/openslides/mediafile/templates/mediafile/mediafile_form.html b/openslides/mediafile/templates/mediafile/mediafile_form.html new file mode 100644 index 000000000..f709153d3 --- /dev/null +++ b/openslides/mediafile/templates/mediafile/mediafile_form.html @@ -0,0 +1,37 @@ +{% extends 'base.html' %} + +{% load i18n %} + +{% block title %} + {{ block.super }} – + {% if mediafile %} + {% trans "Edit media" %} + {% else %} + {% trans "New media" %} + {% endif %} +{% endblock %} + +{% block content %} +

+ {% if mediafile %} + {% trans "Edit media" %} + {% else %} + {% trans "New media" %} + {% endif %} + + {% trans "Back to overview" %} + +

+
{% csrf_token %} + {% include "form.html" %} +

+ {% if perms.mediafile.can_manage %} + {% include "formbuttons_saveapply.html" %} + {% else %} + {% include "formbuttons_save.html" %} + {% endif %} + {% trans 'Cancel' %} +

+ * {% trans "required" %} +
+{% endblock %} diff --git a/openslides/mediafile/templates/mediafile/mediafile_list.html b/openslides/mediafile/templates/mediafile/mediafile_list.html new file mode 100644 index 000000000..73b7b3530 --- /dev/null +++ b/openslides/mediafile/templates/mediafile/mediafile_list.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} + +{% load i18n %} +{% load tags %} + +{% block title %}{{ block.super }} – {% trans 'Media' %}{% endblock %} + +{% block content %} +

{% trans 'Media' %} + + {% if perms.mediafile.can_upload %} + {% trans "New" %} + {% endif %} + +

+ + + + + + + + {% if perms.mediafile.can_manage %} + + {% endif %} + + {% for mediafile in mediafile_list %} + + + + + + + {% if perms.mediafile.can_manage %} + + {% endif %} + + {% empty %} + + + + {% endfor %} +
{% trans 'Title' %}{% trans 'Type' %}{% trans 'Size' %}{% trans 'Upload time' %}{% trans 'Uploader' %}{% trans "Actions" %}
{{ mediafile }}{{ mediafile.filetype }}{{ mediafile.get_filesize }}{{ mediafile.timestamp }}{{ mediafile.uploader }} + + + + +
{% trans 'No media available.' %}
+{% endblock %} diff --git a/openslides/mediafile/urls.py b/openslides/mediafile/urls.py new file mode 100644 index 000000000..d452868f3 --- /dev/null +++ b/openslides/mediafile/urls.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + openslides.mediafile.urls + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + URL patterns for the mediafile app. + + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +from django.conf.urls import url, patterns + +from .models import Mediafile +from .views import (MediafileListView, MediafileCreateView, + MediafileUpdateView, MediafileDeleteView) + + +urlpatterns = patterns('', + url(r'^$', + MediafileListView.as_view(), + name='mediafile_list', + ), + + url(r'^new/$', + MediafileCreateView.as_view(), + name='mediafile_create', + ), + + url(r'^(?P\d+)/edit/$', + MediafileUpdateView.as_view(), + name='mediafile_update', + ), + + url(r'^(?P\d+)/del/$', + MediafileDeleteView.as_view(), + name='mediafile_delete', + ), +) diff --git a/openslides/mediafile/views.py b/openslides/mediafile/views.py new file mode 100644 index 000000000..5a6746784 --- /dev/null +++ b/openslides/mediafile/views.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + openslides.mediafile.views + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Views for the mediafile app. + + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ + +from openslides.utils.template import Tab +from openslides.utils.views import ListView, CreateView, UpdateView, DeleteView + +from .models import Mediafile +from .forms import MediafileNormalUserCreateForm, MediafileUpdateForm + + +class MediafileListView(ListView): + """View to see a table of all uploaded files.""" + model = Mediafile + + def has_permission(self, request, *args, **kwargs): + return (request.user.has_perm('mediafile.can_see') or + request.user.has_perm('mediafile.can_upload') or + request.user.has_perm('mediafile.can_manage')) + + +class MediafileCreateView(CreateView): + """View to upload a new file + + A manager can also set the uploader, else the request user is set as uploader. + + """ + model = Mediafile + permission_required = 'mediafile.can_upload' + success_url_name = 'mediafile_list' + + def get_form(self, form_class): + form_kwargs = self.get_form_kwargs() + if self.request.method == 'GET': + form_kwargs['initial'].update({'uploader': self.request.user.person_id}) # TODO: Check this. + if not self.request.user.has_perm('mediafile.can_manage'): + # Return our own ModelForm + return MediafileNormalUserCreateForm(**form_kwargs) + else: + # Return a ModelForm created by Django. + return form_class(**form_kwargs) + + def manipulate_object(self, *args, **kwargs): + if not self.request.user.has_perm('mediafile.can_manage'): + self.object.uploader = self.request.user + return super(MediafileCreateView, self).manipulate_object(*args, **kwargs) + + +class MediafileUpdateView(UpdateView): + """View to edit the entry of an uploaded file.""" + model = Mediafile + permission_required = 'mediafile.can_manage' + form_class = MediafileUpdateForm + success_url_name = 'mediafile_list' + + def get_form_kwargs(self, *args, **kwargs): + form_kwargs = super(MediafileUpdateView, self).get_form_kwargs(*args, **kwargs) + form_kwargs['initial'].update({'uploader': self.object.uploader.person_id}) + return form_kwargs + + +class MediafileDeleteView(DeleteView): + """View to delete the entry of an uploaded file and the file itself.""" + model = Mediafile + permission_required = 'mediafile.can_manage' + success_url_name = 'mediafile_list' + + def case_yes(self, *args, **kwargs): + """Deletes the file in the filesystem, if user clicks "Yes".""" + self.object.mediafile.delete() + return super(MediafileDeleteView, self).case_yes(*args, **kwargs) + + +def register_tab(request): + """Inserts a new Tab to the views for files.""" + selected = request.path.startswith('/mediafile/') + return Tab( + title=_('Media'), + app='mediafile', # TODO: Rename this to icon='mediafile' later + stylefile='styles/mediafile.css', + url=reverse('mediafile_list'), + permission=(request.user.has_perm('mediafile.can_see') or + request.user.has_perm('mediafile.can_upload') or + request.user.has_perm('mediafile.can_manage')), + selected=selected) diff --git a/openslides/participant/api.py b/openslides/participant/api.py index 7ffd2fe93..bf4310ca8 100644 --- a/openslides/participant/api.py +++ b/openslides/participant/api.py @@ -6,14 +6,13 @@ Useful functions for the participant app. - :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ from random import choice import csv -from django.contrib.auth.models import Permission from django.db import transaction from django.utils.translation import ugettext as _ @@ -22,11 +21,6 @@ from openslides.utils import csv_ext from openslides.participant.models import User, Group -DEFAULT_PERMS = ['can_see_agenda', 'can_see_projector', - 'can_see_motion', 'can_see_assignment', - 'can_see_dashboard'] - - def gen_password(): """ generates a random passwort. diff --git a/openslides/participant/models.py b/openslides/participant/models.py index 754096386..7e0010c6d 100644 --- a/openslides/participant/models.py +++ b/openslides/participant/models.py @@ -26,7 +26,7 @@ from openslides.projector.api import register_slidemodel from openslides.projector.projector import SlideMixin -class User(DjangoUser, PersonMixin, Person, SlideMixin): +class User(PersonMixin, DjangoUser, Person, SlideMixin): prefix = 'user' # This is for the slides person_prefix = 'user' GENDER_CHOICES = ( diff --git a/openslides/participant/signals.py b/openslides/participant/signals.py index 7cace7b4f..84b8fb351 100644 --- a/openslides/participant/signals.py +++ b/openslides/participant/signals.py @@ -42,19 +42,23 @@ def create_builtin_groups(sender, **kwargs): ct_participant = ContentType.objects.get(app_label='participant', model='user') perm_6 = Permission.objects.get(content_type=ct_participant, codename='can_see_participant') + ct_mediafile = ContentType.objects.get(app_label='mediafile', model='mediafile') + perm_6a = Permission.objects.get(content_type=ct_mediafile, codename='can_see') + group_anonymous = Group.objects.create(name=ugettext_noop('Anonymous')) - group_anonymous.permissions.add(perm_1, perm_2, perm_3, perm_4, perm_5, perm_6) + group_anonymous.permissions.add(perm_1, perm_2, perm_3, perm_4, perm_5, perm_6, perm_6a) group_registered = Group.objects.create(name=ugettext_noop('Registered')) - group_registered.permissions.add(perm_1, perm_2, perm_3, perm_4, perm_5, perm_6) + group_registered.permissions.add(perm_1, perm_2, perm_3, perm_4, perm_5, perm_6, perm_6a) # Delegates perm_7 = Permission.objects.get(content_type=ct_motion, codename='can_create_motion') perm_8 = Permission.objects.get(content_type=ct_motion, codename='can_support_motion') perm_9 = Permission.objects.get(content_type=ct_assignment, codename='can_nominate_other') perm_10 = Permission.objects.get(content_type=ct_assignment, codename='can_nominate_self') + perm_10a = Permission.objects.get(content_type=ct_mediafile, codename='can_upload') group_delegates = Group.objects.create(name=ugettext_noop('Delegates')) - group_delegates.permissions.add(perm_7, perm_8, perm_9, perm_10) + group_delegates.permissions.add(perm_7, perm_8, perm_9, perm_10, perm_10a) # Staff perm_11 = Permission.objects.get(content_type=ct_agenda, codename='can_manage_agenda') @@ -62,9 +66,10 @@ def create_builtin_groups(sender, **kwargs): perm_13 = Permission.objects.get(content_type=ct_assignment, codename='can_manage_assignment') perm_14 = Permission.objects.get(content_type=ct_participant, codename='can_manage_participant') perm_15 = Permission.objects.get(content_type=ct_projector, codename='can_manage_projector') + perm_15a = Permission.objects.get(content_type=ct_mediafile, codename='can_manage') ct_config = ContentType.objects.get(app_label='config', model='configstore') perm_16 = Permission.objects.get(content_type=ct_config, codename='can_manage_config') group_staff = Group.objects.create(name=ugettext_noop('Staff')) - group_staff.permissions.add(perm_7, perm_9, perm_10, perm_11, perm_12, perm_13, perm_14, perm_15, perm_16) + group_staff.permissions.add(perm_7, perm_9, perm_10, perm_11, perm_12, perm_13, perm_14, perm_15, perm_15a, perm_16) diff --git a/openslides/templates/base.html b/openslides/templates/base.html index 6f2e9182b..83466239f 100644 --- a/openslides/templates/base.html +++ b/openslides/templates/base.html @@ -12,13 +12,14 @@ {% block title %}{% get_config 'event_name' %}{% endblock %} - - - + + + - - {% block header %} - {% endblock %} + {% for stylefile in extra_stylefiles %} + + {% endfor %} + {% block header %}{% endblock %} @@ -26,7 +27,7 @@
-
+