Merge pull request #14 from alberanid/master

interactions between persons and events
This commit is contained in:
Davide Alberani 2015-04-05 00:57:04 +02:00
commit 7fb2a181f9
7 changed files with 286 additions and 101 deletions

View file

@ -45,4 +45,39 @@
<input type="submit" style="position: absolute; left: -9999px; width: 1px; height: 1px;"/> <input type="submit" style="position: absolute; left: -9999px; width: 1px; height: 1px;"/>
</form> </form>
<div class="panel panel-primary table-striped top5">
<div class="panel-heading">Events</div>
<div class="panel-body">
<form class="form-inline">
<div class="form-group">
<label for="query-persons">Search:</label>
<input type="text" id="query-persons" class="form-control" placeholder="Name or email" ng-model="query">
</div>
<div class="form-group">
<label for="persons-order">Sort by:</label>
<select id="persons-order" class="form-control" ng-model="orderProp">
<option value="name" ng-selected="selected">Alphabetical</option>
<option value="_id">ID</option>
</select>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>Person</th>
<th>Attended</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="person in event.persons | filter:query | orderBy:orderProp">
<td><a href="/#/persons/{{person.person_id}}">{{person.name}} {{person.surname}}</a></td>
<td>
<button class="btn btn-link" name="switch-attended" ng-click="updateAttendee(person, !person.attended)"><span class="glyphicon {{(person.attended) && 'glyphicon-ok-sign text-success' || 'glyphicon-remove-sign text-danger'}}"></span></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div> </div>

View file

