From 485518975ad3d70802a26bf3c67b721cd84616d2 Mon Sep 17 00:00:00 2001 From: Oskar Hahn Date: Wed, 8 May 2013 18:07:09 +0200 Subject: [PATCH 1/2] motion csv import --- extras/csv-examples/motions-demo_de.csv | 12 ++-- openslides/motion/csv_import.py | 58 +++++++++++++++++++ openslides/motion/forms.py | 5 ++ .../motion/motion_form_csv_import.html | 46 +++++++++++++++ openslides/motion/urls.py | 5 ++ openslides/motion/views.py | 26 ++++++++- tests/motion/test_csv_import.py | 35 +++++++++++ 7 files changed, 180 insertions(+), 7 deletions(-) create mode 100644 openslides/motion/csv_import.py create mode 100644 openslides/motion/templates/motion/motion_form_csv_import.html create mode 100644 tests/motion/test_csv_import.py diff --git a/extras/csv-examples/motions-demo_de.csv b/extras/csv-examples/motions-demo_de.csv index 3233ac4b2..2985e6412 100644 --- a/extras/csv-examples/motions-demo_de.csv +++ b/extras/csv-examples/motions-demo_de.csv @@ -1,7 +1,7 @@ -Nummer;Titel;Text;Begründung;Antragsteller (Vorname);Antragsteller (Nachname/Gruppenname);Antragsteller ist eine Gruppe -1;Entlastung des Vorstandes;Die Versammlung möge beschließen, den Vorstand für seine letzte Legislaturperiode zu entlasten.;Bericht erfolgt mündlich.;Volker;Versammlungsleitung;0 -2;Satzungsänderung §2, Abs.3;"Die Versammlung möge beschließen, die Satzung in § 2 Abs. 3 wie folgt zu ändern: +Identifier,Titel,Text,Begründung,Antragsteller ID +1,Entlastung des Vorstandes,"Die Versammlung möge beschließen, den Vorstand für seine letzte Legislaturperiode zu entlasten.","Bericht erfolgt mündlich.",user:5 +2,"Satzungsänderung §2, Abs.3","Die Versammlung möge beschließen, die Satzung in § 2 Abs. 3 wie folgt zu ändern: -Es wird nach dem Wort ""Zweck"" der Satz ""..."" eingefügt.";Die Änderung der Satzung ist aufgrund der letzten Erfahrungen eine sinnvolle Maßnahme, weil ...;David;Delegierter;0 -3;Einführung von elektronischen Abstimmungen mit OpenSlides;"Die Versammlung möge beschließen, öffentliche Abstimmungen künftig elektronisch mit dem OpenSlides Plugin ""VoteCollector"" durchzuführen.";Elektronische Abstimmungen beschleunigen den Ablauf. OpenSlides wird bereits bei uns eingesetzt und bietet ein zusätzliches Plugin, um mit Keypads für jeden Teilnehmer elektronisch abzustimmen. Die Ergebnisse werden direkt in OpenSlides gespeichert. Details gibts über den professional Support auf openslides.org.;Emma;Dampf;0 -;Resolution;Die Versammlung möge beschließen, die Resolution zum Thema OpenSlides vom Ortsverband-Mitte zu verabschieden.;;;Vorstand;1 +Es wird nach dem Wort ""Zweck"" der Satz ""..."" eingefügt.","Die Änderung der Satzung ist aufgrund der letzten Erfahrungen eine sinnvolle Maßnahme, weil ...",user:2 +3,"Einführung von elektronischen Abstimmungen mit OpenSlides","Die Versammlung möge beschließen, öffentliche Abstimmungen künftig elektronisch mit dem OpenSlides Plugin ""VoteCollector"" durchzuführen.", Elektronische Abstimmungen beschleunigen den Ablauf. OpenSlides wird bereits bei uns eingesetzt und bietet ein zusätzliches Plugin, um mit Keypads für jeden Teilnehmer elektronisch abzustimmen. Die Ergebnisse werden direkt in OpenSlides gespeichert. Details gibts über den professional Support auf openslides.org.",user:3 +,"Resolution","Die Versammlung möge beschließen, die Resolution zum Thema OpenSlides vom Ortsverband-Mitte zu verabschieden.",,user:3 diff --git a/openslides/motion/csv_import.py b/openslides/motion/csv_import.py new file mode 100644 index 000000000..b1ea107e8 --- /dev/null +++ b/openslides/motion/csv_import.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + openslides.motion.csv_import + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Functions to import motions from csv. + + :copyright: (c) 2011–2013 by the OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +# TODO: Rename the file to 'csv.py' when we drop python2 support. At the moment +# the name csv has a conflict with the core-module. See: +# http://docs.python.org/2/tutorial/modules.html#intra-package-references + +import csv +from django.db import transaction +from django.utils.translation import ugettext as _ + +from openslides.utils import csv_ext +from .models import Motion + + +def import_motions(csv_file): + error_messages = [] + count_success = 0 + csv_file.read().decode('utf-8') + csv_file.seek(0) + with transaction.commit_on_success(): + for (line_no, line) in enumerate(csv.reader(csv_file)): + if line_no < 1: + # Do not read the header line + continue + + # TODO: test for wrong format + try: + (identifier, title, text, reason, person_id) = line[:5] + except ValueError: + error_messages.append(_('Ignoring malformed line %d in import file.') % (line_no + 1)) + continue + + if identifier: + try: + motion = Motion.objects.get(identifier=identifier) + except Motion.DoesNotExist: + motion = Motion() + else: + motion = Motion() + + motion.title = title + motion.text = text + motion.reason = reason + motion.save() + # TODO: person does not exist + motion.add_submitter(person_id) + count_success += 1 + return (count_success, error_messages) diff --git a/openslides/motion/forms.py b/openslides/motion/forms.py index 908347d17..0e06aa666 100644 --- a/openslides/motion/forms.py +++ b/openslides/motion/forms.py @@ -121,3 +121,8 @@ class MotionIdentifierMixin(forms.ModelForm): raise forms.ValidationError(_('The Identifier is not unique.')) else: return identifier + + +class MotionImportForm(CssClassMixin, forms.Form): + csvfile = forms.FileField(widget=forms.FileInput(attrs={'size': '50'}), + label=ugettext_lazy('CSV File')) diff --git a/openslides/motion/templates/motion/motion_form_csv_import.html b/openslides/motion/templates/motion/motion_form_csv_import.html new file mode 100644 index 000000000..1d82cb6c1 --- /dev/null +++ b/openslides/motion/templates/motion/motion_form_csv_import.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block title %}{{ block.super }} – {% trans "Import motions" %} {% endblock %} + +{% block content %} +

