fixes #144: begin and end time for ticket sales

This commit is contained in:
Davide Alberani 2016-08-01 14:40:29 +02:00
parent 49c924e76f
commit f87f87607a
7 changed files with 169 additions and 45 deletions

View file

@ -12,6 +12,7 @@ Main features:
- no registration is required to join/leave an event
- quickly mark a registered person as an attendee
- easy way to add a new ticket, if it's already known from a previous event or if it's a completely new ticket
- set maximum number of tickets and begin/end date for sales
- can import Eventbrite CSV export files
- RESTful interface that can be called by third-party applications (see the https://github.com/raspibo/event_man/ repository for a simple script that checks people in using a barcode/QR-code reader)
- ability to run triggers to respond to an event (e.g. when a person is marked as attending to an event)
@ -50,6 +51,7 @@ Be sure to have a running MongoDB server, locally. If you want to install the de
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
git clone https://github.com/raspibo/eventman
cd eventman

View file

@ -9,7 +9,7 @@
<span class="fa fa-ticket vcenter"></span>
{{'Tickets' | translate}}
</button>
&nbsp;<button ng-if="event._id && hasPermission('event:tickets-all|create')" ng-click="$state.go('event.ticket.new', {id: event._id})" class="btn btn-success">
&nbsp;<button ng-if="event._id && hasPermission('event:tickets-all|create')" ng-click="$state.go('event.ticket.new', {id: event._id})" ng-class="{btn: true, 'btn-success': true, disabled: event.no_tickets_for_sale}">
<span class="fa fa-user-plus vcenter"></span>
{{'Join this event' | translate}}
</button>
@ -44,33 +44,33 @@
<div class="input-group top5 well form-horizontal" ng-controller="DatetimePickerCtrl">
<div class="form-group">
<label for="begin-date" class="col-sm-3 control-label">{{'begin date:' | translate}}</label>
<div id="begin-date" class="input-group col-sm-9">
<input type="text" class="form-control" datepicker-popup="dd-MMMM-yyyy" ng-model="event['begin-date']" is-open="opened" />
<label for="begin-date" class="col-sm-6 control-label">{{'begin date:' | translate}}</label>
<div id="begin-date" class="input-group col-sm-6">
<input type="text" class="form-control" datepicker-popup="dd-MMMM-yyyy" ng-model="event.begin_date" is-open="opened" />
<span class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="open($event)"><i class="fa fa-calendar"></i></button>
</span>
</div>
</div>
<div class="form-group">
<label for="begin-time" class="col-sm-3 control-label">{{'begin time:' | translate}}</label>
<timepicker id="begin-time" class="input-group" ng-model="event['begin-time']" show-meridian="false"></timepicker>
<label for="begin-time" class="col-sm-6 control-label">{{'begin time:' | translate}}</label>
<timepicker id="begin-time" class="input-group" ng-model="event.begin_time" show-meridian="false"></timepicker>
</div>
</div>
<div class="input-group top5 well form-horizontal" ng-controller="DatetimePickerCtrl">
<div class="form-group">
<label for="end-date" class="col-sm-3 control-label">{{'End date:' | translate}}</label>
<div id="end-date" class="input-group col-sm-9">
<input type="text" class="form-control" datepicker-popup="dd-MMMM-yyyy" ng-model="event['end-date']" is-open="opened" />
<label for="end-date" class="col-sm-6 control-label">{{'end date:' | translate}}</label>
<div id="end-date" class="input-group col-sm-6">
<input type="text" class="form-control" datepicker-popup="dd-MMMM-yyyy" ng-model="event.end_date" is-open="opened" />
<span class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="open($event)"><i class="fa fa-calendar"></i></button>
</span>
</div>
</div>
<div class="form-group">
<label for="end-time" class="col-sm-3 control-label">{{'End time:' | translate}}</label>
<timepicker id="end-time" class="input-group" ng-model="event['end-time']" show-meridian="false"></timepicker>
<label for="end-time" class="col-sm-6 control-label">{{'end time:' | translate}}</label>
<timepicker id="end-time" class="input-group" ng-model="event.end_time" show-meridian="false"></timepicker>
</div>
</div>
@ -83,15 +83,47 @@
<input type="text" class="form-control" placeholder="{{'Used to share persons amongst multiple events. Must be hard to guess (if empty, will be autogenerated)' | translate}}" ng-model="event.group_id">
</div>
<div class="panel panel-default table-striped top5">
<div class="panel panel-default table-striped top30">
<div class="panel-heading">
<h1>{{'Ticket limits'}}</h1>
<h3>{{'Ticket limits'}}</h3>
</div>
<div class="panel-body">
<div class="input-group input-group top5">
<span class="input-group-addon min100">{{'Number of tickets' | translate}}</span>
<input type="number" min="0" class="form-control" placeholder="{{'Number of tickets (0 or empty means unlimited)' | translate}}" ng-model="event.number_of_tickets">
</div>
<div class="input-group top5 well form-horizontal" ng-controller="DatetimePickerCtrl">
<div class="form-group">
<label for="sales-begin-date" class="col-sm-6 control-label">{{'ticket sales begin date:' | translate}}</label>
<div id="sales-begin-date" class="input-group col-sm-6">
<input type="text" class="form-control" datepicker-popup="dd-MMMM-yyyy" ng-model="event.ticket_sales_begin_date" is-open="opened" />
<span class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="open($event)"><i class="fa fa-calendar"></i></button>
</span>
</div>
</div>
<div class="form-group">
<label for="sales-begin-time" class="col-sm-6 control-label">{{'ticket sales begin time:' | translate}}</label>
<timepicker id="sales-begin-time" class="input-group" ng-model="event.ticket_sales_begin_time" show-meridian="false"></timepicker>
</div>
</div>
<div class="input-group top5 well form-horizontal" ng-controller="DatetimePickerCtrl">
<div class="form-group">
<label for="sales-end-date" class="col-sm-6 control-label">{{'ticket sales end date:' | translate}}</label>
<div id="sales-end-date" class="input-group col-sm-6">
<input type="text" class="form-control" datepicker-popup="dd-MMMM-yyyy" ng-model="event.ticket_sales_end_date" is-open="opened" />
<span class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="open($event)"><i class="fa fa-calendar"></i></button>
</span>
</div>
</div>
<div class="form-group">
<label for="sales-end-time" class="col-sm-6 control-label">{{'ticket sales end time:' | translate}}</label>
<timepicker id="sales-end-time" class="input-group" ng-model="event.ticket_sales_end_time" show-meridian="false"></timepicker>
</div>
</div>
</div>
</div>

View file

@ -47,19 +47,30 @@
</span>
<div ng-if="event.tagline"><em>{{event.tagline}}</em></div>
<div ng-if="event.summary"><em>{{event.summary}}</em></div>
<div ng-if="event['begin-date'] || event['end-date'] || event.where" class="top5">
<div ng-if="event.begin_date || event.end_date || event.where" class="top5">
<div ng-if="event.where">{{event.where}}</div>
<span ng-if="event['begin-date']">{{'Begins:' | translate}} {{event['begin-date'] | date:'fullDate'}} {{event['begin-time'] | date:'HH:mm'}}<br/></span>
<span ng-if="event['end-date']">{{'Ends:' | translate}} {{event['end-date'] | date:'fullDate' }} {{event['end-time'] | date:'HH:mm'}}</span>
<span ng-if="event.begin_date">{{'Begins:' | translate}} {{event.begin_date | date:'fullDate'}} {{event.begin_time | date:'HH:mm'}}<br/></span>
<span ng-if="event.end_date">{{'Ends:' | translate}} {{event.end_date | date:'fullDate' }} {{event.end_time | date:'HH:mm'}}</span>
</div>
</td>
<td ng-if="hasPermission('event:tickets-all|read')" class="hcenter">
<p><span ng-init="attendeesNr = ((event.tickets || []) | attendeesFilter).length">{{attendeesNr}}</span> / {{event.tickets_sold || 0}} ({{((attendeesNr / (event.tickets_sold || 0) * 100) || 0).toFixed()}}%)</p>
</td>
<td>
<div ng-if="hasPermission('event:tickets-all|create')" class="top5 hcenter"><button ng-click="$state.go('event.ticket.new', {id: event._id})" class="min150 btn btn-success" type="button" title="{{'Join this event' | translate}}"><span class="fa fa-user-plus vcenter"></span> {{'Join this event' | translate}}</button></div>
<div ng-if="hasPermission('event:tickets-all|create')" class="top5 hcenter"><button ng-click="$state.go('event.ticket.new', {id: event._id})" ng-class="{min150: true, btn: true, 'btn-success': true, disabled: event.no_tickets_for_sale}" type="button" title="{{'Join this event' | translate}}"><span class="fa fa-user-plus vcenter"></span> {{'Join this event' | translate}}</button></div>
<div ng-if="hasPermission('ticket|update')" class="top5 hcenter"><button ng-click="$state.go('event.tickets', {id: event._id})" class="min150 btn btn-primary" type="button" title="{{'Manage tickets' | translate}}"><span class="fa fa-ticket"></span> {{'Manage tickets' | translate}}</button></div>
<div class="top5 hcenter" ng-if="event.number_of_tickets">{{event.number_of_tickets}} {{'tickets' | translate}}, {{event.number_of_tickets - (event.tickets_sold || 0)}} {{'still available' | translate}}</div>
<div class="top5 hcenter" ng-if="event.ticket_sales_begin_date || event.ticket_sales_end_date || event.ticket_sales_begin_time || event.ticket_sales_end_time">
<strong>{{'Tickets for sale:' | translate}}</strong>
<span ng-if="event.ticket_sales_begin_date || event.ticket_sales_begin_time">
<br />
{{'from' | translate}}<span ng-if="event.ticket_sales_begin_date"> {{event.ticket_sales_begin_date | date:'fullDate'}}</span><span ng-if="event.ticket_sales_begin_time"> {{event.ticket_sales_begin_time | date:'HH:mm'}}</span>
</span>
<span ng-if="event.ticket_sales_end_date || event.ticket_sales_end_time">
<br />
{{'until' | translate}}<span ng-if="event.ticket_sales_end_date"> {{event.ticket_sales_end_date | date:'fullDate'}}</span><span ng-if="event.ticket_sales_end_time"> {{event.ticket_sales_end_time | date:'HH:mm'}}</span>
</span>
</div>
</td>
<td ng-if="hasPermission('event|update') || hasPermission('event|delete')">
<div ng-if="hasPermission('event|update')" class="top5 hcenter"><button ng-click="$state.go('event.edit', {id: event._id})" type="button" class="min150 btn btn-warning" title="{{'Edit event' | translate}}"><span class="fa fa-cog"></span> {{'Edit event' | translate}}</button></div>

View file

@ -6,12 +6,12 @@ var eventManServices = angular.module('eventManServices', ['ngResource']);
/* Modify, in place, an object to convert datetime. */
function convert_dates(obj) {
if (obj['begin-date']) {
obj['begin-date'] = obj['begin_date'] = obj['begin-date'].getTime();
}
if (obj['end-date']) {
obj['end-date'] = obj['end_date'] = obj['end-date'].getTime();
}
angular.forEach(['begin_date', 'end_date', 'ticket_sales_begin_date', 'ticket_sales_end_date'], function(key, key_idx) {
if (!obj[key]) {
return;
}
obj[key] = obj[key].getTime();
});
return obj;
}

View file

@ -133,14 +133,19 @@ Stores information about events and tickets.
Main field:
- title
- begin-data
- begin-time
- end-date
- end-time
- begin\_date
- begin\_time
- end\_date
- end\_time
- summary
- description
- where
- group\_id
- number\_of\_tickets
- ticket\_sales\_begin\_date
- ticket\_sales\_begin\_time
- ticket\_sales\_end\_date
- ticket\_sales\_end\_time
- tickets - a list of information about tickets (each entry is a ticket)
- tickets.$.\_id
- tickets.$.ticket\_id

View file

@ -26,6 +26,8 @@ import string
import random
import logging
import datetime
import dateutil.tz
import dateutil.parser
import tornado.httpserver
import tornado.ioloop
@ -589,19 +591,26 @@ class EventsHandler(CollectionHandler):
document = 'event'
collection = 'events'
def filter_get(self, output):
if 'tickets' in output:
output['tickets_sold'] = len([t for t in output['tickets'] if not t.get('cancelled')])
def _mangle_event(self, event):
# Some in-place changes to an event
if 'tickets' in event:
event['tickets_sold'] = len([t for t in event['tickets'] if not t.get('cancelled')])
event['no_tickets_for_sale'] = False
try:
self._check_sales_datetime(event)
self._check_number_of_tickets(event)
except InputException:
event['no_tickets_for_sale'] = True
if not self.has_permission('tickets-all|read'):
output['tickets'] = []
return output
event['tickets'] = []
return event
def filter_get(self, output):
return self._mangle_event(output)
def filter_get_all(self, output):
for event in output.get('events') or []:
if 'tickets' in event:
event['tickets_sold'] = len([t for t in event['tickets'] if not t.get('cancelled')])
if not self.has_permission('tickets-all|read'):
event['tickets'] = []
self._mangle_event(event)
return output
def filter_input_post(self, data):
@ -661,13 +670,70 @@ class EventsHandler(CollectionHandler):
tickets = self._filter_results(event.get('tickets') or [], self.arguments)
return {'tickets': tickets}
def _check_number_of_tickets(self, event):
if self.has_permission('admin|all'):
return
number_of_tickets = event.get('number_of_tickets')
if number_of_tickets is None:
return
try:
number_of_tickets = int(number_of_tickets)
except ValueError:
return
tickets = event.get('tickets') or []
tickets = [t for t in tickets if not t.get('cancelled')]
if len(tickets) >= event['number_of_tickets']:
raise InputException('no more tickets available')
def _check_sales_datetime(self, event):
if self.has_permission('admin|all'):
return
begin_date = event.get('ticket_sales_begin_date')
begin_time = event.get('ticket_sales_begin_time')
end_date = event.get('ticket_sales_end_date')
end_time = event.get('ticket_sales_end_time')
utc = dateutil.tz.tzutc()
is_dst = time.daylight and time.localtime().tm_isdst > 0
utc_offset = - (time.altzone if is_dst else time.timezone)
if begin_date is None:
begin_date = datetime.datetime.now(tz=utc).replace(hour=0, minute=0, second=0, microsecond=0)
else:
begin_date = dateutil.parser.parse(begin_date)
# Compensate UTC and DST offset, that otherwise would be added 2 times (one for date, one for time)
begin_date = begin_date + datetime.timedelta(seconds=utc_offset)
if begin_time is None:
begin_time_h = 0
begin_time_m = 0
else:
begin_time = dateutil.parser.parse(begin_time)
begin_time_h = begin_time.hour
begin_time_m = begin_time.minute
now = datetime.datetime.now(tz=utc)
begin_datetime = begin_date + datetime.timedelta(hours=begin_time_h, minutes=begin_time_m)
if now < begin_datetime:
raise InputException('ticket sales not yet started')
if end_date is None:
end_date = datetime.datetime.today().replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=utc)
else:
end_date = dateutil.parser.parse(end_date)
end_date = end_date + datetime.timedelta(seconds=utc_offset)
if end_time is None:
end_time = end_date
end_time_h = 23
end_time_m = 59
else:
end_time = dateutil.parser.parse(end_time, yearfirst=True)
end_time_h = end_time.hour
end_time_m = end_time.minute
end_datetime = end_date + datetime.timedelta(hours=end_time_h, minutes=end_time_m+1)
if now > end_datetime:
raise InputException('ticket sales has ended')
def handle_post_tickets(self, id_, resource_id, data):
event = self.db.query('events', {'_id': id_})[0]
if 'number_of_tickets' in event:
tickets = event.get('tickets') or []
tickets = [t for t in tickets if not t.get('cancelled')]
if len(tickets) >= event['number_of_tickets']:
raise InputException('no more tickets available')
self._check_sales_datetime(event)
self._check_number_of_tickets(event)
uuid, arguments = self.uuid_arguments
self._clean_dict(data)
data['seq'] = self.get_next_seq('event_%s_tickets' % id_)
@ -710,14 +776,13 @@ class EventsHandler(CollectionHandler):
current_event = current_event[0]
else:
current_event = {}
self._check_sales_datetime(current_event)
tickets = current_event.get('tickets') or []
old_ticket_data = self._get_ticket_data(ticket_query, tickets)
# We updating the "cancelled" status of a ticket; 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'):
active_tickets = [t for t in tickets if not t.get('cancelled')]
if len(active_tickets) >= current_event['number_of_tickets']:
raise InputException('no more tickets available')
self._check_number_of_tickets(current_event)
merged, doc = self.db.update('events', query,
data, updateList='tickets', create=False)

View file

@ -77,6 +77,15 @@ def csvParse(csvStr, remap=None, merge=None):
def hash_password(password, salt=None):
"""Hash a password.
:param password: the cleartext password
:type password: str
:param salt: the optional salt (randomly generated, if None)
:type salt: str
:returns: the hashed password
:rtype: str"""
if salt is None:
salt_pool = string.ascii_letters + string.digits
salt = ''.join(random.choice(salt_pool) for x in xrange(32))