@ -42,20 +42,41 @@ eventManControllers.controller('EventsListCtrl', ['$scope', 'Event',
); );
eventManControllers.controller('EventDetailsCtrl', ['$scope', 'Event', '$routeParams', eventManControllers.controller('EventDetailsCtrl', ['$scope', 'Event', '$routeParams', '$log',
function ($scope, Event, $routeParams) { function ($scope, Event, $routeParams, $log) {
if ($routeParams.id) { if ($routeParams.id) {
$scope.event = Event.get($routeParams); $scope.event = Event.get($routeParams);
} }
// store a new Event or update an existing one // store a new Event or update an existing one
$scope.save = function() { $scope.save = function() {
if ($scope.event.id === undefined) { // avoid override of event.persons list.
$scope.event = Event.save($scope.event); var this_event = angular.copy($scope.event);
if (this_event.persons) {
delete this_event.persons;
}
if (this_event.id === undefined) {
$scope.event = Event.save(this_event);
} else { } else {
$scope.event = Event.update($scope.event); $scope.event = Event.update(this_event);
} }
$scope.eventForm.$dirty = false; $scope.eventForm.$dirty = false;
}; };
$scope.updateAttendee = function(person, attended) {
$log.debug('EventDetailsCtrl.event_id: ' + $routeParams.id);
$log.debug('EventDetailsCtrl.person_id: ' + person.person_id);
$log.debug('EventDetailsCtrl.attended: ' + attended);
Event.personAttended({
_id: $routeParams.id,
person_id: person.person_id,
'persons.$.attended': attended
},
function(data) {
$log.debug('EventDetailsCtrl.personAttended.data');
$log.debug(data);
$scope.event.persons = data;
});
};
}] }]
); );
@ -74,12 +95,12 @@ eventManControllers.controller('PersonsListCtrl', ['$scope', 'Person',
); );
eventManControllers.controller('PersonDetailsCtrl', ['$scope', '$routeParams', 'Person', 'Action', eventManControllers.controller('PersonDetailsCtrl', ['$scope', '$routeParams', 'Person', 'Event', '$log',
function ($scope, $routeParams, Person, Action) { function ($scope, $routeParams, Person, Event, $log) {
if ($routeParams.id) { if ($routeParams.id) {
$scope.person = Person.get($routeParams); $scope.person = Person.get($routeParams);
Action.get({person_id: $routeParams.id}, function(data) { Person.getEvents($routeParams, function(data) {
$scope.actions = angular.fromJson(data).actions; $scope.events = data;
}); });
} }
// store a new Person or update an existing one // store a new Person or update an existing one
@ -90,6 +111,24 @@ eventManControllers.controller('PersonDetailsCtrl', ['$scope', '$routeParams', '
$scope.person = Person.update($scope.person); $scope.person = Person.update($scope.person);
} }
}; };
$scope.updateAttendee = function(event, attended) {
$log.debug('PersonDetailsCtrl.event_id: ' + $routeParams.id);
$log.debug('PersonDetailsCtrl.event_id: ' + event.event_id);
$log.debug('PersonDetailsCtrl.attended: ' + attended);
Event.personAttended({
_id: event._id,
person_id: $routeParams.id,
'persons.$.attended': attended
},
function(data) {
Person.getEvents($routeParams, function(data) {
$log.debug('PersonDetailsCtrl.personAttended.data');
$log.debug(data);
$scope.events = data;
});
}
);
}
}] }]
); );
@ -97,8 +136,8 @@ eventManControllers.controller('PersonDetailsCtrl', ['$scope', '$routeParams', '
eventManControllers.controller('ImportPersonsCtrl', ['$scope', '$log', eventManControllers.controller('ImportPersonsCtrl', ['$scope', '$log',
function ($scope, $log) { function ($scope, $log) {
$scope.ebCSVimport = function() { $scope.ebCSVimport = function() {
$log.info("ImportPersonsCtrl"); $log.debug("ImportPersonsCtrl");
$log.info($scope); $log.debug($scope);
}; };
}] }]
); );
@ -110,7 +149,7 @@ eventManControllers.controller('FileUploadCtrl', ['$scope', '$log', '$upload', '
$scope.reply = {}; $scope.reply = {};
$scope.events = Event.all(); $scope.events = Event.all();
$scope.upload = function(file, url) { $scope.upload = function(file, url) {
$log.info("FileUploadCtrl.upload"); $log.debug("FileUploadCtrl.upload");
$upload.upload({ $upload.upload({
url: url, url: url,
file: file, file: file,

View file

@ -3,15 +3,26 @@
/* Services that are used to interact with the backend. */ /* Services that are used to interact with the backend. */
var eventManServices = angular.module('eventManServices', ['ngResource']); var eventManServices = angular.module('eventManServices', ['ngResource']);
eventManServices.factory('Action', ['$resource', eventManServices.factory('Attendee', ['$resource', 'Event',
function($resource) { function($resource, Event) {
return $resource('actions'); return $resource('events/:id/persons/:person_id', {id: '@_id', person_id: '@person_id'}, {
personAttended: {
method: 'PUT',
params: {'persons.$.attended': true},
transformResponse: function(data, headers) {
console.log('reply');
console.log(angular.fromJson(data));
return angular.fromJson(data).event;
}
}
});
}] }]
); );
eventManServices.factory('Event', ['$resource', eventManServices.factory('Event', ['$resource',
function($resource) { function($resource) {
return $resource('events/:id', {id: '@_id'}, { return $resource('events/:id', {id: '@_id', person_id: '@person_id'}, {
all: { all: {
method: 'GET', method: 'GET',
isArray: true, isArray: true,
@ -31,7 +42,15 @@ eventManServices.factory('Event', ['$resource',
return data; return data;
} }
}, },
update: {method: 'PUT'} update: {method: 'PUT'},
personAttended: {
method: 'PUT',
isArray: true,
url: 'events/:id/persons/:person_id',
transformResponse: function(data, headers) {
return angular.fromJson(data).event.persons;
}
}
}); });
}] }]
); );
@ -47,7 +66,15 @@ eventManServices.factory('Person', ['$resource',
return angular.fromJson(data).persons; return angular.fromJson(data).persons;
} }
}, },
update: {method: 'PUT'} update: {method: 'PUT'},
getEvents: {
method: 'GET',
url: 'persons/:id/events',
isArray: true,
transformResponse: function(data, headers) {
return angular.fromJson(data).events;
}
}
}); });
}] }]
); );

