Merge pull request #148 from alberanid/master

event view page
This commit is contained in:
Davide Alberani 2016-06-27 21:57:32 +02:00 committed by GitHub
commit 9ea958aa01
12 changed files with 284 additions and 154 deletions

View file

@ -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
=============

View file

@ -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>
&nbsp;<span ng-if="!event.title">{{'New event' | translate}}</span>{{event.title}}
&nbsp;<button ng-if="event._id && hasPermission('event:tickets-all|create')" ng-click="$state.go('event.ticket.new', {id: event._id})" class="btn btn-success">
<span class="fa fa-user-plus vcenter"></span>
{{'Register' | translate}}
</button>
&nbsp;<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>

View file

@ -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>&nbsp;<a ui-sref="event.ticket.edit({id: event._id, ticket_id: person.ticket_id})" ng-if="person.ticket_id"><span>{{person.name}}</span>&nbsp;<span>{{person.surname}}</span></a></strong></span><span ng-if="!person.ticket_id"><span>{{person.name}}</span>&nbsp;<span>{{person.surname}}</span></a></strong></span></span><span ng-if="person.email">&nbsp;&lt;{{person.email}}&gt;</span>
<span><strong><a ui-sref="person.info({id: person.person_id})"><span class="fa fa-lg fa-user"></span></a>&nbsp;<a ui-sref="event.ticket.edit({id: event._id, ticket_id: person._id})" ng-if="person._id"><span>{{person.name}}</span>&nbsp;<span>{{person.surname}}</span></a></strong></span><span ng-if="!person._id"><span>{{person.name}}</span>&nbsp;<span>{{person.surname}}</span></a></strong></span></span><span ng-if="person.email">&nbsp;&lt;{{person.email}}&gt;</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">&nbsp;@&nbsp;</span><i ng-if="person.company">{{person.company}}</i></p>
</td>
<td class="text-center">

View file

@ -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>

View file

@ -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()">&times;</button>
</div>
</div>
<!-- all the magic takes place here: the content inside the next div changes accordingly to the location you're visiting -->

View file

@ -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',

View file

@ -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');
};

View file

@ -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]);
}
}

View file

@ -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>
&nbsp;
<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>

View file

@ -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

View file

@ -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))

View file

@ -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; }