fixes #58: import attendees using Eventbrite's API
This commit is contained in:
parent
ecd18542e6
commit
836223395e
10 changed files with 200 additions and 24 deletions
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
10
angular_app/js/controllers.js
vendored
10
angular_app/js/controllers.js
vendored
|
@ -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)})
|
||||
});
|
||||
};
|
||||
|
||||
|
|
6
angular_app/js/services.js
vendored
6
angular_app/js/services.js
vendored
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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
24
docs/Eventbrite_import.md
Normal 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).
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
116
utils.py
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue