manager: add possibility to import events from a CSV file (#13143)

This commit is contained in:
Frédéric Péters 2016-09-16 15:04:02 +02:00
parent afedcf933f
commit e6fa7e3a05
7 changed files with 206 additions and 3 deletions

View File

@ -14,7 +14,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import csv
import datetime
from django import forms
from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _
from chrono.agendas.models import Event, MeetingType, TimePeriod
@ -60,3 +65,54 @@ class TimePeriodForm(forms.ModelForm):
'end_time': widgets.TimeWidget(),
}
exclude = []
class ImportEventsForm(forms.Form):
events_csv_file = forms.FileField(_('Events File'),
help_text=_('CSV file with date, time, number of places, '
'number of places in waiting list, and label '
'as columns.'))
def clean_events_csv_file(self):
content = self.cleaned_data['events_csv_file'].read()
if '\0' in content:
raise ValidationError(_('Invalid file format.'))
try:
dialect = csv.Sniffer().sniff(content)
except Exception:
dialect = None
events = []
for i, csvline in enumerate(csv.reader(content.splitlines())):
if not csvline:
continue
if len(csvline) < 2:
raise ValidationError(_('Invalid file format. (line %d)') % (i+1))
if csvline[0].strip('#') in ('date', 'Date', _('date'), _('Date')):
continue
event = Event()
for datetime_fmt in ('%Y-%m-%d %H:%M', '%d/%m/%Y %H:%M',
'%d/%m/%Y %Hh%M', '%Y-%m-%d %H:%M:%S', '%d/%m/%Y %H:%M:%S'):
try:
event_datetime = datetime.datetime.strptime(
'%s %s' % tuple(csvline[:2]), datetime_fmt)
except ValueError:
continue
event.start_datetime = event_datetime
break
else:
raise ValidationError(_('Invalid file format. (date/time format, line %d)') % (i+1))
try:
event.places = int(csvline[2])
except ValueError:
raise ValidationError(_('Invalid file format. (number of places, line %d)') % (i+1))
if len(csvline) >= 4:
try:
event.waiting_list_places = int(csvline[3])
except ValueError:
raise ValidationError(_('Invalid file format. (number of places in waiting list, line %d)') % (i+1))
if len(csvline) >= 5:
event.label = ' '.join(csvline[4:])
events.append(event)
self.events = events

View File

@ -8,6 +8,7 @@
<a rel="popup" href="{% url 'chrono-manager-agenda-delete' pk=object.id %}">{% trans 'Delete' %}</a>
<a rel="popup" href="{% url 'chrono-manager-agenda-edit' pk=object.id %}">{% trans 'Options' %}</a>
{% if object.kind == "events" %}
<a rel="popup" href="{% url 'chrono-manager-agenda-import-events' pk=object.id %}">{% trans 'Import Events' %}</a>
<a rel="popup" href="{% url 'chrono-manager-agenda-add-event' pk=object.id %}">{% trans 'New Event' %}</a>
{% else %}
<a rel="popup" href="{% url 'chrono-manager-agenda-add-meeting-type' pk=object.id %}">{% trans 'New Meeting Type' %}</a>

View File

@ -0,0 +1,31 @@
{% extends "chrono/manager_agenda_view.html" %}
{% load i18n %}
{% block extrascripts %}
{{ block.super }}
{{ form.media }}
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-agenda-view' pk=agenda.id %}">{{agenda.label}}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Import Events" %}</h2>
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<p>
<a href="{% url 'chrono-manager-sample-events-csv' %}">Download sample file</a>
</p>
<div class="buttons">
<button>{% trans "Import" %}</button>
<a class="cancel" href="{% url 'chrono-manager-agenda-view' pk=agenda.id %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1 @@
{% load i18n %}{% trans 'date' %},{% trans 'time' %},{% trans 'number of places' %},{% trans 'number of places in waiting list' %},{% trans 'label' %}
1 {% load i18n %}{% trans 'date' %} {% trans 'time' %} {% trans 'number of places' %} {% trans 'number of places in waiting list' %} {% trans 'label' %}

View File

