commit
9ea958aa01
12 changed files with 284 additions and 154 deletions
13
README.md
13
README.md
|
@ -3,14 +3,20 @@ Event Man(ager)
|
||||||
|
|
||||||
Your friendly manager of attendees at an event.
|
Your friendly manager of attendees at an event.
|
||||||
|
|
||||||
EventMan will help you handle your list of attendees at an event, managing the list of registered persons and marking persons as present.
|
Event Man(ager) will help you handle your list of attendees at an event, managing the list of registered persons and marking persons as present.
|
||||||
|
|
||||||
Main features:
|
Main features:
|
||||||
|
- an admin (in the future: anyone) can create and manage new events
|
||||||
|
- events can define a registration form with many custom fields
|
||||||
|
- a person can join (or leave) an event, submitting the custom forms
|
||||||
|
- no registration is required to join/leave an event
|
||||||
- quickly mark a registered person as an attendee
|
- quickly mark a registered person as an attendee
|
||||||
- easy way to add a new person, if it's already known from a previous event or if it's a completely new person
|
- easy way to add a new person, if it's already known from a previous event or if it's a completely new person
|
||||||
- can import Eventbrite CSV export files
|
- 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)
|
- 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)
|
- ability to run triggers to respond to an event (e.g. when a person is marked as attending to an event)
|
||||||
|
- can run on HTTPS
|
||||||
|
- multiple workstations are kept in sync (i.e.: marking a person as an attendee is shown in every workstation currently viewing the list of persons registered at an event)
|
||||||
|
|
||||||
See the *screenshots* directory for some images.
|
See the *screenshots* directory for some images.
|
||||||
|
|
||||||
|
@ -25,6 +31,7 @@ Technological stack
|
||||||
===================
|
===================
|
||||||
|
|
||||||
- [AngularJS](https://angularjs.org/) (plus some third-party modules) for the webApp
|
- [AngularJS](https://angularjs.org/) (plus some third-party modules) for the webApp
|
||||||
|
- [Angular Easy form Generator](https://mackentoch.github.io/easyFormGenerator/) for the custom forms
|
||||||
- [Bootstrap](http://getbootstrap.com/) (plus [Angular UI](https://angular-ui.github.io/bootstrap/)) for the eye-candy
|
- [Bootstrap](http://getbootstrap.com/) (plus [Angular UI](https://angular-ui.github.io/bootstrap/)) for the eye-candy
|
||||||
- [Font Awesome](https://fortawesome.github.io/Font-Awesome/) for even more cuteness
|
- [Font Awesome](https://fortawesome.github.io/Font-Awesome/) for even more cuteness
|
||||||
- [Tornado web](http://www.tornadoweb.org/) as web server
|
- [Tornado web](http://www.tornadoweb.org/) as web server
|
||||||
|
@ -56,7 +63,9 @@ If you store SSL key and certificate in the *ssl* directory (default names: even
|
||||||
Authentication
|
Authentication
|
||||||
==============
|
==============
|
||||||
|
|
||||||
By default, authentication is required; default username and password are *admin* and *eventman*. If you want to completely disable authentication, run the daemon with --authentication=off
|
By default, authentication is not required; unregistered and unprivileged users can see and join events, but are unable to edit or handle them. Administrator users can create ed edit events; more information about how permissions are handled can be found in the *docs/DEVELOPMENT.md* file.
|
||||||
|
|
||||||
|
The default administrator username and password are **admin** and **eventman**. If you want to force authentication, run the daemon with --authentication=on
|
||||||
|
|
||||||
Demo database
|
Demo database
|
||||||
=============
|
=============
|
||||||
|
|
|
@ -3,16 +3,21 @@
|
||||||
<div class="panel panel-primary table-striped top5">
|
<div class="panel panel-primary table-striped top5">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h1>
|
<h1>
|
||||||
<button ng-if="event._id" ng-click="$state.go('event.tickets', {id: event._id})" class="btn btn-success">
|
<button ng-if="event._id && hasPermission('persons|read')" ng-click="$state.go('event.tickets', {id: event._id})" class="btn btn-success">
|
||||||
<span class="fa fa-ticket vcenter"></span>
|
<span class="fa fa-ticket vcenter"></span>
|
||||||
{{'Tickets' | translate}}
|
{{'Tickets' | translate}}
|
||||||
</button>
|
</button>
|
||||||
<span ng-if="!event.title">{{'New event' | translate}}</span>{{event.title}}
|
<button ng-if="event._id && hasPermission('event:tickets-all|create')" ng-click="$state.go('event.ticket.new', {id: event._id})" class="btn btn-success">
|
||||||
|
<span class="fa fa-user-plus vcenter"></span>
|
||||||
|
{{'Register' | translate}}
|
||||||
|
</button>
|
||||||
|
<span ng-if="hasPermission('event|create') && !event.title">{{'New event' | translate}}</span>{{event.title}}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<form name="eventForm" ng-model="eventdetails" ng-submit="save()">
|
<form name="eventForm" ng-model="eventdetails" ng-submit="save()">
|
||||||
<div ng-class="{clearfix: true, alert: true, 'alert-success': !eventForm.$dirty, 'alert-danger': eventForm.$dirty}">
|
<fieldset ng-disabled="eventFormDisabled">
|
||||||
|
<div ng-if="!eventFormDisabled" ng-class="{clearfix: true, alert: true, 'alert-success': !eventForm.$dirty, 'alert-danger': eventForm.$dirty}">
|
||||||
<button type="button" class="btn btn-default pull-right" ng-click="save($event)" ng-disabled="!eventForm.$dirty">
|
<button type="button" class="btn btn-default pull-right" ng-click="save($event)" ng-disabled="!eventForm.$dirty">
|
||||||
<span class="fa fa-floppy-o vcenter"></span>
|
<span class="fa fa-floppy-o vcenter"></span>
|
||||||
{{'save' | translate}}
|
{{'save' | translate}}
|
||||||
|
@ -72,11 +77,12 @@
|
||||||
<span class="input-group-addon min100">{{'Where' | translate}}</span>
|
<span class="input-group-addon min100">{{'Where' | translate}}</span>
|
||||||
<input type="text" class="form-control" placeholder="{{'Where' | translate}}" ng-model="event.where">
|
<input type="text" class="form-control" placeholder="{{'Where' | translate}}" ng-model="event.where">
|
||||||
</div>
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
|
||||||
<label></label>
|
<label></label>
|
||||||
<input type="submit" class="outside-screen" />
|
<input type="submit" class="outside-screen" />
|
||||||
<div ng-class="{clearfix: true, alert: true, 'alert-success': !eventForm.$dirty, 'alert-danger': eventForm.$dirty}">
|
<div ng-if="!eventFormDisabled" ng-class="{clearfix: true, alert: true, 'alert-success': !eventForm.$dirty, 'alert-danger': eventForm.$dirty}">
|
||||||
<button type="button" class="btn btn-default pull-right" ng-click="save($event)" ng-disabled="!eventForm.$dirty">
|
<button type="button" class="btn btn-default pull-right" ng-click="save($event)" ng-disabled="!eventForm.$dirty">
|
||||||
<span class="fa fa-floppy-o vcenter"></span>
|
<span class="fa fa-floppy-o vcenter"></span>
|
||||||
{{'save' | translate}}
|
{{'save' | translate}}
|
||||||
|
@ -86,7 +92,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel panel-primary top10">
|
<div class="panel panel-primary top10" ng-if="!eventFormDisabled">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h1>{{'Registration form' | translate}}</h1>
|
<h1>{{'Registration form' | translate}}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
--><div class="col-md-5 col-xs-5 vcenter">
|
--><div class="col-md-5 col-xs-5 vcenter">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2><div class="label label-warning vcenter">{{'Registered:' | translate}} {{event.persons.length || 0}}</div></h2>
|
<h2><div class="label label-warning vcenter">{{'Registered:' | translate}} {{((event.persons || []) | registeredFilter).length}}</div></h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2><div class="label label-info vcenter">{{'Attendees:' | translate}} {{countAttendees}}</div></h2>
|
<h2><div class="label label-info vcenter">{{'Attendees:' | translate}} {{countAttendees}}</div></h2>
|
||||||
|
@ -49,10 +49,10 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr ng-repeat="person in event.persons | splittedFilter:query | orderBy:personsOrder">
|
<tr ng-repeat="person in (event.persons || []) | splittedFilter:query | registeredFilter | orderBy:personsOrder">
|
||||||
<td class="text-right">{{$index+1}}</td>
|
<td class="text-right">{{$index+1}}</td>
|
||||||
<td>
|
<td>
|
||||||
<span><strong><a ui-sref="person.info({id: person.person_id})"><span class="fa fa-lg fa-user"></span></a> <a ui-sref="event.ticket.edit({id: event._id, ticket_id: person.ticket_id})" ng-if="person.ticket_id"><span>{{person.name}}</span> <span>{{person.surname}}</span></a></strong></span><span ng-if="!person.ticket_id"><span>{{person.name}}</span> <span>{{person.surname}}</span></a></strong></span></span><span ng-if="person.email"> <{{person.email}}></span>
|
<span><strong><a ui-sref="person.info({id: person.person_id})"><span class="fa fa-lg fa-user"></span></a> <a ui-sref="event.ticket.edit({id: event._id, ticket_id: person._id})" ng-if="person._id"><span>{{person.name}}</span> <span>{{person.surname}}</span></a></strong></span><span ng-if="!person._id"><span>{{person.name}}</span> <span>{{person.surname}}</span></a></strong></span></span><span ng-if="person.email"> <{{person.email}}></span>
|
||||||
<p ng-if="person.company || person.job_title"><i ng-if="person.job_title">{{person.job_title}}</i><span ng-if="person.company && person.job_title"> @ </span><i ng-if="person.company">{{person.company}}</i></p>
|
<p ng-if="person.company || person.job_title"><i ng-if="person.job_title">{{person.job_title}}</i><span ng-if="person.company && person.job_title"> @ </span><i ng-if="person.company">{{person.company}}</i></p>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
|
|
|
@ -37,15 +37,20 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr ng-repeat="event in events | splittedFilter:query | orderBy:eventsOrderProp">
|
<tr ng-repeat="event in events | splittedFilter:query | orderBy:eventsOrderProp">
|
||||||
<td>
|
<td>
|
||||||
<span><strong>{{event.title}}</strong></span>
|
<span>
|
||||||
|
<strong>
|
||||||
|
<a ui-sref="event.edit({id: event._id})" ng-if="hasPermission('event|update')">{{event.title}}</a>
|
||||||
|
<a ui-sref="event.view({id: event._id})" ng-if="!hasPermission('event|update')">{{event.title}}</a>
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
<p>{{'Begins:' | translate}} {{event['begin-date'] | date:'fullDate'}} {{event['begin-time'] | date:'HH:mm'}}<br/>
|
<p>{{'Begins:' | translate}} {{event['begin-date'] | date:'fullDate'}} {{event['begin-time'] | date:'HH:mm'}}<br/>
|
||||||
{{'Ends:' | translate}} {{event['end-date'] | date:'fullDate' }} {{event['end-time'] | date:'HH:mm'}}</p>
|
{{'Ends:' | translate}} {{event['end-date'] | date:'fullDate' }} {{event['end-time'] | date:'HH:mm'}}</p>
|
||||||
</td>
|
</td>
|
||||||
<td ng-if="hasPermission('persons|read')" class="hcenter">
|
<td ng-if="hasPermission('persons|read')" class="hcenter">
|
||||||
<p><span ng-init="attendeesNr = ((event.persons || []) | attendeesFilter).length">{{attendeesNr}}</span> / {{event.persons.length || 0}} ({{((attendeesNr / (event.persons.length || 0) * 100) || 0).toFixed()}}%)</p>
|
<p><span ng-init="attendeesNr = ((event.persons || []) | attendeesFilter).length">{{attendeesNr}}</span> / {{((event.persons || []) | registeredFilter).length}} ({{((attendeesNr / ((event.persons || []) | registeredFilter).length * 100) || 0).toFixed()}}%)</p>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button ng-if="hasPermission('event:tickets|create')" ng-click="$state.go('event.ticket.new', {id: event._id})" class="btn btn-link fa fa-user-plus" type="button" title="{{'Register' | translate}}"></button>
|
<button ng-if="hasPermission('event:tickets|create')" ng-click="$state.go('event.ticket.new', {id: event._id})" class="btn btn-link fa fa-user-plus" type="button" title="{{'Join this event' | translate}}"></button>
|
||||||
<button ng-if="hasPermission('persons|update')" ng-click="$state.go('event.tickets', {id: event._id})" class="btn btn-link fa fa-list" type="button" title="{{'Manage attendees' | translate}}"></button>
|
<button ng-if="hasPermission('persons|update')" ng-click="$state.go('event.tickets', {id: event._id})" class="btn btn-link fa fa-list" type="button" title="{{'Manage attendees' | translate}}"></button>
|
||||||
<button ng-if="hasPermission('event|update')" ng-click="$state.go('event.edit', {id: event._id})" type="button" class="btn btn-link fa fa-cog fa-lg" title="{{'Edit' | translate}}"></button>
|
<button ng-if="hasPermission('event|update')" ng-click="$state.go('event.edit', {id: event._id})" type="button" class="btn btn-link fa fa-cog fa-lg" title="{{'Edit' | translate}}"></button>
|
||||||
<button ng-if="hasPermission('event|delete')" ng-click="remove(event._id)" type="button" class="btn btn-link fa fa-trash fa-lg" title="{{'Delete' | translate}}"></button>
|
<button ng-if="hasPermission('event|delete')" ng-click="remove(event._id)" type="button" class="btn btn-link fa fa-trash fa-lg" title="{{'Delete' | translate}}"></button>
|
||||||
|
|
|
@ -101,6 +101,7 @@
|
||||||
<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}">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{{error.message}}
|
{{error.message}}
|
||||||
|
<button class="close" ng-click="clearError()">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- all the magic takes place here: the content inside the next div changes accordingly to the location you're visiting -->
|
<!-- all the magic takes place here: the content inside the next div changes accordingly to the location you're visiting -->
|
||||||
|
|
5
angular_app/js/app.js
vendored
5
angular_app/js/app.js
vendored
|
@ -116,6 +116,11 @@ eventManApp.config(['$stateProvider', '$urlRouterProvider',
|
||||||
templateUrl: 'event-edit.html',
|
templateUrl: 'event-edit.html',
|
||||||
controller: 'EventDetailsCtrl'
|
controller: 'EventDetailsCtrl'
|
||||||
})
|
})
|
||||||
|
.state('event.view', {
|
||||||
|
url: '/:id/view',
|
||||||
|
templateUrl: 'event-edit.html',
|
||||||
|
controller: 'EventDetailsCtrl'
|
||||||
|
})
|
||||||
.state('event.edit', {
|
.state('event.edit', {
|
||||||
url: '/:id/edit',
|
url: '/:id/edit',
|
||||||
templateUrl: 'event-edit.html',
|
templateUrl: 'event-edit.html',
|
||||||
|
|
50
angular_app/js/controllers.js
vendored
50
angular_app/js/controllers.js
vendored
|
@ -11,6 +11,10 @@ eventManControllers.controller('NavigationCtrl', ['$scope', '$rootScope', '$loca
|
||||||
function ($scope, $rootScope, $location, Setting, Info) {
|
function ($scope, $rootScope, $location, Setting, Info) {
|
||||||
$scope.logo = {};
|
$scope.logo = {};
|
||||||
|
|
||||||
|
$scope.getLocation = function() {
|
||||||
|
return $location.absUrl();
|
||||||
|
};
|
||||||
|
|
||||||
$scope.go = function(url) {
|
$scope.go = function(url) {
|
||||||
$location.url(url);
|
$location.url(url);
|
||||||
};
|
};
|
||||||
|
@ -103,16 +107,11 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
|
||||||
$scope.event = {};
|
$scope.event = {};
|
||||||
$scope.event.persons = [];
|
$scope.event.persons = [];
|
||||||
$scope.event.formSchema = {};
|
$scope.event.formSchema = {};
|
||||||
|
$scope.eventFormDisabled = false;
|
||||||
$scope.customFields = Setting.query({setting: 'person_custom_field', in_event_details: true});
|
$scope.customFields = Setting.query({setting: 'person_custom_field', in_event_details: true});
|
||||||
|
|
||||||
$scope.newTicket = $state.is('event.ticket.new');
|
|
||||||
|
|
||||||
|
|
||||||
if ($stateParams.id) {
|
if ($stateParams.id) {
|
||||||
$scope.event = Event.get($stateParams, function() {
|
$scope.event = Event.get($stateParams, function() {
|
||||||
if ($scope.newTicket) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$scope.$watchCollection(function() {
|
$scope.$watchCollection(function() {
|
||||||
return $scope.event.persons;
|
return $scope.event.persons;
|
||||||
}, function(prev, old) {
|
}, function(prev, old) {
|
||||||
|
@ -121,6 +120,10 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ($state.is('event.view') || !$rootScope.hasPermission('event|update')) {
|
||||||
|
$scope.eventFormDisabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
if ($state.is('event.tickets')) {
|
if ($state.is('event.tickets')) {
|
||||||
$scope.allPersons = Person.all();
|
$scope.allPersons = Person.all();
|
||||||
|
|
||||||
|
@ -203,7 +206,7 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
|
||||||
}
|
}
|
||||||
var attendees = 0;
|
var attendees = 0;
|
||||||
angular.forEach($scope.event.persons, function(value, key) {
|
angular.forEach($scope.event.persons, function(value, key) {
|
||||||
if (value.attended) {
|
if (value.attended && !value.cancelled) {
|
||||||
attendees += 1;
|
attendees += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -229,9 +232,7 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
|
||||||
person.person_id = person._id;
|
person.person_id = person._id;
|
||||||
person._id = $stateParams.id; // that's the id of the event, not the person.
|
person._id = $stateParams.id; // that's the id of the event, not the person.
|
||||||
Event.addPerson(person, function() {
|
Event.addPerson(person, function() {
|
||||||
if (!$scope.newTicket) {
|
|
||||||
$scope._localAddAttendee(person);
|
$scope._localAddAttendee(person);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
$scope.query = '';
|
$scope.query = '';
|
||||||
return person;
|
return person;
|
||||||
|
@ -255,17 +256,13 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
|
||||||
var personObj = new Person(person);
|
var personObj = new Person(person);
|
||||||
personObj.$save(function(p) {
|
personObj.$save(function(p) {
|
||||||
person = $scope._addAttendee(angular.copy(p));
|
person = $scope._addAttendee(angular.copy(p));
|
||||||
if (!$scope.newTicket) {
|
|
||||||
$scope._setAttended(person);
|
$scope._setAttended(person);
|
||||||
}
|
|
||||||
$scope.newPerson = {};
|
$scope.newPerson = {};
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
person = $scope._addAttendee(angular.copy(person));
|
person = $scope._addAttendee(angular.copy(person));
|
||||||
if (!$scope.newTicket) {
|
|
||||||
$scope._setAttended(person);
|
$scope._setAttended(person);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.addRegisteredPerson = function(person) {
|
$scope.addRegisteredPerson = function(person) {
|
||||||
|
@ -366,19 +363,18 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event', 'EventTicket', 'Person', 'EventUpdates', '$stateParams', 'Setting', '$log', '$translate', '$rootScope',
|
eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event', 'EventTicket', 'Person', 'Setting', '$log', '$translate', '$rootScope',
|
||||||
function ($scope, $state, Event, EventTicket, Person, EventUpdates, $stateParams, Setting, $log, $translate, $rootScope) {
|
function ($scope, $state, Event, EventTicket, Person, Setting, $log, $translate, $rootScope) {
|
||||||
$scope.message = {};
|
$scope.message = {};
|
||||||
$scope.event = {};
|
$scope.event = {};
|
||||||
$scope.ticket = {};
|
$scope.ticket = {};
|
||||||
$scope.formSchema = {};
|
$scope.formSchema = {};
|
||||||
$scope.formData = {};
|
$scope.formData = {};
|
||||||
|
$scope.dangerousActionsEnabled = false;
|
||||||
|
|
||||||
$scope.formFieldsMap = {};
|
$scope.formFieldsMap = {};
|
||||||
$scope.formFieldsMapRev = {};
|
$scope.formFieldsMapRev = {};
|
||||||
|
|
||||||
$scope.newTicket = $state.is('event.ticket.new');
|
|
||||||
|
|
||||||
if ($state.params.id) {
|
if ($state.params.id) {
|
||||||
$scope.event = Event.get({id: $state.params.id}, function(data) {
|
$scope.event = Event.get({id: $state.params.id}, function(data) {
|
||||||
if (!(data && data.formSchema)) {
|
if (!(data && data.formSchema)) {
|
||||||
|
@ -429,16 +425,20 @@ eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event',
|
||||||
person._id = $state.params.id; // that's the id of the event, not the person.
|
person._id = $state.params.id; // that's the id of the event, not the person.
|
||||||
EventTicket.add(person, function(ticket) {
|
EventTicket.add(person, function(ticket) {
|
||||||
$log.debug(ticket);
|
$log.debug(ticket);
|
||||||
$state.go('event.ticket.edit', {ticket_id: ticket._id});
|
$state.go('event.ticket.edit', {id: $scope.event._id, ticket_id: ticket._id});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.updateTicket = function(ticket) {
|
$scope.updateTicket = function(ticket, cb) {
|
||||||
var data = angular.copy(ticket);
|
var data = angular.copy(ticket);
|
||||||
data.ticket_id = data._id;
|
data.ticket_id = data._id;
|
||||||
data._id = $state.params.id;
|
data._id = $state.params.id;
|
||||||
EventTicket.update(data, function(t) {});
|
EventTicket.update(data, function(t) {
|
||||||
|
if (cb) {
|
||||||
|
cb(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.submitForm = function(dataModelSubmitted) {
|
$scope.submitForm = function(dataModelSubmitted) {
|
||||||
|
@ -453,6 +453,16 @@ eventManControllers.controller('EventTicketsCtrl', ['$scope', '$state', 'Event',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.toggleTicket = function() {
|
||||||
|
if (!$scope.ticket._id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$scope.ticket.cancelled = !$scope.ticket.cancelled;
|
||||||
|
$scope.updateTicket($scope.ticket, function() {
|
||||||
|
$scope.dangerousActionsEnabled = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.cancelForm = function() {
|
$scope.cancelForm = function() {
|
||||||
$state.go('events');
|
$state.go('events');
|
||||||
};
|
};
|
||||||
|
|
32
angular_app/js/filters.js
vendored
32
angular_app/js/filters.js
vendored
|
@ -37,6 +37,9 @@ eventManApp.filter('personRegistered', ['$filter',
|
||||||
return inputArray;
|
return inputArray;
|
||||||
}
|
}
|
||||||
for (var x=0; x < data.event.persons.length; x++) {
|
for (var x=0; x < data.event.persons.length; x++) {
|
||||||
|
if (!data.includeCancelled && data.event.persons[x].cancelled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
registeredIDs.push(data.event.persons[x].person_id);
|
registeredIDs.push(data.event.persons[x].person_id);
|
||||||
}
|
}
|
||||||
for (var x=0; x < inputArray.length; x++) {
|
for (var x=0; x < inputArray.length; x++) {
|
||||||
|
@ -65,13 +68,34 @@ eventManApp.filter('splittedFilter', ['$filter',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
/* Filter that returns only the attendees at an event. */
|
/* Filter that returns only the (not) registered persons at an event. */
|
||||||
eventManApp.filter('attendeesFilter', ['$filter',
|
eventManApp.filter('registeredFilter', ['$filter',
|
||||||
function($filter) {
|
function($filter) {
|
||||||
return function(inputArray) {
|
return function(inputArray, data) {
|
||||||
|
if (!data) {
|
||||||
|
data = {};
|
||||||
|
}
|
||||||
var returnArray = [];
|
var returnArray = [];
|
||||||
for (var x=0; x < inputArray.length; x++) {
|
for (var x=0; x < inputArray.length; x++) {
|
||||||
if (inputArray[x]['attended']) {
|
if ((!data.onlyCancelled && !inputArray[x]['cancelled']) ||
|
||||||
|
(data.onlyCancelled && inputArray[x]['cancelled']) ||
|
||||||
|
data.all) {
|
||||||
|
returnArray.push(inputArray[x]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return returnArray;
|
||||||
|
};
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/* Filter that returns only the attendees at an event. */
|
||||||
|
eventManApp.filter('attendeesFilter', ['$filter',
|
||||||
|
function($filter) {
|
||||||
|
return function(inputArray) {
|
||||||
|
var returnArray = [];
|
||||||
|
for (var x=0; x < inputArray.length; x++) {
|
||||||
|
if (inputArray[x]['attended'] && !inputArray[x]['cancelled']) {
|
||||||
returnArray.push(inputArray[x]);
|
returnArray.push(inputArray[x]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,17 +4,23 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-7 col-xs-7 vcenter">
|
<div class="col-md-7 col-xs-7 vcenter">
|
||||||
<h1>{{event.title}} - {{'new ticket' | translate}}</h1>
|
<h1><a ui-sref="event.view({id: event._id})" ng-if="event._id">{{event.title}}</a><span ng-if="!ticket._id"> - {{'join this event' | translate}}</span><span ng-if="ticket._id"> - {{'your ticket' | translate}}</span></h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="panel panel-info table-striped top5">
|
<div class="panel panel-info table-striped top5">
|
||||||
<div class="panel-heading">{{'Register to this event' | translate}}</div>
|
<div class="panel-heading">{{'Join this event' | translate}}</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
<div ng-if="ticket.cancelled" class="clearfix alert alert-danger">
|
||||||
|
{{'Your ticket has been cancelled; you can join again this event using the commands below' | translate}}
|
||||||
|
</div>
|
||||||
<eda-easy-form-viewer
|
<eda-easy-form-viewer
|
||||||
eda-easy-form-viewer-data-model="formData"
|
eda-easy-form-viewer-data-model="formData"
|
||||||
eda-easy-form-viewer-easy-form-generator-fields-model="formSchema"
|
eda-easy-form-viewer-easy-form-generator-fields-model="formSchema"
|
||||||
|
@ -22,6 +28,34 @@
|
||||||
eda-easy-form-viewer-submit-form-event="submitForm(dataModelSubmitted)"
|
eda-easy-form-viewer-submit-form-event="submitForm(dataModelSubmitted)"
|
||||||
eda-easy-form-viewer-cancel-form-event="cancelForm()">
|
eda-easy-form-viewer-cancel-form-event="cancelForm()">
|
||||||
</eda-easy-form-viewer>
|
</eda-easy-form-viewer>
|
||||||
|
<div ng-if="ticket._id">
|
||||||
|
<div ng-controller="NavigationCtrl">
|
||||||
|
<span><strong>{{'Save this URL if you want to modify your order later:'}} <a ng-href="{{getLocation()}}">{{getLocation()}}</a></strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container" ng-if="ticket._id">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="panel panel-danger table-striped top5">
|
||||||
|
<div class="panel-heading">{{'Dangerous stuff' | translate}}</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<button ng-click="dangerousActionsEnabled = !dangerousActionsEnabled" class="btn btn-warning">
|
||||||
|
<span class="fa fa-exclamation-triangle vcenter"></span>
|
||||||
|
{{'Toggle dangerous actions' | translate}}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button ng-disabled="!dangerousActionsEnabled" ng-click="toggleTicket({id: ticket._id})" class="btn btn-danger">
|
||||||
|
<span ng-class="{fa: true, 'fa-sign-out': !ticket.cancelled, 'fa-sign-in': ticket.cancelled, vcenter: true}"></span>
|
||||||
|
<span ng-if="!ticket.cancelled">{{'Leave this event' | translate}}</span>
|
||||||
|
<span ng-if="ticket.cancelled">{{'Join again this event' | translate}}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +1,21 @@
|
||||||
|
Development
|
||||||
|
===========
|
||||||
|
|
||||||
|
As of June 2016, Event Man(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.
|
||||||
|
|
||||||
|
|
||||||
Definitions
|
Definitions
|
||||||
===========
|
===========
|
||||||
|
|
||||||
- **event**: a faire, convention, congress or any other kind of meeting
|
- **event**: a faire, convention, congress or any other kind of meeting
|
||||||
- **registered person**: someone who said it will attend at the event
|
- **person**: everyone hates them
|
||||||
|
- **registered person**: someone who said will attend at the event
|
||||||
- **attendee**: a person who actually *show up* (is checked in) at the event
|
- **attendee**: a person who actually *show up* (is checked in) at the event
|
||||||
|
- **ticket**: an entry in the list of persons registered at an event
|
||||||
|
- **user**: a logged in user of th Event Man web interface (not the same as "person")
|
||||||
|
- **trigger**: an action that will run the execution of some scripts
|
||||||
|
|
||||||
|
|
||||||
Paths
|
Paths
|
||||||
|
@ -16,14 +28,17 @@ These are the paths you see in the browser (AngularJS does client-side routing:
|
||||||
|
|
||||||
- /#/events - the list of events
|
- /#/events - the list of events
|
||||||
- /#/event/new - edit form to create a new event
|
- /#/event/new - edit form to create a new event
|
||||||
- /#/event/:event_id - show information about an existing event (contains the list of registered persons)
|
- /#/event/:event\_id/edit - edit form to modify an existing event
|
||||||
- /#/event/:event_id/edit - edit form to modify an existing event
|
- /#/event/:event\_id/view - show read-only information about an existing event
|
||||||
|
- /#/event/:event\_id/tickets - show the list of persons registered at the event
|
||||||
|
- /#/event/:event\_id/ticket/new - add a new ticket to an event
|
||||||
|
- /#/event/:event\_id/ticket/:ticket\_id/edit - edit an existing ticket
|
||||||
- /#/persons - the list of persons
|
- /#/persons - the list of persons
|
||||||
- /#/person/new - edit form to create a new person
|
- /#/person/new - edit form to create a new person
|
||||||
- /#/person/:person_id - show information about an existing person (contains the list of events the person registered for)
|
- /#/person/:person\_id - show information about an existing person (contains the list of events the person registered for)
|
||||||
- /#/person/:person_id/edit - edit form to modify an existing person
|
- /#/person/:person\_id/edit - edit form to modify an existing person
|
||||||
- /#/import/persons - form used to import persons in bulk
|
- /#/import/persons - form used to import persons in bulk
|
||||||
- /login - login form
|
- /#/login - login form
|
||||||
- /logout - when visited, the user is logged out
|
- /logout - when visited, the user is logged out
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,51 +49,78 @@ The paths used to communicate with the Tornado web server:
|
||||||
|
|
||||||
- /events GET - return the list of events
|
- /events GET - return the list of events
|
||||||
- /events POST - store a new event
|
- /events POST - store a new event
|
||||||
- /events/:event_id GET - return information about an existing event
|
- /events/:event\_id GET - return information about an existing event
|
||||||
- /events/:event_id PUT - update an existing event
|
- /events/:event\_id PUT - update an existing event
|
||||||
- /events/:event_id DELETE - delete an existing event
|
- /events/:event\_id DELETE - delete an existing event
|
||||||
|
- /events/:event\_id/persons GET - return the complete list of persons registered for the event
|
||||||
|
- /events/:event\_id/persons POST - insert a person in the list of registered persons of an event
|
||||||
|
- /events/:event\_id/persons/:person\_id GET - return information about a person related to a given event (e.g.: name, surname, ticket ID, ...)
|
||||||
|
- /events/:event\_id/persons/:person\_id PUT - update the information about a person related to a given event (e.g.: if the person attended)
|
||||||
|
- /events/:event\_id/persons/:person\_id DELETE - remove the entry from the list of registered persons
|
||||||
|
- /events/:event\_id/tickets GET - return the complete list of tickets registered for the event
|
||||||
|
- /events/:event\_id/tickets POST - insert a person in the list of registered tickets of an event
|
||||||
|
- /events/:event\_id/tickets/:ticket\_id GET - return information about a person related to a given event (e.g.: name, surname, ticket ID, ...)
|
||||||
|
- /events/:event\_id/tickets/:ticket\_id PUT - update the information about a person related to a given event (e.g.: if the person attended)
|
||||||
- /persons GET - return the list of persons
|
- /persons GET - return the list of persons
|
||||||
- /persons POST - store a new person
|
- /persons POST - store a new person
|
||||||
- /persons/:person_id GET - return information about an existing person
|
- /persons/:person\_id GET - return information about an existing person
|
||||||
- /persons/:person_id PUT - update an existing person
|
- /persons/:person\_id PUT - update an existing person
|
||||||
- /persons/:person_id DELETE - delete an existing person
|
- /persons/:person\_id DELETE - delete an existing person
|
||||||
- /events/:event_id/persons GET - return the complete list of persons registered for the event
|
- /persons/:person\_id/events GET - the list of events the person registered for
|
||||||
- /events/:event_id/persons/:person_id GET - return information about a person related to a given event (e.g.: name, surname, ticket ID, ...)
|
|
||||||
- /events/:event_id/persons/:person_id PUT - update the information about a person related to a given event (e.g.: if the person attended)
|
|
||||||
- /persons/:person_id/events GET - the list of events the person registered for
|
|
||||||
- /ebcsvpersons POST - csv file upload to import persons
|
- /ebcsvpersons POST - csv file upload to import persons
|
||||||
|
- /users GET - list of users
|
||||||
|
- /users/:user\_id PUT - update an existing user
|
||||||
|
- /settings - settings to customize the GUI (logo, extra columns for events and persons lists)
|
||||||
|
- /info - information about the current user
|
||||||
- /login - login form
|
- /login - login form
|
||||||
- /logout - when visited, the user is logged out
|
- /logout - when visited, the user is logged out
|
||||||
|
|
||||||
Notice that the above paths are the ones used by the webapp. If you plan to use them from an external application (like the _event\_man_ barcode/qrcode scanner) you better prepend all the path with /v1.0, where 1.0 is the current value of API\_VERSION.
|
Notice that the above paths are the ones used by the webapp. If you plan to use them from an external application (like the _event\_man_ barcode/qrcode scanner) you better prepend all the path with /v1.0, where 1.0 is the current value of API\_VERSION.
|
||||||
The main advantage of doing so is that, for every call, a useful status code and a JSON value is returned (also for /v1.0/login that usually would show you the login page).
|
The main advantage of doing so is that, for every call, a useful status code and a JSON value is returned.
|
||||||
|
|
||||||
Also, remember that most of the paths can take query parameters that will be used as a filter, like GET /events/:event_id/persons?name=Mario
|
Also, remember that most of the paths can take query parameters that will be used as a filter, like GET /events/:event\_id/persons?name=Mario
|
||||||
|
|
||||||
|
You have probably noticed that the /events/:event\_id/persons/\* and /events/:event\_id/tickets/\* paths seems to do the same thing. That's mostly true, and if we're talking about the data structure they are indeed the same (i.e.: a GET to /events/:event\_id/tickets/:ticket\_id will return the same {"person": {"name": "Mario", [...]}} structure as a call to /events/:event\_id/persons/:person\_id). The main difference is that the first works on the \_id property of the entry, the other on person\_id. Plus, the input and output are filtered in a different way, for example to prevent a registered person to autonomously set the attendee status or getting the complete list of registered persons.
|
||||||
|
|
||||||
|
Beware that most probably the /persons and /events/:event\_id/persons paths will be removed from a future version of Event Man(mager) in an attempt to rationalize how we handle data.
|
||||||
|
|
||||||
|
|
||||||
|
Permissions
|
||||||
|
===========
|
||||||
|
|
||||||
|
Being too lazy to implement a proper MAC or RBAC, we settled to a simpler mapping on CRUD operations on paths. This will probably change in the future.
|
||||||
|
|
||||||
|
User's permission are stored in the *permission* key, and merged with a set of defaults, valid also for unregistered users. Operations are *read*, *create*, *update* and *delete* (plus the spcial *all* value). There's also the special *admin|all* value: if present, the user has every privilege.
|
||||||
|
|
||||||
|
Permissions are strings: the path and the permission are separated by **|**; the path components (resource:sub-resource, if any) are separated by **:**. In case we are not accessing a specific sub-resource (i.e.: we don't have a sub-resource ID), the **-all** string is appended to the resource name. For example:
|
||||||
|
- **events|read**: ability to retrieve the list of events and their data (some fields, like the list of registered persons, are filtered out if you don't have other permissions)
|
||||||
|
- **event:tickets|all**: ability to do everything to a ticket (provided that you know its ID)
|
||||||
|
- **event:tickets-all|create**: ability to create a new ticket (you don't have an ID, if you're creating a new ticket, hence the -all suffix)
|
||||||
|
|
||||||
|
|
||||||
Triggers
|
Triggers
|
||||||
========
|
========
|
||||||
|
|
||||||
Sometimes we have to execute some script in reaction to an event.
|
Sometimes we have to execute one or more scripts in reaction to an action.
|
||||||
|
|
||||||
In the **data/triggers** we have a series of directories; scripts inside of them will be executed when the related action was performed on the GUI or calling the controller.
|
In the **data/triggers** we have a series of directories; scripts inside of them will be executed when the related action was performed on the GUI or calling the controller.
|
||||||
|
|
||||||
Available triggers:
|
Available triggers:
|
||||||
- **update_person_in_event**: executed every time a person data in a given event is updated.
|
- **update\_person\_in\_event**: executed every time a person data in a given event is updated.
|
||||||
- **attends**: executed only when a person is marked as attending an event.
|
- **attends**: executed only when a person is marked as attending an event.
|
||||||
|
|
||||||
update_person_in_event and attends will receive these information:
|
update\_person\_in\_event and attends will receive these information:
|
||||||
- via *environment*:
|
- via *environment*:
|
||||||
- NAME
|
- NAME
|
||||||
- SURNAME
|
- SURNAME
|
||||||
- EMAIL
|
- EMAIL
|
||||||
- COMPANY
|
- COMPANY
|
||||||
- JOB
|
- JOB
|
||||||
- PERSON_ID
|
- PERSON\_ID
|
||||||
- EVENT_ID
|
- EVENT\_ID
|
||||||
- EVENT_TITLE
|
- EVENT\_TITLE
|
||||||
- SEQ
|
- SEQ
|
||||||
- SEQ_HEX
|
- SEQ\_HEX
|
||||||
- via stdin, a dictionary containing:
|
- via stdin, a dictionary containing:
|
||||||
- dictionary **old** with the old data of the person
|
- dictionary **old** with the old data of the person
|
||||||
- dictionary **new** with the new data of the person
|
- dictionary **new** with the new data of the person
|
||||||
|
@ -87,11 +129,11 @@ update_person_in_event and attends will receive these information:
|
||||||
|
|
||||||
In the **data/triggers-available** there is an example of script: **echo.py**.
|
In the **data/triggers-available** there is an example of script: **echo.py**.
|
||||||
|
|
||||||
|
|
||||||
Database layout
|
Database layout
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Information are stored in MongoDB. Whenever possible, object are converted
|
Information are stored in MongoDB. Whenever possible, object are converted into integer, native ObjectId and datetime.
|
||||||
into integer, native ObjectId and datetime.
|
|
||||||
|
|
||||||
events collection
|
events collection
|
||||||
-----------------
|
-----------------
|
||||||
|
@ -107,8 +149,9 @@ Main field:
|
||||||
- begin-time
|
- begin-time
|
||||||
- end-date
|
- end-date
|
||||||
- end-time
|
- end-time
|
||||||
- persons - a list of information about registered persons
|
- persons - a list of information about registered persons (each entry is a ticket)
|
||||||
- persons.$.person_id
|
- persons.$.\_id
|
||||||
|
- persons.$.person\_id
|
||||||
- persons.$.attended
|
- persons.$.attended
|
||||||
- persons.$.name
|
- persons.$.name
|
||||||
- persons.$.surname
|
- persons.$.surname
|
||||||
|
@ -117,7 +160,7 @@ Main field:
|
||||||
- persons.$.job
|
- persons.$.job
|
||||||
- persons.$.ebqrcode
|
- persons.$.ebqrcode
|
||||||
- persons.$.seq
|
- persons.$.seq
|
||||||
- persons.$.seq_hex
|
- persons.$.seq\_hex
|
||||||
|
|
||||||
|
|
||||||
persons collection
|
persons collection
|
||||||
|
@ -138,7 +181,7 @@ Contains a list of username and associated values, like the password used for au
|
||||||
|
|
||||||
To generate the hash, use:
|
To generate the hash, use:
|
||||||
import utils
|
import utils
|
||||||
print utils.hash_password('MyVerySecretPassword')
|
print utils.hash\_password('MyVerySecretPassword')
|
||||||
|
|
||||||
|
|
||||||
Coding style and conventions
|
Coding style and conventions
|
||||||
|
@ -150,23 +193,3 @@ I suggest four spaces instead of tabs for all the code: Python (**mandatory**),
|
||||||
|
|
||||||
Python code documented following the [Sphinx](http://sphinx-doc.org/) syntax.
|
Python code documented following the [Sphinx](http://sphinx-doc.org/) syntax.
|
||||||
|
|
||||||
|
|
||||||
TODO
|
|
||||||
====
|
|
||||||
|
|
||||||
Next to be done
|
|
||||||
---------------
|
|
||||||
|
|
||||||
- handle datetimes (on GUI with a calendar and on the backend deserializing ISO 8601 strings)
|
|
||||||
- modal on event/person removal
|
|
||||||
|
|
||||||
Nice to have
|
|
||||||
------------
|
|
||||||
|
|
||||||
- a test suite
|
|
||||||
- notifications for form editing and other actions
|
|
||||||
- authentication for administrators
|
|
||||||
- i18n
|
|
||||||
- settings page
|
|
||||||
- logging and debugging code
|
|
||||||
|
|
||||||
|
|
|
@ -92,6 +92,7 @@ class BaseHandler(tornado.web.RequestHandler):
|
||||||
'users|create': True
|
'users|create': True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Cache currently connected users.
|
||||||
_users_cache = {}
|
_users_cache = {}
|
||||||
|
|
||||||
# A property to access the first value of each argument.
|
# A property to access the first value of each argument.
|
||||||
|
@ -513,9 +514,12 @@ class CollectionHandler(BaseHandler):
|
||||||
:param message: message to send
|
:param message: message to send
|
||||||
:type message: str
|
:type message: str
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
ws = yield tornado.websocket.websocket_connect(self.build_ws_url(path))
|
ws = yield tornado.websocket.websocket_connect(self.build_ws_url(path))
|
||||||
ws.write_message(message)
|
ws.write_message(message)
|
||||||
ws.close()
|
ws.close()
|
||||||
|
except Exception, e:
|
||||||
|
self.logger.error('Error yielding WebSocket message: %s', e)
|
||||||
|
|
||||||
|
|
||||||
class PersonsHandler(CollectionHandler):
|
class PersonsHandler(CollectionHandler):
|
||||||
|
@ -615,12 +619,14 @@ class EventsHandler(CollectionHandler):
|
||||||
self._clean_dict(data)
|
self._clean_dict(data)
|
||||||
data['seq'] = self.get_next_seq('event_%s_persons' % id_)
|
data['seq'] = self.get_next_seq('event_%s_persons' % id_)
|
||||||
data['seq_hex'] = '%06X' % data['seq']
|
data['seq_hex'] = '%06X' % data['seq']
|
||||||
doc = self.db.query('events',
|
if person_id is None:
|
||||||
{'_id': id_, 'persons.person_id': person_id})
|
doc = {}
|
||||||
|
else:
|
||||||
|
doc = self.db.query('events', {'_id': id_, 'persons.person_id': person_id})
|
||||||
ret = {'action': 'add', 'person_id': person_id, 'person': data, 'uuid': uuid}
|
ret = {'action': 'add', 'person_id': person_id, 'person': data, 'uuid': uuid}
|
||||||
if '_id' in data:
|
if '_id' in data:
|
||||||
del data['_id']
|
del data['_id']
|
||||||
self.send_ws_message('event/%s/updates' % id_, json.dumps(ret))
|
self.send_ws_message('event/%s/tickets/updates' % id_, json.dumps(ret))
|
||||||
if not doc:
|
if not doc:
|
||||||
data['_id'] = self.gen_id()
|
data['_id'] = self.gen_id()
|
||||||
merged, doc = self.db.update('events',
|
merged, doc = self.db.update('events',
|
||||||
|
@ -660,7 +666,7 @@ class EventsHandler(CollectionHandler):
|
||||||
doc.get('persons') or [])
|
doc.get('persons') or [])
|
||||||
env = self._dict2env(new_person_data)
|
env = self._dict2env(new_person_data)
|
||||||
# always takes the person_id from the new person (it may have
|
# always takes the person_id from the new person (it may have
|
||||||
# be a ticket_id).
|
# been a ticket_id).
|
||||||
person_id = str(new_person_data.get('person_id'))
|
person_id = str(new_person_data.get('person_id'))
|
||||||
env.update({'PERSON_ID': person_id, 'EVENT_ID': id_,
|
env.update({'PERSON_ID': person_id, 'EVENT_ID': id_,
|
||||||
'EVENT_TITLE': doc.get('title', ''), 'WEB_USER': self.current_user,
|
'EVENT_TITLE': doc.get('title', ''), 'WEB_USER': self.current_user,
|
||||||
|
@ -695,7 +701,7 @@ class EventsHandler(CollectionHandler):
|
||||||
{'persons': {'person_id': person_id}},
|
{'persons': {'person_id': person_id}},
|
||||||
operation='delete',
|
operation='delete',
|
||||||
create=False)
|
create=False)
|
||||||
self.send_ws_message('event/%s/updates' % id_, json.dumps(ret))
|
self.send_ws_message('event/%s/tickets/updates' % id_, json.dumps(ret))
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
handle_delete_tickets = handle_delete_persons
|
handle_delete_tickets = handle_delete_persons
|
||||||
|
@ -826,7 +832,6 @@ class WebSocketEventUpdatesHandler(tornado.websocket.WebSocketHandler):
|
||||||
|
|
||||||
def open(self, event_id, *args, **kwds):
|
def open(self, event_id, *args, **kwds):
|
||||||
logging.debug('WebSocketEventUpdatesHandler.on_open event_id:%s' % event_id)
|
logging.debug('WebSocketEventUpdatesHandler.on_open event_id:%s' % event_id)
|
||||||
|
|
||||||
_ws_clients.setdefault(self._clean_url(self.request.uri), set()).add(self)
|
_ws_clients.setdefault(self._clean_url(self.request.uri), set()).add(self)
|
||||||
logging.debug('WebSocketEventUpdatesHandler.on_open %s clients connected' % len(_ws_clients))
|
logging.debug('WebSocketEventUpdatesHandler.on_open %s clients connected' % len(_ws_clients))
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,18 @@ body { padding-top: 70px; }
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a:focus a:hover {
|
||||||
|
color: #23527c;
|
||||||
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #23527c;
|
color: #23527c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type=text].form-control, input[type=search].form-control {
|
||||||
|
border: 1px solid rgb(204, 204, 204);
|
||||||
|
}
|
||||||
|
|
||||||
/* fix styling for empty href */
|
/* fix styling for empty href */
|
||||||
.nav, .pagination, .carousel, .panel-title a { cursor: pointer; }
|
.nav, .pagination, .carousel, .panel-title a { cursor: pointer; }
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue