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]
|
[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:
|
||||||
|
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">
|
<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>
|
||||||
|
@ -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'))
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
|
@ -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'),
|
||||||
|
@ -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' %}.
|
||||||
|
@ -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()
|
||||||
|
@ -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).
|
||||||
|
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
|
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'))
|
|
||||||
|
@ -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">
|
||||||
|
@ -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 %}
|
@ -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/$',
|
||||||
|
@ -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):
|
||||||
|
@ -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.'))
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user