Merge pull request #170 from alberanid/master

getting ready for hib 2017 spring
This commit is contained in:
Davide Alberani 2017-04-02 14:43:26 +02:00 committed by GitHub
commit e25c9ae385
18 changed files with 433 additions and 195 deletions

View file

@ -31,6 +31,7 @@ See the *docs/DEVELOPMENT.md* file for more information about how to contribute.
Technological stack Technological stack
=================== ===================
- [Python 3](https://www.python.org/) for the backend
- [AngularJS](https://angularjs.org/) (plus some third-party modules) for the webApp - [AngularJS](https://angularjs.org/) (plus some third-party modules) for the webApp
- [Angular Easy form Generator](https://mackentoch.github.io/easyFormGenerator/) for the custom forms - [Angular Easy form Generator](https://mackentoch.github.io/easyFormGenerator/) for the custom forms
- [Bootstrap](http://getbootstrap.com/) (plus [Angular UI](https://angular-ui.github.io/bootstrap/)) for the eye-candy - [Bootstrap](http://getbootstrap.com/) (plus [Angular UI](https://angular-ui.github.io/bootstrap/)) for the eye-candy
@ -45,14 +46,14 @@ If you want to print labels using the _print\_label_ trigger, you may also need
Install and run Install and run
=============== ===============
Be sure to have a running MongoDB server, locally. If you want to install the dependencies only locally to the current user, you can append the *--user* argument to the *pip* calls. Please also install the *python-dev* package, before running the following commands. Be sure to have a running MongoDB server, locally. If you want to install the dependencies only locally to the current user, you can append the *--user* argument to the *pip* calls. Please also install the *python3-dev* package, before running the following commands.
wget https://bootstrap.pypa.io/get-pip.py wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py sudo python3 get-pip.py
sudo pip install tornado # version 4.2 or later sudo pip3 install tornado # version 4.2 or later
sudo pip install pymongo # version 3.2.2 or later sudo pip3 install pymongo # version 3.2.2 or later
sudo pip install python-dateutil sudo pip3 install python-dateutil
sudo pip install pycups # only needed if you want to print labels sudo pip3 install pycups # only needed if you want to print labels
git clone https://github.com/raspibo/eventman git clone https://github.com/raspibo/eventman
cd eventman cd eventman
./eventman_server.py --debug ./eventman_server.py --debug
@ -114,7 +115,7 @@ Users can register, but are not forced to do so: tickets can also be issued to u
License and copyright License and copyright
===================== =====================
Copyright 2015-2016 Davide Alberani <da@erlug.linux.it>, RaspiBO <info@raspibo.org> Copyright 2015-2017 Davide Alberani <da@erlug.linux.it>, RaspiBO <info@raspibo.org>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -27,9 +27,12 @@
<form class="form-inline"> <form class="form-inline">
<div class="form-group"> <div class="form-group">
<label for="query-tickets">{{'Search:' | translate}}</label> <label for="query-tickets">{{'Search:' | translate}}</label>
<input eventman-focus type="text" id="query-tickets" class="form-control" placeholder="{{'Name or email' | translate}}" ng-model="query" ng-model-options="{debounce: 600}"> <input eventman-focus type="text" id="query-tickets" class="form-control" placeholder="{{'Name or email' | translate}}" ng-model="query" ng-model-options="{debounce: 350}">
</div>&nbsp;<label>&nbsp;<input type="checkbox" ng-model="registeredFilterOptions.all" /> {{'Show cancelled tickets' | translate}}</label> </div>&nbsp;<label>&nbsp;<input type="checkbox" ng-model="registeredFilterOptions.all" /> {{'Show cancelled tickets' | translate}}</label>
</form> </form>
<pagination ng-model="currentPage" total-items="filteredLength" items-per-page="itemsPerPage"
direction-links="false" boundary-links="true" boundary-link-numbers="true" max-size="maxPaginationSize">
</pagination>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@ -43,7 +46,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="ticket in (event.tickets || []) | splittedFilter:query | registeredFilter:registeredFilterOptions | orderBy:ticketsOrder"> <tr ng-repeat="ticket in shownItems">
<td class="text-right">{{$index+1}}</td> <td class="text-right">{{$index+1}}</td>
<td> <td>
<span> <span>
@ -71,6 +74,9 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<pagination ng-model="currentPage" total-items="filteredLength" items-per-page="itemsPerPage"
direction-links="false" boundary-links="true" boundary-link-numbers="true" max-size="maxPaginationSize">
</pagination>
</div> </div>
</div> </div>
</div> </div>
@ -87,7 +93,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="person in allPersons | splittedFilter:query | personRegistered:{event: event, present: false}"> <tr ng-repeat="person in (query ? allPersons : []) | splittedFilter:query | personRegistered:{event: event, present: false} | limitTo:maxAllPersons">
<td> <td>
<strong>{{person.name}} {{person.surname}}</strong> <strong>{{person.name}} {{person.surname}}</strong>
<br /> <br />

View file

@ -13,7 +13,7 @@
<form class="form-inline"> <form class="form-inline">
<div class="form-group"> <div class="form-group">
<label for="query-events">{{'Search:' | translate}}</label> <label for="query-events">{{'Search:' | translate}}</label>
<input eventman-focus type="text" id="query-events" class="form-control" placeholder="{{'Event title' | translate}}" ng-model="query" ng-model-options="{debounce: 600}"> <input eventman-focus type="text" id="query-events" class="form-control" placeholder="{{'Event title' | translate}}" ng-model="query" ng-model-options="{debounce: 350}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="events-order">Sort by:</label> <label for="events-order">Sort by:</label>

View file

@ -15,7 +15,7 @@
<script type="text/javascript" src="/static/js/angular-resource.min.js"></script> <script type="text/javascript" src="/static/js/angular-resource.min.js"></script>
<script type="text/javascript" src="/static/js/angular-file-upload.min.js"></script> <script type="text/javascript" src="/static/js/angular-file-upload.min.js"></script>
<script type="text/javascript" src="/static/js/angular-ui-router.min.js"></script> <script type="text/javascript" src="/static/js/angular-ui-router.min.js"></script>
<script type="text/javascript" src="/static/js/angular-websocket.js"></script> <script type="text/javascript" src="/static/js/angular-websocket.min.js"></script>
<script type="text/javascript" src="/static/js/angular-translate.min.js"></script> <script type="text/javascript" src="/static/js/angular-translate.min.js"></script>
<script type="text/javascript" src="/static/js/angular-translate-loader-static-files.min.js"></script> <script type="text/javascript" src="/static/js/angular-translate-loader-static-files.min.js"></script>
<script type="text/javascript" src="/static/js/nya-bs-select.min.js"></script> <script type="text/javascript" src="/static/js/nya-bs-select.min.js"></script>
@ -52,7 +52,7 @@
</head> </head>
<!-- <!--
Copyright 2015-2016 Davide Alberani <da@erlug.linux.it> Copyright 2015-2017 Davide Alberani <da@erlug.linux.it>
RaspiBO <info@raspibo.org> RaspiBO <info@raspibo.org>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
/* /*
Copyright 2015-2016 Davide Alberani <da@erlug.linux.it> Copyright 2015-2017 Davide Alberani <da@erlug.linux.it>
RaspiBO <info@raspibo.org> RaspiBO <info@raspibo.org>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");

View file

@ -66,8 +66,9 @@ eventManControllers.controller('ModalConfirmInstanceCtrl', ['$scope', '$uibModal
); );
eventManControllers.controller('EventsListCtrl', ['$scope', 'Event', '$uibModal', '$log', '$translate', '$rootScope', '$state', eventManControllers.controller('EventsListCtrl', ['$scope', 'Event', '$uibModal', '$log', '$translate', '$rootScope', '$state', '$filter',
function ($scope, Event, $uibModal, $log, $translate, $rootScope, $state) { function ($scope, Event, $uibModal, $log, $translate, $rootScope, $state, $filter) {
$scope.query = '';
$scope.tickets = []; $scope.tickets = [];
$scope.events = Event.all(function(events) { $scope.events = Event.all(function(events) {
if (events && $state.is('tickets')) { if (events && $state.is('tickets')) {
@ -79,11 +80,38 @@ eventManControllers.controller('EventsListCtrl', ['$scope', 'Event', '$uibModal'
}); });
$scope.tickets.push.apply($scope.tickets, evt_tickets || []); $scope.tickets.push.apply($scope.tickets, evt_tickets || []);
}); });
$scope.filterTickets();
} }
}); });
$scope.eventsOrderProp = "-begin_date"; $scope.eventsOrderProp = "-begin_date";
$scope.ticketsOrderProp = ["name", "surname"]; $scope.ticketsOrderProp = ["name", "surname"];
$scope.shownItems = [];
$scope.currentPage = 1;
$scope.itemsPerPage = 10;
$scope.filteredLength = 0;
$scope.maxPaginationSize = 10;
$scope.filterTickets = function() {
var tickets = $scope.tickets || [];
tickets = $filter('splittedFilter')(tickets, $scope.query);
tickets = $filter('orderBy')(tickets, $scope.ticketsOrderProp);
$scope.filteredLength = tickets.length;
tickets = $filter('pagination')(tickets, $scope.currentPage, $scope.itemsPerPage);
$scope.shownItems = tickets;
};
$scope.$watch('query', function() {
if (!$scope.query) {
$scope.currentPage = 1;
}
$scope.filterTickets();
});
$scope.$watch('currentPage + itemsPerPage', function() {
$scope.filterTickets();
});
$scope.confirm_delete = 'Do you really want to delete this event?'; $scope.confirm_delete = 'Do you really want to delete this event?';
$rootScope.$on('$translateChangeSuccess', function () { $rootScope.$on('$translateChangeSuccess', function () {
$translate('Do you really want to delete this event?').then(function (translation) { $translate('Do you really want to delete this event?').then(function (translation) {
@ -123,8 +151,8 @@ eventManControllers.controller('EventsListCtrl', ['$scope', 'Event', '$uibModal'
} }
); );
$scope.ticketsOrderProp = new_order; $scope.ticketsOrderProp = new_order;
$scope.filterTickets();
}; };
}] }]
); );
@ -166,13 +194,15 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
); );
eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event', 'EventTicket', 'Setting', '$log', '$translate', '$rootScope', 'EventUpdates', '$uibModal', eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event', 'EventTicket', 'Setting', '$log', '$translate', '$rootScope', 'EventUpdates', '$uibModal', '$filter',
function ($scope, $state, Event, EventTicket, Setting, $log, $translate, $rootScope, EventUpdates, $uibModal) { function ($scope, $state, Event, EventTicket, Setting, $log, $translate, $rootScope, EventUpdates, $uibModal, $filter) {
$scope.ticketsOrder = ["name", "surname"]; $scope.ticketsOrder = ["name", "surname"];
$scope.countAttendees = 0; $scope.countAttendees = 0;
$scope.message = {}; $scope.message = {};
$scope.query = '';
$scope.event = {}; $scope.event = {};
$scope.event.tickets = []; $scope.event.tickets = [];
$scope.shownItems = [];
$scope.ticket = {}; // current ticket, for the event.ticket.* states $scope.ticket = {}; // current ticket, for the event.ticket.* states
$scope.tickets = []; // list of all tickets, for the 'tickets' state $scope.tickets = []; // list of all tickets, for the 'tickets' state
$scope.formSchema = {}; $scope.formSchema = {};
@ -180,16 +210,47 @@ eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event',
$scope.guiOptions = {dangerousActionsEnabled: false}; $scope.guiOptions = {dangerousActionsEnabled: false};
$scope.customFields = Setting.query({setting: 'ticket_custom_field', in_event_details: true}); $scope.customFields = Setting.query({setting: 'ticket_custom_field', in_event_details: true});
$scope.registeredFilterOptions = {all: false}; $scope.registeredFilterOptions = {all: false};
$scope.formFieldsMap = {}; $scope.formFieldsMap = {};
$scope.formFieldsMapRev = {}; $scope.formFieldsMapRev = {};
$scope.currentPage = 1;
$scope.itemsPerPage = 10;
$scope.filteredLength = 0;
$scope.maxPaginationSize = 10;
$scope.maxAllPersons = 10;
$scope.filterTickets = function() {
var tickets = $scope.event.tickets || [];
tickets = $filter('splittedFilter')(tickets, $scope.query);
tickets = $filter('registeredFilter')(tickets, $scope.registeredFilterOptions);
tickets = $filter('orderBy')(tickets, $scope.ticketsOrder);
$scope.filteredLength = tickets.length;
tickets = $filter('pagination')(tickets, $scope.currentPage, $scope.itemsPerPage);
$scope.shownItems = tickets;
};
$scope.$watch('query', function() {
if (!$scope.query) {
$scope.currentPage = 1;
}
$scope.filterTickets();
});
$scope.$watchCollection('registeredFilterOptions', function() {
$scope.filterTickets();
});
$scope.$watch('currentPage + itemsPerPage', function() {
$scope.filterTickets();
});
if ($state.params.id) { if ($state.params.id) {
$scope.event = Event.get({id: $state.params.id}, function(data) { $scope.event = Event.get({id: $state.params.id}, function(data) {
$scope.$watchCollection(function() { $scope.$watchCollection(function() {
return $scope.event.tickets; return $scope.event.tickets;
}, function(new_collection, old_collection) { }, function(new_collection, old_collection) {
$scope.calcAttendees(); $scope.calcAttendees();
$scope.filterTickets();
} }
); );
@ -248,6 +309,12 @@ eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event',
} }
if (data.action == 'update' && ticket_idx != -1 && $scope.event.tickets[ticket_idx] != data.ticket) { if (data.action == 'update' && ticket_idx != -1 && $scope.event.tickets[ticket_idx] != data.ticket) {
// if we're updating the 'attended' key and the action came from us (same user, possibly on
// a different station), also show a message.
if (data.ticket.attended != $scope.event.tickets[ticket_idx].attended &&
$scope.info.user.username == data.username) {
$scope.showAttendedMessage(data.ticket, data.ticket.attended);
}
$scope.event.tickets[ticket_idx] = data.ticket; $scope.event.tickets[ticket_idx] = data.ticket;
} else if (data.action == 'add' && ticket_idx == -1) { } else if (data.action == 'add' && ticket_idx == -1) {
$scope._localAddTicket(data.ticket); $scope._localAddTicket(data.ticket);
@ -406,20 +473,24 @@ eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event',
} }
if (key === 'attended' && !hideMessage) { if (key === 'attended' && !hideMessage) {
var msg = {}; $scope.showAttendedMessage(data.ticket, value);
var name = $scope.buildTicketLabel(data.ticket);
if (value) {
msg.message = name + ' successfully added to event ' + $scope.event.title;
} else {
msg.message = name + ' successfully removed from event ' + $scope.event.title;
msg.isError = true;
}
$scope.showMessage(msg);
} }
}); });
}; };
$scope.showAttendedMessage = function(ticket, attends) {
var msg = {};
var name = $scope.buildTicketLabel(ticket);
if (attends) {
msg.message = name + ' successfully added to event ' + $scope.event.title;
} else {
msg.message = name + ' successfully removed from event ' + $scope.event.title;
msg.isError = true;
}
$scope.showMessage(msg);
};
$scope.setTicketAttributeAndRefocus = function(ticket, key, value) { $scope.setTicketAttributeAndRefocus = function(ticket, key, value) {
$scope.setTicketAttribute(ticket, key, value); $scope.setTicketAttribute(ticket, key, value);
$scope.query = ''; $scope.query = '';
@ -575,6 +646,7 @@ eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event',
} }
); );
$scope.ticketsOrder = new_order; $scope.ticketsOrder = new_order;
$scope.filterTickets();
}; };
$scope.showMessage = function(cfg) { $scope.showMessage = function(cfg) {
@ -649,7 +721,7 @@ eventManControllers.controller('UsersCtrl', ['$scope', '$rootScope', '$state', '
User.login(loginData, function(data) { User.login(loginData, function(data) {
if (!data.error) { if (!data.error) {
$rootScope.readInfo(function(info) { $rootScope.readInfo(function(info) {
$log.debug('logged in user: ' + info.user.username); $log.debug('logged in user: ' + $scope.info.user.username);
$rootScope.clearError(); $rootScope.clearError();
$state.go('events'); $state.go('events');
}); });

View file

@ -68,6 +68,18 @@ eventManApp.filter('registeredFilter', ['$filter',
); );
/* Filter that implements a generic pagination. */
eventManApp.filter('pagination', ['$filter',
function($filter) {
return function(inputArray, page, itemsPerPage) {
var begin = (page - 1) * itemsPerPage;
var end = begin + itemsPerPage;
return inputArray.slice(begin, end);;
};
}]
);
/* Filter that returns only the attendees at an event. */ /* Filter that returns only the attendees at an event. */
eventManApp.filter('attendeesFilter', ['$filter', eventManApp.filter('attendeesFilter', ['$filter',
function($filter) { function($filter) {

View file

@ -11,10 +11,13 @@
<form class="form-inline"> <form class="form-inline">
<div class="form-group"> <div class="form-group">
<label for="query-tickets">{{'Search:' | translate}}</label> <label for="query-tickets">{{'Search:' | translate}}</label>
<input eventman-focus type="text" id="query-tickets" class="form-control" placeholder="{{'Name or email' | translate}}" ng-model="query" ng-model-options="{debounce: 600}"> <input eventman-focus type="text" id="query-tickets" class="form-control" placeholder="{{'Name or email' | translate}}" ng-model="query" ng-model-options="{debounce: 350}">
</div> </div>
</form> </form>
<pagination ng-model="currentPage" total-items="filteredLength" items-per-page="itemsPerPage"
direction-links="false" boundary-links="true" boundary-link-numbers="true" max-size="maxPaginationSize">
</pagination>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@ -25,7 +28,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="ticket in tickets | splittedFilter:query | orderBy:ticketsOrderProp"> <tr ng-repeat="ticket in shownItems">
<td class="text-right">{{$index+1}}</td> <td class="text-right">{{$index+1}}</td>
<td> <td>
<span><strong><a ui-sref="event.ticket.edit({id: ticket.event_id, ticket_id: ticket._id})"><span>{{ticket.name}}</span>&nbsp;<span>{{ticket.surname}}</span></a></strong></span><span ng-if="ticket.email">&nbsp;&lt;{{ticket.email}}&gt;</span> <span><strong><a ui-sref="event.ticket.edit({id: ticket.event_id, ticket_id: ticket._id})"><span>{{ticket.name}}</span>&nbsp;<span>{{ticket.surname}}</span></a></strong></span><span ng-if="ticket.email">&nbsp;&lt;{{ticket.email}}&gt;</span>
@ -40,6 +43,9 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<pagination ng-model="currentPage" total-items="filteredLength" items-per-page="itemsPerPage"
direction-links="false" boundary-links="true" boundary-link-numbers="true" max-size="maxPaginationSize">
</pagination>
</div> </div>
</div> </div>
</div> </div>

View file

@ -31,7 +31,7 @@
<form class="form-inline"> <form class="form-inline">
<div class="form-group"> <div class="form-group">
<label for="query-tickets">{{'Search:' | translate}}</label> <label for="query-tickets">{{'Search:' | translate}}</label>
<input eventman-focus type="text" id="query-tickets" class="form-control" placeholder="{{'Event title' | translate}}" ng-model="query" ng-model-options="{debounce: 600}"> <input eventman-focus type="text" id="query-tickets" class="form-control" placeholder="{{'Event title' | translate}}" ng-model="query" ng-model-options="{debounce: 350}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="tickets-order">{{'Sort by:' | translate}}</label> <label for="tickets-order">{{'Sort by:' | translate}}</label>

View file

@ -14,7 +14,7 @@
<form class="form-inline"> <form class="form-inline">
<div class="form-group"> <div class="form-group">
<label for="query-users">{{'Search:' | translate}}</label> <label for="query-users">{{'Search:' | translate}}</label>
<input userman-focus type="text" id="query-users" class="form-control" placeholder="{{'Username or email' | translate}}" ng-model="query" ng-model-options="{debounce: 600}"> <input userman-focus type="text" id="query-users" class="form-control" placeholder="{{'Username or email' | translate}}" ng-model="query" ng-model-options="{debounce: 350}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="users-order">Sort by:</label> <label for="users-order">Sort by:</label>

View file

@ -1,8 +1,6 @@
Development Development
=========== ===========
As of July 2016, EventMan(ager) is under heavy refactoring. For a list of main changes that will be introduced, see https://github.com/raspibo/eventman/issues
Every contribution, in form of code or ideas, is welcome. Every contribution, in form of code or ideas, is welcome.

View file

@ -1,9 +1,9 @@
#!/usr/bin/env python #!/usr/bin/env python3
"""EventMan(ager) """EventMan(ager)
Your friendly manager of attendees at an event. Your friendly manager of attendees at an event.
Copyright 2015-2016 Davide Alberani <da@erlug.linux.it> Copyright 2015-2017 Davide Alberani <da@erlug.linux.it>
RaspiBO <info@raspibo.org> RaspiBO <info@raspibo.org>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -38,7 +38,8 @@ import tornado.websocket
from tornado import gen, escape, process from tornado import gen, escape, process
import utils import utils
import backend import monco
import collections
ENCODING = 'utf-8' ENCODING = 'utf-8'
PROCESS_TIMEOUT = 60 PROCESS_TIMEOUT = 60
@ -102,8 +103,8 @@ class BaseHandler(tornado.web.RequestHandler):
_users_cache = {} _users_cache = {}
# A property to access the first value of each argument. # A property to access the first value of each argument.
arguments = property(lambda self: dict([(k, v[0]) arguments = property(lambda self: dict([(k, v[0].decode('utf-8'))
for k, v in self.request.arguments.iteritems()])) for k, v in self.request.arguments.items()]))
# A property to access both the UUID and the clean arguments. # A property to access both the UUID and the clean arguments.
@property @property
@ -150,23 +151,26 @@ class BaseHandler(tornado.web.RequestHandler):
"""Convert some textual values to boolean.""" """Convert some textual values to boolean."""
if isinstance(obj, (list, tuple)): if isinstance(obj, (list, tuple)):
obj = obj[0] obj = obj[0]
if isinstance(obj, (str, unicode)): if isinstance(obj, str):
obj = obj.lower() obj = obj.lower()
return self._bool_convert.get(obj, obj) return self._bool_convert.get(obj, obj)
def arguments_tobool(self): def arguments_tobool(self):
"""Return a dictionary of arguments, converted to booleans where possible.""" """Return a dictionary of arguments, converted to booleans where possible."""
return dict([(k, self.tobool(v)) for k, v in self.arguments.iteritems()]) return dict([(k, self.tobool(v)) for k, v in self.arguments.items()])
def initialize(self, **kwargs): def initialize(self, **kwargs):
"""Add every passed (key, value) as attributes of the instance.""" """Add every passed (key, value) as attributes of the instance."""
for key, value in kwargs.iteritems(): for key, value in kwargs.items():
setattr(self, key, value) setattr(self, key, value)
@property @property
def current_user(self): def current_user(self):
"""Retrieve current user name from the secure cookie.""" """Retrieve current user name from the secure cookie."""
return self.get_secure_cookie("user") current_user = self.get_secure_cookie("user")
if isinstance(current_user, bytes):
current_user = current_user.decode('utf-8')
return current_user
@property @property
def current_user_info(self): def current_user_info(self):
@ -174,16 +178,16 @@ class BaseHandler(tornado.web.RequestHandler):
current_user = self.current_user current_user = self.current_user
if current_user in self._users_cache: if current_user in self._users_cache:
return self._users_cache[current_user] return self._users_cache[current_user]
permissions = set([k for (k, v) in self.permissions.iteritems() if v is True]) permissions = set([k for (k, v) in self.permissions.items() if v is True])
user_info = {'permissions': permissions} user_info = {'permissions': permissions}
if current_user: if current_user:
user_info['username'] = current_user user_info['_id'] = current_user
res = self.db.query('users', {'username': current_user}) user = self.db.getOne('users', {'_id': current_user})
if res: if user:
user = res[0]
user_info = user user_info = user
permissions.update(set(user.get('permissions') or [])) permissions.update(set(user.get('permissions') or []))
user_info['permissions'] = permissions user_info['permissions'] = permissions
user_info['isRegistered'] = True
self._users_cache[current_user] = user_info self._users_cache[current_user] = user_info
return user_info return user_info
@ -204,7 +208,7 @@ class BaseHandler(tornado.web.RequestHandler):
collection_permission = self.permissions.get(permission) collection_permission = self.permissions.get(permission)
if isinstance(collection_permission, bool): if isinstance(collection_permission, bool):
return collection_permission return collection_permission
if callable(collection_permission): if isinstance(collection_permission, collections.Callable):
return collection_permission(permission) return collection_permission(permission)
return False return False
@ -305,7 +309,7 @@ class CollectionHandler(BaseHandler):
:rtype: str""" :rtype: str"""
t = str(time.time()).replace('.', '_') t = str(time.time()).replace('.', '_')
seq = str(self.get_next_seq(seq)) seq = str(self.get_next_seq(seq))
rand = ''.join([random.choice(self._id_chars) for x in xrange(random_alpha)]) rand = ''.join([random.choice(self._id_chars) for x in range(random_alpha)])
return '-'.join((t, seq, rand)) return '-'.join((t, seq, rand))
def _filter_results(self, results, params): def _filter_results(self, results, params):
@ -320,11 +324,11 @@ class CollectionHandler(BaseHandler):
:rtype: list""" :rtype: list"""
if not params: if not params:
return results return results
params = backend.convert(params) params = monco.convert(params)
filtered = [] filtered = []
for result in results: for result in results:
add = True add = True
for key, value in params.iteritems(): for key, value in params.items():
if key not in result or result[key] != value: if key not in result or result[key] != value:
add = False add = False
break break
@ -338,8 +342,8 @@ class CollectionHandler(BaseHandler):
:param data: dictionary to clean :param data: dictionary to clean
:type data: dict""" :type data: dict"""
if isinstance(data, dict): if isinstance(data, dict):
for key in data.keys(): for key in list(data.keys()):
if isinstance(key, (str, unicode)) and key.startswith('$'): if isinstance(key, str) and key.startswith('$'):
del data[key] del data[key]
return data return data
@ -349,7 +353,7 @@ class CollectionHandler(BaseHandler):
:param data: dictionary to convert :param data: dictionary to convert
:type data: dict""" :type data: dict"""
ret = {} ret = {}
for key, value in data.iteritems(): for key, value in data.items():
if isinstance(value, (list, tuple, dict)): if isinstance(value, (list, tuple, dict)):
continue continue
try: try:
@ -357,7 +361,7 @@ class CollectionHandler(BaseHandler):
key = re_env_key.sub('', key) key = re_env_key.sub('', key)
if not key: if not key:
continue continue
ret[key] = unicode(value).encode(ENCODING) ret[key] = str(value).encode(ENCODING)
except: except:
continue continue
return ret return ret
@ -382,7 +386,7 @@ class CollectionHandler(BaseHandler):
if acl and not self.has_permission(permission): if acl and not self.has_permission(permission):
return self.build_error(status=401, message='insufficient permissions: %s' % permission) return self.build_error(status=401, message='insufficient permissions: %s' % permission)
handler = getattr(self, 'handle_get_%s' % resource, None) handler = getattr(self, 'handle_get_%s' % resource, None)
if handler and callable(handler): if handler and isinstance(handler, collections.Callable):
output = handler(id_, resource_id, **kwargs) or {} output = handler(id_, resource_id, **kwargs) or {}
output = self.apply_filter(output, 'get_%s' % resource) output = self.apply_filter(output, 'get_%s' % resource)
self.write(output) self.write(output)
@ -432,7 +436,7 @@ class CollectionHandler(BaseHandler):
return self.build_error(status=401, message='insufficient permissions: %s' % permission) return self.build_error(status=401, message='insufficient permissions: %s' % permission)
# Handle access to sub-resources. # Handle access to sub-resources.
handler = getattr(self, 'handle_%s_%s' % (method, resource), None) handler = getattr(self, 'handle_%s_%s' % (method, resource), None)
if handler and callable(handler): if handler and isinstance(handler, collections.Callable):
data = self.apply_filter(data, 'input_%s_%s' % (method, resource)) data = self.apply_filter(data, 'input_%s_%s' % (method, resource))
output = handler(id_, resource_id, data, **kwargs) output = handler(id_, resource_id, data, **kwargs)
output = self.apply_filter(output, 'get_%s' % resource) output = self.apply_filter(output, 'get_%s' % resource)
@ -478,7 +482,7 @@ class CollectionHandler(BaseHandler):
if not self.has_permission(permission): if not self.has_permission(permission):
return self.build_error(status=401, message='insufficient permissions: %s' % permission) return self.build_error(status=401, message='insufficient permissions: %s' % permission)
method = getattr(self, 'handle_delete_%s' % resource, None) method = getattr(self, 'handle_delete_%s' % resource, None)
if method and callable(method): if method and isinstance(method, collections.Callable):
output = method(id_, resource_id, **kwargs) output = method(id_, resource_id, **kwargs)
env['RESOURCE'] = resource env['RESOURCE'] = resource
if resource_id: if resource_id:
@ -584,7 +588,7 @@ class CollectionHandler(BaseHandler):
ws = yield tornado.websocket.websocket_connect(self.build_ws_url(path)) ws = yield tornado.websocket.websocket_connect(self.build_ws_url(path))
ws.write_message(message) ws.write_message(message)
ws.close() ws.close()
except Exception, e: except Exception as e:
self.logger.error('Error yielding WebSocket message: %s', e) self.logger.error('Error yielding WebSocket message: %s', e)
@ -641,7 +645,7 @@ class EventsHandler(CollectionHandler):
if group_id is None: if group_id is None:
return {'persons': persons} return {'persons': persons}
this_persons = [p for p in (this_event.get('tickets') or []) if not p.get('cancelled')] this_persons = [p for p in (this_event.get('tickets') or []) if not p.get('cancelled')]
this_emails = filter(None, [p.get('email') for p in this_persons]) this_emails = [_f for _f in [p.get('email') for p in this_persons] if _f]
all_query = {'group_id': group_id} all_query = {'group_id': group_id}
events = self.db.query('events', all_query) events = self.db.query('events', all_query)
for event in events: for event in events:
@ -655,7 +659,7 @@ class EventsHandler(CollectionHandler):
or which set of keys specified in a dictionary match their respective values.""" or which set of keys specified in a dictionary match their respective values."""
for ticket in tickets: for ticket in tickets:
if isinstance(ticket_id_or_query, dict): if isinstance(ticket_id_or_query, dict):
if all(ticket.get(k) == v for k, v in ticket_id_or_query.iteritems()): if all(ticket.get(k) == v for k, v in ticket_id_or_query.items()):
return ticket return ticket
else: else:
if str(ticket.get('_id')) == ticket_id_or_query: if str(ticket.get('_id')) == ticket_id_or_query:
@ -752,7 +756,7 @@ class EventsHandler(CollectionHandler):
ticket = self._get_ticket_data(ticket_id, doc.get('tickets') or []) ticket = self._get_ticket_data(ticket_id, doc.get('tickets') or [])
env = dict(ticket) env = dict(ticket)
env.update({'PERSON_ID': ticket_id, 'TICKED_ID': ticket_id, 'EVENT_ID': id_, env.update({'PERSON_ID': ticket_id, 'TICKED_ID': ticket_id, 'EVENT_ID': id_,
'EVENT_TITLE': doc.get('title', ''), 'WEB_USER': self.current_user, 'EVENT_TITLE': doc.get('title', ''), 'WEB_USER': self.current_user_info.get('username', ''),
'WEB_REMOTE_IP': self.request.remote_ip}) 'WEB_REMOTE_IP': self.request.remote_ip})
stdin_data = {'new': ticket, stdin_data = {'new': ticket,
'event': doc, 'event': doc,
@ -765,7 +769,7 @@ class EventsHandler(CollectionHandler):
# Update an existing entry for a ticket registered at this event. # Update an existing entry for a ticket registered at this event.
self._clean_dict(data) self._clean_dict(data)
uuid, arguments = self.uuid_arguments uuid, arguments = self.uuid_arguments
query = dict([('tickets.%s' % k, v) for k, v in arguments.iteritems()]) query = dict([('tickets.%s' % k, v) for k, v in arguments.items()])
query['_id'] = id_ query['_id'] = id_
if ticket_id is not None: if ticket_id is not None:
query['tickets._id'] = ticket_id query['tickets._id'] = ticket_id
@ -794,7 +798,7 @@ class EventsHandler(CollectionHandler):
# always takes the ticket_id from the new ticket # always takes the ticket_id from the new ticket
ticket_id = str(new_ticket_data.get('_id')) ticket_id = str(new_ticket_data.get('_id'))
env.update({'PERSON_ID': ticket_id, 'TICKED_ID': ticket_id, 'EVENT_ID': id_, env.update({'PERSON_ID': ticket_id, 'TICKED_ID': ticket_id, 'EVENT_ID': id_,
'EVENT_TITLE': doc.get('title', ''), 'WEB_USER': self.current_user, 'EVENT_TITLE': doc.get('title', ''), 'WEB_USER': self.current_user_info.get('username', ''),
'WEB_REMOTE_IP': self.request.remote_ip}) 'WEB_REMOTE_IP': self.request.remote_ip})
stdin_data = {'old': old_ticket_data, stdin_data = {'old': old_ticket_data,
'new': new_ticket_data, 'new': new_ticket_data,
@ -806,7 +810,8 @@ class EventsHandler(CollectionHandler):
if new_ticket_data.get('attended'): if new_ticket_data.get('attended'):
self.run_triggers('attends', stdin_data=stdin_data, env=env) self.run_triggers('attends', stdin_data=stdin_data, env=env)
ret = {'action': 'update', '_id': ticket_id, 'ticket': new_ticket_data, 'uuid': uuid} ret = {'action': 'update', '_id': ticket_id, 'ticket': new_ticket_data,
'uuid': uuid, 'username': self.current_user_info.get('username', '')}
if old_ticket_data != new_ticket_data: if old_ticket_data != new_ticket_data:
self.send_ws_message('event/%s/tickets/updates' % id_, json.dumps(ret)) self.send_ws_message('event/%s/tickets/updates' % id_, json.dumps(ret))
return ret return ret
@ -827,7 +832,7 @@ class EventsHandler(CollectionHandler):
self.send_ws_message('event/%s/tickets/updates' % id_, json.dumps(ret)) self.send_ws_message('event/%s/tickets/updates' % id_, json.dumps(ret))
env = dict(ticket) env = dict(ticket)
env.update({'PERSON_ID': ticket_id, 'TICKED_ID': ticket_id, 'EVENT_ID': id_, env.update({'PERSON_ID': ticket_id, 'TICKED_ID': ticket_id, 'EVENT_ID': id_,
'EVENT_TITLE': rdoc.get('title', ''), 'WEB_USER': self.current_user, 'EVENT_TITLE': rdoc.get('title', ''), 'WEB_USER': self.current_user_info.get('username', ''),
'WEB_REMOTE_IP': self.request.remote_ip}) 'WEB_REMOTE_IP': self.request.remote_ip})
stdin_data = {'old': ticket, stdin_data = {'old': ticket,
'event': rdoc, 'event': rdoc,
@ -872,7 +877,7 @@ class UsersHandler(CollectionHandler):
@authenticated @authenticated
def get(self, id_=None, resource=None, resource_id=None, acl=True, **kwargs): def get(self, id_=None, resource=None, resource_id=None, acl=True, **kwargs):
if id_ is not None: if id_ is not None:
if (self.has_permission('user|read') or str(self.current_user_info.get('_id')) == id_): if (self.has_permission('user|read') or self.current_user == id_):
acl = False acl = False
super(UsersHandler, self).get(id_, resource, resource_id, acl=acl, **kwargs) super(UsersHandler, self).get(id_, resource, resource_id, acl=acl, **kwargs)
@ -896,12 +901,17 @@ class UsersHandler(CollectionHandler):
if new_pwd is not None: if new_pwd is not None:
del data['new_password'] del data['new_password']
authorized, user = self.user_authorized(data['username'], old_pwd) authorized, user = self.user_authorized(data['username'], old_pwd)
if not (self.has_permission('user|update') or (authorized and self.current_user == data['username'])): if not (self.has_permission('user|update') or (authorized and
self.current_user_info.get('username') == data['username'])):
raise InputException('not authorized to change password') raise InputException('not authorized to change password')
data['password'] = utils.hash_password(new_pwd) data['password'] = utils.hash_password(new_pwd)
if '_id' in data: if '_id' in data:
# Avoid overriding _id
del data['_id'] del data['_id']
if 'username' in data:
del data['username']
# for the moment, prevent the ability to update permissions via web
if 'permissions' in data:
del data['permissions']
return data return data
@gen.coroutine @gen.coroutine
@ -909,7 +919,7 @@ class UsersHandler(CollectionHandler):
def put(self, id_=None, resource=None, resource_id=None, **kwargs): def put(self, id_=None, resource=None, resource_id=None, **kwargs):
if id_ is None: if id_ is None:
return self.build_error(status=404, message='unable to access the resource') return self.build_error(status=404, message='unable to access the resource')
if not (self.has_permission('user|update') or str(self.current_user_info.get('_id')) == id_): if not (self.has_permission('user|update') or self.current_user == id_):
return self.build_error(status=401, message='insufficient permissions: user|update or current user') return self.build_error(status=401, message='insufficient permissions: user|update or current user')
super(UsersHandler, self).put(id_, resource, resource_id, **kwargs) super(UsersHandler, self).put(id_, resource, resource_id, **kwargs)
@ -970,7 +980,7 @@ class EbCSVImportPersonsHandler(BaseHandler):
#[x.get('email') for x in (event_details[0].get('tickets') or []) if x.get('email')]) #[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 []): for ticket in (event_details[0].get('tickets') or []):
all_emails.add('%s_%s_%s' % (ticket.get('name'), ticket.get('surname'), ticket.get('email'))) all_emails.add('%s_%s_%s' % (ticket.get('name'), ticket.get('surname'), ticket.get('email')))
for fieldname, contents in self.request.files.iteritems(): for fieldname, contents in self.request.files.items():
for content in contents: for content in contents:
filename = content['filename'] filename = content['filename']
parseStats, persons = utils.csvParse(content['body'], remap=self.csvRemap) parseStats, persons = utils.csvParse(content['body'], remap=self.csvRemap)
@ -1038,7 +1048,7 @@ class WebSocketEventUpdatesHandler(tornado.websocket.WebSocketHandler):
logging.debug('WebSocketEventUpdatesHandler.on_message url:%s' % url) logging.debug('WebSocketEventUpdatesHandler.on_message url:%s' % url)
count = 0 count = 0
_to_delete = set() _to_delete = set()
for uuid, client in _ws_clients.get(url, {}).iteritems(): for uuid, client in _ws_clients.get(url, {}).items():
try: try:
client.write_message(message) client.write_message(message)
except: except:
@ -1079,10 +1089,11 @@ class LoginHandler(RootHandler):
self.write({'error': True, 'message': 'missing username or password'}) self.write({'error': True, 'message': 'missing username or password'})
return return
authorized, user = self.user_authorized(username, password) authorized, user = self.user_authorized(username, password)
if authorized and user.get('username'): if authorized and 'username' in user and '_id' in user:
id_ = str(user['_id'])
username = user['username'] username = user['username']
logging.info('successful login for user %s' % username) logging.info('successful login for user %s (id: %s)' % (username, id_))
self.set_secure_cookie("user", username) self.set_secure_cookie("user", id_)
self.write({'error': False, 'message': 'successful login'}) self.write({'error': False, 'message': 'successful login'})
return return
logging.info('login failed for user %s' % username) logging.info('login failed for user %s' % username)
@ -1132,7 +1143,7 @@ def run():
ssl_options = dict(certfile=options.ssl_cert, keyfile=options.ssl_key) ssl_options = dict(certfile=options.ssl_cert, keyfile=options.ssl_key)
# database backend connector # database backend connector
db_connector = backend.EventManDB(url=options.mongo_url, dbName=options.db_name) db_connector = monco.Monco(url=options.mongo_url, dbName=options.db_name)
init_params = dict(db=db_connector, data_dir=options.data_dir, listen_port=options.port, init_params = dict(db=db_connector, data_dir=options.data_dir, listen_port=options.port,
authentication=options.authentication, logger=logger, ssl_options=ssl_options) authentication=options.authentication, logger=logger, ssl_options=ssl_options)
@ -1173,7 +1184,7 @@ def run():
], ],
template_path=os.path.join(os.path.dirname(__file__), "templates"), template_path=os.path.join(os.path.dirname(__file__), "templates"),
static_path=os.path.join(os.path.dirname(__file__), "static"), static_path=os.path.join(os.path.dirname(__file__), "static"),
cookie_secret='__COOKIE_SECRET__', cookie_secret=cookie_secret,
login_url='/login', login_url='/login',
debug=options.debug) debug=options.debug)
http_server = tornado.httpserver.HTTPServer(application, ssl_options=ssl_options or None) http_server = tornado.httpserver.HTTPServer(application, ssl_options=ssl_options or None)

View file

@ -1,8 +1,8 @@
"""EventMan(ager) database backend """Monco: a MongoDB database backend
Classes and functions used to manage events and attendees database. Classes and functions used to issue queries to a MongoDB database.
Copyright 2015-2016 Davide Alberani <da@erlug.linux.it> Copyright 2016-2017 Davide Alberani <da@erlug.linux.it>
RaspiBO <info@raspibo.org> RaspiBO <info@raspibo.org>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -23,6 +23,7 @@ from bson.objectid import ObjectId
re_objectid = re.compile(r'[0-9a-f]{24}') re_objectid = re.compile(r'[0-9a-f]{24}')
_force_conversion = { _force_conversion = {
'_id': ObjectId,
'seq_hex': str, 'seq_hex': str,
'tickets.seq_hex': str 'tickets.seq_hex': str
} }
@ -40,7 +41,8 @@ def convert_obj(obj):
if isinstance(obj, bool): if isinstance(obj, bool):
return obj return obj
try: try:
return ObjectId(obj) if re_objectid.match(obj):
return ObjectId(obj)
except: except:
pass pass
return obj return obj
@ -56,9 +58,12 @@ def convert(seq):
""" """
if isinstance(seq, dict): if isinstance(seq, dict):
d = {} d = {}
for key, item in seq.iteritems(): for key, item in seq.items():
if key in _force_conversion: if key in _force_conversion:
d[key] = _force_conversion[key](item) try:
d[key] = _force_conversion[key](item)
except:
d[key] = item
else: else:
d[key] = convert(item) d[key] = convert(item)
return d return d
@ -67,23 +72,35 @@ def convert(seq):
return convert_obj(seq) return convert_obj(seq)
class EventManDB(object): class MoncoError(Exception):
"""Base class for Monco exceptions."""
pass
class MoncoConnectionError(MoncoError):
"""Monco exceptions raise when a connection problem occurs."""
pass
class Monco(object):
"""MongoDB connector.""" """MongoDB connector."""
db = None db = None
connection = None connection = None
# map operations on lists of items. # map operations on lists of items.
_operations = { _operations = {
'update': '$set', 'update': '$set',
'append': '$push', 'append': '$push',
'appendUnique': '$addToSet', 'appendUnique': '$addToSet',
'delete': '$pull', 'delete': '$pull',
'increment': '$inc' 'increment': '$inc'
} }
def __init__(self, url=None, dbName='eventman'): def __init__(self, dbName, url=None):
"""Initialize the instance, connecting to the database. """Initialize the instance, connecting to the database.
:param dbName: name of the database
:type dbName: str (or None to use the dbName passed at initialization)
:param url: URL of the database :param url: URL of the database
:type url: str (or None to connect to localhost) :type url: str (or None to connect to localhost)
""" """
@ -91,9 +108,11 @@ class EventManDB(object):
self._dbName = dbName self._dbName = dbName
self.connect(url) self.connect(url)
def connect(self, url=None, dbName=None): def connect(self, dbName=None, url=None):
"""Connect to the database. """Connect to the database.
:param dbName: name of the database
:type dbName: str (or None to use the dbName passed at initialization)
:param url: URL of the database :param url: URL of the database
:type url: str (or None to connect to localhost) :type url: str (or None to connect to localhost)
@ -106,10 +125,26 @@ class EventManDB(object):
self._url = url self._url = url
if dbName: if dbName:
self._dbName = dbName self._dbName = dbName
if not self._dbName:
raise MoncoConnectionError('no database name specified')
self.connection = pymongo.MongoClient(self._url) self.connection = pymongo.MongoClient(self._url)
self.db = self.connection[self._dbName] self.db = self.connection[self._dbName]
return self.db return self.db
def getOne(self, collection, query=None):
"""Get a single document with the specified `query`.
:param collection: search the document in this collection
:type collection: str
:param query: query to filter the documents
:type query: dict or None
:returns: the first document matching the query
:rtype: dict
"""
results = self.query(collection, convert(query))
return results and results[0] or {}
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`.
@ -121,8 +156,7 @@ class EventManDB(object):
:returns: the document with the given `_id` :returns: the document with the given `_id`
:rtype: dict :rtype: dict
""" """
results = self.query(collection, convert({'_id': _id})) return self.getOne(collection, {'_id': _id})
return results and results[0] or {}
def query(self, collection, query=None, condition='or'): def query(self, collection, query=None, condition='or'):
"""Get multiple documents matching a query. """Get multiple documents matching a query.
@ -130,7 +164,7 @@ class EventManDB(object):
:param collection: search for documents in this collection :param collection: search for documents in this collection
:type collection: str :type collection: str
:param query: search for documents with those attributes :param query: search for documents with those attributes
:type query: dict or None :type query: dict, list or None
:returns: list of matching documents :returns: list of matching documents
:rtype: list :rtype: list
@ -222,7 +256,7 @@ class EventManDB(object):
operator = self._operations.get(operation) operator = self._operations.get(operation)
if updateList: if updateList:
newData = {} newData = {}
for key, value in data.iteritems(): for key, value in data.items():
newData['%s.$.%s' % (updateList, key)] = value newData['%s.$.%s' % (updateList, key)] = value
data = newData data = newData
res = db[collection].find_and_modify(query=_id_or_query, res = db[collection].find_and_modify(query=_id_or_query,
@ -230,6 +264,30 @@ class EventManDB(object):
lastErrorObject = res.get('lastErrorObject') or {} lastErrorObject = res.get('lastErrorObject') or {}
return lastErrorObject.get('updatedExisting', False), res.get('value') or {} return lastErrorObject.get('updatedExisting', False), res.get('value') or {}
def updateMany(self, collection, query, data):
"""Update multiple existing documents.
query can be an ID or a dict representing a query.
:param collection: update documents in this collection
:type collection: str
:param query: a query or a list of attributes in the data that must match
:type query: str or :class:`~bson.objectid.ObjectId` or iterable
:param data: the updated information to store
:type data: dict
:returns: a dict with the success state and number of updated items
:rtype: dict
"""
db = self.connect()
data = convert(data or {})
query = convert(query)
if not isinstance(query, dict):
query = {'_id': query}
if '_id' in data:
del data['_id']
return db[collection].update(query, {'$set': data}, multi=True)
def delete(self, collection, _id_or_query=None, force=False): def delete(self, collection, _id_or_query=None, force=False):
"""Remove one or more documents from a collection. """Remove one or more documents from a collection.
@ -240,8 +298,8 @@ class EventManDB(object):
:param force: force the deletion of all documents, when `_id_or_query` is empty :param force: force the deletion of all documents, when `_id_or_query` is empty
:type force: bool :type force: bool
:returns: how many documents were removed :returns: dictionary with the number or removed documents
:rtype: int :rtype: dict
""" """
if not _id_or_query and not force: if not _id_or_query and not force:
return return
@ -250,4 +308,3 @@ class EventManDB(object):
_id_or_query = {'_id': _id_or_query} _id_or_query = {'_id': _id_or_query}
_id_or_query = convert(_id_or_query) _id_or_query = convert(_id_or_query)
return db[collection].remove(_id_or_query) return db[collection].remove(_id_or_query)

View file

@ -1,28 +1,72 @@
(function() { (function (global, factory) {
if (typeof define === "function" && define.amd) {
define(['module', 'exports', 'angular', 'ws'], factory);
} else if (typeof exports !== "undefined") {
factory(module, exports, require('angular'), require('ws'));
} else {
var mod = {
exports: {}
};
factory(mod, mod.exports, global.angular, global.ws);
global.angularWebsocket = mod.exports;
}
})(this, function (module, exports, _angular, ws) {
'use strict'; 'use strict';
var noop = angular.noop; Object.defineProperty(exports, "__esModule", {
var objectFreeze = (Object.freeze) ? Object.freeze : noop; value: true
});
var _angular2 = _interopRequireDefault(_angular);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj;
};
var Socket;
if (typeof window === 'undefined') {
try {
Socket = ws.Client || ws.client || ws;
} catch (e) {}
}
// Browser
Socket = Socket || window.WebSocket || window.MozWebSocket;
var noop = _angular2.default.noop;
var objectFreeze = Object.freeze ? Object.freeze : noop;
var objectDefineProperty = Object.defineProperty; var objectDefineProperty = Object.defineProperty;
var isString = angular.isString; var isString = _angular2.default.isString;
var isFunction = angular.isFunction; var isFunction = _angular2.default.isFunction;
var isDefined = angular.isDefined; var isDefined = _angular2.default.isDefined;
var isObject = angular.isObject; var isObject = _angular2.default.isObject;
var isArray = angular.isArray; var isArray = _angular2.default.isArray;
var forEach = angular.forEach; var forEach = _angular2.default.forEach;
var arraySlice = Array.prototype.slice; var arraySlice = Array.prototype.slice;
// ie8 wat // ie8 wat
if (!Array.prototype.indexOf) { if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(elt /*, from*/) { Array.prototype.indexOf = function (elt /*, from*/) {
var len = this.length >>> 0; var len = this.length >>> 0;
var from = Number(arguments[1]) || 0; var from = Number(arguments[1]) || 0;
from = (from < 0) ? Math.ceil(from) : Math.floor(from); from = from < 0 ? Math.ceil(from) : Math.floor(from);
if (from < 0) { if (from < 0) {
from += len; from += len;
} }
for (; from < len; from++) { for (; from < len; from++) {
if (from in this && this[from] === elt) { return from; } if (from in this && this[from] === elt) {
return from;
}
} }
return -1; return -1;
}; };
@ -48,20 +92,20 @@
// this.buffer = []; // this.buffer = [];
// TODO: refactor options to use isDefined // TODO: refactor options to use isDefined
this.scope = options && options.scope || $rootScope; this.scope = options && options.scope || $rootScope;
this.rootScopeFailover = options && options.rootScopeFailover && true; this.rootScopeFailover = options && options.rootScopeFailover && true;
this.useApplyAsync = options && options.useApplyAsync || false; this.useApplyAsync = options && options.useApplyAsync || false;
this.initialTimeout = options && options.initialTimeout || 500; // 500ms this.initialTimeout = options && options.initialTimeout || 500; // 500ms
this.maxTimeout = options && options.maxTimeout || 5 * 60 * 1000; // 5 minutes this.maxTimeout = options && options.maxTimeout || 5 * 60 * 1000; // 5 minutes
this.reconnectIfNotNormalClose = options && options.reconnectIfNotNormalClose || false; this.reconnectIfNotNormalClose = options && options.reconnectIfNotNormalClose || false;
this.binaryType = options && options.binaryType || 'blob'; this.binaryType = options && options.binaryType || 'blob';
this._reconnectAttempts = 0; this._reconnectAttempts = 0;
this.sendQueue = []; this.sendQueue = [];
this.onOpenCallbacks = []; this.onOpenCallbacks = [];
this.onMessageCallbacks = []; this.onMessageCallbacks = [];
this.onErrorCallbacks = []; this.onErrorCallbacks = [];
this.onCloseCallbacks = []; this.onCloseCallbacks = [];
objectFreeze(this._readyStateConstants); objectFreeze(this._readyStateConstants);
@ -70,10 +114,8 @@
} else { } else {
this._setInternalState(0); this._setInternalState(0);
} }
} }
$WebSocket.prototype._readyStateConstants = { $WebSocket.prototype._readyStateConstants = {
'CONNECTING': 0, 'CONNECTING': 0,
'OPEN': 1, 'OPEN': 1,
@ -84,9 +126,7 @@
$WebSocket.prototype._normalCloseCode = 1000; $WebSocket.prototype._normalCloseCode = 1000;
$WebSocket.prototype._reconnectableStatusCodes = [ $WebSocket.prototype._reconnectableStatusCodes = [4000];
4000
];
$WebSocket.prototype.safeDigest = function safeDigest(autoApply) { $WebSocket.prototype.safeDigest = function safeDigest(autoApply) {
if (autoApply && !this.scope.$$phase) { if (autoApply && !this.scope.$$phase) {
@ -99,7 +139,7 @@
if (scope) { if (scope) {
this.scope = scope; this.scope = scope;
if (this.rootScopeFailover) { if (this.rootScopeFailover) {
this.scope.$on('$destroy', function() { this.scope.$on('$destroy', function () {
self.scope = $rootScope; self.scope = $rootScope;
}); });
} }
@ -110,10 +150,10 @@
$WebSocket.prototype._connect = function _connect(force) { $WebSocket.prototype._connect = function _connect(force) {
if (force || !this.socket || this.socket.readyState !== this._readyStateConstants.OPEN) { if (force || !this.socket || this.socket.readyState !== this._readyStateConstants.OPEN) {
this.socket = $websocketBackend.create(this.url, this.protocols); this.socket = $websocketBackend.create(this.url, this.protocols);
this.socket.onmessage = angular.bind(this, this._onMessageHandler); this.socket.onmessage = _angular2.default.bind(this, this._onMessageHandler);
this.socket.onopen = angular.bind(this, this._onOpenHandler); this.socket.onopen = _angular2.default.bind(this, this._onOpenHandler);
this.socket.onerror = angular.bind(this, this._onErrorHandler); this.socket.onerror = _angular2.default.bind(this, this._onErrorHandler);
this.socket.onclose = angular.bind(this, this._onCloseHandler); this.socket.onclose = _angular2.default.bind(this, this._onCloseHandler);
this.socket.binaryType = this.binaryType; this.socket.binaryType = this.binaryType;
} }
}; };
@ -122,9 +162,7 @@
while (this.sendQueue.length && this.socket.readyState === this._readyStateConstants.OPEN) { while (this.sendQueue.length && this.socket.readyState === this._readyStateConstants.OPEN) {
var data = this.sendQueue.shift(); var data = this.sendQueue.shift();
this.socket.send( this.socket.send(isString(data.message) || this.binaryType != 'blob' ? data.message : JSON.stringify(data.message));
isString(data.message) || this.binaryType != "blob" ? data.message : JSON.stringify(data.message)
);
data.deferred.resolve(); data.deferred.resolve();
} }
}; };
@ -162,7 +200,6 @@
return this; return this;
}; };
$WebSocket.prototype.onMessage = function onMessage(callback, options) { $WebSocket.prototype.onMessage = function onMessage(callback, options) {
if (!isFunction(callback)) { if (!isFunction(callback)) {
throw new Error('Callback must be a function'); throw new Error('Callback must be a function');
@ -189,14 +226,14 @@
$WebSocket.prototype._onCloseHandler = function _onCloseHandler(event) { $WebSocket.prototype._onCloseHandler = function _onCloseHandler(event) {
var self = this; var self = this;
if (self.useApplyAsync) { if (self.useApplyAsync) {
self.scope.$applyAsync(function() { self.scope.$applyAsync(function () {
self.notifyCloseCallbacks(event); self.notifyCloseCallbacks(event);
}); });
} else { } else {
self.notifyCloseCallbacks(event); self.notifyCloseCallbacks(event);
self.safeDigest(autoApply); self.safeDigest(true);
} }
if ((this.reconnectIfNotNormalClose && event.code !== this._normalCloseCode) || this._reconnectableStatusCodes.indexOf(event.code) > -1) { if (this.reconnectIfNotNormalClose && event.code !== this._normalCloseCode || this._reconnectableStatusCodes.indexOf(event.code) > -1) {
this.reconnect(); this.reconnect();
} }
}; };
@ -204,12 +241,12 @@
$WebSocket.prototype._onErrorHandler = function _onErrorHandler(event) { $WebSocket.prototype._onErrorHandler = function _onErrorHandler(event) {
var self = this; var self = this;
if (self.useApplyAsync) { if (self.useApplyAsync) {
self.scope.$applyAsync(function() { self.scope.$applyAsync(function () {
self.notifyErrorCallbacks(event); self.notifyErrorCallbacks(event);
}); });
} else { } else {
self.notifyErrorCallbacks(event); self.notifyErrorCallbacks(event);
self.safeDigest(autoApply); self.safeDigest(true);
} }
}; };
@ -223,12 +260,10 @@
if (pattern) { if (pattern) {
if (isString(pattern) && message.data === pattern) { if (isString(pattern) && message.data === pattern) {
applyAsyncOrDigest(currentCallback.fn, currentCallback.autoApply, message); applyAsyncOrDigest(currentCallback.fn, currentCallback.autoApply, message);
} } else if (pattern instanceof RegExp && pattern.exec(message.data)) {
else if (pattern instanceof RegExp && pattern.exec(message.data)) {
applyAsyncOrDigest(currentCallback.fn, currentCallback.autoApply, message); applyAsyncOrDigest(currentCallback.fn, currentCallback.autoApply, message);
} }
} } else {
else {
applyAsyncOrDigest(currentCallback.fn, currentCallback.autoApply, message); applyAsyncOrDigest(currentCallback.fn, currentCallback.autoApply, message);
} }
} }
@ -236,7 +271,7 @@
function applyAsyncOrDigest(callback, autoApply, args) { function applyAsyncOrDigest(callback, autoApply, args) {
args = arraySlice.call(arguments, 2); args = arraySlice.call(arguments, 2);
if (self.useApplyAsync) { if (self.useApplyAsync) {
self.scope.$applyAsync(function() { self.scope.$applyAsync(function () {
callback.apply(self, args); callback.apply(self, args);
}); });
} else { } else {
@ -244,7 +279,6 @@
self.safeDigest(autoApply); self.safeDigest(autoApply);
} }
} }
}; };
$WebSocket.prototype.close = function close(force) { $WebSocket.prototype.close = function close(force) {
@ -261,8 +295,7 @@
if (self.readyState === self._readyStateConstants.RECONNECT_ABORTED) { if (self.readyState === self._readyStateConstants.RECONNECT_ABORTED) {
deferred.reject('Socket connection has been closed'); deferred.reject('Socket connection has been closed');
} } else {
else {
self.sendQueue.push({ self.sendQueue.push({
message: data, message: data,
deferred: deferred deferred: deferred
@ -274,7 +307,7 @@
function cancelableify(promise) { function cancelableify(promise) {
promise.cancel = cancel; promise.cancel = cancel;
var then = promise.then; var then = promise.then;
promise.then = function() { promise.then = function () {
var newPromise = then.apply(this, arguments); var newPromise = then.apply(this, arguments);
return cancelableify(newPromise); return cancelableify(newPromise);
}; };
@ -287,8 +320,7 @@
return self; return self;
} }
if ($websocketBackend.isMocked && $websocketBackend.isMocked() && if ($websocketBackend.isMocked && $websocketBackend.isMocked() && $websocketBackend.isConnected(this.url)) {
$websocketBackend.isConnected(this.url)) {
this._onMessageHandler($websocketBackend.mockSend()); this._onMessageHandler($websocketBackend.mockSend());
} }
@ -303,7 +335,7 @@
var backoffDelaySeconds = backoffDelay / 1000; var backoffDelaySeconds = backoffDelay / 1000;
console.log('Reconnecting in ' + backoffDelaySeconds + ' seconds'); console.log('Reconnecting in ' + backoffDelaySeconds + ' seconds');
$timeout(angular.bind(this, this._connect), backoffDelay); $timeout(_angular2.default.bind(this, this._connect), backoffDelay);
return this; return this;
}; };
@ -330,8 +362,7 @@
} }
this._internalConnectionState = state; this._internalConnectionState = state;
forEach(this.sendQueue, function (pending) {
forEach(this.sendQueue, function(pending) {
pending.deferred.reject('Message cancelled due to closed socket connection'); pending.deferred.reject('Message cancelled due to closed socket connection');
}); });
}; };
@ -339,62 +370,46 @@
// Read only .readyState // Read only .readyState
if (objectDefineProperty) { if (objectDefineProperty) {
objectDefineProperty($WebSocket.prototype, 'readyState', { objectDefineProperty($WebSocket.prototype, 'readyState', {
get: function() { get: function get() {
return this._internalConnectionState || this.socket.readyState; return this._internalConnectionState || this.socket.readyState;
}, },
set: function() { set: function set() {
throw new Error('The readyState property is read-only'); throw new Error('The readyState property is read-only');
} }
}); });
} }
return function(url, protocols, options) { return function (url, protocols, options) {
return new $WebSocket(url, protocols, options); return new $WebSocket(url, protocols, options);
}; };
} }
// $WebSocketBackendProvider.$inject = ['$window', '$log']; // $WebSocketBackendProvider.$inject = ['$log'];
function $WebSocketBackendProvider($window, $log) { function $WebSocketBackendProvider($log) {
this.create = function create(url, protocols) { this.create = function create(url, protocols) {
var match = /wss?:\/\//.exec(url); var match = /wss?:\/\//.exec(url);
var Socket, ws;
if (!match) { if (!match) {
throw new Error('Invalid url provided'); throw new Error('Invalid url provided');
} }
// CommonJS
if (typeof exports === 'object' && require) {
try {
ws = require('ws');
Socket = (ws.Client || ws.client || ws);
} catch(e) {}
}
// Browser
Socket = Socket || $window.WebSocket || $window.MozWebSocket;
if (protocols) { if (protocols) {
return new Socket(url, protocols); return new Socket(url, protocols);
} }
return new Socket(url); return new Socket(url);
}; };
this.createWebSocketBackend = function createWebSocketBackend(url, protocols) { this.createWebSocketBackend = function createWebSocketBackend(url, protocols) {
$log.warn('Deprecated: Please use .create(url, protocols)'); $log.warn('Deprecated: Please use .create(url, protocols)');
return this.create(url, protocols); return this.create(url, protocols);
}; };
} }
angular.module('ngWebSocket', []) _angular2.default.module('ngWebSocket', []).factory('$websocket', ['$rootScope', '$q', '$timeout', '$websocketBackend', $WebSocketProvider]).factory('WebSocket', ['$rootScope', '$q', '$timeout', 'WebsocketBackend', $WebSocketProvider]).service('$websocketBackend', ['$log', $WebSocketBackendProvider]).service('WebSocketBackend', ['$log', $WebSocketBackendProvider]);
.factory('$websocket', ['$rootScope', '$q', '$timeout', '$websocketBackend', $WebSocketProvider])
.factory('WebSocket', ['$rootScope', '$q', '$timeout', 'WebsocketBackend', $WebSocketProvider])
.service('$websocketBackend', ['$window', '$log', $WebSocketBackendProvider])
.service('WebSocketBackend', ['$window', '$log', $WebSocketBackendProvider]);
_angular2.default.module('angular-websocket', ['ngWebSocket']);
angular.module('angular-websocket', ['ngWebSocket']); exports.default = _angular2.default.module('ngWebSocket');
module.exports = exports['default'];
if (typeof module === 'object' && typeof define !== 'function') { });
module.exports = angular.module('ngWebSocket');
}
}());

2
static/js/angular-websocket.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
tools/monco.py Symbolic link
View file

@ -0,0 +1 @@
../monco.py

50
tools/qrcode_reader.py Executable file
View file

@ -0,0 +1,50 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import io
import serial
import requests
class Connector():
def __init__(self, login_url, checkin_url, username=None, password=None):
self.login_url = login_url
self.checkin_url = checkin_url
self.session = requests.Session()
json = {}
if username:
json['username'] = username
if password:
json['password'] = password
req = self.session.post(login_url, json=json, verify=False)
req.raise_for_status()
req.connection.close()
def checkin(self, code):
req = self.session.put(self.checkin_url + '?order_nr=' + code[:9], json={'attended': True}, verify=False)
req.raise_for_status()
req.connection.close()
def scan():
ser = serial.Serial(port='/dev/ttyACM0', timeout=1)
ser_io = io.TextIOWrapper(io.BufferedRWPair(ser, ser, 1), newline='\r', line_buffering=True)
while True:
line = ser_io.readline().strip()
if not line:
continue
yield line
if __name__ == '__main__':
connector = Connector(login_url='https://localhost:5242/v1.0/login',
checkin_url='https://localhost:5242/v1.0/events/1490640884_8820477-7-7gvft6nlrs2o73fza54a6yeywiowmj8v/tickets/',
username='admin',
password='eventman')
try:
for code in scan():
print(code)
connector.checkin(code)
except KeyboardInterrupt:
print('exiting...')

View file

@ -22,7 +22,7 @@ import string
import random import random
import hashlib import hashlib
import datetime import datetime
import StringIO import io
from bson.objectid import ObjectId from bson.objectid import ObjectId
@ -39,7 +39,9 @@ def csvParse(csvStr, remap=None, merge=None):
:returns: tuple with a dict of total and valid lines and the data :returns: tuple with a dict of total and valid lines and the data
:rtype: tuple :rtype: tuple
""" """
fd = StringIO.StringIO(csvStr) if isinstance(csvStr, bytes):
csvStr = csvStr.decode('utf-8')
fd = io.StringIO(csvStr)
reader = csv.reader(fd) reader = csv.reader(fd)
remap = remap or {} remap = remap or {}
merge = merge or {} merge = merge or {}
@ -47,7 +49,7 @@ def csvParse(csvStr, remap=None, merge=None):
reply = dict(total=0, valid=0) reply = dict(total=0, valid=0)
results = [] results = []
try: try:
headers = reader.next() headers = next(reader)
fields = len(headers) fields = len(headers)
except (StopIteration, csv.Error): except (StopIteration, csv.Error):
return reply, {} return reply, {}
@ -63,8 +65,7 @@ def csvParse(csvStr, remap=None, merge=None):
reply['total'] += 1 reply['total'] += 1
if len(row) != fields: if len(row) != fields:
continue continue
row = [unicode(cell, 'utf-8', 'replace') for cell in row] values = dict(zip(headers, row))
values = dict(map(None, headers, row))
values.update(merge) values.update(merge)
results.append(values) results.append(values)
reply['valid'] += 1 reply['valid'] += 1
@ -88,19 +89,25 @@ def hash_password(password, salt=None):
:rtype: str""" :rtype: str"""
if salt is None: if salt is None:
salt_pool = string.ascii_letters + string.digits salt_pool = string.ascii_letters + string.digits
salt = ''.join(random.choice(salt_pool) for x in xrange(32)) salt = ''.join(random.choice(salt_pool) for x in range(32))
hash_ = hashlib.sha512('%s%s' % (salt, password)) pwd = '%s%s' % (salt, password)
hash_ = hashlib.sha512(pwd.encode('utf-8'))
return '$%s$%s' % (salt, hash_.hexdigest()) return '$%s$%s' % (salt, hash_.hexdigest())
class ImprovedEncoder(json.JSONEncoder): class ImprovedEncoder(json.JSONEncoder):
"""Enhance the default JSON encoder to serialize datetime and ObjectId instances.""" """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, bytes):
try:
return o.decode('utf-8')
except:
pass
elif isinstance(o, (datetime.datetime, datetime.date,
datetime.time, datetime.timedelta, ObjectId)): datetime.time, datetime.timedelta, ObjectId)):
try: try:
return str(o) return str(o)
except Exception, e: except Exception as e:
pass pass
elif isinstance(o, set): elif isinstance(o, set):
return list(o) return list(o)