Merge pull request #176 from alberanid/master
toaster and basic qr code scanner
This commit is contained in:
commit
e8e10ced5d
9 changed files with 137 additions and 69 deletions
|
@ -1,6 +1,5 @@
|
||||||
<!-- show details of an Event -->
|
<!-- show details of an Event -->
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div eventman-message="eventman-message" control="message"></div>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
|
|
|
@ -98,6 +98,7 @@
|
||||||
|
|
||||||
<div class="main-header">
|
<div class="main-header">
|
||||||
</div>
|
</div>
|
||||||
|
<toaster-container toaster-options="{'time-out': 4000, 'position-class': 'toast-top-center'}"></toaster-container>
|
||||||
|
|
||||||
<!-- main error handling -->
|
<!-- main error handling -->
|
||||||
<div ng-if="error.error" ng-class="{clearfix: true, alert: true, 'alert-danger': true}">
|
<div ng-if="error.error" ng-class="{clearfix: true, alert: true, 'alert-danger': true}">
|
||||||
|
|
20
angular_app/js/controllers.js
vendored
20
angular_app/js/controllers.js
vendored
|
@ -192,11 +192,10 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event', 'EventTicket', 'Setting', '$log', '$translate', '$rootScope', 'EventUpdates', '$uibModal', '$filter',
|
eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event', 'EventTicket', 'Setting', '$log', '$translate', '$rootScope', 'EventUpdates', '$uibModal', '$filter', 'toaster',
|
||||||
function ($scope, $state, Event, EventTicket, Setting, $log, $translate, $rootScope, EventUpdates, $uibModal, $filter) {
|
function ($scope, $state, Event, EventTicket, Setting, $log, $translate, $rootScope, EventUpdates, $uibModal, $filter, toaster) {
|
||||||
$scope.ticketsOrder = ["name", "surname"];
|
$scope.ticketsOrder = ["name", "surname"];
|
||||||
$scope.countAttendees = 0;
|
$scope.countAttendees = 0;
|
||||||
$scope.message = {};
|
|
||||||
$scope.query = '';
|
$scope.query = '';
|
||||||
$scope.event = {};
|
$scope.event = {};
|
||||||
$scope.event.tickets = [];
|
$scope.event.tickets = [];
|
||||||
|
@ -293,6 +292,10 @@ eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event',
|
||||||
$log.debug('do not process our own message');
|
$log.debug('do not process our own message');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (data.error && data.message) {
|
||||||
|
toaster.pop({type: 'error', title: 'Error', body: data.message, timeout: 5000});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!$scope.event.tickets) {
|
if (!$scope.event.tickets) {
|
||||||
$scope.event.tickets = [];
|
$scope.event.tickets = [];
|
||||||
}
|
}
|
||||||
|
@ -478,15 +481,16 @@ eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event',
|
||||||
|
|
||||||
$scope.showAttendedMessage = function(ticket, attends) {
|
$scope.showAttendedMessage = function(ticket, attends) {
|
||||||
var msg = {};
|
var msg = {};
|
||||||
|
var msg_type = 'success';
|
||||||
var name = $scope.buildTicketLabel(ticket);
|
var name = $scope.buildTicketLabel(ticket);
|
||||||
|
|
||||||
if (attends) {
|
if (attends) {
|
||||||
msg.message = name + ' successfully added to event ' + $scope.event.title;
|
msg.message = name + ' successfully added to event ' + $scope.event.title;
|
||||||
} else {
|
} else {
|
||||||
msg.message = name + ' successfully removed from event ' + $scope.event.title;
|
msg.message = name + ' successfully removed from event ' + $scope.event.title;
|
||||||
msg.isError = true;
|
msg_type = 'warning';
|
||||||
}
|
}
|
||||||
$scope.showMessage(msg);
|
toaster.pop({type: msg_type, title: msg.message});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.setTicketAttributeAndRefocus = function(ticket, key, value) {
|
$scope.setTicketAttributeAndRefocus = function(ticket, key, value) {
|
||||||
|
@ -593,7 +597,7 @@ eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event',
|
||||||
});
|
});
|
||||||
if ($state.is('event.ticket.edit')) {
|
if ($state.is('event.ticket.edit')) {
|
||||||
$scope.updateTicket($scope.ticket, function() {
|
$scope.updateTicket($scope.ticket, function() {
|
||||||
$scope.showMessage({message: 'ticket successfully updated'});
|
toaster.pop({type: 'info', title: 'ticket successfully updated'});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
$scope.addTicket($scope.ticket);
|
$scope.addTicket($scope.ticket);
|
||||||
|
@ -647,10 +651,6 @@ eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event',
|
||||||
$scope.filterTickets();
|
$scope.filterTickets();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.showMessage = function(cfg) {
|
|
||||||
$scope.message && $scope.message.show && $scope.message.show(cfg);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.$on('$destroy', function() {
|
$scope.$on('$destroy', function() {
|
||||||
$scope.EventUpdates && $scope.EventUpdates.close();
|
$scope.EventUpdates && $scope.EventUpdates.close();
|
||||||
});
|
});
|
||||||
|
|
28
angular_app/js/directives.js
vendored
28
angular_app/js/directives.js
vendored
|
@ -40,31 +40,3 @@ eventManApp.directive('resetFocus', function () {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
eventManApp.directive('eventmanMessage', ['$timeout',
|
|
||||||
function($timeout) {
|
|
||||||
function link(scope, element, attrs) {
|
|
||||||
scope.dControl = scope.control || {};
|
|
||||||
scope.dControl.isVisible = false;
|
|
||||||
|
|
||||||
scope.dControl.show = function(cfg) {
|
|
||||||
cfg = cfg || {};
|
|
||||||
scope.dControl.isVisible = true;
|
|
||||||
scope.dControl.message = cfg.message;
|
|
||||||
scope.dControl.isError = cfg.isError;
|
|
||||||
$timeout(function () {
|
|
||||||
scope.dControl.isVisible = false;
|
|
||||||
}, cfg.timeout || 4000);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
scope: {
|
|
||||||
control: '='
|
|
||||||
},
|
|
||||||
link: link,
|
|
||||||
template: '<div ng-if="dControl.isVisible" ng-class="{\'eventman-message\': true, clearfix: true, \'alert\': true, \'alert-success\': !dControl.isError, \'alert-danger\': dControl.isError}">{{dControl.message}}</div>'
|
|
||||||
};
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
<!-- show details of an Event -->
|
<!-- show details of an Event -->
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div eventman-message="eventman-message" control="message"></div>
|
|
||||||
|
|
||||||
<!-- FIXME: ideally, here we would have put a ng-if="!ticket.cancelled" directive, but for some
|
<!-- FIXME: ideally, here we would have put a ng-if="!ticket.cancelled" directive, but for some
|
||||||
odd reason, any kind ng-if directive will prevent the form being populated with the formData model.
|
odd reason, any kind ng-if directive will prevent the form being populated with the formData model.
|
||||||
-->
|
-->
|
||||||
|
|
|
@ -668,17 +668,26 @@ class EventsHandler(CollectionHandler):
|
||||||
persons += [p for p in (event.get('tickets') or []) if p.get('email') and p.get('email') not in this_emails]
|
persons += [p for p in (event.get('tickets') or []) if p.get('email') and p.get('email') not in this_emails]
|
||||||
return {'persons': persons}
|
return {'persons': persons}
|
||||||
|
|
||||||
def _get_ticket_data(self, ticket_id_or_query, tickets):
|
def _get_ticket_data(self, ticket_id_or_query, tickets, only_one=True):
|
||||||
"""Filter a list of tickets returning the first item with a given _id
|
"""Filter a list of tickets returning the first item with a given _id
|
||||||
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."""
|
||||||
|
matches = []
|
||||||
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.items()):
|
if all(ticket.get(k) == v for k, v in ticket_id_or_query.items()):
|
||||||
return ticket
|
matches.append(ticket)
|
||||||
|
if only_one:
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
if str(ticket.get('_id')) == ticket_id_or_query:
|
if str(ticket.get('_id')) == ticket_id_or_query:
|
||||||
return ticket
|
matches.append(ticket)
|
||||||
|
if only_one:
|
||||||
|
break
|
||||||
|
if only_one:
|
||||||
|
if matches:
|
||||||
|
return matches[0]
|
||||||
return {}
|
return {}
|
||||||
|
return matches
|
||||||
|
|
||||||
def handle_get_tickets(self, id_, resource_id=None):
|
def handle_get_tickets(self, id_, resource_id=None):
|
||||||
# Return every ticket registered at this event, or the information
|
# Return every ticket registered at this event, or the information
|
||||||
|
@ -799,7 +808,18 @@ class EventsHandler(CollectionHandler):
|
||||||
current_event = {}
|
current_event = {}
|
||||||
self._check_sales_datetime(current_event)
|
self._check_sales_datetime(current_event)
|
||||||
tickets = current_event.get('tickets') or []
|
tickets = current_event.get('tickets') or []
|
||||||
old_ticket_data = self._get_ticket_data(ticket_query, tickets)
|
matching_tickets = self._get_ticket_data(ticket_query, tickets, only_one=False)
|
||||||
|
nr_matches = len(matching_tickets)
|
||||||
|
if nr_matches > 1:
|
||||||
|
ret = {'error': True, 'message': 'more than one ticket matched', 'query': query,
|
||||||
|
'uuid': uuid, 'username': self.current_user_info.get('username', '')}
|
||||||
|
self.send_ws_message('event/%s/tickets/updates' % id_, json.dumps(ret))
|
||||||
|
self.set_status(400)
|
||||||
|
return ret
|
||||||
|
elif nr_matches == 1:
|
||||||
|
old_ticket_data = matching_tickets[0]
|
||||||
|
else:
|
||||||
|
old_ticket_data = {}
|
||||||
|
|
||||||
# We have changed the "cancelled" status of a ticket to False; check if we still have a ticket available
|
# We have changed the "cancelled" status of a ticket to False; check if we still have a ticket available
|
||||||
if 'number_of_tickets' in current_event and old_ticket_data.get('cancelled') and not data.get('cancelled'):
|
if 'number_of_tickets' in current_event and old_ticket_data.get('cancelled') and not data.get('cancelled'):
|
||||||
|
|
|
@ -99,3 +99,7 @@ input[type=text].form-control, input[type=search].form-control {
|
||||||
.registered-counter {
|
.registered-counter {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#toast-container.toast-bottom-center>div, #toast-container.toast-center>div, #toast-container.toast-top-center>div {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
17
tools/qrcode_reader.ini
Normal file
17
tools/qrcode_reader.ini
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[connection]
|
||||||
|
port = /dev/ttyACM0
|
||||||
|
|
||||||
|
[eventman]
|
||||||
|
url = https://localhost:5242/
|
||||||
|
username = admin
|
||||||
|
password = eventman
|
||||||
|
ca =
|
||||||
|
|
||||||
|
[event]
|
||||||
|
id = 1492099112_2612922-3896-9zwsccuvguz91jtw9y6lwvkud11ba7wt
|
||||||
|
field = order_nr
|
||||||
|
limit_field = 9
|
||||||
|
|
||||||
|
[actions]
|
||||||
|
attended = True
|
||||||
|
checked_in_by = ${eventman:username}
|
|
@ -1,34 +1,92 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
"""qrcode_reader
|
||||||
|
|
||||||
|
Scan the output of a serial QR Code reader.
|
||||||
|
|
||||||
|
Copyright 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.
|
||||||
|
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import io
|
import io
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
import serial
|
import serial
|
||||||
|
import urllib
|
||||||
import requests
|
import requests
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
|
||||||
class Connector():
|
class Connector():
|
||||||
def __init__(self, login_url, checkin_url, username=None, password=None):
|
def __init__(self, cfg):
|
||||||
self.login_url = login_url
|
self.cfg = cfg
|
||||||
self.checkin_url = checkin_url
|
self.session = None
|
||||||
|
self.url = cfg['eventman']['url']
|
||||||
|
self.login_url = urllib.parse.urljoin(self.url, '/v1.0/login')
|
||||||
|
self.checkin_url = urllib.parse.urljoin(self.url, os.path.join('/v1.0/events/',
|
||||||
|
cfg['event']['id'], 'tickets/'))
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
try:
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
json = {}
|
self.session.verify = False
|
||||||
|
ca = cfg['eventman'].get('ca')
|
||||||
|
if ca and os.path.isfile(ca):
|
||||||
|
self.session.verify = ca
|
||||||
|
username = cfg['eventman'].get('username')
|
||||||
|
password = cfg['eventman'].get('password')
|
||||||
|
params = {}
|
||||||
if username:
|
if username:
|
||||||
json['username'] = username
|
params['username'] = username
|
||||||
if password:
|
if password:
|
||||||
json['password'] = password
|
params['password'] = password
|
||||||
req = self.session.post(login_url, json=json, verify=False)
|
req = self.session.post(self.login_url, json=params)
|
||||||
req.raise_for_status()
|
req.raise_for_status()
|
||||||
req.connection.close()
|
req.connection.close()
|
||||||
|
except requests.exceptions.ConnectionError as ex:
|
||||||
|
print('unable to connect to %s: %s' % (self.login_url, ex))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
def checkin(self, code):
|
def checkin(self, code):
|
||||||
req = self.session.put(self.checkin_url + '?order_nr=' + code[:9], json={'attended': True}, verify=False)
|
limit_field = self.cfg['event'].getint('limit_field')
|
||||||
|
if limit_field:
|
||||||
|
code = code[:limit_field]
|
||||||
|
checkin_url = self.checkin_url + '?' + urllib.parse.urlencode({cfg['event']['field']: code})
|
||||||
|
params = dict(self.cfg['actions'])
|
||||||
|
req = self.session.put(checkin_url, json=params)
|
||||||
|
try:
|
||||||
req.raise_for_status()
|
req.raise_for_status()
|
||||||
|
except requests.exceptions.HTTPError as ex:
|
||||||
|
print('error: %s' % req.json().get('message'))
|
||||||
req.connection.close()
|
req.connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def scan(port):
|
||||||
def scan():
|
retry = 1
|
||||||
ser = serial.Serial(port='/dev/ttyACM0', timeout=1)
|
while True:
|
||||||
|
print('waiting for connection on port %s...' % port)
|
||||||
|
try:
|
||||||
|
ser = serial.Serial(port=port, timeout=1)
|
||||||
|
break
|
||||||
|
except serial.serialutil.SerialException as ex:
|
||||||
|
if retry >= 10:
|
||||||
|
print('unable to connect: %s' % ex)
|
||||||
|
sys.exit(2)
|
||||||
|
time.sleep(1)
|
||||||
|
retry += 1
|
||||||
|
print('connected to %s' % port)
|
||||||
ser_io = io.TextIOWrapper(io.BufferedRWPair(ser, ser, 1), newline='\r', line_buffering=True)
|
ser_io = io.TextIOWrapper(io.BufferedRWPair(ser, ser, 1), newline='\r', line_buffering=True)
|
||||||
while True:
|
while True:
|
||||||
line = ser_io.readline().strip()
|
line = ser_io.readline().strip()
|
||||||
|
@ -38,13 +96,12 @@ def scan():
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
connector = Connector(login_url='https://localhost:5242/v1.0/login',
|
cfg = configparser.ConfigParser()
|
||||||
checkin_url='https://localhost:5242/v1.0/events/1490640884_8820477-7-7gvft6nlrs2o73fza54a6yeywiowmj8v/tickets/',
|
cfg.read('qrcode_reader.ini')
|
||||||
username='admin',
|
connector = Connector(cfg)
|
||||||
password='eventman')
|
|
||||||
try:
|
try:
|
||||||
for code in scan():
|
for code in scan(port=cfg['connection']['port']):
|
||||||
print(code)
|
print('received code %s' % code)
|
||||||
connector.checkin(code)
|
connector.checkin(code)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print('exiting...')
|
print('exiting...')
|
||||||
|
|
Loading…
Reference in a new issue