fixes #58: import attendees using Eventbrite's API

This commit is contained in:
Davide Alberani 2017-09-10 16:06:32 +02:00
parent ecd18542e6
commit 836223395e
10 changed files with 200 additions and 24 deletions

View file

@ -13,7 +13,7 @@ Main features:
- quickly mark a registered person as an attendee
- easy way to add a new ticket, if it's already known from a previous event or if it's a completely new ticket
- set maximum number of tickets and begin/end date for sales
- can import Eventbrite CSV export files
- can import from Eventbrite using API and CSV file; see the docs/Eventbrite\_import.md file
- RESTful interface that can be called by third-party applications
- ability to run triggers to respond to an event (e.g. when a person is marked as attending to an event)
- can run on HTTPS

View file

@ -7,7 +7,7 @@
<div class="panel-body">
<form name="ebAPIForm" class="well">
<div class="form-group">
<label for="eb-api-key">{{'API key' | translate}}</label>
<label for="eb-api-key">{{'OAuth token' | translate}}</label>
<input ng-model="ebAPIkey" id="eb-api-key" type="password" ng-required="true">
</div>
<div class="form-group">
@ -16,6 +16,7 @@
</div>
<input type="checkbox" ng-model="createNewEvent"> {{'create a new event' | translate}}
<br />
<div class="form-group" ng-disabled="createNewEvent">
<label for="forEvent">{{'Associate tickets to this event' | translate}}</label>

View file

