Skip to content

Commit

Permalink
Allow importing contributions from a CSV file
Browse files Browse the repository at this point in the history
  • Loading branch information
pferreir committed Nov 20, 2017
1 parent 7d957c4 commit bab2646
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 41 deletions.
2 changes: 2 additions & 0 deletions indico/modules/events/contributions/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
management.RHContributionsExportPDFBook, methods=('POST',))
_bp.add_url_rule('/manage/contributions/book-sorted.pdf', 'contributions_pdf_export_book_sorted',
management.RHContributionsExportPDFBookSorted, methods=('POST',))
_bp.add_url_rule('/manage/contributions/import', 'contributions_import',
management.RHContributionsImportCSV, methods=('GET', 'POST'))

# Single contribution
_bp.add_url_rule('/manage/contributions/<int:contrib_id>', 'manage_contrib_rest', management.RHContributionREST,
Expand Down
22 changes: 21 additions & 1 deletion indico/modules/events/contributions/controllers/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@
delete_contribution, delete_subcontribution,
update_contribution, update_subcontribution)
from indico.modules.events.contributions.util import (contribution_type_row, generate_spreadsheet_from_contributions,
make_contribution_form)
import_contributions_from_csv, make_contribution_form)
from indico.modules.events.contributions.views import WPManageContributions
from indico.modules.events.logs import EventLogKind, EventLogRealm
from indico.modules.events.management.controllers import RHManageEventBase
from indico.modules.events.management.controllers.base import RHContributionPersonListMixin
from indico.modules.events.management.util import flash_if_unregistered
from indico.modules.events.models.references import ReferenceType
from indico.modules.events.sessions import Session
from indico.modules.events.timetable.forms import ImportContributionsForm
from indico.modules.events.timetable.operations import update_timetable_entry
from indico.modules.events.tracks.models.tracks import Track
from indico.modules.events.util import (check_event_locked, get_field_values, track_time_changes,
Expand Down Expand Up @@ -471,6 +472,25 @@ def _process(self):
return send_file('book-of-abstracts.pdf', pdf.generate(), 'application/pdf')


class RHContributionsImportCSV(RHManageContributionsActionsBase):
"""Import contributions from a CSV file"""

def _process(self):
form = ImportContributionsForm()

if form.validate_on_submit():
contributions, changes = import_contributions_from_csv(self.event, form.source_file.data)
flash(ngettext("{} contribution has been imported.",
"{} contributions have been imported.",
len(contributions)).format(len(contributions)), 'success')
if changes:
flash(_("Event dates/times adjusted due to imported data."), 'warning')
return jsonify_data(flash=False, redirect=url_for('.manage_contributions', self.event),
redirect_no_loading=True)
return jsonify_template('events/contributions/management/import_contributions.html', form=form,
event=self.event)


class RHManageContributionTypes(RHManageContributionsBase):
"""Dialog to manage the ContributionTypes of an event"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,43 +86,52 @@
data-href="{{ url_for('.material_package', event) }}">
{%- trans %}Material package{% endtrans -%}
</a>
<a class="i-button js-enable-if-checked arrow disabled"
title="{% trans %}Export data{% endtrans %}"
data-toggle="dropdown">
{%- trans %}Export{% endtrans -%}
</a>
<ul class="dropdown">
<li>
<a href="#" class="icon-file-pdf js-submit-form js-enable-if-checked disabled"
data-href="{{ url_for('.contributions_pdf_export', event) }}">
{%- trans %}PDF{% endtrans -%}
</a>
</li>
<li>
<a href="#" class="icon-file-pdf js-submit-form js-enable-if-checked disabled"
data-href="{{ url_for('.contributions_pdf_export_book', event) }}">
{%- trans %}PDF (book of abstracts){% endtrans -%}
</a>
</li>
<li>
<a href="#" class="icon-file-pdf js-submit-form js-enable-if-checked disabled"
data-href="{{ url_for('.contributions_pdf_export_book_sorted', event) }}">
{%- trans %}PDF (book of abstracts - sorted by board number){% endtrans -%}
</a>
</li>
<li>
<a href="#" class="icon-file-spreadsheet js-submit-form js-enable-if-checked disabled"
data-href="{{ url_for('.contributions_csv_export', event) }}">
{%- trans %}CSV{% endtrans -%}
</a>
</li>
<li>
<a href="#" class="icon-file-excel js-submit-form js-enable-if-checked disabled"
data-href="{{ url_for('.contributions_excel_export', event) }}">
{%- trans %}XLSX (Excel){% endtrans -%}
</a>
</li>
</ul>
<div class="group">
<a class="i-button js-enable-if-checked arrow disabled"
title="{% trans %}Export data{% endtrans %}"
data-toggle="dropdown">
{%- trans %}Export{% endtrans -%}
</a>
<ul class="dropdown">
<li>
<a href="#" class="icon-file-pdf js-submit-form js-enable-if-checked disabled"
data-href="{{ url_for('.contributions_pdf_export', event) }}">
{%- trans %}PDF{% endtrans -%}
</a>
</li>
<li>
<a href="#" class="icon-file-pdf js-submit-form js-enable-if-checked disabled"
data-href="{{ url_for('.contributions_pdf_export_book', event) }}">
{%- trans %}PDF (book of abstracts){% endtrans -%}
</a>
</li>
<li>
<a href="#" class="icon-file-pdf js-submit-form js-enable-if-checked disabled"
data-href="{{ url_for('.contributions_pdf_export_book_sorted', event) }}">
{%- trans %}PDF (book of abstracts - sorted by board number){% endtrans -%}
</a>
</li>
<li>
<a href="#" class="icon-file-spreadsheet js-submit-form js-enable-if-checked disabled"
data-href="{{ url_for('.contributions_csv_export', event) }}">
{%- trans %}CSV{% endtrans -%}
</a>
</li>
<li>
<a href="#" class="icon-file-excel js-submit-form js-enable-if-checked disabled"
data-href="{{ url_for('.contributions_excel_export', event) }}">
{%- trans %}XLSX (Excel){% endtrans -%}
</a>
</li>
</ul>
<a class="i-button"
title="{% trans %}Import contributions from CSV file{% endtrans %}"
data-href="{{ url_for('.contributions_import', event) }}"
data-title="{% trans %}Import Registrants from CSV File{% endtrans %}"
data-ajax-dialog>
{%- trans %}Import{% endtrans -%}
</a>
</div>
</div>
<div class="toolbar">
<div class="group" id="filter-statistics">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% from 'forms/_form.html' import simple_form %}
{% from 'message_box.html' import message_box %}

{% call message_box('highlight', large_icon=true) %}
{% trans -%}
You should upload a CSV (comma-separated values) file with exactly 7 columns in the
following order:
<ul>
<li>Start date/time in <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a> format</li>
<ul>
<li>
<em>e.g. 2017-12-03T12:00:00</em>
</li>
</ul>
<li>Duration (minutes)</li>
<li>Title</li>
<li>Speaker - First name</li>
<li>Speaker - Last name</li>
<li>Speaker - Affiliation</li>
<li>Speaker - E-mail address</li>
</ul>
Only the field "Title" is mandatory. Users will be matched with existing
Indico identities through their e-mail.
{%- endtrans %}
{% endcall %}

{{ simple_form(form, back=_('Cancel'), footer_align_right=true,
form_header_kwargs={'multipart': true, 'action': url_for('.contributions_import', event)}) }}
58 changes: 56 additions & 2 deletions indico/modules/events/contributions/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,29 @@

from __future__ import unicode_literals

import csv
import dateutil.parser
from collections import defaultdict
from datetime import timedelta
from io import BytesIO
from operator import attrgetter

from sqlalchemy.orm import contains_eager, joinedload, load_only, noload

from indico.core.db import db
from indico.core.errors import UserValueError
from indico.modules.attachments.util import get_attached_items
from indico.modules.events.contributions.models.contributions import Contribution
from indico.modules.events.contributions.models.persons import ContributionPersonLink, SubContributionPersonLink
from indico.modules.events.contributions.models.principals import ContributionPrincipal
from indico.modules.events.contributions.models.subcontributions import SubContribution
from indico.modules.events.contributions.operations import create_contribution
from indico.modules.events.models.events import Event
from indico.modules.events.models.persons import EventPerson
from indico.modules.events.util import serialize_person_link
from indico.util.date_time import format_datetime, format_human_timedelta
from indico.modules.events.persons.util import get_event_person
from indico.modules.events.util import serialize_person_link, track_time_changes
from indico.util.date_time import format_human_timedelta
from indico.util.i18n import _
from indico.web.flask.templating import get_template_module
from indico.web.flask.util import url_for
from indico.web.http_api.metadata.serializer import Serializer
Expand Down Expand Up @@ -189,3 +196,50 @@ def get_contribution_ical_file(contrib):
data = {'results': serialize_contribution_for_ical(contrib)}
serializer = Serializer.create('ics')
return BytesIO(serializer(data))


def import_contributions_from_csv(event, f):
"""Import timetable contributions from a CSV file into an event."""
reader = csv.reader(f)
contributions = []
for n_row, row in enumerate(reader):
try:
start_dt, duration, title, first_name, last_name, affiliation, email = row
except ValueError:
raise UserValueError(_('Row {}: malformed CSV data - please check that the number of columns is correct')
.format(n_row))
try:
parsed_start_dt = event.tzinfo.localize(dateutil.parser.parse(start_dt)) if start_dt else None
except ValueError:
raise UserValueError(_("Row {}: can't parse date: \"{}\"").format(n_row, start_dt))

if parsed_start_dt and not duration:
raise UserValueError(_("Row {}: a start date/time requires a duration").format(n_row))

try:
parsed_duration = timedelta(minutes=int(duration)) if duration else None
except ValueError:
raise UserValueError(_("Row {}: can't parse duration: {}").format(n_row, duration))

with track_time_changes() as changes:
contribution = create_contribution(event, {
'start_dt': parsed_start_dt,
'duration': parsed_duration or timedelta(minutes=20),
'title': title
}, extend_parent=True)

if not email:
continue

# set the information of the speaker
person = get_event_person(event, {
'firstName': first_name,
'familyName': last_name,
'affiliation': affiliation,
'email': email
})
link = ContributionPersonLink(person=person, is_speaker=True)
contribution.person_links.append(link)
contributions.append(contribution)

return contributions, changes
6 changes: 5 additions & 1 deletion indico/modules/events/timetable/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from indico.util.i18n import _
from indico.web.forms.base import FormDefaults, IndicoForm, generated_data
from indico.web.forms.colors import get_colors
from indico.web.forms.fields import (IndicoLocationField, IndicoPalettePickerField,
from indico.web.forms.fields import (FileField, IndicoLocationField, IndicoPalettePickerField,
IndicoSelectMultipleCheckboxBooleanField, TimeDeltaField)
from indico.web.forms.util import get_form_field_names
from indico.web.forms.validators import HiddenUnless, MaxDuration
Expand Down Expand Up @@ -209,3 +209,7 @@ def data_for_format(self):
for fieldname in fields:
data.update(getattr(self, fieldname).data)
return data


class ImportContributionsForm(IndicoForm):
source_file = FileField(_("Source File"), accepted_file_types='.csv')

0 comments on commit bab2646

Please sign in to comment.