diff --git a/CHANGELOG b/CHANGELOG index 9c729d002..81c4e33e6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,8 @@ Version 1.6 (unreleased) ======================== [https://github.com/OpenSlides/OpenSlides/issues?milestone=14] +Agenda: +- Added CSV import. Assignment: - Coupled assignment candidates with list of speakers. Participants: diff --git a/extras/csv-examples/agenda-demo_de.csv b/extras/csv-examples/agenda-demo_de.csv new file mode 100644 index 000000000..470f21557 --- /dev/null +++ b/extras/csv-examples/agenda-demo_de.csv @@ -0,0 +1,5 @@ +Titel,Text,Dauer +Begrüßung,Begrüßung durch den Vorstand,10 +Bericht des Vorstands,Es spricht Herr Müller,30 +Entlastung des Vorstandes,,10 +Sonstiges,,5 diff --git a/extras/csv-examples/agenda-demo_en.csv b/extras/csv-examples/agenda-demo_en.csv new file mode 100644 index 000000000..2b1bfe499 --- /dev/null +++ b/extras/csv-examples/agenda-demo_en.csv @@ -0,0 +1,5 @@ +Title,Text,Duration +Welcome,Mr. Miller,10 +Presentation of employee of the year award,Ms. Schmidt,20 +Exchange of small gifts,,10 +Dinner and dancing,,120 diff --git a/openslides/agenda/csv_import.py b/openslides/agenda/csv_import.py new file mode 100644 index 000000000..e270c8ed9 --- /dev/null +++ b/openslides/agenda/csv_import.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +import csv +import re + +from django.db import transaction +from django.utils.translation import ugettext as _ + +from openslides.utils import csv_ext + +from .models import Item + + +def import_agenda_items(csvfile): + """ + Performs the import of agenda items form a csv file. + """ + # Check encoding + try: + csvfile.read().decode('utf8') + except UnicodeDecodeError: + return_value = '', '', _('Import file has wrong character encoding, only UTF-8 is supported!') + else: + csvfile.seek(0) + # Check dialect + dialect = csv.Sniffer().sniff(csvfile.readline()) + dialect = csv_ext.patchup(dialect) + csvfile.seek(0) + # Parse CSV file + with transaction.commit_on_success(): + success_lines = [] + error_lines = [] + for (line_no, line) in enumerate(csv.reader(csvfile, dialect=dialect)): + if line_no == 0: + # Do not read the header line + continue + # Check format + try: + title, text, duration = line[:3] + except ValueError: + error_lines.append(line_no + 1) + continue + if duration and re.match('^(?:[0-9]{1,2}:[0-5][0-9]|[0-9]+)$', duration) is None: + error_lines.append(line_no + 1) + continue + Item.objects.create(title=title, text=text, duration=duration) + success_lines.append(line_no + 1) + success = _('%d items successfully imported.') % len(success_lines) + if error_lines: + error = _('Error in the following lines: %s.') % ', '.join(str(number) for number in error_lines) + else: + error = '' + return_value = success, '', error + return return_value diff --git a/openslides/agenda/templates/agenda/item_form_csv_import.html b/openslides/agenda/templates/agenda/item_form_csv_import.html new file mode 100644 index 000000000..c346a3a3e --- /dev/null +++ b/openslides/agenda/templates/agenda/item_form_csv_import.html @@ -0,0 +1,47 @@ +{% extends 'base.html' %} + +{% load i18n %} + +{% block title %}{% trans 'Import agenda items' %} – {{ block.super }}{% endblock %} + +{% block content %} +

+ {% trans 'Import agenda items' %} + + {% trans 'Back to overview' %} + +

+ +

{% trans 'Select a CSV file to import agenda items' %}.

+ +

{% trans 'Please note' %}:

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

+ + + {% trans 'Cancel' %} + +

+ * {% trans "required" %} +
+{% endblock %} diff --git a/openslides/agenda/templates/agenda/overview.html b/openslides/agenda/templates/agenda/overview.html index 1002bedbe..6dea8027d 100644 --- a/openslides/agenda/templates/agenda/overview.html +++ b/openslides/agenda/templates/agenda/overview.html @@ -36,6 +36,7 @@ {% if perms.agenda.can_manage_agenda %} {% trans "New" %} + {% trans "Import" %} {% endif %} PDF diff --git a/openslides/agenda/urls.py b/openslides/agenda/urls.py index 194e44855..3e926950d 100644 --- a/openslides/agenda/urls.py +++ b/openslides/agenda/urls.py @@ -87,5 +87,8 @@ urlpatterns = patterns( url(r'^list_of_speakers/end_speach/$', views.CurrentListOfSpeakersView.as_view(end_speach=True), - name='agenda_end_speach_on_current_list_of_speakers') -) + name='agenda_end_speach_on_current_list_of_speakers'), + + url(r'^csv_import/$', + views.ItemCSVImportView.as_view(), + name='item_csv_import')) diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index efc0605dc..0122d7e2e 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -16,10 +16,18 @@ from openslides.projector.api import get_active_slide, update_projector from openslides.utils.exceptions import OpenSlidesError from openslides.utils.pdf import stylesheet from openslides.utils.utils import html_strong -from openslides.utils.views import (CreateView, DeleteView, FormView, PDFView, - RedirectView, SingleObjectMixin, - TemplateView, UpdateView) +from openslides.utils.views import ( + CreateView, + CSVImportView, + DeleteView, + FormView, + PDFView, + RedirectView, + SingleObjectMixin, + TemplateView, + UpdateView) +from .csv_import import import_agenda_items from .forms import AppendSpeakerForm, ItemForm, ItemOrderForm, RelatedItemForm from .models import Item, Speaker @@ -620,3 +628,13 @@ class CurrentListOfSpeakersView(RedirectView): return reverse('core_dashboard') else: return reverse('item_view', args=[item.pk]) + + +class ItemCSVImportView(CSVImportView): + """ + Imports agenda items from an uploaded csv file. + """ + import_function = staticmethod(import_agenda_items) + permission_required = 'agenda.can_manage_agenda' + success_url_name = 'item_overview' + template_name = 'agenda/item_form_csv_import.html' diff --git a/openslides/motion/csv_import.py b/openslides/motion/csv_import.py index aeb6a35e3..a17d5fc25 100644 --- a/openslides/motion/csv_import.py +++ b/openslides/motion/csv_import.py @@ -17,14 +17,14 @@ from openslides.utils.utils import html_strong from .models import Category, Motion -def import_motions(csv_file, default_submitter, override=False, importing_person=None): +def import_motions(csvfile, default_submitter, override, importing_person=None): """ Imports motions from a csv file. The file must be encoded in utf8. The first line (header) is ignored. If no or multiple submitters found, the default submitter is used. If a motion with a given identifier already exists, the motion is overridden, - when the flag 'override' is true. If no or multiple categories found, + when the flag 'override' is True. If no or multiple categories found, the category is set to None. """ count_success = 0 @@ -32,18 +32,18 @@ def import_motions(csv_file, default_submitter, override=False, importing_person # Check encoding try: - csv_file.read().decode('utf8') + csvfile.read().decode('utf8') except UnicodeDecodeError: - return (0, 0, [_('Import file has wrong character encoding, only UTF-8 is supported!')], []) - csv_file.seek(0) + return '', '', _('Import file has wrong character encoding, only UTF-8 is supported!') + csvfile.seek(0) with transaction.commit_on_success(): - dialect = csv.Sniffer().sniff(csv_file.readline()) + dialect = csv.Sniffer().sniff(csvfile.readline()) dialect = csv_ext.patchup(dialect) - csv_file.seek(0) + csvfile.seek(0) all_error_messages = [] all_warning_messages = [] - for (line_no, line) in enumerate(csv.reader(csv_file, dialect=dialect)): + for (line_no, line) in enumerate(csv.reader(csvfile, dialect=dialect)): warning = [] if line_no < 1: # Do not read the header line @@ -115,7 +115,7 @@ def import_motions(csv_file, default_submitter, override=False, importing_person count_success += 1 # Build final error message with all error items (one bullet point for each csv line) - full_error_message = None + full_error_message = '' if all_error_messages: full_error_message = "%s " # Build final warning message with all warning items (one bullet point for each csv line) - full_warning_message = None + full_warning_message = '' if all_warning_messages: full_warning_message = "%s " - return (count_success, count_lines, [full_error_message], [full_warning_message]) + # Build final success message + if count_success: + success_message = '%s
%s' % ( + _('Summary'), + _('%(counts)d of %(total)d motions successfully imported.') + % {'counts': count_success, 'total': count_lines}) + else: + success_message = '' + + return success_message, full_warning_message, full_error_message diff --git a/openslides/motion/forms.py b/openslides/motion/forms.py index eb72d729c..a4148a870 100644 --- a/openslides/motion/forms.py +++ b/openslides/motion/forms.py @@ -6,7 +6,7 @@ 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) + CSVImportForm, LocalizedModelChoiceField) from openslides.utils.person import MultiplePersonFormField, PersonFormField from .models import Category, Motion, Workflow @@ -156,18 +156,10 @@ class MotionWorkflowMixin(forms.ModelForm): 'If you do so, the state of the motion will be reset.')) -class MotionImportForm(CssClassMixin, forms.Form): +class MotionCSVImportForm(CSVImportForm): """ Form for motion import via csv file. """ - csvfile = forms.FileField( - widget=forms.FileInput(attrs={'size': '50'}), - label=ugettext_lazy('CSV File'), - help_text=ugettext_lazy('The file has to be encoded in UTF-8.')) - """ - CSV filt with import data. - """ - override = forms.BooleanField( required=False, label=ugettext_lazy('Override existing motions with the same identifier'), diff --git a/openslides/motion/templates/motion/motion_form_csv_import.html b/openslides/motion/templates/motion/motion_form_csv_import.html index d70847a63..af5419345 100644 --- a/openslides/motion/templates/motion/motion_form_csv_import.html +++ b/openslides/motion/templates/motion/motion_form_csv_import.html @@ -18,7 +18,7 @@