manager: add possibility to import events from a CSV file (#13143)
This commit is contained in:
parent
afedcf933f
commit
e6fa7e3a05
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
|
@ -0,0 +1 @@
|
|||
{% load i18n %}{% trans 'date' %},{% trans 'time' %},{% trans 'number of places' %},{% trans 'number of places in waiting list' %},{% trans 'label' %}
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue