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.
|
||||
|
||||
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:
|
||||
- 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
|
||||
- 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
|
||||
- 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)
|
||||
- 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.
|
||||
|
||||
|
@ -25,6 +31,7 @@ Technological stack
|
|||
===================
|
||||
|
||||
- [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
|
||||
- [Font Awesome](https://fortawesome.github.io/Font-Awesome/) for even more cuteness
|
||||
- [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
|
||||
==============
|
||||
|
||||
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
|
||||
=============
|
||||
|
|
|
@ -3,80 +3,86 @@
|
|||
<div class="panel panel-primary table-striped top5">
|
||||
<div class="panel-heading">
|
||||
<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>
|
||||
{{'Tickets' | translate}}
|
||||
</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>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form name="eventForm" ng-model="eventdetails" ng-submit="save()">
|
||||
<div 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">
|
||||
<span class="fa fa-floppy-o vcenter"></span>
|
||||
{{'save' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<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">
|
||||
<span class="fa fa-floppy-o vcenter"></span>
|
||||
{{'save' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-lg">
|
||||
<span class="input-group-addon min100">{{'Title' | translate}}</span>
|
||||
<input type="text" class="form-control" placeholder="{{'Title' | translate}}" ng-model="event.title" ng-required="1">
|
||||
</div>
|
||||
<div class="input-group input-group-lg top5">
|
||||
<span class="input-group-addon min100">{{'Tagline' | translate}}</span>
|
||||
<input type="text" class="form-control" placeholder="{{'Tagline' | translate}}" ng-model="event.tagline">
|
||||
</div>
|
||||
<div class="input-group input-group-lg top5">
|
||||
<span class="input-group-addon min100">{{'Short summary' | translate}}</span>
|
||||
<input type="text" class="form-control" placeholder="{{'Short summary' | translate}}" ng-model="event.summary">
|
||||
</div>
|
||||
<div class="input-group top5">
|
||||
<span class="input-group-addon min100">{{'Long description' | translate}}</span>
|
||||
<textarea class="form-control" placeholder="{{'Long description' | translate}}" ng-model="event.description" rows="5"></textarea>
|
||||
</div>
|
||||
<div class="input-group input-group-lg">
|
||||
<span class="input-group-addon min100">{{'Title' | translate}}</span>
|
||||
<input type="text" class="form-control" placeholder="{{'Title' | translate}}" ng-model="event.title" ng-required="1">
|
||||
</div>
|
||||
<div class="input-group input-group-lg top5">
|
||||
<span class="input-group-addon min100">{{'Tagline' | translate}}</span>
|
||||
<input type="text" class="form-control" placeholder="{{'Tagline' | translate}}" ng-model="event.tagline">
|
||||
</div>
|
||||
<div class="input-group input-group-lg top5">
|
||||
<span class="input-group-addon min100">{{'Short summary' | translate}}</span>
|
||||
<input type="text" class="form-control" placeholder="{{'Short summary' | translate}}" ng-model="event.summary">
|
||||
</div>
|
||||
<div class="input-group top5">
|
||||
<span class="input-group-addon min100">{{'Long description' | translate}}</span>
|
||||
<textarea class="form-control" placeholder="{{'Long description' | translate}}" ng-model="event.description" rows="5"></textarea>
|
||||
</div>
|
||||
|
||||
<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" ng-required="true" />
|
||||
<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 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" ng-required="true" />
|
||||
<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>
|
||||
</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>
|
||||
</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" ng-required="true" />
|
||||
<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 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" ng-required="true" />
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-lg top5">
|
||||
<span class="input-group-addon min100">{{'Where' | translate}}</span>
|
||||
<input type="text" class="form-control" placeholder="{{'Where' | translate}}" ng-model="event.where">
|
||||
</div>
|
||||
<div class="input-group input-group-lg top5">
|
||||
<span class="input-group-addon min100">{{'Where' | translate}}</span>
|
||||
<input type="text" class="form-control" placeholder="{{'Where' | translate}}" ng-model="event.where">
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
|
||||
<label></label>
|
||||
<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">
|
||||
<span class="fa fa-floppy-o vcenter"></span>
|
||||
{{'save' | translate}}
|
||||
|
@ -86,7 +92,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-primary top10">
|
||||
<div class="panel panel-primary top10" ng-if="!eventFormDisabled">
|
||||
<div class="panel-heading">
|
||||
<h1>{{'Registration form' | translate}}</h1>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
--><div class="col-md-5 col-xs-5 vcenter">
|
||||
<div class="row">
|
||||
<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 class="col-md-6">
|
||||
<h2><div class="label label-info vcenter">{{'Attendees:' | translate}} {{countAttendees}}</div></h2>
|
||||
|
@ -49,10 +49,10 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
<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>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
|
|
|
@ -37,15 +37,20 @@
|
|||
<tbody>
|
||||
<tr ng-repeat="event in events | splittedFilter:query | orderBy:eventsOrderProp">
|
||||
<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/>
|
||||
{{'Ends:' | translate}} {{event['end-date'] | date:'fullDate' }} {{event['end-time'] | date:'HH:mm'}}</p>
|
||||
</td>
|
||||
<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>
|
||||
<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('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>
|
||||
|
|
|
@ -101,6 +101,7 @@
|
|||
<div ng-if="error.error" ng-class="{clearfix: true, alert: true, 'alert-danger': true}">
|
||||
<div class="container">
|
||||
{{error.message}}
|
||||
<button class="close" ng-click="clearError()">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- all the magic takes place here: the content inside the next div changes accordingly to the location you're visiting -->
|
||||
|
|
7
angular_app/js/app.js
vendored
7
angular_app/js/app.js
vendored
|
@ -25,7 +25,7 @@ var eventManApp = angular.module('eventManApp', [
|
|||
'angularFileUpload',
|
||||
'angular-websocket',
|
||||
'eda.easyFormViewer',
|
||||
'eda.easyformGen.stepway'
|
||||
'eda.easyformGen.stepway'
|
||||
]);
|
||||
|
||||
|
||||
|
@ -116,6 +116,11 @@ eventManApp.config(['$stateProvider', '$urlRouterProvider',
|
|||
templateUrl: 'event-edit.html',
|
||||
controller: 'EventDetailsCtrl'
|
||||
})
|
||||
.state('event.view', {
|
||||
url: '/:id/view',
|
||||
templateUrl: 'event-edit.html',
|
||||
controller: 'EventDetailsCtrl'
|
||||
})
|
||||
.state('event.edit', {
|
||||
url: '/:id/edit',
|
||||
templateUrl: 'event-edit.html',
|
||||
|
|
56
angular_app/js/controllers.js
vendored
56
angular_app/js/controllers.js
vendored
|
@ -11,6 +11,10 @@ eventManControllers.controller('NavigationCtrl', ['$scope', '$rootScope', '$loca
|
|||
function ($scope, $rootScope, $location, Setting, Info) {
|
||||
$scope.logo = {};
|
||||
|
||||
$scope.getLocation = function() {
|
||||
return $location.absUrl();
|
||||
};
|
||||
|
||||
$scope.go = function(url) {
|
||||
$location.url(url);
|
||||
};
|
||||
|
@ -103,16 +107,11 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
|
|||
$scope.event = {};
|
||||
$scope.event.persons = [];
|
||||
$scope.event.formSchema = {};
|
||||
$scope.eventFormDisabled = false;
|
||||
$scope.customFields = Setting.query({setting: 'person_custom_field', in_event_details: true});
|
||||
|
||||
$scope.newTicket = $state.is('event.ticket.new');
|
||||
|
||||
|
||||
if ($stateParams.id) {
|
||||
$scope.event = Event.get($stateParams, function() {
|
||||
if ($scope.newTicket) {
|
||||
return;
|
||||
}
|
||||
$scope.$watchCollection(function() {
|
||||
return $scope.event.persons;
|
||||
}, 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')) {
|
||||
$scope.allPersons = Person.all();
|
||||
|
||||
|
@ -203,7 +206,7 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
|
|||
}
|
||||
var attendees = 0;
|
||||
angular.forEach($scope.event.persons, function(value, key) {
|
||||
if (value.attended) {
|
||||
if (value.attended && !value.cancelled) {
|
||||
attendees += 1;
|
||||
}
|
||||
});
|
||||
|
@ -229,9 +232,7 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
|
|||
person.person_id = person._id;
|
||||
person._id = $stateParams.id; // that's the id of the event, not the person.
|
||||
Event.addPerson(person, function() {
|
||||
if (!$scope.newTicket) {
|
||||
$scope._localAddAttendee(person);
|
||||
}
|
||||
$scope._localAddAttendee(person);
|
||||
});
|
||||
$scope.query = '';
|
||||
return person;
|
||||
|
@ -255,16 +256,12 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
|
|||
var personObj = new Person(person);
|
||||
personObj.$save(function(p) {
|
||||
person = $scope._addAttendee(angular.copy(p));
|
||||
if (!$scope.newTicket) {
|
||||
$scope._setAttended(person);
|
||||
}
|
||||
$scope._setAttended(person);
|
||||
$scope.newPerson = {};
|
||||
});
|
||||
} else {
|
||||
person = $scope._addAttendee(angular.copy(person));
|
||||
if (!$scope.newTicket) {
|
||||
$scope._setAttended(person);
|
||||
}
|
||||
$scope._setAttended(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',
|
||||
function ($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, Setting, $log, $translate, $rootScope) {
|
||||
$scope.message = {};
|
||||
$scope.event = {};
|
||||
$scope.ticket = {};
|
||||
$scope.formSchema = {};
|
||||
$scope.formData = {};
|
||||
$scope.dangerousActionsEnabled = false;
|
||||
|
||||
$scope.formFieldsMap = {};
|
||||
$scope.formFieldsMapRev = {};
|
||||
|
||||
$scope.newTicket = $state.is('event.ticket.new');
|
||||
|
||||
if ($state.params.id) {
|
||||
$scope.event = Event.get({id: $state.params.id}, function(data) {
|
||||
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.
|
||||
EventTicket.add(person, function(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);
|
||||
data.ticket_id = data._id;
|
||||
data._id = $state.params.id;
|
||||
EventTicket.update(data, function(t) {});
|
||||
EventTicket.update(data, function(t) {
|
||||
if (cb) {
|
||||
cb(t);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$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() {
|
||||
$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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
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. */
|
||||
eventManApp.filter('attendeesFilter', ['$filter',
|
||||
/* Filter that returns only the (not) registered persons at an event. */
|
||||
eventManApp.filter('registeredFilter', ['$filter',
|
||||
function($filter) {
|
||||
return function(inputArray) {
|
||||
return function(inputArray, data) {
|
||||
if (!data) {
|
||||
data = {};
|
||||
}
|
||||
var returnArray = [];
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,23 @@
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<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>
|
||||
|
||||
<!-- 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="row">
|
||||
<div class="col-md-12">
|
||||
<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 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-data-model="formData"
|
||||
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-cancel-form-event="cancelForm()">
|
||||
</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>
|
||||
|
|
|
@ -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
|
||||
===========
|
||||
|
||||
- **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
|
||||
- **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
|
||||
|
@ -16,14 +28,17 @@ These are the paths you see in the browser (AngularJS does client-side routing:
|
|||
|
||||
- /#/events - the list of events
|
||||
- /#/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
|
||||
- /#/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/edit - edit form to modify an existing person
|
||||
- /#/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
|
||||
- /#/import/persons - form used to import persons in bulk
|
||||
- /login - login form
|
||||
- /#/login - login form
|
||||
- /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 POST - store a new event
|
||||
- /events/:event_id GET - return information about an existing event
|
||||
- /events/:event_id PUT - update an existing event
|
||||
- /events/:event_id DELETE - delete an existing event
|
||||
- /events/:event\_id GET - return information about an existing event
|
||||
- /events/:event\_id PUT - update 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 POST - store a new person
|
||||
- /persons/:person_id GET - return information about an existing person
|
||||
- /persons/:person_id PUT - update 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
|
||||
- /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
|
||||
- /persons/:person\_id GET - return information about an existing person
|
||||
- /persons/:person\_id PUT - update an existing person
|
||||
- /persons/:person\_id DELETE - delete an existing person
|
||||
- /persons/:person\_id/events GET - the list of events the person registered for
|
||||
- /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
|
||||
- /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.
|
||||
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
|
||||
========
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
update_person_in_event and attends will receive these information:
|
||||
update\_person\_in\_event and attends will receive these information:
|
||||
- via *environment*:
|
||||
- NAME
|
||||
- SURNAME
|
||||
- EMAIL
|
||||
- COMPANY
|
||||
- JOB
|
||||
- PERSON_ID
|
||||
- EVENT_ID
|
||||
- EVENT_TITLE
|
||||
- PERSON\_ID
|
||||
- EVENT\_ID
|
||||
- EVENT\_TITLE
|
||||
- SEQ
|
||||
- SEQ_HEX
|
||||
- SEQ\_HEX
|
||||
- via stdin, a dictionary containing:
|
||||
- dictionary **old** with the old 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**.
|
||||
|
||||
|
||||
Database layout
|
||||
===============
|
||||
|
||||
Information are stored in MongoDB. Whenever possible, object are converted
|
||||
into integer, native ObjectId and datetime.
|
||||
Information are stored in MongoDB. Whenever possible, object are converted into integer, native ObjectId and datetime.
|
||||
|
||||
events collection
|
||||
-----------------
|
||||
|
@ -107,8 +149,9 @@ Main field:
|
|||
- begin-time
|
||||
- end-date
|
||||
- end-time
|
||||
- persons - a list of information about registered persons
|
||||
- persons.$.person_id
|
||||
- persons - a list of information about registered persons (each entry is a ticket)
|
||||
- persons.$.\_id
|
||||
- persons.$.person\_id
|
||||
- persons.$.attended
|
||||
- persons.$.name
|
||||
- persons.$.surname
|
||||
|
@ -117,7 +160,7 @@ Main field:
|
|||
- persons.$.job
|
||||
- persons.$.ebqrcode
|
||||
- persons.$.seq
|
||||
- persons.$.seq_hex
|
||||
- persons.$.seq\_hex
|
||||
|
||||
|
||||
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:
|
||||
import utils
|
||||
print utils.hash_password('MyVerySecretPassword')
|
||||
print utils.hash\_password('MyVerySecretPassword')
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# Cache currently connected users.
|
||||
_users_cache = {}
|
||||
|
||||
# A property to access the first value of each argument.
|
||||
|
@ -513,9 +514,12 @@ class CollectionHandler(BaseHandler):
|
|||
:param message: message to send
|
||||
:type message: str
|
||||
"""
|
||||
ws = yield tornado.websocket.websocket_connect(self.build_ws_url(path))
|
||||
ws.write_message(message)
|
||||
ws.close()
|
||||
try:
|
||||
ws = yield tornado.websocket.websocket_connect(self.build_ws_url(path))
|
||||
ws.write_message(message)
|
||||
ws.close()
|
||||
except Exception, e:
|
||||
self.logger.error('Error yielding WebSocket message: %s', e)
|
||||
|
||||
|
||||
class PersonsHandler(CollectionHandler):
|
||||
|
@ -615,12 +619,14 @@ class EventsHandler(CollectionHandler):
|
|||
self._clean_dict(data)
|
||||
data['seq'] = self.get_next_seq('event_%s_persons' % id_)
|
||||
data['seq_hex'] = '%06X' % data['seq']
|
||||
doc = self.db.query('events',
|
||||
{'_id': id_, 'persons.person_id': person_id})
|
||||
if person_id is None:
|
||||
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}
|
||||
if '_id' in data:
|
||||
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:
|
||||
data['_id'] = self.gen_id()
|
||||
merged, doc = self.db.update('events',
|
||||
|
@ -660,7 +666,7 @@ class EventsHandler(CollectionHandler):
|
|||
doc.get('persons') or [])
|
||||
env = self._dict2env(new_person_data)
|
||||
# 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'))
|
||||
env.update({'PERSON_ID': person_id, 'EVENT_ID': id_,
|
||||
'EVENT_TITLE': doc.get('title', ''), 'WEB_USER': self.current_user,
|
||||
|
@ -695,7 +701,7 @@ class EventsHandler(CollectionHandler):
|
|||
{'persons': {'person_id': person_id}},
|
||||
operation='delete',
|
||||
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
|
||||
|
||||
handle_delete_tickets = handle_delete_persons
|
||||
|
@ -826,7 +832,6 @@ class WebSocketEventUpdatesHandler(tornado.websocket.WebSocketHandler):
|
|||
|
||||
def open(self, event_id, *args, **kwds):
|
||||
logging.debug('WebSocketEventUpdatesHandler.on_open event_id:%s' % event_id)
|
||||
|
||||
_ws_clients.setdefault(self._clean_url(self.request.uri), set()).add(self)
|
||||
logging.debug('WebSocketEventUpdatesHandler.on_open %s clients connected' % len(_ws_clients))
|
||||
|
||||
|
|
|
@ -8,10 +8,18 @@ body { padding-top: 70px; }
|
|||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
a:focus a:hover {
|
||||
color: #23527c;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #23527c;
|
||||
}
|
||||
|
||||
input[type=text].form-control, input[type=search].form-control {
|
||||
border: 1px solid rgb(204, 204, 204);
|
||||
}
|
||||
|
||||
/* fix styling for empty href */
|
||||
.nav, .pagination, .carousel, .panel-title a { cursor: pointer; }
|
||||
|
||||
|
|
Loading…
Reference in a new issue