+ {% trans 'Import motions' %} + + {% trans "Back to overview" %} + +

+ +

{% trans 'Select a CSV file to import motions!' %}

+ +

{% trans 'Please note' %}:

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

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

+ * {% trans "required" %} +
+{% endblock %} diff --git a/openslides/motion/urls.py b/openslides/motion/urls.py index 666d824e1..46da0edfd 100644 --- a/openslides/motion/urls.py +++ b/openslides/motion/urls.py @@ -139,4 +139,9 @@ urlpatterns = patterns('openslides.motion.views', 'category_delete', name='motion_category_delete', ), + + url(r'^csv_import/$', + 'motion_csv_import', + name='motion_csv_import', + ), ) diff --git a/openslides/motion/views.py b/openslides/motion/views.py index 811f90470..0e851937c 100644 --- a/openslides/motion/views.py +++ b/openslides/motion/views.py @@ -38,8 +38,9 @@ from .models import (Motion, MotionSubmitter, MotionSupporter, MotionPoll, MotionVersion, State, WorkflowError, Category) from .forms import (BaseMotionForm, MotionSubmitterMixin, MotionSupporterMixin, MotionDisableVersioningMixin, MotionCategoryMixin, - MotionIdentifierMixin) + MotionIdentifierMixin, MotionImportForm) from .pdf import motions_to_pdf, motion_to_pdf, motion_poll_to_pdf +from .csv_import import import_motions class MotionListView(ListView): @@ -667,6 +668,29 @@ class CategoryDeleteView(DeleteView): category_delete = CategoryDeleteView.as_view() +class MotionCSVImportView(FormView): + """ + Import motions via csv. + """ + permission_required = 'motions.can_manage_participant' + template_name = 'motion/motion_form_csv_import.html' + form_class = MotionImportForm + success_url_name = 'motion_list' + + def form_valid(self, form): + # check for valid encoding (will raise UnicodeDecodeError if not) + count_success, error_messages = import_motions(self.request.FILES['csvfile']) + for message in error_messages: + messages.error(self.request, message) + if count_success: + messages.success( + self.request, + _('%d motions were successfully imported.') % count_success) + return super(MotionCSVImportView, self).form_valid(form) + +motion_csv_import = MotionCSVImportView.as_view() + + def register_tab(request): """Return the motion tab.""" # TODO: Find a better way to set the selected var. diff --git a/tests/motion/test_csv_import.py b/tests/motion/test_csv_import.py new file mode 100644 index 000000000..96212817a --- /dev/null +++ b/tests/motion/test_csv_import.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + Tests for openslides.motion.models + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2011–2013 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +import os + +from django.test.client import Client + +from openslides.config.api import config +from openslides.utils.test import TestCase +from openslides.participant.models import User +from openslides.motion.models import Motion +from openslides.motion.csv_import import import_motions + + +class CSVImport(TestCase): + def setUp(self): + # Admin + self.admin = User.objects.create_superuser('admin', 'admin@admin.admin', 'admin') + self.admin_client = Client() + self.admin_client.login(username='admin', password='admin') + + def test_example_file(self): + csv_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'extras', 'csv-examples') + with open(csv_dir + '/motions-demo_de.csv') as f: + import_motions(f) + self.assertEqual(Motion.objects.count(), 4) + + From 0f17f74feefc85c8a07abc936b440e5df860d95c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Sun, 12 May 2013 00:47:49 +0200 Subject: [PATCH 2/2] Fix and update motion csv import. --- extras/csv-examples/motions-demo_de.csv | 12 +-- extras/csv-examples/motions-demo_en.csv | 9 +-- openslides/motion/csv_import.py | 71 +++++++++++++--- openslides/motion/forms.py | 30 ++++++- openslides/motion/models.py | 3 + .../motion/motion_form_csv_import.html | 18 ++--- .../motion/templates/motion/motion_list.html | 2 +- openslides/motion/views.py | 21 ++++- tests/motion/test_csv_import.py | 80 +++++++++++++++++-- 9 files changed, 200 insertions(+), 46 deletions(-) diff --git a/extras/csv-examples/motions-demo_de.csv b/extras/csv-examples/motions-demo_de.csv index 2985e6412..98aad1bbc 100644 --- a/extras/csv-examples/motions-demo_de.csv +++ b/extras/csv-examples/motions-demo_de.csv @@ -1,7 +1,7 @@ -Identifier,Titel,Text,Begründung,Antragsteller ID -1,Entlastung des Vorstandes,"Die Versammlung möge beschließen, den Vorstand für seine letzte Legislaturperiode zu entlasten.","Bericht erfolgt mündlich.",user:5 -2,"Satzungsänderung §2, Abs.3","Die Versammlung möge beschließen, die Satzung in § 2 Abs. 3 wie folgt zu ändern: +Bezeichner,Titel,Text,Begründung,Antragsteller,Sachgebiet +1,Entlastung des Vorstandes,"Die Versammlung möge beschließen, den Vorstand für seine letzte Legislaturperiode zu entlasten.","Bericht erfolgt mündlich.",Max Mustermann,Vorstandsangelegenheiten +S 2,"Satzungsänderung § 2 Abs. 3","Die Versammlung möge beschließen, die Satzung in § 2 Abs. 3 wie folgt zu ändern: -Es wird nach dem Wort ""Zweck"" der Satz ""..."" eingefügt.","Die Änderung der Satzung ist aufgrund der letzten Erfahrungen eine sinnvolle Maßnahme, weil ...",user:2 -3,"Einführung von elektronischen Abstimmungen mit OpenSlides","Die Versammlung möge beschließen, öffentliche Abstimmungen künftig elektronisch mit dem OpenSlides Plugin ""VoteCollector"" durchzuführen.", Elektronische Abstimmungen beschleunigen den Ablauf. OpenSlides wird bereits bei uns eingesetzt und bietet ein zusätzliches Plugin, um mit Keypads für jeden Teilnehmer elektronisch abzustimmen. Die Ergebnisse werden direkt in OpenSlides gespeichert. Details gibts über den professional Support auf openslides.org.",user:3 -,"Resolution","Die Versammlung möge beschließen, die Resolution zum Thema OpenSlides vom Ortsverband-Mitte zu verabschieden.",,user:3 +Es wird nach dem Wort ""Zweck"" der Satz ""..."" eingefügt.","Die Änderung der Satzung ist aufgrund der letzten Erfahrungen eine sinnvolle Maßnahme, weil ...",Fritz Fleiner,Satzung +3,"Einführung von elektronischen Abstimmungen mit OpenSlides","Die Versammlung möge beschließen, öffentliche Abstimmungen künftig elektronisch mit dem OpenSlides Plugin ""VoteCollector"" durchzuführen.","Elektronische Abstimmungen beschleunigen den Ablauf. OpenSlides wird bereits bei uns eingesetzt und bietet ein zusätzliches Plugin, um mit Keypads für jeden Teilnehmer elektronisch abzustimmen. Die Ergebnisse werden direkt in OpenSlides gespeichert. Details gibts über den professional Support auf http://openslides.org.",, +,"Resolution","Die Versammlung möge beschließen, die Resolution zum Thema OpenSlides vom Ortsverband-Mitte zu verabschieden.",,Dr. Hilde Müller,Resolution diff --git a/extras/csv-examples/motions-demo_en.csv b/extras/csv-examples/motions-demo_en.csv index 05dc88ead..52769611e 100644 --- a/extras/csv-examples/motions-demo_en.csv +++ b/extras/csv-examples/motions-demo_en.csv @@ -1,7 +1,2 @@ -Number;Title;Text;Reason;Submitter (First Name);Submitter (Last Name);Submitter is Group -1;Entlastung des Vorstandes;Die Versammlung möge beschließen, den Vorstand für seine letzte Legislaturperiode zu entlasten.;Bericht erfolgt mündlich.;Volker;Versammlungsleitung;0 -2;Satzungsänderung §2, Abs.3;"Die Versammlung möge beschließen, die Satzung in § 2 Abs. 3 wie folgt zu ändern: - -Es wird nach dem Wort ""Zweck"" der Satz ""..."" eingefügt.";Die Änderung der Satzung ist aufgrund der letzten Erfahrungen eine sinnvolle Maßnahme, weil ...;David;Delegierter;0 -3;Einführung von elektronischen Abstimmungen mit OpenSlides;"Die Versammlung möge beschließen, öffentliche Abstimmungen künftig elektronisch mit dem OpenSlides Plugin ""VoteCollector"" durchzuführen.";Elektronische Abstimmungen beschleunigen den Ablauf. OpenSlides wird bereits bei uns eingesetzt und bietet ein zusätzliches Plugin, um mit Keypads für jeden Teilnehmer elektronisch abzustimmen. Die Ergebnisse werden direkt in OpenSlides gespeichert. Details gibts über den professional Support auf openslides.org.;Emma;Dampf;0 -;Resolution;Die Versammlung möge beschließen, die Resolution zum Thema OpenSlides vom Ortsverband-Mitte zu verabschieden.;;;Vorstand;1 +Identifier,Title,Text,Reason,Submitter,Category +H 1,New proposal,"The assembly may decide, that everyone should eat more apples, esp. ""Golden Delicious"".",Apples are very tasty.,John Smith,Health diff --git a/openslides/motion/csv_import.py b/openslides/motion/csv_import.py index b1ea107e8..a710977b8 100644 --- a/openslides/motion/csv_import.py +++ b/openslides/motion/csv_import.py @@ -4,7 +4,7 @@ openslides.motion.csv_import ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Functions to import motions from csv. + Functions to import motions from a csv file. :copyright: (c) 2011–2013 by the OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. @@ -15,44 +15,93 @@ # http://docs.python.org/2/tutorial/modules.html#intra-package-references import csv + from django.db import transaction from django.utils.translation import ugettext as _ -from openslides.utils import csv_ext -from .models import Motion +from openslides.utils.person.api import Persons + +from .models import Motion, Category -def import_motions(csv_file): +def import_motions(csv_file, default_submitter, override=False): + """ + 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, + the category is set to None. + """ error_messages = [] + warning_messages = [] count_success = 0 - csv_file.read().decode('utf-8') + + # Check encoding + try: + csv_file.read().decode('utf8') + except UnicodeDecodeError: + return (0, [_('Encoding error in import file. Ensure using UTF-8.')], []) csv_file.seek(0) + with transaction.commit_on_success(): for (line_no, line) in enumerate(csv.reader(csv_file)): if line_no < 1: # Do not read the header line continue - # TODO: test for wrong format + # Check format try: - (identifier, title, text, reason, person_id) = line[:5] + (identifier, title, text, reason, submitter, category) = line[:6] except ValueError: error_messages.append(_('Ignoring malformed line %d in import file.') % (line_no + 1)) continue + # Check existing motions according to the identifier if identifier: try: motion = Motion.objects.get(identifier=identifier) except Motion.DoesNotExist: - motion = Motion() + motion = Motion(identifier=identifier) + else: + if not override: + error_messages.append(_('Line %d in import file: Ignoring existing motion.') % (line_no + 1)) + continue else: motion = Motion() + # Insert data motion.title = title motion.text = text motion.reason = reason + if category: + try: + motion.category = Category.objects.get(name=category) + except Category.DoesNotExist: + error_messages.append(_('Line %d in import file: Category not found.') % (line_no + 1)) + except Category.MultipleObjectsReturned: + error_messages.append(_('Line %d in import file: Multiple categories found.') % (line_no + 1)) motion.save() - # TODO: person does not exist - motion.add_submitter(person_id) + + # Add submitter + person_found = False + if submitter: + for person in Persons(): + if person.clean_name == submitter.decode('utf8'): + if person_found: + error_messages.append(_('Line %d in import file: Multiple persons found.') % (line_no + 1)) + person_found = False + break + else: + new_submitter = person + person_found = True + if not person_found: + warning_messages.append(_('Line %d in import file: Default submitter is used.') % (line_no + 1)) + new_submitter = default_submitter + motion.clear_submitters() + motion.add_submitter(new_submitter) + count_success += 1 - return (count_success, error_messages) + + return (count_success, error_messages, warning_messages) diff --git a/openslides/motion/forms.py b/openslides/motion/forms.py index 0e06aa666..05d19e3bf 100644 --- a/openslides/motion/forms.py +++ b/openslides/motion/forms.py @@ -124,5 +124,31 @@ class MotionIdentifierMixin(forms.ModelForm): class MotionImportForm(CssClassMixin, forms.Form): - csvfile = forms.FileField(widget=forms.FileInput(attrs={'size': '50'}), - label=ugettext_lazy('CSV File')) + """ + 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 should 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'), + help_text=ugettext_lazy('If this is active, every motion with the same identifier as in your csv file will be overridden.')) + """ + Flag to decide whether existing motions (according to the identifier) + should be overridden. + """ + + default_submitter = PersonFormField( + required=True, + label=ugettext_lazy('Default submitter'), + help_text=ugettext_lazy('This person is used as submitter for any line of your csv file which does not contain valid submitter data.')) + """ + Person which is used as submitter, if the line of the csv file does + not contain valid submitter data. + """ diff --git a/openslides/motion/models.py b/openslides/motion/models.py index e54379c87..679198d0c 100644 --- a/openslides/motion/models.py +++ b/openslides/motion/models.py @@ -386,6 +386,9 @@ class Motion(SlideMixin, models.Model): def add_submitter(self, person): MotionSubmitter.objects.create(motion=self, person=person) + def clear_submitters(self): + MotionSubmitter.objects.filter(motion=self).delete() + def is_supporter(self, person): """ Return True, if person is a supporter of this motion. Else: False. diff --git a/openslides/motion/templates/motion/motion_form_csv_import.html b/openslides/motion/templates/motion/motion_form_csv_import.html index 1d82cb6c1..9d52c84f6 100644 --- a/openslides/motion/templates/motion/motion_form_csv_import.html +++ b/openslides/motion/templates/motion/motion_form_csv_import.html @@ -1,30 +1,30 @@ -{% extends "base.html" %} +{% extends 'base.html' %} {% load i18n %} -{% block title %}{{ block.super }} – {% trans "Import motions" %} {% endblock %} +{% block title %}{{ block.super }} – {% trans 'Import motions' %} {% endblock %} {% block content %}

{% trans 'Import motions' %} - {% trans "Back to overview" %} + {% trans 'Back to overview' %}

-

{% trans 'Select a CSV file to import motions!' %}

+

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

{% trans 'Please note' %}:

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