View file

@ -20,21 +20,38 @@
</form> </form>
<div class="panel panel-primary table-striped top5"> <div class="panel panel-primary table-striped top5">
<div class="panel-heading">Actions</div> <div class="panel-heading">Events</div>
<div class="panel-body">
<form class="form-inline">
<div class="form-group">
<label for="query-persons">Search:</label>
<input type="text" id="query-persons" class="form-control" placeholder="Name or email" ng-model="query">
</div>
<div class="form-group">
<label for="events-order">Sort by:</label>
<select id="events-order" class="form-control" ng-model="orderProp">
<option value="name" ng-selected="selected">Alphabetical</option>
<option value="date">Date</option>
</select>
</div>
</form>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>Action</th> <th>Event</th>
<th>Event ID</th> <th>Attended</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="action in actions"> <tr ng-repeat="event in events">
<td>{{action.action}}</td> <td><a href="/#/events/{{event._id}}">{{event.title}}</a></td>
<td>{{action.event_id}}</td> <td>
<button class="btn btn-link" name="switch-attended" ng-click="updateAttendee(event, !event.person_data.attended)"><span class="glyphicon {{(event.person_data.attended) && 'glyphicon-ok-sign text-success' || 'glyphicon-remove-sign text-danger'}}"></span></button>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</div> </div>

View file

