Added CSV import, fixed #1186. Also cleaned up motion and user CSV import.

This commit is contained in:
Norman Jäckel 2014-03-27 20:30:15 +01:00
parent 6a7784153d
commit 2a10feecad
23 changed files with 354 additions and 163 deletions

View File

@ -8,6 +8,8 @@ Version 1.6 (unreleased)
======================== ========================
[https://github.com/OpenSlides/OpenSlides/issues?milestone=14] [https://github.com/OpenSlides/OpenSlides/issues?milestone=14]
Agenda:
- Added CSV import.
Assignment: Assignment:
- Coupled assignment candidates with list of speakers. - Coupled assignment candidates with list of speakers.
Participants: Participants:

View File

@ -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
1 Titel Text Dauer
2 Begrüßung Begrüßung durch den Vorstand 10
3 Bericht des Vorstands Es spricht Herr Müller 30
4 Entlastung des Vorstandes 10
5 Sonstiges 5

View File

@ -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
1 Title Text Duration
2 Welcome Mr. Miller 10
3 Presentation of employee of the year award Ms. Schmidt 20
4 Exchange of small gifts 10
5 Dinner and dancing 120

View File

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

View File

@ -0,0 +1,47 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans 'Import agenda items' %} {{ block.super }}{% endblock %}
{% block content %}
<h1>
{% trans 'Import agenda items' %}
<small class="pull-right">
<a href="{% url 'item_overview' %}" 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 agenda items' %}.</p>
<p>{% trans 'Please note' %}:</p>
<ul>
<li>
{% trans 'Required comma separated values' %}:<br />
<code>{% trans 'title, text, duration' %}</code>
</li>
<li>
{% trans 'Text and duration are optional and may be empty' %}.
</li>
<li>{% trans 'The first line (header) is ignored' %}.</li>
<li>
{% trans 'Required CSV file encoding is 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 'item_overview' %}" class="btn">
{% trans 'Cancel' %}
</a>
</p>
<small>* {% trans "required" %}</small>
</form>
{% endblock %}

View File

@ -36,6 +36,7 @@
<small class="pull-right"> <small class="pull-right">
{% if perms.agenda.can_manage_agenda %} {% if perms.agenda.can_manage_agenda %}
<a href="{% url 'item_new' %}" class="btn btn-mini btn-primary" rel="tooltip" data-original-title="{% trans 'New item' %}"><i class="icon-plus icon-white"></i> {% trans "New" %}</a> <a href="{% url 'item_new' %}" class="btn btn-mini btn-primary" rel="tooltip" data-original-title="{% trans 'New item' %}"><i class="icon-plus icon-white"></i> {% trans "New" %}</a>
<a href="{% url 'item_csv_import' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Import agenda items' %}"><i class="icon-import"></i> {% trans "Import" %}</a>
{% endif %} {% endif %}
<a href="{% url 'print_agenda' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Print agenda as PDF' %}" target="_blank"><i class="icon-print"></i> PDF</a> <a href="{% url 'print_agenda' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Print agenda as PDF' %}" target="_blank"><i class="icon-print"></i> PDF</a>
</small> </small>

View File

@ -87,5 +87,8 @@ urlpatterns = patterns(
url(r'^list_of_speakers/end_speach/$', url(r'^list_of_speakers/end_speach/$',
views.CurrentListOfSpeakersView.as_view(end_speach=True), 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'))

View File

@ -16,10 +16,18 @@ from openslides.projector.api import get_active_slide, update_projector
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.pdf import stylesheet from openslides.utils.pdf import stylesheet
from openslides.utils.utils import html_strong from openslides.utils.utils import html_strong
from openslides.utils.views import (CreateView, DeleteView, FormView, PDFView, from openslides.utils.views import (
RedirectView, SingleObjectMixin, CreateView,
TemplateView, UpdateView) CSVImportView,
DeleteView,
FormView,
PDFView,
RedirectView,
SingleObjectMixin,
TemplateView,
UpdateView)
from .csv_import import import_agenda_items
from .forms import AppendSpeakerForm, ItemForm, ItemOrderForm, RelatedItemForm from .forms import AppendSpeakerForm, ItemForm, ItemOrderForm, RelatedItemForm
from .models import Item, Speaker from .models import Item, Speaker
@ -620,3 +628,13 @@ class CurrentListOfSpeakersView(RedirectView):
return reverse('core_dashboard') return reverse('core_dashboard')
else: else:
return reverse('item_view', args=[item.pk]) 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'

View File

@ -17,14 +17,14 @@ from openslides.utils.utils import html_strong
from .models import Category, Motion 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. Imports motions from a csv file.
The file must be encoded in utf8. The first line (header) is ignored. 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 If no or multiple submitters found, the default submitter is used. If
a motion with a given identifier already exists, the motion is overridden, 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. the category is set to None.
""" """
count_success = 0 count_success = 0
@ -32,18 +32,18 @@ def import_motions(csv_file, default_submitter, override=False, importing_person
# Check encoding # Check encoding
try: try:
csv_file.read().decode('utf8') csvfile.read().decode('utf8')
except UnicodeDecodeError: except UnicodeDecodeError:
return (0, 0, [_('Import file has wrong character encoding, only UTF-8 is supported!')], []) return '', '', _('Import file has wrong character encoding, only UTF-8 is supported!')
csv_file.seek(0) csvfile.seek(0)
with transaction.commit_on_success(): with transaction.commit_on_success():
dialect = csv.Sniffer().sniff(csv_file.readline()) dialect = csv.Sniffer().sniff(csvfile.readline())
dialect = csv_ext.patchup(dialect) dialect = csv_ext.patchup(dialect)
csv_file.seek(0) csvfile.seek(0)
all_error_messages = [] all_error_messages = []
all_warning_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 = [] warning = []
if line_no < 1: if line_no < 1:
# Do not read the header line # Do not read the header line
@ -115,7 +115,7 @@ def import_motions(csv_file, default_submitter, override=False, importing_person
count_success += 1 count_success += 1
# Build final error message with all error items (one bullet point for each csv line) # 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: if all_error_messages:
full_error_message = "%s <ul>" % html_strong(_("Errors")) full_error_message = "%s <ul>" % html_strong(_("Errors"))
for error in all_error_messages: for error in all_error_messages:
@ -123,11 +123,20 @@ def import_motions(csv_file, default_submitter, override=False, importing_person
full_error_message += "</ul>" full_error_message += "</ul>"
# Build final warning message with all warning items (one bullet point for each csv line) # 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: if all_warning_messages:
full_warning_message = "%s <ul>" % html_strong(_("Warnings")) full_warning_message = "%s <ul>" % html_strong(_("Warnings"))
for warning in all_warning_messages: for warning in all_warning_messages:
full_warning_message += "<li>%s</li>" % warning full_warning_message += "<li>%s</li>" % warning
full_warning_message += "</ul>" full_warning_message += "</ul>"
return (count_success, count_lines, [full_error_message], [full_warning_message]) # Build final success message
if count_success:
success_message = '<strong>%s</strong><br>%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

View File

@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy
from openslides.config.api import config from openslides.config.api import config
from openslides.mediafile.models import Mediafile from openslides.mediafile.models import Mediafile
from openslides.utils.forms import (CleanHtmlFormMixin, CssClassMixin, from openslides.utils.forms import (CleanHtmlFormMixin, CssClassMixin,
LocalizedModelChoiceField) CSVImportForm, LocalizedModelChoiceField)
from openslides.utils.person import MultiplePersonFormField, PersonFormField from openslides.utils.person import MultiplePersonFormField, PersonFormField
from .models import Category, Motion, Workflow 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.')) '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. 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( override = forms.BooleanField(
required=False, required=False,
label=ugettext_lazy('Override existing motions with the same identifier'), label=ugettext_lazy('Override existing motions with the same identifier'),

View File

@ -18,7 +18,7 @@
<ul> <ul>
<li> <li>
{% trans 'Required comma separated values' %}:<br /> {% trans 'Required comma separated values' %}:<br />
<code>({% trans 'identifier, title, text, reason, submitter (clean name), category' %})</code> <code>{% trans 'identifier, title, text, reason, submitter (clean name), category' %}</code>
</li> </li>
<li> <li>
{% trans 'Identifier, reason, submitter and category are optional and may be empty' %}. {% trans 'Identifier, reason, submitter and category are optional and may be empty' %}.

View File

@ -13,14 +13,14 @@ from openslides.config.api import config
from openslides.poll.views import PollFormView from openslides.poll.views import PollFormView
from openslides.projector.api import get_active_slide, update_projector from openslides.projector.api import get_active_slide, update_projector
from openslides.utils.utils import html_strong, htmldiff from openslides.utils.utils import html_strong, htmldiff
from openslides.utils.views import (CreateView, DeleteView, DetailView, from openslides.utils.views import (CreateView, CSVImportView, DeleteView, DetailView,
FormView, ListView, PDFView, QuestionView, ListView, PDFView, QuestionView,
RedirectView, SingleObjectMixin, UpdateView) RedirectView, SingleObjectMixin, UpdateView)
from .csv_import import import_motions from .csv_import import import_motions
from .forms import (BaseMotionForm, MotionCategoryMixin, from .forms import (BaseMotionForm, MotionCategoryMixin,
MotionDisableVersioningMixin, MotionIdentifierMixin, MotionDisableVersioningMixin, MotionIdentifierMixin,
MotionImportForm, MotionSubmitterMixin, MotionCSVImportForm, MotionSubmitterMixin,
MotionSupporterMixin, MotionWorkflowMixin) MotionSupporterMixin, MotionWorkflowMixin)
from .models import (Category, Motion, MotionPoll, MotionSubmitter, from .models import (Category, Motion, MotionPoll, MotionSubmitter,
MotionSupporter, MotionVersion, State) MotionSupporter, MotionVersion, State)
@ -769,14 +769,14 @@ class CategoryDeleteView(DeleteView):
category_delete = CategoryDeleteView.as_view() 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' permission_required = 'motion.can_manage_motion'
template_name = 'motion/motion_form_csv_import.html'
form_class = MotionImportForm
success_url_name = 'motion_list' success_url_name = 'motion_list'
template_name = 'motion/motion_form_csv_import.html'
def get_initial(self, *args, **kwargs): def get_initial(self, *args, **kwargs):
""" """
@ -787,25 +787,11 @@ class MotionCSVImportView(FormView):
return return_value return return_value
def form_valid(self, form): def form_valid(self, form):
""" success, warning, error = import_motions(importing_person=self.request.user, **form.cleaned_data)
Processes the import function. messages.success(self.request, success)
""" messages.warning(self.request, warning)
count_success, count_lines, error_messages, warning_messages = import_motions( messages.error(self.request, error)
self.request.FILES['csvfile'], # Overleap method of CSVImportView
default_submitter=form.cleaned_data['default_submitter'], return super(CSVImportView, self).form_valid(form)
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,
"<strong>%s</strong><br>%s" % (
_('Summary'),
_('%(counts)d of %(total)d motions successfully imported.')
% {'counts': count_success, 'total': count_lines}))
return super(MotionCSVImportView, self).form_valid(form)
motion_csv_import = MotionCSVImportView.as_view() motion_csv_import = MotionCSVImportView.as_view()

View File

@ -1,13 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import csv
from random import choice 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 from .models import Group, User
@ -47,64 +41,6 @@ def gen_username(first_name, last_name):
return test_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(): def get_registered_group():
""" """
Returns the group 'Registered' (pk=2). Returns the group 'Registered' (pk=2).

View File

@ -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 <ul>" % html_strong(_("Errors"))
for error in error_messages:
full_error_message += "<li>%s</li>" % error
full_error_message += "</ul>"
return success_message, '', full_error_message

View File

@ -172,8 +172,3 @@ class UsersettingsForm(CssClassMixin, forms.ModelForm):
model = User model = User
fields = ('user_name', 'title', 'first_name', 'last_name', 'gender', 'email', fields = ('user_name', 'title', 'first_name', 'last_name', 'gender', 'email',
'committee', 'about_me') 'committee', 'about_me')
class UserImportForm(CssClassMixin, forms.Form):
csvfile = forms.FileField(widget=forms.FileInput(attrs={'size': '50'}),
label=ugettext_lazy('CSV File'))

View File

@ -23,7 +23,7 @@
<a href="{% url 'user_new' %}" class="btn btn-mini btn-primary" rel="tooltip" data-original-title="{% trans 'New participant' %}"><i class="icon-plus icon-white"></i> {% trans "New" %}</a> <a href="{% url 'user_new' %}" class="btn btn-mini btn-primary" rel="tooltip" data-original-title="{% trans 'New participant' %}"><i class="icon-plus icon-white"></i> {% trans "New" %}</a>
<a href="{% url 'user_new_multiple' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'New multiple participants' %}"><i class="icon-plus"></i> {% trans 'New multiple' %}</a> <a href="{% url 'user_new_multiple' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'New multiple participants' %}"><i class="icon-plus"></i> {% trans 'New multiple' %}</a>
<a href="{% url 'user_group_overview' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'All groups' %}"><i class="icon-group"></i> {% trans "Groups" %}</a> <a href="{% url 'user_group_overview' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'All groups' %}"><i class="icon-group"></i> {% trans "Groups" %}</a>
<a href="{% url 'user_import' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Import participants' %}"><i class="icon-import"></i> {% trans 'Import' %}</a> <a href="{% url 'user_csv_import' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Import participants' %}"><i class="icon-import"></i> {% trans 'Import' %}</a>
{% endif %} {% endif %}
{% if perms.participant.can_see_participant and perms.participant.can_manage_participant %} {% if perms.participant.can_see_participant and perms.participant.can_manage_participant %}
<div class="btn-group"> <div class="btn-group">

View File

@ -18,11 +18,11 @@
<ul> <ul>
<li> <li>
{% trans 'Required comma separated values' %}:<br> {% trans 'Required comma separated values' %}:<br>
<code>({% trans 'title, first name, last name, gender, email, group id, structure level, committee, about me, comment, is active' %})</code> <code>{% trans 'title, first name, last name, gender, email, group id, structure level, committee, about me, comment, is active' %}</code>
</li> </li>
<li> <li>
{% trans 'Default groups' %}: {% trans 'Default groups' %}:
{% trans 'Delegate' %} (<code>3</code>), {% trans 'Staff' %} (<code>4</code>) {% trans 'Delegate' %} <code>3</code>, {% trans 'Staff' %} <code>4</code>
</li> </li>
<li> <li>
{% trans 'At least first name or last name have to be filled in. All other fields are optional and may be empty.' %} {% 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 @@
<li> <li>
{% trans 'Required CSV file encoding is UTF-8' %}. {% trans 'Required CSV file encoding is UTF-8' %}.
</li> </li>
<li><a href="https://github.com/OpenSlides/OpenSlides/wiki/CSV-Import" target="_blank">{% trans 'Use the CSV example file from OpenSlides Wiki.' %}</a></li> <li><a href="https://github.com/OpenSlides/OpenSlides/wiki/CSV-Import">{% trans 'Use the CSV example file from OpenSlides Wiki.' %}</a></li>
</ul> </ul>
<form enctype="multipart/form-data" action="" method="post">{% csrf_token %} <form enctype="multipart/form-data" action="" method="post">{% csrf_token %}

View File

@ -50,9 +50,9 @@ urlpatterns = patterns(
{'action': 'toggle'}, {'action': 'toggle'},
name='user_status_toggle'), name='user_status_toggle'),
url(r'^import/$', url(r'^csv_import/$',
views.UserImportView.as_view(), views.UserCSVImportView.as_view(),
name='user_import'), name='user_csv_import'),
# Group # Group
url(r'^group/$', url(r'^group/$',

View File

@ -13,13 +13,14 @@ from django.utils.translation import activate, ugettext_lazy
from openslides.config.api import config from openslides.config.api import config
from openslides.utils.utils import (delete_default_permissions, html_strong, from openslides.utils.utils import (delete_default_permissions, html_strong,
template) template)
from openslides.utils.views import (CreateView, DeleteView, DetailView, from openslides.utils.views import (CreateView, CSVImportView, DeleteView, DetailView,
FormView, ListView, PDFView, FormView, ListView, PDFView,
PermissionMixin, QuestionView, PermissionMixin, QuestionView,
RedirectView, SingleObjectMixin, UpdateView) RedirectView, SingleObjectMixin, UpdateView)
from .api import gen_password, gen_username, import_users from .api import gen_password, gen_username
from .forms import (GroupForm, UserCreateForm, UserMultipleCreateForm, UserImportForm, from .csv_import import import_users
from .forms import (GroupForm, UserCreateForm, UserMultipleCreateForm,
UsersettingsForm, UserUpdateForm) UsersettingsForm, UserUpdateForm)
from .models import get_protected_perm, Group, User from .models import get_protected_perm, Group, User
from .pdf import participants_to_pdf, participants_passwords_to_pdf from .pdf import participants_to_pdf, participants_passwords_to_pdf
@ -237,25 +238,14 @@ class ParticipantsPasswordsPDF(PDFView):
participants_passwords_to_pdf(pdf) participants_passwords_to_pdf(pdf)
class UserImportView(FormView): class UserCSVImportView(CSVImportView):
""" """
Import Users via csv. Import users via CSV.
""" """
import_function = staticmethod(import_users)
permission_required = 'participant.can_manage_participant' permission_required = 'participant.can_manage_participant'
template_name = 'participant/import.html'
form_class = UserImportForm
success_url_name = 'user_overview' success_url_name = 'user_overview'
template_name = 'participant/user_form_csv_import.html'
def form_valid(self, form):
# check for valid encoding (will raise UnicodeDecodeError if not)
success, error_messages = import_users(self.request.FILES['csvfile'])
for message in error_messages:
messages.error(self.request, message)
if success:
messages.success(
self.request,
_('%d new participants were successfully imported.') % success)
return super(UserImportView, self).form_valid(form)
class ResetPasswordView(SingleObjectMixin, QuestionView): class ResetPasswordView(SingleObjectMixin, QuestionView):

View File

@ -98,3 +98,13 @@ class CleanHtmlFormMixin(object):
# The field 'field' is not pressent. Do not change cleaned_data # The field 'field' is not pressent. Do not change cleaned_data
pass pass
return cleaned_data return cleaned_data
class CSVImportForm(CssClassMixin, forms.Form):
"""
Form for the CSVImportView.
"""
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.'))

View File

@ -19,6 +19,7 @@ from reportlab.lib.units import cm
from reportlab.platypus import SimpleDocTemplate, Spacer from reportlab.platypus import SimpleDocTemplate, Spacer
from .exceptions import OpenSlidesError from .exceptions import OpenSlidesError
from .forms import CSVImportForm
from .pdf import firstPage, laterPages from .pdf import firstPage, laterPages
from .signals import template_manipulation from .signals import template_manipulation
from .utils import html_strong from .utils import html_strong
@ -581,3 +582,38 @@ class PDFView(PermissionMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return self.render_to_response(self.get_filename()) return self.render_to_response(self.get_filename())
class CSVImportView(FormView):
"""
View for a csv import of some data.
The attribute import_function might to be a staticmethod.
"""
form_class = CSVImportForm
import_function = None
def get_import_function(self):
"""
Override this to return a specific function to import data from
a given csv file using some extra kwargs. This function has to
return a three-tuple of strings which are the messages for the
user.
Example function:
def my_import(csvfile, **kwargs):
# Parse file and import data
return success_message, warning_message, error_message
"""
if self.import_function is None:
raise NotImplementedError('A CSVImportView must provide an import_function '
'attribute or override a get_import_function method.')
return self.import_function
def form_valid(self, form):
success, warning, error = self.get_import_function()(**form.cleaned_data)
messages.success(self.request, success)
messages.warning(self.request, warning)
messages.error(self.request, error)
return super(CSVImportView, self).form_valid(form)

View File

@ -2,6 +2,7 @@
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test.client import Client from django.test.client import Client
from mock import patch from mock import patch
@ -275,6 +276,18 @@ class ViewTest(TestCase):
response = client.get('/agenda/2/') response = client.get('/agenda/2/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_csv_import(self):
item_number = Item.objects.all().count()
new_csv_file = SimpleUploadedFile(
name='new_csv_file.csv',
content='Title,text,duration\nTitle thei5KieK6ohphuilahs,Text Chai1ioWae3ASh0Eloh1,42\n,Bad line\n')
self.adminClient.post('/agenda/csv_import/', {'csvfile': new_csv_file})
self.assertEqual(Item.objects.all().count(), item_number + 1)
item = Item.objects.get(pk=3)
self.assertEqual(item.title, 'Title thei5KieK6ohphuilahs')
self.assertEqual(item.text, 'Text Chai1ioWae3ASh0Eloh1')
self.assertEqual(item.duration, '42')
class ConfigTest(TestCase): class ConfigTest(TestCase):
def setUp(self): def setUp(self):

View File

@ -43,9 +43,9 @@ class CSVImport(TestCase):
csv_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'extras', 'csv-examples') csv_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'extras', 'csv-examples')
self.assertEqual(Motion.objects.count(), 0) self.assertEqual(Motion.objects.count(), 0)
with open(csv_dir + '/motions-demo_de.csv') as f: with open(csv_dir + '/motions-demo_de.csv') as f:
count_success, count_lines, error_messages, warning_messages = import_motions(csv_file=f, default_submitter=self.normal_user.person_id) success_message, warning_message, error_message = import_motions(
csvfile=f, default_submitter=self.normal_user.person_id, override=False, importing_person=self.admin)
self.assertEqual(Motion.objects.count(), 11) self.assertEqual(Motion.objects.count(), 11)
self.assertEqual(count_success, 11)
motion1 = Motion.objects.get(pk=1) motion1 = Motion.objects.get(pk=1)
self.assertEqual(motion1.identifier, '1') self.assertEqual(motion1.identifier, '1')
@ -55,8 +55,8 @@ class CSVImport(TestCase):
self.assertEqual(len(motion1.submitter.all()), 1) self.assertEqual(len(motion1.submitter.all()), 1)
self.assertEqual(motion1.submitter.all()[0].person, self.normal_user) self.assertEqual(motion1.submitter.all()[0].person, self.normal_user)
self.assertTrue(motion1.category is None) self.assertTrue(motion1.category is None)
self.assertTrue(any('Submitter unknown.' in w for w in warning_messages)) self.assertTrue('Submitter unknown.' in warning_message)
self.assertTrue(any('Category unknown.' in w for w in warning_messages)) self.assertTrue('Category unknown.' in warning_message)
motion2 = Motion.objects.get(pk=2) motion2 = Motion.objects.get(pk=2)
self.assertEqual(motion2.identifier, 'SA 1') self.assertEqual(motion2.identifier, 'SA 1')
@ -69,25 +69,26 @@ class CSVImport(TestCase):
self.assertEqual(motion2.category, self.category1) self.assertEqual(motion2.category, self.category1)
# check user 'John Doe' # check user 'John Doe'
self.assertTrue(any('Several suitable submitters found.' in w for w in warning_messages)) self.assertTrue('Several suitable submitters found.' in warning_message)
# check category 'Bildung' # check category 'Bildung'
self.assertTrue(any('Several suitable categories found.' in w for w in warning_messages)) self.assertTrue('Several suitable categories found.' in warning_message)
def test_malformed_file(self): def test_malformed_file(self):
csv_file = StringIO.StringIO() csv_file = StringIO.StringIO()
csv_file.write('Header\nMalformed data,\n,Title,Text,,,\n') csv_file.write('Header\nMalformed data,\n,Title,Text,,,\n')
count_success, count_lines, error_messages, warning_messages = import_motions(csv_file=csv_file, default_submitter=self.normal_user.person_id) success_message, warning_message, error_message = import_motions(
self.assertEqual(count_success, 0) csvfile=csv_file, default_submitter=self.normal_user.person_id, override=False)
self.assertTrue(any('Line is malformed.' in e for e in error_messages)) self.assertEqual(success_message, '')
self.assertTrue('Line is malformed.' in error_message)
def test_wrong_encoding(self): def test_wrong_encoding(self):
csv_file = StringIO.StringIO() csv_file = StringIO.StringIO()
text = u'Müller'.encode('iso-8859-15') text = u'Müller'.encode('iso-8859-15')
csv_file.write(text) csv_file.write(text)
csv_file.seek(0) csv_file.seek(0)
count_success, count_lines, error_messages, warning_messages = import_motions( success_message, warning_message, error_message = import_motions(
csv_file=csv_file, csvfile=csv_file,
default_submitter=self.normal_user.person_id) default_submitter=self.normal_user.person_id,
self.assertEqual(count_success, 0) override=False)
self.assertEqual(count_lines, 0) self.assertEqual(success_message, '')
self.assertTrue('Import file has wrong character encoding, only UTF-8 is supported!' in error_messages) self.assertTrue('Import file has wrong character encoding, only UTF-8 is supported!' in error_message)