@ -826,8 +826,8 @@ eventManControllers.controller('UsersCtrl', ['$scope', '$rootScope', '$state', '
);
eventManControllers.controller('FileUploadCtrl', ['$scope', '$log', '$upload', 'EbAPI', 'Event',
function ($scope, $log, $upload, EbAPI, Event) {
eventManControllers.controller('FileUploadCtrl', ['$scope', '$log', '$upload', 'EbAPI', 'Event', 'toaster',
function ($scope, $log, $upload, EbAPI, Event, toaster) {
$scope.file = null;
$scope.progress = 0;
$scope.progressbarType = 'warning';
@ -848,7 +848,11 @@ eventManControllers.controller('FileUploadCtrl', ['$scope', '$log', '$upload', '
create: $scope.createNewEvent,
eventID: $scope.ebEventID,
targetEvent: $scope.targetEvent,
apiKey: $scope.ebAPIkey
oauthToken: $scope.ebAPIkey
}, function(data) {
console.log(data);
toaster.pop({type: 'info', title: 'imported tickets',
body: 'total: ' + data.total + ' errors: ' + (data.total - data.valid)})
});
};

View file

@ -203,11 +203,7 @@ eventManServices.factory('EbAPI', ['$resource', '$rootScope',
interceptor: {responseError: $rootScope.errorHandler},
isArray: false,
transformResponse: function(data, headers) {
data = angular.fromJson(data);
if (data.error) {
return data;
}
return data.info || {};
return angular.fromJson(data);
}
}
});

View file

@ -58,7 +58,8 @@ The paths used to communicate with the Tornado web server:
- /users/:user\_id PUT - update an existing user
- /settings GET - settings to customize the GUI (logo, extra columns for events and tickets lists)
- /info GET - information about the current user
- /ebcsvpersons POST - csv file upload to import persons
- /ebcsvpersons POST - csv file upload to import tickets
- /ebapi POST - import tickets (and optionally a complete event) using Eventbrite API
- /login POST - log a user in
- /logout GET - when visited, the user is logged out

24
docs/Eventbrite_import.md Normal file
View file

@ -0,0 +1,24 @@
# Eventbrite import
It's possible to import attendees from Eventbrite using both an exported CSV file or the Eventbrite's API.
## API import
On the Evenbrite site, go to the "Account Settings" -> "Developer" -> "App management" page.
Create a new app. Click on the "Show Client Secret and OAuth Token" link and copy "Your personal OAuth token".
Now go to the Eventbrite web page of your event, and copy the "eid" field of the URL.
In the EventMan "Import tickets" page, set the copied OAuth token and Event ID; it's also possible to select an existing event that will receive the new attendees, or create a brand new event with the information from Eventbrite.
## CSV import
On the Evenbrite site, go to the "manage" page of your event and then go to the "Manage Attendees" -> "Orders" page.
From there, use the "Export to CSV" feature to get the CSV file.
Load this file in the EventMan "Import tickets" page, select an existing event that will receive the new attendees and click the "Import" button.
If you select this import method, please beware that you probably also want to edit the tools/qrcode_reader.ini configuration file (see the comment about the limit\_field setting).

View file

@ -436,8 +436,11 @@ class CollectionHandler(BaseHandler):
@gen.coroutine
@authenticated
def post(self, id_=None, resource=None, resource_id=None, **kwargs):
data = escape.json_decode(self.request.body or '{}')
def post(self, id_=None, resource=None, resource_id=None, _rawData=None, **kwargs):
if _rawData:
data = _rawData
else:
data = escape.json_decode(self.request.body or '{}')
self._clean_dict(data)
method = self.request.method.lower()
crud_method = 'create' if method == 'post' else 'update'
@ -768,7 +771,7 @@ class EventsHandler(CollectionHandler):
if now > end_datetime:
raise InputException('ticket sales has ended')
def handle_post_tickets(self, id_, resource_id, data):
def handle_post_tickets(self, id_, resource_id, data, _skipTriggers=False):
event = self.db.query('events', {'_id': id_})[0]
self._check_sales_datetime(event)
self._check_number_of_tickets(event)
@ -784,7 +787,7 @@ class EventsHandler(CollectionHandler):
{'tickets': data},
operation='appendUnique',
create=False)
if doc:
if doc and not _skipTriggers:
self.send_ws_message('event/%s/tickets/updates' % id_, json.dumps(ret))
ticket = self._get_ticket_data(ticket_id, doc.get('tickets') or [])
env = dict(ticket)
@ -1000,12 +1003,41 @@ class EbAPIImportHandler(BaseHandler):
@gen.coroutine
@authenticated
def post(self, *args, **kwargs):
reply = {}
reply = dict(total=0, valid=0, merged=0, new_in_event=0)
data = escape.json_decode(self.request.body or '{}')
apiKey = data.get('apiKey')
targetEvent = data.get('targetEvent')
oauthToken = data.get('oauthToken')
eventID = data.get('eventID')
create = data.get('create')
targetEventID = data.get('targetEvent')
create = True
try:
create = self.tobool(data.get('create'))
except:
pass
try:
eb_info = utils.ebAPIFetch(oauthToken=oauthToken, eventID=eventID)
except Exception as e:
return self.build_error('Error using Eventbrite API: %s' % e)
if 'event' not in eb_info or 'attendees' not in eb_info:
return self.build_error('Missing information from Eventbrite API')
event_handler = EventsHandler(self.application, self.request)
event_handler.db = self.db
event_handler.logger = self.logger
event_handler.authentication = self.authentication
if create:
event_handler.post(_rawData=eb_info['event'])
event_in_db = self.db.query('events', {'eb_event_id': eb_info['event']['eb_event_id']})
if not event_in_db:
return self.build_error('Unable to create a new event')
targetEventID = event_in_db[0]['_id']
for ticket in eb_info.get('attendees') or []:
reply['total'] += 1
ticket['event_id'] = targetEventID
try:
event_handler.handle_post_tickets(targetEventID, None, ticket, _skipTriggers=True)
except Exception as e:
self.logger.warn('failed to add a ticket: ' % e)
reply['new_in_event'] += 1
reply['valid'] += 1
self.write(reply)
@ -1067,7 +1099,6 @@ class EbCSVImportPersonsHandler(BaseHandler):
if not event_details:
return self.build_error('invalid event')
all_persons = set()
#[x.get('email') for x in (event_details[0].get('tickets') or []) if x.get('email')])
for ticket in (event_details[0].get('tickets') or []):
all_persons.add('%s_%s_%s' % (ticket.get('name'), ticket.get('surname'), ticket.get('email')))
for fieldname, contents in self.request.files.items():
@ -1088,7 +1119,7 @@ class EbCSVImportPersonsHandler(BaseHandler):
if duplicate_check in all_persons:
continue
all_persons.add(duplicate_check)
event_handler.handle_post_tickets(event_id, None, person)
event_handler.handle_post_tickets(event_id, None, person, _skipTriggers=True)
reply['new_in_event'] += 1
self.write(reply)

View file

@ -59,7 +59,7 @@
"Delete event": "Cancella l'evento",
"Delete all tickets in event": "Cancella tutti i ticket dell'evento",
"Import tickets with Eventbrite API": "Importa i biglietti tramite le API di Eventbrite",
"API key": "API key",
"OAuth token": "OAuth token",
"Eventbrite Event ID": "Eventbrite Event ID",
"create a new event": "crea un nuovo evento",
"Associate tickets to this event": "Associa i biglietti a questo evento",

View file

@ -13,11 +13,14 @@ ca =
# in the 'event' section you have to specify the ID of the event,
# the name of the field used to search for tickets and - optionally -
# the number of chars in the field value that will be considered
# for the match (limit_field)
# for the match (limit_field).
# Leave limit_field commented if you're managing data imported
# using the Eventbrite API; if the data was imported from an
# Eventbrite CSV, set it to 9.
[event]
id = 1492099112_2612922-3896-9zwsccuvguz91jtw9y6lwvkud11ba7wt
field = order_nr
limit_field = 9
#limit_field = 9
# the 'actions' section key: value pairs are used in the PUT method.
[actions]

116
utils.py
View file

@ -18,6 +18,7 @@ limitations under the License.
"""
import csv
import copy
import json
import string
import random
@ -96,6 +97,121 @@ def hash_password(password, salt=None):
return '$%s$%s' % (salt, hash_.hexdigest())
has_eventbrite_sdk = False
try:
from eventbrite import Eventbrite
has_eventbrite_sdk = True
except ImportError:
Eventbrite = object
class CustomEventbrite(Eventbrite):
"""Custom methods to override official SDK limitations; code take from Yuval Hager; see:
https://github.com/eventbrite/eventbrite-sdk-python/issues/18
This class should be removed onces the official SDK supports pagination.
"""
def custom_get_event_attendees(self, event_id, status=None, changed_since=None, page=1):
data = {}
if status:
data['status'] = status
if changed_since:
data['changed_since'] = changed_since
data['page'] = page
return self.get("/events/{0}/attendees/".format(event_id), data=data)
def get_all_event_attendees(self, event_id, status=None, changed_since=None):
page = 1
attendees = []
while True:
r = self.custom_get_event_attendees(event_id, status, changed_since, page=page)
attendees.extend(r['attendees'])
if r['pagination']['page_count'] <= page:
break
page += 1
return attendees
KEYS_REMAP = {
('capacity', 'number_of_tickets'),
('changed', 'updated_at', lambda x: x.replace('T', ' ').replace('Z', '')),
('created', 'created_at', lambda x: x.replace('T', ' ').replace('Z', '')),
('description', 'description', lambda x: x.get('text', ''))
}
def reworkObj(obj, kind='event'):
"""Rename and fix some key in the data from the Eventbrite API."""
for remap in KEYS_REMAP:
transf = lambda x: x
if len(remap) == 2:
old, new = remap
else:
old, new, transf = remap
if old in obj:
obj[new] = transf(obj[old])
if old != new:
del obj[old]
if kind == 'event':
if 'name' in obj:
obj['title'] = obj.get('name', {}).get('text') or ''
del obj['name']
if 'start' in obj:
t = obj['start'].get('utc') or ''
obj['begin_date'] = obj['begin_time'] = t.replace('T', ' ').replace('Z', '')
if 'end' in obj:
t = obj['end'].get('utc') or ''
obj['end_date'] = obj['end_time'] = t.replace('T', ' ').replace('Z', '')
else:
profile = obj.get('profile') or {}
complete_name = profile['name']
obj['surname'] = profile.get('last_name') or ''
obj['name'] = profile.get('first_name') or ''
if not (obj['surname'] and obj['name']):
obj['surname'] = complete_name
obj['email'] = profile.get('email') or ''
return obj
def expandBarcodes(attendees):
"""Generate an attendee for each barcode in the Eventbrite API data."""
for attendee in attendees:
barcodes = attendee.get('barcodes') or []
if not barcodes:
yield attendee
barcodes = [b.get('barcode') for b in barcodes if b.get('barcode')]
for code in barcodes:
att_copy = copy.deepcopy(attendee)
att_copy['order_nr'] = code
yield att_copy
def ebAPIFetch(oauthToken, eventID):
"""Fetch an event, complete with all attendees using Eventbrite API.
:param oauthToken: Eventbrite API key
:type oauthToken: str
:param eventID: Eventbrite ID of the even to be fetched
:type eventID: str
:returns: information about the event and all its attendees
:rtype: dict
"""
if not has_eventbrite_sdk:
raise Exception('unable to import eventbrite module')
eb = CustomEventbrite(oauthToken)
event = eb.get_event(eventID)
eb_attendees = eb.get_all_event_attendees(eventID)
event = reworkObj(event, kind='event')
attendees = []
for eb_attendee in eb_attendees:
reworkObj(eb_attendee, kind='attendee')
attendees.append(eb_attendee)
event['eb_event_id'] = eventID
attendees = list(expandBarcodes(attendees))
info = {'event': event, 'attendees': attendees}
return info
class ImprovedEncoder(json.JSONEncoder):
"""Enhance the default JSON encoder to serialize datetime and ObjectId instances."""
def default(self, o):