@ -16,9 +16,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
import re
import pymongo import pymongo
from bson.objectid import ObjectId from bson.objectid import ObjectId
re_objectid = re.compile(r'[0-9a-f]{24}')
class EventManDB(object): class EventManDB(object):
"""MongoDB connector.""" """MongoDB connector."""
@ -54,18 +56,30 @@ class EventManDB(object):
self.db = self.connection[self._dbName] self.db = self.connection[self._dbName]
return self.db return self.db
def toID(self, _id): def convert_obj(self, obj):
"""Convert a string to a MongoDB ID. """Convert a string to an object for MongoDB.
:param _id: string to convert to :class:`~bson.objectid.ObjectId` :param obj: object to convert
:type _id: str
:return: MongoDB ID
:rtype: :class:`~bson.objectid.ObjectId`
""" """
if not isinstance(_id, ObjectId): try:
_id = ObjectId(_id) return ObjectId(obj)
return _id except:
pass
try:
return int(obj)
except:
pass
return obj
def convert(self, seq):
if isinstance(seq, dict):
d = {}
for key, item in seq.iteritems():
d[key] = self.convert_obj(item)
return d
if isinstance(seq, (list, tuple)):
return [self.convert_obj(x) for x in seq]
return self.convert_obj(seq)
def get(self, collection, _id): def get(self, collection, _id):
"""Get a single document with the specified `_id`. """Get a single document with the specified `_id`.
@ -78,8 +92,7 @@ class EventManDB(object):
:return: the document with the given `_id` :return: the document with the given `_id`
:rtype: dict :rtype: dict
""" """
_id = self.toID(_id) results = self.query(collection, self.convert({'_id': _id}))
results = self.query(collection, {'_id': _id})
return results and results[0] or {} return results and results[0] or {}
def query(self, collection, query=None): def query(self, collection, query=None):
@ -94,13 +107,8 @@ class EventManDB(object):
:rtype: list :rtype: list
""" """
db = self.connect() db = self.connect()
query = query or {} query = self.convert(query or {})
if'_id' in query: return list(db[collection].find(query))
query['_id'] = self.toID(query['_id'])
results = list(db[collection].find(query))
for result in results:
result['_id'] = str(result['_id'])
return results
def add(self, collection, data): def add(self, collection, data):
"""Insert a new document. """Insert a new document.
@ -132,12 +140,12 @@ class EventManDB(object):
ret = db[collection].update(data, {'$set': data}, upsert=True) ret = db[collection].update(data, {'$set': data}, upsert=True)
return ret['updatedExisting'] return ret['updatedExisting']
def update(self, collection, _id, data): def update(self, collection, _id_or_query, data, operator='$set', create=True):
"""Update an existing document. """Update an existing document.
:param collection: update a document in this collection :param collection: update a document in this collection
:type collection: str :type collection: str
:param _id: unique ID of the document to be updatd :param _id: unique ID of the document to be updated
:type _id: str or :class:`~bson.objectid.ObjectId` :type _id: str or :class:`~bson.objectid.ObjectId`
:param data: the updated information to store :param data: the updated information to store
:type data: dict :type data: dict
@ -147,12 +155,30 @@ class EventManDB(object):
""" """
db = self.connect() db = self.connect()
data = data or {} data = data or {}
if _id_or_query is None:
_id_or_query = {'_id': None}
elif isinstance(_id_or_query, (list, tuple)):
_id_or_query = {'$or': self.buildSearchPattern(data, _id_or_query)}
elif not isinstance(_id_or_query, dict):
_id_or_query = {'_id': _id_or_query}
_id_or_query = self.convert(_id_or_query)
if '_id' in data: if '_id' in data:
del data['_id'] del data['_id']
db[collection].update({'_id': self.toID(_id)}, {'$set': data}) res = db[collection].find_and_modify(query=_id_or_query,
return self.get(collection, _id) update={operator: data}, full_response=True, new=True, upsert=create)
lastErrorObject = res.get('lastErrorObject') or {}
return lastErrorObject.get('updatedExisting', False), res.get('value') or {}
def merge(self, collection, data, searchBy): def buildSearchPattern(self, data, searchBy):
_or = []
for searchPattern in searchBy:
try:
_or.append(dict([(k, data[k]) for k in searchPattern]))
except KeyError:
continue
return _or
def merge(self, collection, data, searchBy, operator='$set'):
"""Update an existing document. """Update an existing document.
:param collection: update a document in this collection :param collection: update a document in this collection
@ -172,7 +198,8 @@ class EventManDB(object):
continue continue
if not _or: if not _or:
return False, None return False, None
ret = db[collection].update({'$or': _or}, {'$set': data}, upsert=True) # Two-steps merge/find to count the number of merged documents
ret = db[collection].update({'$or': _or}, {operator: data}, upsert=True)
_id = ret.get('upserted') _id = ret.get('upserted')
if _id is None: if _id is None:
newDoc = db[collection].find_one(data) newDoc = db[collection].find_one(data)
@ -194,6 +221,6 @@ class EventManDB(object):
return return
db = self.connect() db = self.connect()
if not isinstance(_id_or_query, dict): if not isinstance(_id_or_query, dict):
_id_or_query = self.toID(_id_or_query) _id_or_query = {'_id': _id_or_query}
db[collection].remove(_id_or_query) db[collection].remove(_id_or_query)

View file

