Merge pull request #645 from normanjaeckel/Motion_CSV_Import

Motion csv import
This commit is contained in:
Oskar Hahn 2013-05-13 11:49:41 -07:00
commit e306f53906
10 changed files with 342 additions and 15 deletions

View File

@ -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:
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 ...;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 ...",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

1 Nummer Bezeichner Titel Text Begründung Antragsteller (Vorname) Antragsteller Antragsteller (Nachname/Gruppenname) Sachgebiet Antragsteller ist eine Gruppe
2 1 1 Entlastung des Vorstandes Die Versammlung möge beschließen, den Vorstand für seine letzte Legislaturperiode zu entlasten. Bericht erfolgt mündlich. Volker Max Mustermann Versammlungsleitung Vorstandsangelegenheiten 0
3 2 S 2 Satzungsänderung §2, Abs.3 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 Fritz Fleiner Delegierter Satzung 0
4 3 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. 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. Emma Dampf 0
5 Resolution Die Versammlung möge beschließen, die Resolution zum Thema OpenSlides vom Ortsverband-Mitte zu verabschieden. Dr. Hilde Müller Vorstand Resolution 1
6
7

View File

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

1 Number Identifier Title Text Reason Submitter (First Name) Submitter Submitter (Last Name) Category Submitter is Group
2 1 H 1 Entlastung des Vorstandes New proposal Die Versammlung möge beschließen, den Vorstand für seine letzte Legislaturperiode zu entlasten. The assembly may decide, that everyone should eat more apples, esp. "Golden Delicious". Bericht erfolgt mündlich. Apples are very tasty. Volker John Smith Versammlungsleitung Health 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

View File