@ -30,6 +30,8 @@ urlpatterns = patterns('chrono.views',
name='chrono-manager-agenda-delete'),
url(r'^agendas/(?P<pk>\w+)/add-event$', views.agenda_add_event,
name='chrono-manager-agenda-add-event'),
url(r'^agendas/(?P<pk>\w+)/import-events$', views.agenda_import_events,
name='chrono-manager-agenda-import-events'),
url(r'^events/(?P<pk>\w+)/$', views.event_edit,
name='chrono-manager-event-edit'),
url(r'^events/(?P<pk>\w+)/delete$', views.event_delete,
@ -45,5 +47,8 @@ urlpatterns = patterns('chrono.views',
url(r'^timeperiods/(?P<pk>\w+)/edit$', views.time_period_edit,
name='chrono-manager-time-period-edit'),
url(r'^agendas/events.csv$', views.agenda_import_events_sample_csv,
name='chrono-manager-sample-events-csv'),
url(r'^menu.json$', views.menu_json),
)

View File

@ -16,16 +16,18 @@
import json
from django.contrib import messages
from django.core.urlresolvers import reverse, reverse_lazy
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text
from django.views.generic import (DetailView, CreateView, UpdateView,
ListView, DeleteView)
ListView, DeleteView, FormView, TemplateView)
from chrono.agendas.models import Agenda, Event, MeetingType, TimePeriod
from .forms import EventForm, MeetingTypeForm, TimePeriodForm
from .forms import EventForm, MeetingTypeForm, TimePeriodForm, ImportEventsForm
class HomepageView(ListView):
@ -87,6 +89,40 @@ class AgendaAddEventView(CreateView):
agenda_add_event = AgendaAddEventView.as_view()
class AgendaImportEventsSampleView(TemplateView):
template_name = 'chrono/manager_sample_events.csv'
content_type = 'text/csv'
agenda_import_events_sample_csv = AgendaImportEventsSampleView.as_view()
class AgendaImportEventsView(FormView):
form_class = ImportEventsForm
template_name = 'chrono/manager_import_events.html'
error_msg = None
def dispatch(self, request, *args, **kwargs):
self.agenda = get_object_or_404(Agenda, id=kwargs.get('pk'))
return super(AgendaImportEventsView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(AgendaImportEventsView, self).get_context_data(**kwargs)
context['agenda'] = self.agenda
return context
def form_valid(self, form):
if form.events:
for event in form.events:
event.agenda = self.agenda
event.save()
messages.info(self.request, _('%d events have been imported.') % len(form.events))
return super(AgendaImportEventsView, self).form_valid(form)
def get_success_url(self):
return reverse('chrono-manager-agenda-view', kwargs={'pk': self.agenda.id})
agenda_import_events = AgendaImportEventsView.as_view()
class EventEditView(UpdateView):
template_name = 'chrono/manager_event_form.html'
model = Event

View File

@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.models import User
from django.utils.timezone import make_aware
import datetime
import pytest
from webtest import TestApp
from webtest import TestApp, Upload
from chrono.wsgi import application
@ -198,6 +199,78 @@ def test_delete_event(app, admin_user):
assert resp.location == 'http://localhost:80/manage/agendas/%s/' % agenda.id
assert Event.objects.count() == 0
def test_import_events(app, admin_user):
agenda = Agenda(label=u'Foo bar')
agenda.save()
app = login(app)
resp = app.get('/manage/agendas/%s/' % agenda.id, status=200)
resp = resp.click('Import Events')
sample_csv_resp = resp.click('Download sample file')
assert sample_csv_resp.content_type == 'text/csv'
assert sample_csv_resp.body.startswith('date,time,')
resp.form['events_csv_file'] = Upload('t.csv', sample_csv_resp.body, 'text/csv')
resp = resp.form.submit(status=302)
assert Event.objects.count() == 0
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv', 'xx', 'text/csv')
resp = resp.form.submit(status=200)
assert 'Invalid file format.' in resp.body
resp.form['events_csv_file'] = Upload('t.csv', 'xxxx\0\0xxxx', 'text/csv')
resp = resp.form.submit(status=200)
assert 'Invalid file format.' in resp.body
resp.form['events_csv_file'] = Upload('t.csv', '2016-14-16,18:00,10', 'text/csv')
resp = resp.form.submit(status=200)
assert 'Invalid file format. (date/time format' in resp.body
resp.form['events_csv_file'] = Upload('t.csv', '2016-09-16,18:00,blah', 'text/csv')
resp = resp.form.submit(status=200)
assert 'Invalid file format. (number of places,' in resp.body
resp.form['events_csv_file'] = Upload('t.csv', '2016-09-16,18:00,10,blah', 'text/csv')
resp = resp.form.submit(status=200)
assert 'Invalid file format. (number of places in waiting list,' in resp.body
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv', '2016-09-16,18:00,10', 'text/csv')
resp = resp.form.submit(status=302)
assert Event.objects.count() == 1
assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0))
assert Event.objects.all()[0].places == 10
Event.objects.all().delete()
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv', '2016-09-16,18:00,10,5', 'text/csv')
resp = resp.form.submit(status=302)
assert Event.objects.count() == 1
assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0))
assert Event.objects.all()[0].places == 10
assert Event.objects.all()[0].waiting_list_places == 5
Event.objects.all().delete()
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv', '2016-09-16,18:00,10,5,bla bla bla', 'text/csv')
resp = resp.form.submit(status=302)
assert Event.objects.count() == 1
assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0))
assert Event.objects.all()[0].places == 10
assert Event.objects.all()[0].waiting_list_places == 5
assert Event.objects.all()[0].label == 'bla bla bla'
Event.objects.all().delete()
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv', 'date,time,etc.\n'
'2016-09-16,18:00,10,5,bla bla bla\n'
'2016-09-19,18:00,10', 'text/csv')
resp = resp.form.submit(status=302)
assert Event.objects.count() == 2
Event.objects.all().delete()
def test_add_meetings_agenda(app, admin_user):
app = login(app)
resp = app.get('/manage/', status=200)