fixes #144: begin and end time for ticket sales
This commit is contained in:
parent
49c924e76f
commit
f87f87607a
7 changed files with 169 additions and 45 deletions
|
@ -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
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<span class="fa fa-ticket vcenter"></span>
|
||||
{{'Tickets' | translate}}
|
||||
</button>
|
||||
<button ng-if="event._id && hasPermission('event:tickets-all|create')" ng-click="$state.go('event.ticket.new', {id: event._id})" class="btn btn-success">
|
||||
<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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
12
angular_app/js/services.js
vendored
12
angular_app/js/services.js
vendored
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
9
utils.py
9
utils.py
|
@ -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))
|
||||
|
|
Loading…
Reference in a new issue