@ -0,0 +1,107 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
openslides.motion.csv_import
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Functions to import motions from a csv file.
:copyright: (c) 20112013 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.person.api import Persons
from .models import Motion, Category
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
# 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
# Check format
try:
(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(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()
# 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, warning_messages)

View File

@ -121,3 +121,34 @@ class MotionIdentifierMixin(forms.ModelForm):
raise forms.ValidationError(_('The Identifier is not unique.'))
else:
return identifier
class MotionImportForm(CssClassMixin, forms.Form):
"""
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.
"""

View File

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

View File

@ -0,0 +1,46 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{{ block.super }} {% trans 'Import motions' %} {% endblock %}
{% block content %}
<h1>
{% trans 'Import motions' %}
<small class="pull-right">
<a href="{% url 'motion_list' %}" class="btn btn-mini"><i class="icon-chevron-left"></i> {% trans 'Back to overview' %}</a>
</small>
</h1>
<p>{% trans 'Select a CSV file to import motions' %}.</p>
<p>{% trans 'Please note' %}:</p>
<ul>
<li>
{% trans 'Required comma separated values' %}:<br />
<code>({% trans 'identifier, title, text, reason, submitter (clean name), category' %})</code>
</li>
<li>
{% trans 'Identifier, reason, submitter and category are optional and may be empty' %}.
</li>
<li>
{% trans 'Required CSV file encoding: UTF-8.' %}
</li>
<li>
<a href="https://github.com/OpenSlides/OpenSlides/wiki/CSV-Import">{% trans 'Use the CSV example file from OpenSlides Wiki.' %}</a>
</li>
</ul>
<form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
{% include 'form.html' %}
<p>
<button class="btn btn-primary" type="submit">
<span class="icon import">{% trans 'Import' %}</span>
</button>
<a href="{% url 'motion_list' %}" class="btn">
{% trans 'Cancel' %}
</a>
</p>
<small>* {% trans "required" %}</small>
</form>
{% endblock %}

View File

@ -16,7 +16,7 @@
{% endif %}
{% if perms.motion.can_manage_motion %}
<a href="{% url 'motion_category_list' %}" class="btn btn-mini"><i class="icon-th-large"></i> {% trans 'Categories' %}</a>
{# <a href="{% url 'motion_import' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Import motions' %}"><i class="icon-import"></i> {% trans 'Import' %}</a>#}
<a href="{% url 'motion_csv_import' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Import motions' %}"><i class="icon-import"></i> {% trans 'Import' %}</a>
{% endif %}
<a href="{% url 'motion_list_pdf' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Print all motions as PDF' %}"><i class="icon-print"></i> PDF</a>
</small>

View File

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

View File

@ -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,44 @@ class CategoryDeleteView(DeleteView):
category_delete = CategoryDeleteView.as_view()
class MotionCSVImportView(FormView):
"""
Import motions via csv.
"""
permission_required = 'motion.can_manage_motion'
template_name = 'motion/motion_form_csv_import.html'
form_class = MotionImportForm
success_url_name = 'motion_list'
def get_initial(self, *args, **kwargs):
"""
Sets the request user as initial for the default submitter.
"""
return_value = super(MotionCSVImportView, self).get_initial(*args, **kwargs)
return_value.update({'default_submitter': self.request.user.person_id})
return return_value
def form_valid(self, form):
"""
Processes the import function.
"""
count_success, error_messages, warning_messages = import_motions(
self.request.FILES['csvfile'],
default_submitter=self.request.user,
override=form.cleaned_data['override'])
for message in error_messages:
messages.error(self.request, message)
for message in warning_messages:
messages.warning(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.

View File

@ -0,0 +1,101 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Tests for openslides.motion.models
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:copyright: 20112013 by OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details.
"""
import StringIO
import os
from django.test.client import Client
from openslides.config.api import config
from openslides.motion.models import Motion, Category
from openslides.motion.csv_import import import_motions
from openslides.participant.models import User
from openslides.utils.test import TestCase
class CSVImport(TestCase):
def setUp(self):
# Admin
self.admin = User.objects.create_superuser('Admin_ieY0Eereimeimeizuosh', 'admin@admin.admin', 'eHiK1aiRahxaix0Iequ2')
self.admin_client = Client()
self.admin_client.login(username='Admin_ieY0Eereimeimeizuosh', password='eHiK1aiRahxaix0Iequ2')
# Normal user
self.normal_user = User.objects.create_user('User_CiuNgo4giqueeChie5oi', 'user@user.user', 'eihi1Eequaek4eagaiKu')
self.normal_client = Client()
self.normal_client.login(username='User_CiuNgo4giqueeChie5oi', password='eihi1Eequaek4eagaiKu')
# Category
self.category1 = Category.objects.create(name='Satzung', prefix='S')
self.category2 = Category.objects.create(name='Resolution', prefix='R1')
self.category3 = Category.objects.create(name='Resolution', prefix='R2')
def test_example_file_de(self):
special_user = User.objects.create_user(username='Fritz_Fleiner',
email='fritz@fritz.fritz',
password='iegheeChaje7guthie4a',
first_name='Fritz',
last_name='Fleiner')
for i in range(2):
username = 'Hilde_Müller_%d' % i
User.objects.create_user(username=username,
email='hilde@hilde.hilde',
password='default',
first_name='Hilde',
last_name='Müller',
title='Dr.')
csv_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'extras', 'csv-examples')
self.assertEqual(Motion.objects.count(), 0)
with open(csv_dir + '/motions-demo_de.csv') as f:
count_success, error_messages, warning_messages = import_motions(csv_file=f, default_submitter=self.normal_user.person_id)
self.assertEqual(Motion.objects.count(), 4)
self.assertEqual(count_success, 4)
motion1 = Motion.objects.get(pk=1)
self.assertEqual(motion1.identifier, '1')
self.assertEqual(motion1.title, u'Entlastung des Vorstandes')
self.assertEqual(motion1.text, u'Die Versammlung möge beschließen, den Vorstand für seine letzte Legislaturperiode zu entlasten.')
self.assertEqual(motion1.reason, u'Bericht erfolgt mündlich.')
self.assertEqual(len(motion1.submitter.all()), 1)
self.assertEqual(motion1.submitter.all()[0].person, self.normal_user)
self.assertTrue(motion1.category is None)
self.assertTrue('Line 2 in import file: Default submitter is used.' in warning_messages)
self.assertTrue('Line 2 in import file: Category not found.' in error_messages)
motion2 = Motion.objects.get(pk=2)
self.assertEqual(motion2.identifier, 'S 2')
self.assertEqual(motion2.title, u'Satzungsänderung § 2 Abs. 3')
self.assertEqual(motion2.text, u'Die Versammlung möge beschließen, die Satzung in § 2 Abs. 3 wie folgt zu ändern:\n\n'
u'Es wird nach dem Wort "Zweck" der Satz "..." eingefügt.')
self.assertEqual(motion2.reason, u'Die Änderung der Satzung ist aufgrund der letzten Erfahrungen eine sinnvolle Maßnahme, weil ...')
self.assertEqual(len(motion2.submitter.all()), 1)
self.assertEqual(motion2.submitter.all()[0].person, special_user)
self.assertEqual(motion2.category, self.category1)
self.assertTrue('Line 5 in import file: Multiple persons found.' in error_messages)
self.assertTrue('Line 5 in import file: Multiple categories found.' in error_messages)
def test_malformed_file(self):
csv_file = StringIO.StringIO()
csv_file.write('Header\nMalformed data,\n,Title,Text,,,\n')
count_success, error_messages, warning_messages = import_motions(csv_file=csv_file, default_submitter=self.normal_user.person_id)
self.assertEqual(count_success, 1)
self.assertTrue('Ignoring malformed line 2 in import file.' in error_messages)
def test_wrong_encoding(self):
csv_file = StringIO.StringIO()
text = u'Müller'.encode('iso-8859-15')
csv_file.write(text)
csv_file.seek(0)
count_success, error_messages, warning_messages = import_motions(csv_file=csv_file, default_submitter=self.normal_user.person_id)
self.assertEqual(count_success, 0)
self.assertTrue('Encoding error in import file. Ensure using UTF-8.' in error_messages)