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
===================
- [Python 3](https://www.python.org/) for the backend
- [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
- [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
===============
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
sudo python get-pip.py
sudo pip install tornado # version 4.2 or later
sudo pip install pymongo # version 3.2.2 or later
sudo pip install python-dateutil
sudo pip install pycups # only needed if you want to print labels
sudo python3 get-pip.py
sudo pip3 install tornado # version 4.2 or later
sudo pip3 install pymongo # version 3.2.2 or later
sudo pip3 install python-dateutil
sudo pip3 install pycups # only needed if you want to print labels
git clone https://github.com/raspibo/eventman
cd eventman
./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
=====================
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");
you may not use this file except in compliance with the License.

View file

@ -27,9 +27,12 @@
<form class="form-inline">
<div class="form-group">
<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>
</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">
<thead>
<tr>
@ -43,7 +46,7 @@
</tr>
</thead>
<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>
<span>
@ -71,6 +74,9 @@
</tr>
</tbody>
</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>
@ -87,7 +93,7 @@
</tr>
</thead>
<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>
<strong>{{person.name}} {{person.surname}}</strong>
<br />

View file

@ -13,7 +13,7 @@
<form class="form-inline">
<div class="form-group">
<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 class="form-group">
<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-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-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-loader-static-files.min.js"></script>
<script type="text/javascript" src="/static/js/nya-bs-select.min.js"></script>
@ -52,7 +52,7 @@
</head>
<!--
Copyright 2015-2016 Davide Alberani <da@erlug.linux.it>
Copyright 2015-2017 Davide Alberani <da@erlug.linux.it>
RaspiBO <info@raspibo.org>
Licensed under the Apache License, Version 2.0 (the "License");

View file

@ -1,6 +1,6 @@
'use strict';
/*
Copyright 2015-2016 Davide Alberani <da@erlug.linux.it>
Copyright 2015-2017 Davide Alberani <da@erlug.linux.it>
RaspiBO <info@raspibo.org>
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',
function ($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, $filter) {
$scope.query = '';
$scope.tickets = [];
$scope.events = Event.all(function(events) {
if (events && $state.is('tickets')) {
@ -79,11 +80,38 @@ eventManControllers.controller('EventsListCtrl', ['$scope', 'Event', '$uibModal'
});
$scope.tickets.push.apply($scope.tickets, evt_tickets || []);
});
$scope.filterTickets();
}
});
$scope.eventsOrderProp = "-begin_date";
$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?';
$rootScope.$on('$translateChangeSuccess', function () {
$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.filterTickets();
};
}]
);
@ -166,13 +194,15 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
);
eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event', 'EventTicket', 'Setting', '$log', '$translate', '$rootScope', 'EventUpdates', '$uibModal',
function ($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, $filter) {
$scope.ticketsOrder = ["name", "surname"];
$scope.countAttendees = 0;
$scope.message = {};
$scope.query = '';
$scope.event = {};
$scope.event.tickets = [];
$scope.shownItems = [];
$scope.ticket = {}; // current ticket, for the event.ticket.* states
$scope.tickets = []; // list of all tickets, for the 'tickets' state
$scope.formSchema = {};
@ -180,16 +210,47 @@ eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event',
$scope.guiOptions = {dangerousActionsEnabled: false};
$scope.customFields = Setting.query({setting: 'ticket_custom_field', in_event_details: true});
$scope.registeredFilterOptions = {all: false};
$scope.formFieldsMap = {};
$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) {
$scope.event = Event.get({id: $state.params.id}, function(data) {
$scope.$watchCollection(function() {
return $scope.event.tickets;
}, function(new_collection, old_collection) {
$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 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;
} else if (data.action == 'add' && ticket_idx == -1) {
$scope._localAddTicket(data.ticket);
@ -406,20 +473,24 @@ eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event',
}
if (key === 'attended' && !hideMessage) {
var msg = {};
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(data.ticket, value);
}
});
};
$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.setTicketAttribute(ticket, key, value);
$scope.query = '';
@ -575,6 +646,7 @@ eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event',
}
);
$scope.ticketsOrder = new_order;
$scope.filterTickets();
};
$scope.showMessage = function(cfg) {
@ -649,7 +721,7 @@ eventManControllers.controller('UsersCtrl', ['$scope', '$rootScope', '$state', '
User.login(loginData, function(data) {
if (!data.error) {
$rootScope.readInfo(function(info) {
$log.debug('logged in user: ' + info.user.username);
$log.debug('logged in user: ' + $scope.info.user.username);
$rootScope.clearError();
$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. */
eventManApp.filter('attendeesFilter', ['$filter',
function($filter) {

View file

@ -11,10 +11,13 @@
<form class="form-inline">
<div class="form-group">
<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>
</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">
<thead>
<tr>
@ -25,7 +28,7 @@
</tr>
</thead>
<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>
<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>
</tbody>
</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>

View file

@ -31,7 +31,7 @@
<form class="form-inline">
<div class="form-group">
<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 class="form-group">
<label for="tickets-order">{{'Sort by:' | translate}}</label>

View file

@ -14,7 +14,7 @@
<form class="form-inline">
<div class="form-group">
<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 class="form-group">
<label for="users-order">Sort by:</label>

View file

@ -1,8 +1,6 @@
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.

View file

@ -1,9 +1,9 @@
#!/usr/bin/env python
#!/usr/bin/env python3
"""EventMan(ager)
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>
Licensed under the Apache License, Version 2.0 (the "License");
@ -38,7 +38,8 @@ import tornado.websocket
from tornado import gen, escape, process
import utils
import backend
import monco
import collections
ENCODING = 'utf-8'
PROCESS_TIMEOUT = 60
@ -102,8 +103,8 @@ class BaseHandler(tornado.web.RequestHandler):
_users_cache = {}
# A property to access the first value of each argument.
arguments = property(lambda self: dict([(k, v[0])
for k, v in self.request.arguments.iteritems()]))
arguments = property(lambda self: dict([(k, v[0].decode('utf-8'))
for k, v in self.request.arguments.items()]))
# A property to access both the UUID and the clean arguments.
@property
@ -150,23 +151,26 @@ class BaseHandler(tornado.web.RequestHandler):
"""Convert some textual values to boolean."""
if isinstance(obj, (list, tuple)):
obj = obj[0]
if isinstance(obj, (str, unicode)):
if isinstance(obj, str):
obj = obj.lower()
return self._bool_convert.get(obj, obj)
def arguments_tobool(self):
"""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):
"""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)
@property
def current_user(self):
"""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
def current_user_info(self):
@ -174,16 +178,16 @@ class BaseHandler(tornado.web.RequestHandler):
current_user = self.current_user
if current_user in self._users_cache:
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}
if current_user:
user_info['username'] = current_user
res = self.db.query('users', {'username': current_user})
if res:
user = res[0]
user_info['_id'] = current_user
user = self.db.getOne('users', {'_id': current_user})
if user:
user_info = user
permissions.update(set(user.get('permissions') or []))
user_info['permissions'] = permissions
user_info['isRegistered'] = True
self._users_cache[current_user] = user_info
return user_info
@ -204,7 +208,7 @@ class BaseHandler(tornado.web.RequestHandler):
collection_permission = self.permissions.get(permission)
if isinstance(collection_permission, bool):
return collection_permission
if callable(collection_permission):
if isinstance(collection_permission, collections.Callable):
return collection_permission(permission)
return False
@ -305,7 +309,7 @@ class CollectionHandler(BaseHandler):
:rtype: str"""
t = str(time.time()).replace('.', '_')
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))
def _filter_results(self, results, params):
@ -320,11 +324,11 @@ class CollectionHandler(BaseHandler):
:rtype: list"""
if not params:
return results
params = backend.convert(params)
params = monco.convert(params)
filtered = []
for result in results:
add = True
for key, value in params.iteritems():
for key, value in params.items():
if key not in result or result[key] != value:
add = False
break
@ -338,8 +342,8 @@ class CollectionHandler(BaseHandler):
:param data: dictionary to clean
:type data: dict"""
if isinstance(data, dict):
for key in data.keys():
if isinstance(key, (str, unicode)) and key.startswith('$'):
for key in list(data.keys()):
if isinstance(key, str) and key.startswith('$'):
del data[key]
return data
@ -349,7 +353,7 @@ class CollectionHandler(BaseHandler):
:param data: dictionary to convert
:type data: dict"""
ret = {}
for key, value in data.iteritems():
for key, value in data.items():
if isinstance(value, (list, tuple, dict)):
continue
try:
@ -357,7 +361,7 @@ class CollectionHandler(BaseHandler):
key = re_env_key.sub('', key)
if not key:
continue
ret[key] = unicode(value).encode(ENCODING)
ret[key] = str(value).encode(ENCODING)
except:
continue
return ret
@ -382,7 +386,7 @@ class CollectionHandler(BaseHandler):
if acl and not self.has_permission(permission):
return self.build_error(status=401, message='insufficient permissions: %s' % permission)
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 = self.apply_filter(output, 'get_%s' % resource)
self.write(output)
@ -432,7 +436,7 @@ class CollectionHandler(BaseHandler):
return self.build_error(status=401, message='insufficient permissions: %s' % permission)
# Handle access to sub-resources.
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))
output = handler(id_, resource_id, data, **kwargs)
output = self.apply_filter(output, 'get_%s' % resource)
@ -478,7 +482,7 @@ class CollectionHandler(BaseHandler):
if not self.has_permission(permission):
return self.build_error(status=401, message='insufficient permissions: %s' % permission)
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)
env['RESOURCE'] = resource
if resource_id:
@ -584,7 +588,7 @@ class CollectionHandler(BaseHandler):
ws = yield tornado.websocket.websocket_connect(self.build_ws_url(path))
ws.write_message(message)
ws.close()
except Exception, e:
except Exception as e:
self.logger.error('Error yielding WebSocket message: %s', e)
@ -641,7 +645,7 @@ class EventsHandler(CollectionHandler):
if group_id is None:
return {'persons': persons}
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}
events = self.db.query('events', all_query)
for event in events:
@ -655,7 +659,7 @@ class EventsHandler(CollectionHandler):
or which set of keys specified in a dictionary match their respective values."""
for ticket in tickets:
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
else:
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 [])
env = dict(ticket)
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})
stdin_data = {'new': ticket,
'event': doc,
@ -765,7 +769,7 @@ class EventsHandler(CollectionHandler):
# Update an existing entry for a ticket registered at this event.
self._clean_dict(data)
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_
if ticket_id is not None:
query['tickets._id'] = ticket_id
@ -794,7 +798,7 @@ class EventsHandler(CollectionHandler):
# always takes the ticket_id from the new ticket
ticket_id = str(new_ticket_data.get('_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})
stdin_data = {'old': old_ticket_data,
'new': new_ticket_data,
@ -806,7 +810,8 @@ class EventsHandler(CollectionHandler):
if new_ticket_data.get('attended'):
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:
self.send_ws_message('event/%s/tickets/updates' % id_, json.dumps(ret))
return ret
@ -827,7 +832,7 @@ class EventsHandler(CollectionHandler):
self.send_ws_message('event/%s/tickets/updates' % id_, json.dumps(ret))
env = dict(ticket)
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})
stdin_data = {'old': ticket,
'event': rdoc,
@ -872,7 +877,7 @@ class UsersHandler(CollectionHandler):
@authenticated
def get(self, id_=None, resource=None, resource_id=None, acl=True, **kwargs):
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
super(UsersHandler, self).get(id_, resource, resource_id, acl=acl, **kwargs)
@ -896,12 +901,17 @@ class UsersHandler(CollectionHandler):
if new_pwd is not None:
del data['new_password']
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')
data['password'] = utils.hash_password(new_pwd)
if '_id' in data:
# Avoid overriding _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
@gen.coroutine
@ -909,7 +919,7 @@ class UsersHandler(CollectionHandler):
def put(self, id_=None, resource=None, resource_id=None, **kwargs):
if id_ is None:
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')
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')])
for ticket in (event_details[0].get('tickets') or []):
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:
filename = content['filename']
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)
count = 0
_to_delete = set()
for uuid, client in _ws_clients.get(url, {}).iteritems():
for uuid, client in _ws_clients.get(url, {}).items():
try:
client.write_message(message)
except:
@ -1079,10 +1089,11 @@ class LoginHandler(RootHandler):
self.write({'error': True, 'message': 'missing username or password'})
return
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']
logging.info('successful login for user %s' % username)
self.set_secure_cookie("user", username)
logging.info('successful login for user %s (id: %s)' % (username, id_))
self.set_secure_cookie("user", id_)
self.write({'error': False, 'message': 'successful login'})
return
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)
# 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,
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"),
static_path=os.path.join(os.path.dirname(__file__), "static"),
cookie_secret='__COOKIE_SECRET__',
cookie_secret=cookie_secret,
login_url='/login',
debug=options.debug)
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>
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}')
_force_conversion = {
'_id': ObjectId,
'seq_hex': str,
'tickets.seq_hex': str
}
@ -40,7 +41,8 @@ def convert_obj(obj):
if isinstance(obj, bool):
return obj
try:
return ObjectId(obj)
if re_objectid.match(obj):
return ObjectId(obj)
except:
pass
return obj
@ -56,9 +58,12 @@ def convert(seq):
"""
if isinstance(seq, dict):
d = {}
for key, item in seq.iteritems():
for key, item in seq.items():
if key in _force_conversion:
d[key] = _force_conversion[key](item)
try:
d[key] = _force_conversion[key](item)
except:
d[key] = item
else:
d[key] = convert(item)
return d
@ -67,23 +72,35 @@ def convert(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."""
db = None
connection = None
# map operations on lists of items.
_operations = {
'update': '$set',
'append': '$push',
'appendUnique': '$addToSet',
'delete': '$pull',
'increment': '$inc'
'update': '$set',
'append': '$push',
'appendUnique': '$addToSet',
'delete': '$pull',
'increment': '$inc'
}
def __init__(self, url=None, dbName='eventman'):
def __init__(self, dbName, url=None):
"""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
:type url: str (or None to connect to localhost)
"""
@ -91,9 +108,11 @@ class EventManDB(object):
self._dbName = dbName
self.connect(url)
def connect(self, url=None, dbName=None):
def connect(self, dbName=None, url=None):
"""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
:type url: str (or None to connect to localhost)
@ -106,10 +125,26 @@ class EventManDB(object):
self._url = url
if dbName:
self._dbName = dbName
if not self._dbName:
raise MoncoConnectionError('no database name specified')
self.connection = pymongo.MongoClient(self._url)
self.db = self.connection[self._dbName]
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):
"""Get a single document with the specified `_id`.
@ -121,8 +156,7 @@ class EventManDB(object):
:returns: the document with the given `_id`
:rtype: dict
"""
results = self.query(collection, convert({'_id': _id}))
return results and results[0] or {}
return self.getOne(collection, {'_id': _id})
def query(self, collection, query=None, condition='or'):
"""Get multiple documents matching a query.
@ -130,7 +164,7 @@ class EventManDB(object):
:param collection: search for documents in this collection
:type collection: str
:param query: search for documents with those attributes
:type query: dict or None
:type query: dict, list or None
:returns: list of matching documents
:rtype: list
@ -222,7 +256,7 @@ class EventManDB(object):
operator = self._operations.get(operation)
if updateList:
newData = {}
for key, value in data.iteritems():
for key, value in data.items():
newData['%s.$.%s' % (updateList, key)] = value
data = newData
res = db[collection].find_and_modify(query=_id_or_query,
@ -230,6 +264,30 @@ class EventManDB(object):
lastErrorObject = res.get('lastErrorObject') 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):
"""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
:type force: bool
:returns: how many documents were removed
:rtype: int
:returns: dictionary with the number or removed documents
:rtype: dict
"""
if not _id_or_query and not force:
return
@ -250,4 +308,3 @@ class EventManDB(object):
_id_or_query = {'_id': _id_or_query}
_id_or_query = convert(_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';
var noop = angular.noop;
var objectFreeze = (Object.freeze) ? Object.freeze : noop;
Object.defineProperty(exports, "__esModule", {
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 isString = angular.isString;
var isFunction = angular.isFunction;
var isDefined = angular.isDefined;
var isObject = angular.isObject;
var isArray = angular.isArray;
var forEach = angular.forEach;
var isString = _angular2.default.isString;
var isFunction = _angular2.default.isFunction;
var isDefined = _angular2.default.isDefined;
var isObject = _angular2.default.isObject;
var isArray = _angular2.default.isArray;
var forEach = _angular2.default.forEach;
var arraySlice = Array.prototype.slice;
// ie8 wat
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(elt /*, from*/) {
Array.prototype.indexOf = function (elt /*, from*/) {
var len = this.length >>> 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) {
from += len;
}
for (; from < len; from++) {
if (from in this && this[from] === elt) { return from; }
if (from in this && this[from] === elt) {
return from;
}
}
return -1;
};
@ -48,20 +92,20 @@
// this.buffer = [];
// TODO: refactor options to use isDefined
this.scope = options && options.scope || $rootScope;
this.rootScopeFailover = options && options.rootScopeFailover && true;
this.useApplyAsync = options && options.useApplyAsync || false;
this.initialTimeout = options && options.initialTimeout || 500; // 500ms
this.maxTimeout = options && options.maxTimeout || 5 * 60 * 1000; // 5 minutes
this.reconnectIfNotNormalClose = options && options.reconnectIfNotNormalClose || false;
this.binaryType = options && options.binaryType || 'blob';
this.scope = options && options.scope || $rootScope;
this.rootScopeFailover = options && options.rootScopeFailover && true;
this.useApplyAsync = options && options.useApplyAsync || false;
this.initialTimeout = options && options.initialTimeout || 500; // 500ms
this.maxTimeout = options && options.maxTimeout || 5 * 60 * 1000; // 5 minutes
this.reconnectIfNotNormalClose = options && options.reconnectIfNotNormalClose || false;
this.binaryType = options && options.binaryType || 'blob';
this._reconnectAttempts = 0;
this.sendQueue = [];
this.onOpenCallbacks = [];
this.sendQueue = [];
this.onOpenCallbacks = [];
this.onMessageCallbacks = [];
this.onErrorCallbacks = [];
this.onCloseCallbacks = [];
this.onErrorCallbacks = [];
this.onCloseCallbacks = [];
objectFreeze(this._readyStateConstants);
@ -70,10 +114,8 @@
} else {
this._setInternalState(0);
}
}
$WebSocket.prototype._readyStateConstants = {
'CONNECTING': 0,
'OPEN': 1,
@ -84,9 +126,7 @@
$WebSocket.prototype._normalCloseCode = 1000;
$WebSocket.prototype._reconnectableStatusCodes = [
4000
];
$WebSocket.prototype._reconnectableStatusCodes = [4000];
$WebSocket.prototype.safeDigest = function safeDigest(autoApply) {
if (autoApply && !this.scope.$$phase) {
@ -99,7 +139,7 @@
if (scope) {
this.scope = scope;
if (this.rootScopeFailover) {
this.scope.$on('$destroy', function() {
this.scope.$on('$destroy', function () {
self.scope = $rootScope;
});
}
@ -110,10 +150,10 @@
$WebSocket.prototype._connect = function _connect(force) {
if (force || !this.socket || this.socket.readyState !== this._readyStateConstants.OPEN) {
this.socket = $websocketBackend.create(this.url, this.protocols);
this.socket.onmessage = angular.bind(this, this._onMessageHandler);
this.socket.onopen = angular.bind(this, this._onOpenHandler);
this.socket.onerror = angular.bind(this, this._onErrorHandler);
this.socket.onclose = angular.bind(this, this._onCloseHandler);
this.socket.onmessage = _angular2.default.bind(this, this._onMessageHandler);
this.socket.onopen = _angular2.default.bind(this, this._onOpenHandler);
this.socket.onerror = _angular2.default.bind(this, this._onErrorHandler);
this.socket.onclose = _angular2.default.bind(this, this._onCloseHandler);
this.socket.binaryType = this.binaryType;
}
};
@ -122,9 +162,7 @@
while (this.sendQueue.length && this.socket.readyState === this._readyStateConstants.OPEN) {
var data = this.sendQueue.shift();
this.socket.send(
isString(data.message) || this.binaryType != "blob" ? data.message : JSON.stringify(data.message)
);
this.socket.send(isString(data.message) || this.binaryType != 'blob' ? data.message : JSON.stringify(data.message));
data.deferred.resolve();
}
};
@ -162,7 +200,6 @@
return this;
};
$WebSocket.prototype.onMessage = function onMessage(callback, options) {
if (!isFunction(callback)) {
throw new Error('Callback must be a function');
@ -189,14 +226,14 @@
$WebSocket.prototype._onCloseHandler = function _onCloseHandler(event) {
var self = this;
if (self.useApplyAsync) {
self.scope.$applyAsync(function() {
self.scope.$applyAsync(function () {
self.notifyCloseCallbacks(event);
});
} else {
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();
}
};
@ -204,12 +241,12 @@
$WebSocket.prototype._onErrorHandler = function _onErrorHandler(event) {
var self = this;
if (self.useApplyAsync) {
self.scope.$applyAsync(function() {
self.scope.$applyAsync(function () {
self.notifyErrorCallbacks(event);
});
} else {
self.notifyErrorCallbacks(event);
self.safeDigest(autoApply);
self.safeDigest(true);
}
};
@ -223,12 +260,10 @@
if (pattern) {
if (isString(pattern) && message.data === pattern) {
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);
}
}
else {
} else {
applyAsyncOrDigest(currentCallback.fn, currentCallback.autoApply, message);
}
}
@ -236,7 +271,7 @@
function applyAsyncOrDigest(callback, autoApply, args) {
args = arraySlice.call(arguments, 2);
if (self.useApplyAsync) {
self.scope.$applyAsync(function() {
self.scope.$applyAsync(function () {
callback.apply(self, args);
});
} else {
@ -244,7 +279,6 @@
self.safeDigest(autoApply);
}
}
};
$WebSocket.prototype.close = function close(force) {
@ -261,8 +295,7 @@
if (self.readyState === self._readyStateConstants.RECONNECT_ABORTED) {
deferred.reject('Socket connection has been closed');
}
else {
} else {
self.sendQueue.push({
message: data,
deferred: deferred
@ -274,7 +307,7 @@
function cancelableify(promise) {
promise.cancel = cancel;
var then = promise.then;
promise.then = function() {
promise.then = function () {
var newPromise = then.apply(this, arguments);
return cancelableify(newPromise);
};
@ -287,8 +320,7 @@
return self;
}
if ($websocketBackend.isMocked && $websocketBackend.isMocked() &&
$websocketBackend.isConnected(this.url)) {
if ($websocketBackend.isMocked && $websocketBackend.isMocked() && $websocketBackend.isConnected(this.url)) {
this._onMessageHandler($websocketBackend.mockSend());
}
@ -303,7 +335,7 @@
var backoffDelaySeconds = backoffDelay / 1000;
console.log('Reconnecting in ' + backoffDelaySeconds + ' seconds');
$timeout(angular.bind(this, this._connect), backoffDelay);
$timeout(_angular2.default.bind(this, this._connect), backoffDelay);
return this;
};
@ -330,8 +362,7 @@
}
this._internalConnectionState = state;
forEach(this.sendQueue, function(pending) {
forEach(this.sendQueue, function (pending) {
pending.deferred.reject('Message cancelled due to closed socket connection');
});
};
@ -339,62 +370,46 @@
// Read only .readyState
if (objectDefineProperty) {
objectDefineProperty($WebSocket.prototype, 'readyState', {
get: function() {
get: function get() {
return this._internalConnectionState || this.socket.readyState;
},
set: function() {
set: function set() {
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);
};
}
// $WebSocketBackendProvider.$inject = ['$window', '$log'];
function $WebSocketBackendProvider($window, $log) {
// $WebSocketBackendProvider.$inject = ['$log'];
function $WebSocketBackendProvider($log) {
this.create = function create(url, protocols) {
var match = /wss?:\/\//.exec(url);
var Socket, ws;
if (!match) {
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) {
return new Socket(url, protocols);
}
return new Socket(url);
};
this.createWebSocketBackend = function createWebSocketBackend(url, protocols) {
$log.warn('Deprecated: Please use .create(url, protocols)');
return this.create(url, protocols);
};
}
angular.module('ngWebSocket', [])
.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('ngWebSocket', []).factory('$websocket', ['$rootScope', '$q', '$timeout', '$websocketBackend', $WebSocketProvider]).factory('WebSocket', ['$rootScope', '$q', '$timeout', 'WebsocketBackend', $WebSocketProvider]).service('$websocketBackend', ['$log', $WebSocketBackendProvider]).service('WebSocketBackend', ['$log', $WebSocketBackendProvider]);
_angular2.default.module('angular-websocket', ['ngWebSocket']);
angular.module('angular-websocket', ['ngWebSocket']);
if (typeof module === 'object' && typeof define !== 'function') {
module.exports = angular.module('ngWebSocket');
}
}());
exports.default = _angular2.default.module('ngWebSocket');
module.exports = exports['default'];
});

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