Merge pull request #1226 from normanjaeckel/AgendaImport
Added CSV import, fixed #1186.
This commit is contained in:
commit
1583907571
@ -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:
|
||||
|
5
extras/csv-examples/agenda-demo_de.csv
Normal file
5
extras/csv-examples/agenda-demo_de.csv
Normal 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
|
|
5
extras/csv-examples/agenda-demo_en.csv
Normal file
5
extras/csv-examples/agenda-demo_en.csv
Normal 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
|
|
54
openslides/agenda/csv_import.py
Normal file
54
openslides/agenda/csv_import.py
Normal 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
|
47
openslides/agenda/templates/agenda/item_form_csv_import.html
Normal file
47
openslides/agenda/templates/agenda/item_form_csv_import.html
Normal 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 %}
|
@ -36,6 +36,7 @@
|
||||
<small class="pull-right">
|
||||
{% 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_csv_import' %}" class="btn btn-mini" rel="tooltip" data-original-title="{% trans 'Import agenda items' %}"><i class="icon-import"></i> {% trans "Import" %}</a>
|
||||
{% 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>
|
||||
</small>
|
||||
|
@ -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'))
|
||||
|
@ -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'
|
||||
|
@ -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 <ul>" % 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 += "</ul>"
|
||||
|
||||
# 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 <ul>" % html_strong(_("Warnings"))
|
||||
for warning in all_warning_messages:
|
||||
full_warning_message += "<li>%s</li>" % warning
|
||||
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
|
||||
|
@ -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'),
|
||||
|
@ -18,7 +18,7 @@
|
||||
<ul>
|
||||
<li>
|
||||
{% 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>
|
||||
{% trans 'Identifier, reason, submitter and category are optional and may be empty' %}.
|
||||
|
@ -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,
|
||||
"<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)
|
||||
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()
|
||||
|
@ -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).
|
||||
|
88
openslides/participant/csv_import.py
Normal file
88
openslides/participant/csv_import.py
Normal 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
|
@ -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'))
|
||||
|
@ -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_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_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 %}
|
||||
{% if perms.participant.can_see_participant and perms.participant.can_manage_participant %}
|
||||
<div class="btn-group">
|
||||
|
@ -18,11 +18,11 @@
|
||||
<ul>
|
||||
<li>
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% trans 'Required CSV file encoding is UTF-8' %}.
|
||||
</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>
|
||||
|
||||
<form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
|
@ -50,9 +50,9 @@ urlpatterns = patterns(
|
||||
{'action': 'toggle'},
|
||||
name='user_status_toggle'),
|
||||
|
||||
url(r'^import/$',
|
||||
views.UserImportView.as_view(),
|
||||
name='user_import'),
|
||||
url(r'^csv_import/$',
|
||||
views.UserCSVImportView.as_view(),
|
||||
name='user_csv_import'),
|
||||
|
||||
# Group
|
||||
url(r'^group/$',
|
||||
|
@ -13,13 +13,14 @@ from django.utils.translation import activate, ugettext_lazy
|
||||
from openslides.config.api import config
|
||||
from openslides.utils.utils import (delete_default_permissions, html_strong,
|
||||
template)
|
||||
from openslides.utils.views import (CreateView, DeleteView, DetailView,
|
||||
from openslides.utils.views import (CreateView, CSVImportView, DeleteView, DetailView,
|
||||
FormView, ListView, PDFView,
|
||||
PermissionMixin, QuestionView,
|
||||
RedirectView, SingleObjectMixin, UpdateView)
|
||||
|
||||
from .api import gen_password, gen_username, import_users
|
||||
from .forms import (GroupForm, UserCreateForm, UserMultipleCreateForm, UserImportForm,
|
||||
from .api import gen_password, gen_username
|
||||
from .csv_import import import_users
|
||||
from .forms import (GroupForm, UserCreateForm, UserMultipleCreateForm,
|
||||
UsersettingsForm, UserUpdateForm)
|
||||
from .models import get_protected_perm, Group, User
|
||||
from .pdf import participants_to_pdf, participants_passwords_to_pdf
|
||||
@ -237,25 +238,14 @@ class ParticipantsPasswordsPDF(PDFView):
|
||||
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'
|
||||
template_name = 'participant/import.html'
|
||||
form_class = UserImportForm
|
||||
success_url_name = 'user_overview'
|
||||
|
||||
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)
|
||||
template_name = 'participant/user_form_csv_import.html'
|
||||
|
||||
|
||||
class ResetPasswordView(SingleObjectMixin, QuestionView):
|
||||
|
@ -98,3 +98,13 @@ class CleanHtmlFormMixin(object):
|
||||
# The field 'field' is not pressent. Do not change cleaned_data
|
||||
pass
|
||||
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.'))
|
||||
|
@ -19,6 +19,7 @@ from reportlab.lib.units import cm
|
||||
from reportlab.platypus import SimpleDocTemplate, Spacer
|
||||
|
||||
from .exceptions import OpenSlidesError
|
||||
from .forms import CSVImportForm
|
||||
from .pdf import firstPage, laterPages
|
||||
from .signals import template_manipulation
|
||||
from .utils import html_strong
|
||||
@ -581,3 +582,38 @@ class PDFView(PermissionMixin, View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
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)
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test.client import Client
|
||||
from mock import patch
|
||||
|
||||
@ -275,6 +276,18 @@ class ViewTest(TestCase):
|
||||
response = client.get('/agenda/2/')
|
||||
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):
|
||||
def setUp(self):
|
||||
|
@ -43,9 +43,9 @@ class CSVImport(TestCase):
|
||||
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, 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(count_success, 11)
|
||||
|
||||
motion1 = Motion.objects.get(pk=1)
|
||||
self.assertEqual(motion1.identifier, '1')
|
||||
@ -55,8 +55,8 @@ class CSVImport(TestCase):
|
||||
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(any('Submitter unknown.' in w for w in warning_messages))
|
||||
self.assertTrue(any('Category unknown.' in w for w in warning_messages))
|
||||
self.assertTrue('Submitter unknown.' in warning_message)
|
||||
self.assertTrue('Category unknown.' in warning_message)
|
||||
|
||||
motion2 = Motion.objects.get(pk=2)
|
||||
self.assertEqual(motion2.identifier, 'SA 1')
|
||||
@ -69,25 +69,26 @@ class CSVImport(TestCase):
|
||||
self.assertEqual(motion2.category, self.category1)
|
||||
|
||||
# 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'
|
||||
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):
|
||||
csv_file = StringIO.StringIO()
|
||||
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)
|
||||
self.assertEqual(count_success, 0)
|
||||
self.assertTrue(any('Line is malformed.' in e for e in error_messages))
|
||||
success_message, warning_message, error_message = import_motions(
|
||||
csvfile=csv_file, default_submitter=self.normal_user.person_id, override=False)
|
||||
self.assertEqual(success_message, '')
|
||||
self.assertTrue('Line is malformed.' in error_message)
|
||||
|
||||
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, count_lines, error_messages, warning_messages = import_motions(
|
||||
csv_file=csv_file,
|
||||
default_submitter=self.normal_user.person_id)
|
||||
self.assertEqual(count_success, 0)
|
||||
self.assertEqual(count_lines, 0)
|
||||
self.assertTrue('Import file has wrong character encoding, only UTF-8 is supported!' in error_messages)
|
||||
success_message, warning_message, error_message = import_motions(
|
||||
csvfile=csv_file,
|
||||
default_submitter=self.normal_user.person_id,
|
||||
override=False)
|
||||
self.assertEqual(success_message, '')
|
||||
self.assertTrue('Import file has wrong character encoding, only UTF-8 is supported!' in error_message)
|
||||
|
Loading…
Reference in New Issue
Block a user