@ -57,12 +57,12 @@ class CollectionHandler(BaseHandler):
collection = None collection = None
@gen.coroutine @gen.coroutine
def get(self, id_=None, resource=None, **kwargs): def get(self, id_=None, resource=None, resource_id=None, **kwargs):
if resource: if resource:
method = getattr(self, 'handle_%s' % resource, None) method = getattr(self, 'handle_get_%s' % resource, None)
if method and callable(method): if method and callable(method):
try: try:
self.write(method(id_, **kwargs)) self.write(method(id_, resource_id, **kwargs))
return return
except: except:
pass pass
@ -77,14 +77,20 @@ class CollectionHandler(BaseHandler):
self.write({self.collection: self.db.query(self.collection)}) self.write({self.collection: self.db.query(self.collection)})
@gen.coroutine @gen.coroutine
def post(self, id_=None, **kwargs): def post(self, id_=None, resource=None, resource_id=None, **kwargs):
data = escape.json_decode(self.request.body or {}) data = escape.json_decode(self.request.body or {})
if resource:
method = getattr(self, 'handle_%s_%s' % (self.request.method.lower(), resource), None)
if method and callable(method):
try:
self.write(method(id_, resource_id, data, **kwargs))
return
except:
pass
if id_ is None: if id_ is None:
# insert a new document
newData = self.db.add(self.collection, data) newData = self.db.add(self.collection, data)
else: else:
# update an existing document merged, newData = self.db.update(self.collection, id_, data)
newData = self.db.update(self.collection, id_, data)
self.write(newData) self.write(newData)
# PUT (update an existing document) is handled by the POST (create a new document) method # PUT (update an existing document) is handled by the POST (create a new document) method
@ -98,28 +104,43 @@ class CollectionHandler(BaseHandler):
class PersonsHandler(CollectionHandler): class PersonsHandler(CollectionHandler):
"""Handle requests for Persons.""" """Handle requests for Persons."""
collection = 'persons' collection = 'persons'
object_id = 'person_id'
def handle_events(self, _id, **kwds): def handle_get_events(self, id_, resource_id=None, **kwargs):
return {'events': []} query = {'persons.person_id': id_}
if resource_id:
query['_id'] = resource_id
events = self.db.query('events', query)
for event in events:
person_data = {}
for persons in event.get('persons') or []:
if str(persons.get('person_id')) == id_:
person_data = persons
break
event['person_data'] = person_data
return {'events': events}
class EventsHandler(CollectionHandler): class EventsHandler(CollectionHandler):
"""Handle requests for Events.""" """Handle requests for Events."""
collection = 'events' collection = 'events'
object_id = 'event_id'
def handle_get_persons(self, id_, resource_id=None):
query = {'_id': id_}
event = self.db.query('events', query)[0]
if resource_id:
for person in event.get('persons', []):
if str(person.get('person_id')) == resource_id:
return {'person': person}
return {'persons': event.get('persons') or {}}
class ActionsHandler(CollectionHandler): def handle_put_persons(self, id_, person_id, data):
"""Handle requests for Actions.""" merged, doc = self.db.update('events',
collection = 'actions' {'_id': id_, 'persons.person_id': person_id},
data, create=False)
def get(self, *args, **kwargs): return {'event': doc}
params = self.request.arguments or {}
if 'event_id' in params:
params['event_id'] = self.db.toID(params['event_id'][0])
if 'person_id' in params:
params['person_id'] = self.db.toID(params['person_id'][0])
data = self.db.query(self.collection, params)
self.write({'actions': data})
class EbCSVImportPersonsHandler(BaseHandler): class EbCSVImportPersonsHandler(BaseHandler):
@ -131,14 +152,19 @@ class EbCSVImportPersonsHandler(BaseHandler):
'Cognome acquirente': 'surname', 'Cognome acquirente': 'surname',
'Nome acquirente': 'name', 'Nome acquirente': 'name',
'E-mail acquirente': 'email', 'E-mail acquirente': 'email',
'Cognome': 'original_surname', 'Cognome': 'surname',
'Nome': 'original_name', 'Nome': 'name',
'E-mail': 'original_email', 'E-mail': 'email',
'Indirizzo e-mail': 'email',
'Tipologia biglietto': 'ticket_kind', 'Tipologia biglietto': 'ticket_kind',
'Data partecipazione': 'attending_datetime', 'Data partecipazione': 'attending_datetime',
'Data check-in': 'checkin_datetime', 'Data check-in': 'checkin_datetime',
'Ordine n.': 'order_nr', 'Ordine n.': 'order_nr',
'ID ordine': 'order_nr',
'Prefisso (Sig., Sig.ra, ecc.)': 'name_title',
} }
keepPersonData = ('name', 'surname', 'email')
@gen.coroutine @gen.coroutine
def post(self, **kwargs): def post(self, **kwargs):
targetEvent = None targetEvent = None
@ -146,7 +172,7 @@ class EbCSVImportPersonsHandler(BaseHandler):
targetEvent = self.get_body_argument('targetEvent') targetEvent = self.get_body_argument('targetEvent')
except: except:
pass pass
reply = dict(total=0, valid=0, merged=0) reply = dict(total=0, valid=0, merged=0, new_in_event=0)
for fieldname, contents in self.request.files.iteritems(): for fieldname, contents in self.request.files.iteritems():
for content in contents: for content in contents:
filename = content['filename'] filename = content['filename']
@ -154,17 +180,27 @@ class EbCSVImportPersonsHandler(BaseHandler):
reply['total'] += parseStats['total'] reply['total'] += parseStats['total']
reply['valid'] += parseStats['valid'] reply['valid'] += parseStats['valid']
for person in persons: for person in persons:
merged, _id = self.db.merge('persons', person, person_data = dict([(k, person[k]) for k in self.keepPersonData
searchBy=[('email',), ('name', 'surname')]) if k in person])
merged, person = self.db.update('persons',
[('email',), ('name', 'surname')],
person_data)
if merged: if merged:
reply['merged'] += 1 reply['merged'] += 1
if targetEvent and _id: if targetEvent and person:
event_id = targetEvent
person_id = person['_id']
registered_data = { registered_data = {
'event_id': self.db.toID(targetEvent), 'person_id': person_id,
'person_id': self.db.toID(_id), 'attended': False,
'action': 'registered',
'from_file': filename} 'from_file': filename}
self.db.insertOne('actions', registered_data) person.update(registered_data)
if not self.db.query('events',
{'_id': event_id, 'persons.person_id': person_id}):
self.db.update('events', {'_id': event_id},
{'persons': person},
operator='$addToSet')
reply['new_in_event'] += 1
self.write(reply) self.write(reply)
@ -189,9 +225,8 @@ def run():
init_params = dict(db=db_connector) init_params = dict(db=db_connector)
application = tornado.web.Application([ application = tornado.web.Application([
(r"/persons/?(?P<id_>\w+)?/?(?P<resource>\w+)?", PersonsHandler, init_params), (r"/persons/?(?P<id_>\w+)?/?(?P<resource>\w+)?/?(?P<resource_id>\w+)?", PersonsHandler, init_params),
(r"/events/?(?P<id_>\w+)?", EventsHandler, init_params), (r"/events/?(?P<id_>\w+)?/?(?P<resource>\w+)?/?(?P<resource_id>\w+)?", EventsHandler, init_params),
(r"/actions/?.*", ActionsHandler, init_params),
(r"/(?:index.html)?", RootHandler, init_params), (r"/(?:index.html)?", RootHandler, init_params),
(r"/ebcsvpersons", EbCSVImportPersonsHandler, init_params), (r"/ebcsvpersons", EbCSVImportPersonsHandler, init_params),
(r'/(.*)', tornado.web.StaticFileHandler, {"path": "angular_app"}) (r'/(.*)', tornado.web.StaticFileHandler, {"path": "angular_app"})

View file

@ -16,6 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
import re
import csv import csv
import json import json
import datetime import datetime
@ -72,13 +73,17 @@ def csvParse(csvStr, remap=None, merge=None):
class ImprovedEncoder(json.JSONEncoder): class ImprovedEncoder(json.JSONEncoder):
"""Enhance the default JSON encoder to serialize datetime objects.""" """Enhance the default JSON encoder to serialize datetime and ObjectId instances."""
def default(self, o): def default(self, o):
if isinstance(o, (datetime.datetime, datetime.date, if isinstance(o, (datetime.datetime, datetime.date,
datetime.time, datetime.timedelta, ObjectId)): datetime.time, datetime.timedelta, ObjectId)):
try:
return str(o) return str(o)
except Exception, e:
pass
return json.JSONEncoder.default(self, o) return json.JSONEncoder.default(self, o)
# Inject our class as the default encoder. # Inject our class as the default encoder.
json._default_encoder = ImprovedEncoder() json._default_encoder = ImprovedEncoder()