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 'Select a CSV file to import agenda items' %}.
+
+ {% trans 'Please note' %}:
+
+ -
+ {% trans 'Required comma separated values' %}:
+ {% trans 'title, text, duration' %}
+
+ -
+ {% trans 'Text and duration are optional and may be empty' %}.
+
+ - {% trans 'The first line (header) is ignored' %}.
+ -
+ {% trans 'Required CSV file encoding is UTF-8' %}.
+
+ -
+ {% trans 'Use the CSV example file from OpenSlides Wiki.' %}
+
+
+
+
+{% 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 " % html_strong(_("Errors"))
for error in all_error_messages:
@@ -123,11 +123,20 @@ def import_motions(csv_file, default_submitter, override=False, importing_person
full_error_message += "
"
# 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 " % html_strong(_("Warnings"))
for warning in all_warning_messages:
full_warning_message += "- %s
" % warning
full_warning_message += "
"
- 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 @@
-
{% trans 'Required comma separated values' %}:
- ({% trans 'identifier, title, text, reason, submitter (clean name), category' %})
+ {% trans 'identifier, title, text, reason, submitter (clean name), category' %}
-
{% trans 'Identifier, reason, submitter and category are optional and may be empty' %}.
diff --git a/openslides/motion/views.py b/openslides/motion/views.py
index f2d6627d0..ec547d313 100644
--- a/openslides/motion/views.py
+++ b/openslides/motion/views.py
@@ -13,14 +13,14 @@ from openslides.config.api import config
from openslides.poll.views import PollFormView
from openslides.projector.api import get_active_slide, update_projector
from openslides.utils.utils import html_strong, htmldiff
-from openslides.utils.views import (CreateView, DeleteView, DetailView,
- FormView, ListView, PDFView, QuestionView,
+from openslides.utils.views import (CreateView, CSVImportView, DeleteView, DetailView,
+ ListView, PDFView, QuestionView,
RedirectView, SingleObjectMixin, UpdateView)
from .csv_import import import_motions
from .forms import (BaseMotionForm, MotionCategoryMixin,
MotionDisableVersioningMixin, MotionIdentifierMixin,
- MotionImportForm, MotionSubmitterMixin,
+ MotionCSVImportForm, MotionSubmitterMixin,
MotionSupporterMixin, MotionWorkflowMixin)
from .models import (Category, Motion, MotionPoll, MotionSubmitter,
MotionSupporter, MotionVersion, State)
@@ -769,14 +769,14 @@ class CategoryDeleteView(DeleteView):
category_delete = CategoryDeleteView.as_view()
-class MotionCSVImportView(FormView):
+class MotionCSVImportView(CSVImportView):
"""
- Import motions via csv.
+ Imports motions from an uploaded csv file.
"""
+ form_class = MotionCSVImportForm
permission_required = 'motion.can_manage_motion'
- template_name = 'motion/motion_form_csv_import.html'
- form_class = MotionImportForm
success_url_name = 'motion_list'
+ template_name = 'motion/motion_form_csv_import.html'
def get_initial(self, *args, **kwargs):
"""
@@ -787,25 +787,11 @@ class MotionCSVImportView(FormView):
return return_value
def form_valid(self, form):
- """
- Processes the import function.
- """
- count_success, count_lines, error_messages, warning_messages = import_motions(
- self.request.FILES['csvfile'],
- default_submitter=form.cleaned_data['default_submitter'],
- override=form.cleaned_data['override'],
- importing_person=self.request.user)
- 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,
- "%s
%s" % (
- _('Summary'),
- _('%(counts)d of %(total)d motions successfully imported.')
- % {'counts': count_success, 'total': count_lines}))
- return super(MotionCSVImportView, self).form_valid(form)
+ success, warning, error = import_motions(importing_person=self.request.user, **form.cleaned_data)
+ messages.success(self.request, success)
+ messages.warning(self.request, warning)
+ messages.error(self.request, error)
+ # Overleap method of CSVImportView
+ return super(CSVImportView, self).form_valid(form)
motion_csv_import = MotionCSVImportView.as_view()
diff --git a/openslides/participant/api.py b/openslides/participant/api.py
index dcd60294a..a52875266 100644
--- a/openslides/participant/api.py
+++ b/openslides/participant/api.py
@@ -1,13 +1,7 @@
# -*- coding: utf-8 -*-
-import csv
from random import choice
-from django.db import transaction
-from django.utils.translation import ugettext as _
-
-from openslides.utils import csv_ext
-
from .models import Group, User
@@ -47,64 +41,6 @@ def gen_username(first_name, last_name):
return test_name
-def import_users(csv_file):
- error_messages = []
- count_success = 0
- try:
- # check for valid encoding (will raise UnicodeDecodeError if not)
- csv_file.read().decode('utf-8')
- csv_file.seek(0)
-
- with transaction.commit_on_success():
- dialect = csv.Sniffer().sniff(csv_file.readline())
- dialect = csv_ext.patchup(dialect)
- csv_file.seek(0)
-
- for (line_no, line) in enumerate(csv.reader(csv_file,
- dialect=dialect)):
- if line_no:
- try:
- (title, first_name, last_name, gender, email, groups,
- structure_level, committee, about_me, comment, is_active) = line[:11]
- except ValueError:
- error_messages.append(_('Ignoring malformed line %d in import file.') % (line_no + 1))
- continue
- user = User()
- user.title = title
- user.last_name = last_name
- user.first_name = first_name
- user.username = gen_username(first_name, last_name)
- user.gender = gender
- user.email = email
- user.structure_level = structure_level
- user.committee = committee
- user.about_me = about_me
- user.comment = comment
- if is_active == '1':
- user.is_active = True
- else:
- user.is_active = False
- user.default_password = gen_password()
- user.save()
- for groupid in groups:
- try:
- if groupid != ",":
- Group.objects.get(pk=groupid).user_set.add(user)
- except ValueError:
- error_messages.append(_('Ignoring malformed group id in line %d.') % (line_no + 1))
- continue
- except Group.DoesNotExist:
- error_messages.append(_('Group id %(id)s does not exists (line %(line)d).') % {'id': groupid, 'line': line_no + 1})
- continue
- user.reset_password()
- count_success += 1
- except csv.Error:
- error_messages.append(_('Import aborted because of severe errors in the input file.'))
- except UnicodeDecodeError:
- error_messages.append(_('Import file has wrong character encoding, only UTF-8 is supported!'))
- return (count_success, error_messages)
-
-
def get_registered_group():
"""
Returns the group 'Registered' (pk=2).
diff --git a/openslides/participant/csv_import.py b/openslides/participant/csv_import.py
new file mode 100644
index 000000000..55c16c2c4
--- /dev/null
+++ b/openslides/participant/csv_import.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+
+import csv
+
+from django.db import transaction
+from django.utils.translation import ugettext as _
+
+from openslides.utils import csv_ext
+from openslides.utils.utils import html_strong
+
+from .api import gen_password, gen_username
+from .models import Group, User
+
+
+def import_users(csvfile):
+ error_messages = []
+ count_success = 0
+ try:
+ # check for valid encoding (will raise UnicodeDecodeError if not)
+ csvfile.read().decode('utf-8')
+ csvfile.seek(0)
+
+ with transaction.commit_on_success():
+ dialect = csv.Sniffer().sniff(csvfile.readline())
+ dialect = csv_ext.patchup(dialect)
+ csvfile.seek(0)
+
+ for (line_no, line) in enumerate(csv.reader(csvfile,
+ dialect=dialect)):
+ if line_no:
+ try:
+ (title, first_name, last_name, gender, email, groups,
+ structure_level, committee, about_me, comment, is_active) = line[:11]
+ except ValueError:
+ error_messages.append(_('Ignoring malformed line %d in import file.') % (line_no + 1))
+ continue
+ if not first_name and not last_name:
+ error_messages.append(_("In line %d you have to provide either 'first_name' or 'last_name'.") % (line_no + 1))
+ continue
+ user = User()
+ user.title = title
+ user.last_name = last_name
+ user.first_name = first_name
+ user.username = gen_username(first_name, last_name)
+ user.gender = gender
+ user.email = email
+ user.structure_level = structure_level
+ user.committee = committee
+ user.about_me = about_me
+ user.comment = comment
+ if is_active == '1':
+ user.is_active = True
+ else:
+ user.is_active = False
+ user.default_password = gen_password()
+ user.save()
+ for groupid in groups:
+ try:
+ if groupid != ",":
+ Group.objects.get(pk=groupid).user_set.add(user)
+ except ValueError:
+ error_messages.append(_('Ignoring malformed group id in line %d.') % (line_no + 1))
+ continue
+ except Group.DoesNotExist:
+ error_messages.append(_('Group id %(id)s does not exists (line %(line)d).') % {'id': groupid, 'line': line_no + 1})
+ continue
+ user.reset_password()
+ count_success += 1
+ except csv.Error:
+ error_messages.append(_('Import aborted because of severe errors in the input file.'))
+ except UnicodeDecodeError:
+ error_messages.append(_('Import file has wrong character encoding, only UTF-8 is supported!'))
+
+ # Build final success message
+ if count_success:
+ success_message = _('%d new participants were successfully imported.') % count_success
+ else:
+ success_message = ''
+
+ # Build final error message with all error items (one bullet point for each csv line)
+ full_error_message = ''
+ if error_messages:
+ full_error_message = "%s " % html_strong(_("Errors"))
+ for error in error_messages:
+ full_error_message += "- %s
" % error
+ full_error_message += "
"
+
+ return success_message, '', full_error_message
diff --git a/openslides/participant/forms.py b/openslides/participant/forms.py
index f4065a853..7504110b7 100644
--- a/openslides/participant/forms.py
+++ b/openslides/participant/forms.py
@@ -172,8 +172,3 @@ class UsersettingsForm(CssClassMixin, forms.ModelForm):
model = User
fields = ('user_name', 'title', 'first_name', 'last_name', 'gender', 'email',
'committee', 'about_me')
-
-
-class UserImportForm(CssClassMixin, forms.Form):
- csvfile = forms.FileField(widget=forms.FileInput(attrs={'size': '50'}),
- label=ugettext_lazy('CSV File'))
diff --git a/openslides/participant/templates/participant/overview.html b/openslides/participant/templates/participant/overview.html
index bd73e1e4d..7e4cacd3b 100644
--- a/openslides/participant/templates/participant/overview.html
+++ b/openslides/participant/templates/participant/overview.html
@@ -23,7 +23,7 @@
{% trans "New" %}
{% trans 'New multiple' %}
{% trans "Groups" %}
- {% trans 'Import' %}
+ {% trans 'Import' %}
{% endif %}
{% if perms.participant.can_see_participant and perms.participant.can_manage_participant %}
diff --git a/openslides/participant/templates/participant/import.html b/openslides/participant/templates/participant/user_form_csv_import.html
similarity index 81%
rename from openslides/participant/templates/participant/import.html
rename to openslides/participant/templates/participant/user_form_csv_import.html
index 34b7661ef..22d1581de 100644
--- a/openslides/participant/templates/participant/import.html
+++ b/openslides/participant/templates/participant/user_form_csv_import.html
@@ -18,11 +18,11 @@
-
{% trans 'Required comma separated values' %}:
- ({% trans 'title, first name, last name, gender, email, group id, structure level, committee, about me, comment, is active' %})
+ {% trans 'title, first name, last name, gender, email, group id, structure level, committee, about me, comment, is active' %}
-
{% trans 'Default groups' %}:
- {% trans 'Delegate' %} (
3
), {% trans 'Staff' %} (4
)
+ {% trans 'Delegate' %} 3
, {% trans 'Staff' %} 4
-
{% trans 'At least first name or last name have to be filled in. All other fields are optional and may be empty.' %}
@@ -31,7 +31,7 @@
-
{% trans 'Required CSV file encoding is UTF-8' %}.
- - {% trans 'Use the CSV example file from OpenSlides Wiki.' %}
+ - {% trans 'Use the CSV example file from OpenSlides Wiki.' %}