Merge pull request #150 from alberanid/master

removed the Person collection
This commit is contained in:
Davide Alberani 2016-07-10 17:36:47 +02:00 committed by GitHub
commit a53239d7b2
44 changed files with 1281 additions and 1215 deletions

1
.gitignore vendored
View file

@ -1,4 +1,3 @@
data/triggers/*.d
ssl/*.pem
angular_app/node_modules/

View file

@ -1,9 +1,9 @@
Event Man(ager)
EventMan(ager)
===============
Your friendly manager of attendees at an event.
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.
EventMan(ager) will help you handle your list of attendees at an event, managing the list of tickets and marking persons as present.
Main features:
- an admin (in the future: anyone) can create and manage new events
@ -11,12 +11,12 @@ Main features:
- 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
- easy way to add a new ticket, if it's already known from a previous event or if it's a completely new ticket
- 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)
- multiple workstations are kept in sync (i.e.: marking a person as an attendee is shown in every workstation currently viewing the list of tickets of an event)
See the *screenshots* directory for some images.
@ -60,17 +60,47 @@ Open browser and navigate to: http://localhost:5242/
If you store SSL key and certificate in the *ssl* directory (default names: eventman\_key.pem and eventman\_cert.pem), HTTPS will be used: https://localhost:5242/
Basic workflow
==============
So, you've just installed it and you have the server running. Let's create an event:
- login with the **admin** user (default password: **eventman**)
- click "Add an event"
- edit basic information about the event and save it
- in the second panel ("Registration form"), edit the form that will be presented to the persons that want to join your event:
- first, define how many rows the form will have
- then define how many columns will be in each rows
- now edit every form field
- give a name to the form (not really meaningful) and save it
Now persons can start joining your event:
- click on "Join this event" in the list of events
- compile the form and submit it
- the user will have to keep the provided link, if they want to edit their information later
- from this, a person can also mark a ticket as "cancelled" (not counted in the list of tickets), or they can enable it again
- if the person was a registered user, it's possible to see the list of own tickets in the personal page
As an administrator, you can now go to the list of tickets of the event:
- from there, once the event has started, you can mark persons as attendees
- it's also possible to quickly add a new ticket or delete an existing one (the ticket is effectively deleted, it's not the same as the cancelled action)
Some notes about the event registration form:
- field names are important (case is not considered). You can use whatever you want, but "name", "surname" and "email" are internally used to show the tickets list, so please add at least one of them
- please notice that the "Email" field type has a very silly regular expression and will create a lot of problems: please use "Text input" and names the field "Email"
About the "Group ID" of events and "Unregistered persons" list:
- "Group ID" is a random non-guessable identifier associated to an event. You can use whatever you want; if left empty, it will be autogenerated. It's not the same as the Event ID (the \_id key in the db): the Group ID is supposed to be secret
- if two or more events share the same Group ID, persons that are registered in others events and are not present in the list of tickets you're looking for are added to the list of "Unregistered persons" to quickly add them. For example: if you are managing the third edition of a series of events, you can set the same Group ID to every event, and then you can quickly add a person that was present at a previous edition
- in the "Unregistered persons" list there will also be deleted tickets (beware that they are transitory: if the page is refreshed, they'll be gone for good); to match tickets between the list of tickets and "Unregistered persons" list, the email field is used, if present
Authentication
==============
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
=============
In the *data/dumps/eventman\_test\_db.tar.gz* you can find a sample db with over 1000 fake persons and a couple of events to play with. Decompress it and use *mongorestore* to import it.
The default administrator username and password are **admin** and **eventman**. If you want to force authentication (you usually don't), run the daemon with --authentication=on
License and copyright

View file

@ -3,15 +3,16 @@
<div class="panel panel-primary table-striped top5">
<div class="panel-heading">
<h1>
<button ng-if="event._id && hasPermission('persons|read')" ng-click="$state.go('event.tickets', {id: event._id})" class="btn btn-success">
{{event.title}}
<span ng-if="hasPermission('event|create') && !event._id">{{'New event' | translate}}</span>
&nbsp;<button ng-if="event._id && hasPermission('event:tickets-all|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;<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}}
{{'Join this event' | translate}}
</button>
&nbsp;<span ng-if="hasPermission('event|create') && !event.title">{{'New event' | translate}}</span>{{event.title}}
</h1>
</div>
<div class="panel-body">
@ -26,7 +27,7 @@
<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">
<input type="text" class="form-control" placeholder="{{'Title' | translate}}" ng-model="event.title" ng-required="true">
</div>
<div class="input-group input-group-lg top5">
<span class="input-group-addon min100">{{'Tagline' | translate}}</span>
@ -45,7 +46,7 @@
<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" />
<input type="text" class="form-control" datepicker-popup="dd-MMMM-yyyy" ng-model="event['begin-date']" is-open="opened" />
<span class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="open($event)"><i class="fa fa-calendar"></i></button>
</span>
@ -61,7 +62,7 @@
<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" />
<input type="text" class="form-control" datepicker-popup="dd-MMMM-yyyy" ng-model="event['end-date']" is-open="opened" />
<span class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="open($event)"><i class="fa fa-calendar"></i></button>
</span>
@ -77,6 +78,10 @@
<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">{{'Group ID' | translate}}</span>
<input type="text" class="form-control" placeholder="{{'Used to share persons amongst multiple events. Must be hard to guess (if empty, will be autogenerated)' | translate}}" ng-model="event.group_id">
</div>
</fieldset>

View file

@ -1,8 +1,2 @@
<!-- main view for Event -->
<!-- div class="container">
<ul class="nav nav-tabs">
<li ui-sref-active="active"><a ui-sref="event.tickets({id: $state.params.id})">Info</a></li>
<li ui-sref-active="active"><a ui-sref="event.edit({id: $state.params.id})">Edit</a></li>
</ul -->
<div ui-view></div>
<!-- /div -->
<div ui-view></div>

View file

@ -1,46 +1,40 @@
<!-- show details of an Event -->
<div class="container">
<div eventman-message="eventman-message" control="message"></div>
<div class="container">
<div class="row">
<div class="col-md-7 col-xs-7 vcenter">
<h1>{{event.title}}
<button ng-if="event._id" ng-click="$state.go('event.edit', {id: event._id})" class="btn btn-success">
<span class="fa fa-pencil-square-o vcenter"></span>
{{'Edit' | translate}}
</button>
</h1>
</div><!--
--><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 || []) | registeredFilter).length}}</div></h2>
</div>
<div class="col-md-6">
<h2><div class="label label-info vcenter">{{'Attendees:' | translate}} {{countAttendees}}</div></h2>
</div>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="panel panel-primary table-striped top5">
<div class="panel-heading">{{'Persons' | translate}}</div>
<div class="panel-heading">
<h1>{{event.title}} - {{'tickets' | translate}}
<button ng-if="event._id" ng-click="$state.go('event.edit', {id: event._id})" class="btn btn-success">
<span class="fa fa-gear vcenter"></span>
{{'Edit event' | translate}}
</button>
&nbsp;
<button ng-if="event._id" ng-click="openQuickAddTicket()" class="btn btn-success">
<span class="fa fa-user-plus vcenter"></span>
{{'Quick add ticket' | translate}}
</button>
<span>
<span class="label label-info vcenter pull-right">{{'Attendees:' | translate}} {{countAttendees}}</span>
&nbsp;
<span class="label label-warning vcenter pull-right">{{'Registered:' | translate}} {{((event.tickets || []) | registeredFilter).length}}</span>
</span>
</h1>
</div>
<div class="panel-body">
<form class="form-inline">
<div class="form-group">
<label for="query-persons">{{'Search:' | translate}}</label>
<input eventman-focus type="text" id="query-persons" class="form-control" placeholder="{{'Name or email' | translate}}" ng-model="query" ng-model-options="{debounce: 600}">
</div>
<label for="query-tickets">{{'Search:' | translate}}</label>
<input eventman-focus type="text" id="query-tickets" class="form-control" placeholder="{{'Name or email' | translate}}" ng-model="query" ng-model-options="{debounce: 600}">
</div>&nbsp;<label>&nbsp;<input type="checkbox" ng-model="registeredFilterOptions.all" /> {{'Show cancelled tickets' | translate}}</label>
</form>
<table class="table table-striped">
<thead>
<tr>
<th class="text-right nowrap">#</th>
<th class="nowrap">{{'Person' | translate}} <a ng-click="updateOrded('name')" href=""><i class="fa fa-caret-up"></i></a>{{'Name' | translate}}<a ng-click="updateOrded('-name')" href=""><i class="fa fa-caret-down"></i></a> <a ng-click="updateOrded('surname')" href=""><i class="fa fa-caret-up"></i></a>{{'Surname' | translate}}<a ng-click="updateOrded('-surname')" href=""><i class="fa fa-caret-down"></i></a></th>
<th class="nowrap"><a ng-click="updateOrded('name')" href=""><i class="fa fa-caret-up"></i></a>{{'Name' | translate}}<a ng-click="updateOrded('-name')" href=""><i class="fa fa-caret-down"></i></a> <a ng-click="updateOrded('surname')" href=""><i class="fa fa-caret-up"></i></a>{{'Surname' | translate}}<a ng-click="updateOrded('-surname')" href=""><i class="fa fa-caret-down"></i></a></th>
<th class="text-center nowrap"><a ng-click="updateOrded('-attended')" href=""><i class="fa fa-caret-up"></i></a>{{'Attended' | translate}}<a ng-click="updateOrded('attended')" href=""><i class="fa fa-caret-down"></i></a></th>
<th class="text-center nowrap" ng-repeat="col in customFields">
<a ng-click="updateOrded(col.key)" href=""><i class="fa fa-caret-up"></i></a>{{col.label | translate}}<a ng-click="updateOrded('-' + col.key)" href=""><i class="fa fa-caret-down"></i></a>
@ -49,25 +43,30 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="person in (event.persons || []) | splittedFilter:query | registeredFilter | orderBy:personsOrder">
<tr ng-repeat="ticket in (event.tickets || []) | splittedFilter:query | registeredFilter:registeredFilterOptions | orderBy:ticketsOrder">
<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._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>
<span>
<strong>
<a ui-sref="event.ticket.edit({id: event._id, ticket_id: ticket._id})"><span>{{ticket.name}}</span>&nbsp;<span>{{ticket.surname}}</span></a>
</strong>
</span>
<span ng-if="ticket.email">&nbsp;&lt;{{ticket.email}}&gt;</span><span ng-if="ticket.cancelled">&nbsp;({{'cancelled' | translate}})</span>
<p ng-if="ticket.company || ticket.job_title"><i ng-if="ticket.job_title">{{ticket.job_title}}</i><span ng-if="ticket.company && ticket.job_title">&nbsp;@&nbsp;</span><i ng-if="ticket.company">{{ticket.company}}</i></p>
</td>
<td class="text-center">
<button class="btn btn-link" reset-focus name="switch-attended" ng-click="setPersonAttributeAndRefocus(person, 'attended', !person.attended)"><span class="fa fa-lg {{(person.attended) && 'fa-check-circle text-success' || 'fa-times-circle text-danger'}}"></span></button>
<button class="btn btn-link" reset-focus name="switch-attended" ng-click="setTicketAttributeAndRefocus(ticket, 'attended', !ticket.attended)"><span class="fa fa-lg {{(ticket.attended) && 'fa-check-circle text-success' || 'fa-times-circle text-danger'}}"></span></button>
</td>
<td class="text-center" ng-repeat="col in customFields">
<span ng-if="col.type == 'boolean'">
<button class="btn btn-link" ng-click="setPersonAttribute(person, col.key, !person[col.key])"><span class="fa fa-lg {{(person[col.key]) && 'fa-check-circle text-success' || 'fa-times-circle text-danger'}}"></span></button>
<button class="btn btn-link" ng-click="setTicketAttribute(ticket, col.key, !ticket[col.key])"><span class="fa fa-lg {{(ticket[col.key]) && 'fa-check-circle text-success' || 'fa-times-circle text-danger'}}"></span></button>
</span>
<span ng-if="col.type != 'boolean'">
{{person[col.key]}}
{{ticket[col.key]}}
</span>
</td>
<td class="text-center">
<button ng-click="removeAttendee(person)" type="button" class="btn btn-link fa fa-lg fa-trash"></button>
<button ng-click="deleteTicket(ticket)" type="button" class="btn btn-link fa fa-lg fa-trash"></button>
</td>
</tr>
</tbody>
@ -77,42 +76,8 @@
</div>
<div class="col-md-4">
<div class="panel panel-info table-striped top5">
<div class="panel-heading">{{'Quick add' | translate}}</div>
<div class="panel-body">
<form>
<div class="input-group input-group-sm">
<span class="input-group-addon min70-compact">{{'Name' | translate}}</span>
<input type="text" class="form-control" placeholder="{{'Name' | translate}}" ng-model="newPerson.name" ng-required="1">
</div>
<div class="input-group input-group-sm top5">
<span class="input-group-addon min70-compact">{{'Surname' | translate}}</span>
<input type="text" class="form-control" placeholder="{{'Surname' | translate}}" ng-model="newPerson.surname">
</div>
<div class="input-group top5">
<span class="input-group-addon min70-compact">{{'Email' | translate}}</span>
<input type="email" name="email" class="form-control" placeholder="{{'name.surname@example.com' | translate}}" ng-model="newPerson.email">
</div>
<div class="input-group top5">
<span class="input-group-addon min70-compact">{{'Company' | translate}}</span>
<input name="company" class="form-control" placeholder="{{'Acme Corporation' | translate}}" ng-model="newPerson.company">
</div>
<div class="input-group top5">
<span class="input-group-addon min70-compact">{{'Job' | translate}}</span>
<input name="job_title" class="form-control" placeholder="{{'Evil Ruler' | translate}}" ng-model="newPerson.job_title">
</div>
<button reset-focus ng-disabled="!(newPerson.name && newPerson.surname)" ng-click="fastAddAttendee(newPerson, true)" class="btn btn-success top5">
<span class="fa fa-plus-circle vcenter"></span>
{{'Add' | translate}}
</button>
</form>
</div>
</div>
<div class="panel panel-info top5">
<div class="panel-heading">{{'Unregistered persons' | translate}}</div>
<div class="panel-heading"><h1>{{'Unregistered persons' | translate}}</h1></div>
<div class="panel-body small-table">
<table class="table table-striped table-condensed">
<thead>
@ -124,12 +89,12 @@
<tbody>
<tr ng-repeat="person in allPersons | splittedFilter:query | personRegistered:{event: event, present: false}">
<td>
<strong><a ui-sref="person.info({id: person._id})">{{person.name}} {{person.surname}}</a></strong>
<strong>{{person.name}} {{person.surname}}</strong>
<br />
{{person.email}}
</td>
<td class="text-left">
<button reset-focus ng-click="fastAddAttendee(person)" type="button" class="btn btn-link fa fa-plus-circle vcenter"></button>
<button reset-focus ng-click="addTicket(person)" type="button" class="btn btn-link fa fa-plus-circle vcenter"></button>
</td>
</tr>
</tbody>

View file

@ -1,13 +1,14 @@
<!-- show the list of Events -->
<div class="container">
<h1>{{'Events' | translate}}
<button ng-click="$state.go('event.new')" class="btn btn-success">
<span class="fa fa-plus-circle vcenter"></span>
{{'Add event' | translate}}
</button>
</h1>
<div class="panel panel-primary table-striped top5">
<div class="panel-heading">{{'Events' | translate}}</div>
<div class="panel-heading">
<h1>{{'Events' | translate}}
<button ng-click="$state.go('event.new')" class="btn btn-success" ng-if="hasPermission('event|create')">
<span class="fa fa-calendar vcenter"></span>
{{'Add event' | translate}}
</button>
</h1>
</div>
<div class="panel-body">
<form class="form-inline">
<div class="form-group">
@ -43,17 +44,17 @@
<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>
<p><span ng-if="event['begin-date']">{{'Begins:' | translate}} {{event['begin-date'] | date:'fullDate'}} {{event['begin-time'] | date:'HH:mm'}}<br/></span>
<span ng-if="event['end-date']">{{'Ends:' | translate}} {{event['end-date'] | date:'fullDate' }} {{event['end-time'] | date:'HH:mm'}}</span></p>
</td>
<td ng-if="hasPermission('persons|read')" class="hcenter">
<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 ng-if="hasPermission('event:tickets-all|read')" class="hcenter">
<p><span ng-init="attendeesNr = ((event.tickets || []) | attendeesFilter).length">{{attendeesNr}}</span> / {{((event.tickets || []) | registeredFilter).length}} ({{((attendeesNr / ((event.tickets || []) | 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="{{'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:tickets-all|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('ticket|update')" ng-click="$state.go('event.tickets', {id: event._id})" class="btn btn-link fa fa-ticket" type="button" title="{{'Manage tickets' | 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="deleteEvent(event._id)" type="button" class="btn btn-link fa fa-trash fa-lg" title="{{'Delete' | translate}}"></button>
</td>
</tr>
</tbody>

View file

@ -1,21 +1,20 @@
<!-- import persons -->
<!-- import tickets -->
<div class="container">
<h1>{{'Import persons' | translate}}</h1>
<div class="panel panel-primary">
<div class="panel-heading">
<div class="panel-title">{{'Import persons from eventbrite CSV' | translate}}</div>
<div class="panel-title"><h1>{{'Import tickets from Eventbrite CSV' | translate}}</h1></div>
</div>
<div class="panel-body">
<form name="ebCSVForm" class="well">
<div class="form-group">
<label for="eb-csv-import">{{'CSV file' | translate}}</label>
<input name="file" ng-file-select ng-model="file" type="file" id="eb-csv-import" ng-required="true">
<p class="help-block">{{'CSV exported from eventbrite' | translate}}</p>
<p class="help-block">{{'CSV exported from Eventbrite' | translate}}</p>
</div>
<div class="form-group">
<label for="forEvent">{{'Associate users to this event' | translate}}</label>
<select class="form-control" id="forEvent" ng-model="targetEvent">
<label for="forEvent">{{'Associate tickets to this event' | translate}}</label>
<select class="form-control" id="forEvent" ng-model="targetEvent" ng-required="true">
<option ng-repeat="event in events" value="{{event._id}}">{{event.title}}</option>
</select>
</div>

View file

@ -1,7 +1,7 @@
<!doctype html>
<html ng-app="eventManApp">
<head>
<title>Event Man(ager)</title>
<title>EventMan(ager)</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
@ -73,15 +73,17 @@
<div ng-if="logo.imgURL" class="navbar-brand"><a ng-if="logo.link" href="{{logo.link}}" target="_blank"><img src="{{logo.imgURL}}" /></a></div>
<ul class="nav navbar-nav">
<li ng-class="{active: isActive('/events') || isActive('/event')}"><a ui-sref="events">{{'Events' | translate}}</a></li>
<li ng-if="hasPermission('persons|read')" ng-class="{active: isActive('/persons') || isActive('/person') || isActive('/import/persons')}"><a ui-sref="persons">{{'Persons' | translate}}</a></li>
<li ng-if="hasPermission('admin|all')" ng-class="{active: isActive('/users') || isActive('/user')}"><a ui-sref="users">{{'Users' | translate}}</a></li>
<li ng-if="hasPermission('admin|all')" ng-class="{active: isActive('/tickets')}"><a ui-sref="tickets">{{'All tickets' | translate}}</a></li>
<li ng-if="hasPermission('admin|all')" ng-class="{active: isActive('/import/persons')}"><a ui-sref="import.persons">{{'Import tickets' | translate}}</a></li>
</ul>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li ng-if="info.user.username">
<span class="btn">{{info.user.username}}</span>
<li ng-if="info && info.user && info.user.username && info.user._id">
<span class="btn"><a ui-sref="user.edit({id: info.user._id})">{{info.user.username}}</a></span>
<span class="btn btn-link">
<a ng-controller="LoginCtrl" ng-click="logout()"><span class="fa fa-sign-out vcenter"></span>&nbsp;{{'logout' | translate}}</a>
<a ng-controller="UsersCtrl" ng-click="logout()"><span class="fa fa-sign-out vcenter"></span>&nbsp;{{'logout' | translate}}</a>
</span>
</li>
<li ng-if="!info.user.username">
@ -109,7 +111,7 @@
<nav class="navbar navbar-default navbar-fixed-bottom">
<div class="main-footer">
<a href="https://github.com/raspibo/eventman/" target="_blank">Event Man(ager)</a> by RaspiBO: <a href="http://raspibo.org">wiki</a> | <a href="http://social.raspibo.org">social network</a> | <a href="http://liste.raspibo.org/wws/">mailing lists</a> | <a href="mailto:info@raspibo.org">contact</a>
<a href="https://github.com/raspibo/eventman/" target="_blank">EventMan(ager)</a> by RaspiBO: <a href="http://raspibo.org">wiki</a> | <a href="http://social.raspibo.org">social network</a> | <a href="http://liste.raspibo.org/wws/">mailing lists</a> | <a href="mailto:info@raspibo.org">contact</a>
</div>
</nav>
</body>

52
angular_app/js/app.js vendored
View file

@ -33,6 +33,7 @@ var eventManApp = angular.module('eventManApp', [
eventManApp.run(['$rootScope', '$state', '$stateParams', '$log', 'Info',
function($rootScope, $state, $stateParams, $log, Info) {
$rootScope.app_uuid = guid();
$rootScope.info = {};
$log.debug('App UUID: ' + $rootScope.app_uuid);
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
@ -42,8 +43,10 @@ eventManApp.run(['$rootScope', '$state', '$stateParams', '$log', 'Info',
$rootScope.readInfo = function(callback) {
Info.get({}, function(data) {
$rootScope.info = data || {};
if (callback) {
callback();
if (data.authentication_required && !(data.user && data.user._id)) {
$state.go('login');
} else if (callback) {
callback(data);
}
});
};
@ -129,7 +132,7 @@ eventManApp.config(['$stateProvider', '$urlRouterProvider',
.state('event.tickets', {
url: '/:id/tickets',
templateUrl: 'event-tickets.html',
controller: 'EventDetailsCtrl'
controller: 'EventTicketsCtrl'
})
.state('event.ticket', {
url: '/:id/ticket',
@ -145,29 +148,10 @@ eventManApp.config(['$stateProvider', '$urlRouterProvider',
templateUrl: 'ticket-edit.html',
controller: 'EventTicketsCtrl'
})
.state('persons', {
url: '/persons',
templateUrl: 'persons-list.html',
controller: 'PersonsListCtrl'
})
.state('person', {
url: '/person',
templateUrl: 'person-main.html'
})
.state('person.new', {
url: '/new',
templateUrl: 'person-edit.html',
controller: 'PersonDetailsCtrl'
})
.state('person.edit', {
url: '/:id/edit',
templateUrl: 'person-edit.html',
controller: 'PersonDetailsCtrl'
})
.state('person.info', {
url: '/:id',
templateUrl: 'person-info.html',
controller: 'PersonDetailsCtrl'
.state('tickets', {
url: '/tickets',
templateUrl: 'tickets-list.html',
controller: 'EventsListCtrl'
})
.state('import', {
url: '/import',
@ -178,10 +162,24 @@ eventManApp.config(['$stateProvider', '$urlRouterProvider',
templateUrl: 'import-persons.html',
controller: 'FileUploadCtrl'
})
.state('users', {
url: '/users',
templateUrl: 'users-list.html',
controller: 'UsersCtrl'
})
.state('user', {
url: '/user',
templateUrl: 'user-main.html'
})
.state('user.edit', {
url: ':id/edit',
templateUrl: 'user-edit.html',
controller: 'UsersCtrl'
})
.state('login', {
url: '/login',
templateUrl: 'login.html',
controller: 'LoginCtrl'
controller: 'UsersCtrl'
});
}
]);

File diff suppressed because it is too large Load diff

View file

@ -2,27 +2,6 @@
/* Filters for EventMan(ager) lists of objects. */
/* Filter for events that have (or not) information about a registered person. */
eventManApp.filter('eventWithPersonData', ['$filter',
function($filter) {
return function(inputArray, mustBePresent) {
if (mustBePresent === undefined) {
mustBePresent = true;
}
inputArray = inputArray || [];
var returnArray = [];
for (var x=0; x < inputArray.length; x++) {
var found = inputArray[x].person_data && inputArray[x].person_data.person_id;
if ((found && mustBePresent) || (!found && !mustBePresent)) {
returnArray.push(inputArray[x]);
}
}
return returnArray;
};
}]
);
/* Filter for persons (not) registered for a given event. */
eventManApp.filter('personRegistered', ['$filter',
function($filter) {
@ -33,14 +12,14 @@ eventManApp.filter('personRegistered', ['$filter',
inputArray = inputArray || [];
var returnArray = [];
var registeredIDs = [];
if (!(data.event && data.event.persons && data.event.persons.length)) {
if (!(data.event && data.event.tickets && data.event.tickets.length)) {
return inputArray;
}
for (var x=0; x < data.event.persons.length; x++) {
if (!data.includeCancelled && data.event.persons[x].cancelled) {
for (var x=0; x < data.event.tickets.length; x++) {
if (!data.includeCancelled && data.event.tickets[x].cancelled) {
continue;
}
registeredIDs.push(data.event.persons[x].person_id);
registeredIDs.push(data.event.tickets[x]._id);
}
for (var x=0; x < inputArray.length; x++) {
var found = registeredIDs.indexOf(inputArray[x]._id) != -1;
@ -68,7 +47,7 @@ eventManApp.filter('splittedFilter', ['$filter',
);
/* Filter that returns only the (not) registered persons at an event. */
/* Filter that returns only the (not) registered tickets at an event. */
eventManApp.filter('registeredFilter', ['$filter',
function($filter) {
return function(inputArray, data) {

View file

@ -1,6 +1,5 @@
/* i18n for Event(man) */
'use strict';
eventManApp.config(['$translateProvider', function ($translateProvider) {
$translateProvider.useStaticFilesLoader({

View file

@ -18,7 +18,7 @@ function convert_dates(obj) {
eventManServices.factory('Event', ['$resource', '$rootScope',
function($resource, $rootScope) {
return $resource('events/:id', {id: '@_id', person_id: '@person_id'}, {
return $resource('events/:id', {id: '@_id'}, {
all: {
method: 'GET',
@ -44,10 +44,10 @@ eventManServices.factory('Event', ['$resource', '$rootScope',
data = angular.fromJson(data);
convert_dates(data);
// strip empty keys.
angular.forEach(data.persons || [], function(person, person_idx) {
angular.forEach(person, function(value, key) {
angular.forEach(data.tickets || [], function(ticket, ticket_idx) {
angular.forEach(ticket, function(value, key) {
if (value === "") {
delete person[key];
delete ticket[key];
}
});
});
@ -60,36 +60,13 @@ eventManServices.factory('Event', ['$resource', '$rootScope',
interceptor : {responseError: $rootScope.errorHandler}
},
updatePerson: {
method: 'PUT',
interceptor : {responseError: $rootScope.errorHandler},
isArray: false,
url: 'events/:id/persons/:person_id',
params: {uuid: $rootScope.app_uuid},
group_persons: {
method: 'GET',
url: 'events/:id/group_persons',
isArray: true,
transformResponse: function(data, headers) {
return angular.fromJson(data);
}
},
addPerson: {
method: 'POST',
interceptor : {responseError: $rootScope.errorHandler},
isArray: false,
url: 'events/:id/persons/:person_id',
params: {uuid: $rootScope.app_uuid},
transformResponse: function(data, headers) {
return angular.fromJson(data);
}
},
deletePerson: {
method: 'DELETE',
interceptor : {responseError: $rootScope.errorHandler},
isArray: false,
url: 'events/:_id/persons/:person_id',
params: {uuid: $rootScope.app_uuid},
transformResponse: function(data, headers) {
return angular.fromJson(data);
data = angular.fromJson(data);
return data.persons || [];
}
}
});
@ -99,7 +76,17 @@ eventManServices.factory('Event', ['$resource', '$rootScope',
eventManServices.factory('EventTicket', ['$resource', '$rootScope',
function($resource, $rootScope) {
return $resource('events/:id/tickets', {id: '@_id', ticket_id: '@ticket_id'}, {
return $resource('events/:id/tickets', {event_id: '@event_id', ticket_id: '@_id'}, {
all: {
method: 'GET',
url: '/tickets',
interceptor : {responseError: $rootScope.errorHandler},
isArray: true,
transformResponse: function(data, headers) {
data = angular.fromJson(data);
return data.tickets;
}
},
get: {
method: 'GET',
@ -107,7 +94,10 @@ eventManServices.factory('EventTicket', ['$resource', '$rootScope',
interceptor : {responseError: $rootScope.errorHandler},
transformResponse: function(data, headers) {
data = angular.fromJson(data);
return data.person;
if (data.error) {
return data;
}
return data.ticket;
}
},
@ -115,11 +105,14 @@ eventManServices.factory('EventTicket', ['$resource', '$rootScope',
method: 'POST',
interceptor : {responseError: $rootScope.errorHandler},
isArray: false,
url: 'events/:id/tickets/:ticket_id',
url: 'events/:event_id/tickets',
params: {uuid: $rootScope.app_uuid},
transformResponse: function(data, headers) {
data = angular.fromJson(data);
return data.person;
if (data.error) {
return data;
}
return data.ticket;
}
},
@ -127,18 +120,21 @@ eventManServices.factory('EventTicket', ['$resource', '$rootScope',
method: 'PUT',
interceptor : {responseError: $rootScope.errorHandler},
isArray: false,
url: 'events/:id/tickets/:ticket_id',
url: 'events/:event_id/tickets/:ticket_id',
params: {uuid: $rootScope.app_uuid},
transformResponse: function(data, headers) {
if (data.error) {
return data;
}
return angular.fromJson(data);
}
},
deleteTicket: {
'delete': {
method: 'DELETE',
interceptor : {responseError: $rootScope.errorHandler},
isArray: false,
url: 'events/:_id/tickets/:ticket_id',
url: 'events/:event_id/tickets/:ticket_id',
params: {uuid: $rootScope.app_uuid},
transformResponse: function(data, headers) {
return angular.fromJson(data);
@ -149,53 +145,9 @@ eventManServices.factory('EventTicket', ['$resource', '$rootScope',
);
eventManServices.factory('Person', ['$resource', '$rootScope',
function($resource, $rootScope) {
return $resource('persons/:id', {id: '@_id'}, {
all: {
method: 'GET',
interceptor : {responseError: $rootScope.errorHandler},
isArray: true,
transformResponse: function(data, headers) {
data = angular.fromJson(data);
if (data.error) {
return data;
}
return data.persons;
}
},
update: {
method: 'PUT',
interceptor : {responseError: $rootScope.errorHandler}
},
getEvents: {
method: 'GET',
interceptor : {responseError: $rootScope.errorHandler},
url: 'persons/:_id/events',
isArray: true,
transformResponse: function(data, headers) {
data = angular.fromJson(data);
if (data.error) {
return data;
}
angular.forEach(data.events || [], function(event_, event_idx) {
convert_dates(event_);
});
return data.events;
}
}
});
}]
);
eventManServices.factory('Setting', ['$resource', '$rootScope',
function($resource, $rootScope) {
return $resource('settings/', {}, {
query: {
method: 'GET',
interceptor : {responseError: $rootScope.errorHandler},
@ -241,15 +193,24 @@ eventManServices.factory('Info', ['$resource', '$rootScope',
eventManServices.factory('User', ['$resource', '$rootScope',
function($resource, $rootScope) {
return $resource('users/:id', {id: '@_id'}, {
get: {
all: {
method: 'GET',
interceptor : {responseError: $rootScope.errorHandler},
isArray: true,
transformResponse: function(data, headers) {
data = angular.fromJson(data);
if (data.error) {
return data;
}
return data.user || {};
return data.users;
}
},
get: {
method: 'GET',
interceptor : {responseError: $rootScope.errorHandler},
transformResponse: function(data, headers) {
return angular.fromJson(data);
}
},
@ -258,6 +219,11 @@ eventManServices.factory('User', ['$resource', '$rootScope',
interceptor : {responseError: $rootScope.errorHandler}
},
update: {
method: 'PUT',
interceptor : {responseError: $rootScope.errorHandler}
},
login: {
method: 'POST',
url: '/login',
@ -274,12 +240,10 @@ eventManServices.factory('User', ['$resource', '$rootScope',
);
/* WebSocket collection used to update the list of persons of an Event. */
eventManApp.factory('EventUpdates', ['$websocket', '$location', '$log',
function($websocket, $location, $log) {
/* WebSocket collection used to update the list of tickets of an Event. */
eventManApp.factory('EventUpdates', ['$websocket', '$location', '$log', '$rootScope',
function($websocket, $location, $log, $rootScope) {
var dataStream = null;
var data = {};
var methods = {
@ -289,18 +253,19 @@ eventManApp.factory('EventUpdates', ['$websocket', '$location', '$log',
dataStream.close();
},
open: function() {
$log.debug('open WebSocket connection');
dataStream && dataStream.close();
var proto = $location.protocol() == 'https' ? 'wss' : 'ws';
dataStream = $websocket(proto + '://' + $location.host() + ':' + $location.port() +
'/ws/' + $location.path() + '/updates');
var url = proto + '://' + $location.host() + ':' + $location.port() +
'/ws/' + $location.path() + '/updates?uuid=' + $rootScope.app_uuid;
$log.debug('open WebSocket connection to ' + url);
//dataStream && dataStream.close();
dataStream = $websocket(url);
dataStream.onMessage(function(message) {
$log.debug('EventUpdates message received');
data.update = angular.fromJson(message.data);
});
}
};
return methods;
}]
);

View file

@ -3,7 +3,7 @@
<div class="row">
<div class="col-md-7 col-xs-7">
<div class="panel panel-primary table-striped">
<div class="panel-heading">{{'Login' | translate}}</div>
<div class="panel-heading"><h1>{{'Login' | translate}}</h1></div>
<div class="panel-body">
<form method="POST">
<div class="input-group input-group-lg">
@ -24,7 +24,7 @@
</div>
<div class="col-md-4 col-xs-4">
<div class="panel panel-success table-striped">
<div class="panel-heading">{{'Register a new user' | translate}}</div>
<div class="panel-heading"><h1>{{'Register a new user' | translate}}</h1></div>
<div class="panel-body">
<form method="POST">
<div class="input-group input-group-lg">

View file

@ -0,0 +1,19 @@
<div>
<div class="modal-header">
<h3>{{'Quick add a ticket' | translate}}</h3>
</div>
<div class="modal-body" ng-cloak>
<eda-easy-form-viewer
eda-easy-form-viewer-data-model="formData"
eda-easy-form-viewer-easy-form-generator-fields-model="formSchema"
eda-easy-form-viewer-submit-form-event="submitForm(dataModelSubmitted)"
eda-easy-form-viewer-cancel-form-event="cancelForm()">
</eda-easy-form-viewer>
</div>
<div class="modal-footer">
</div>
</div>

View file

@ -1,69 +0,0 @@
<!-- show details of a Person -->
<div class="container">
<div class="panel panel-primary table-striped top5">
<div class="panel-heading">
<h1>
<button ng-if="person._id" ng-click="$state.go('person.info', {id: person._id})" class="btn btn-success">
<span class="fa fa-info-circle vcenter"></span>
{{'Info' | translate}}
</button>
&nbsp;<span ng-if="!(person.name || person.surname)">{{'New person' | translate}}</span>{{person.name}} {{person.surname}}
</h1>
</div>
<div class="panel-body">
<form name="personForm" ng-model="persondetails" ng-submit="save()">
<div ng-class="{clearfix: true, alert: true, 'alert-success': !personForm.$dirty, 'alert-danger': personForm.$dirty}">
<button type="button" class="btn btn-default pull-right" ng-click="save($event)" ng-disabled="!personForm.$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 min120">{{'Name' | translate}}</span>
<input type="text" class="form-control" placeholder="{{'Name' | translate}}" ng-model="person.name" ng-required="1">
</div>
<div class="input-group input-group-lg top5">
<span class="input-group-addon min120">{{'Surname' | translate}}</span>
<input type="text" class="form-control" placeholder="{{'Surname' | translate}}" ng-model="person.surname">
</div>
<div class="input-group input-group-lg top5">
<span class="input-group-addon min120">{{'Email' | translate}}</span>
<input type="email" name="email" class="form-control" placeholder="{{'name.surname@example.com' | translate}}" ng-model="person.email">
</div>
<div class="input-group input-group-lg top5">
<span class="input-group-addon min120">{{'Company' | translate}}</span>
<input name="company" class="form-control" placeholder="{{'Acme Corporation' | translate}}" ng-model="person.company">
</div>
<div class="input-group input-group-lg top5">
<span class="input-group-addon min120">{{'Job' | translate}}</span>
<input name="job_title" class="form-control" placeholder="{{'Evil Ruler' | translate}}" ng-model="person.job_title">
</div>
<div class="form-group top5">
<label for="addToEvent">{{'Add to event:' | translate}}</label>
<select class="form-control" id="addToEvent" ng-model="addToEvent">
<option value=""></option>
<option ng-repeat="event in events | eventWithPersonData:false" value="{{event._id}}">{{event.title}}</option>
</select>
<option>
<tr ng-repeat="event in events | splittedFilter:query | orderBy:eventsOrderProp">
</div>
<div ng-repeat="custom in customFields" class="form-group top5">
<label for="custom_{{custom['key']}}">{{custom.label | translate}}</span>
<input ng-if="custom.type == 'boolean'" id="custom_{{custm['key']}}" type="checkbox" class="form-control" placeholder="{{custom.label | translate}}" ng-model="person[custom.key]">
<input ng-if="custom.type != 'boolean'" id="custom_{{custm['key']}}" type="text" class="form-control" placeholder="{{custom.label | translate}}" ng-model="person[custom.key]">
</div>
<input type="submit" class="outside-screen" />
</form>
</div>
</div>
</div>

View file

@ -1,50 +0,0 @@
<!-- show details of a Person -->
<div class="container">
<h1>{{person.name}} {{person.surname}}
<button ng-if="person._id" ng-click="$state.go('person.edit', {id: person._id})" class="btn btn-success">
<span class="fa fa-pencil-square-o vcenter"></span>
{{'Edit' | translate}}
</button>
</h1>
<div class="panel panel-primary table-striped top5">
<div class="panel-heading">Events</div>
<div class="panel-body">
<form class="form-inline">
<div class="form-group">
<label for="query-persons">{{'Search:' | translate}}</label>
<input eventman-focus type="text" id="query-persons" class="form-control" placeholder="{{'Name or email' | translate}}" ng-model="query" ng-model-options="{debounce: 600}">
</div>
<div class="form-group">
<label for="events-order">{{'Sort by:' | translate}}</label>
<select id="events-order" class="form-control" ng-model="eventsOrderProp">
<option value="title">{{'Title' | translate}}</option>
<option value="-title">{{'Title (descending)' | translate}}</option>
<option value="begin_date">{{'Date' | translate}}</option>
<option value="-begin_date">{{'Date (descending)' | translate}}</option>
</select>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>{{'Event' | translate}}</th>
<th class="text-center">{{'Registered' | translate}}</th>
<th class="text-center">{{'Attended' | translate}}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="event in events | splittedFilter:query | orderBy:eventsOrderProp">
<td><strong><a ui-sref="event.tickets({id: event._id})">{{event.title}}</a></strong></td>
<td class="text-center">
<button class="btn btn-link" name="switch-registered" ng-click="switchRegistered(event, person, !event.person_data.person_id)"><span class="fa fa-lg {{(event.person_data.person_id) && 'fa-check-circle text-success' || 'fa-times-circle text-danger'}}"></span></button>
</td>
<td class="text-center">
<button ng-disabled="!event.person_data.person_id" class="btn btn-link" name="switch-attended" ng-click="setPersonAttributeAtEvent(event, 'attended', !event.person_data.attended)"><span class="fa fa-lg {{(event.person_data.attended) && 'fa-check-circle text-success' || 'fa-times-circle text-danger'}}"></span></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View file

@ -1,2 +0,0 @@
<!-- main view for Person -->
<div ui-view></div>

View file

@ -1,70 +0,0 @@
<!-- show the list of Persons -->
<div class="container">
<div class="container">
<div class="row">
<div class="col-md-7 col-xs-7 vcenter">
<h1>{{'Persons' | translate}}
<button ng-click="$state.go('person.new')" class="btn btn-success">
<span class="fa fa-plus-circle vcenter"></span>
{{'Add person' | translate}}
</button>
<button ng-click="$state.go('import.persons')" class="btn btn-success">
<span class="fa fa-download vcenter"></span>
{{'Import persons' | translate}}
</button>
</h1>
</div><!--
--><div class="col-md-5 col-xs-5 vcenter">
<div class="row">
<div class="col-md-6">
<h2><div class="label label-info vcenter">{{'Persons:' | translate}} {{persons.length || 0}}</div></h2>
</div>
</div>
</div>
</div>
<div class="panel panel-primary table-striped top5">
<div class="panel-heading">{{'Persons' | translate}}</div>
<div class="panel-body">
<form class="form-inline">
<div class="form-group">
<label for="query-persons">{{'Search:' | translate}}</label>
<input eventman-focus type="text" id="query-persons" class="form-control" placeholder="{{'Name or email' | translate}}" ng-model="query" ng-model-options="{debounce: 600}">
</div>
</form>
<div ng-include=" 'modal-confirm-action.html' " class="hidden"></div>
<table class="table table-striped">
<thead>
<tr>
<th class="text-right nowrap">#</th>
<th class="nowrap">{{'Person' | translate}} <a ng-click="updateOrded('name')" href=""><i class="fa fa-caret-up"></i></a>{{'Name' | translate}}<a ng-click="updateOrded('-name')" href=""><i class="fa fa-caret-down"></i></a> <a ng-click="updateOrded('surname')" href=""><i class="fa fa-caret-up"></i></a>{{'Surname' | translate}}<a ng-click="updateOrded('-surname')" href=""><i class="fa fa-caret-down"></i></a></th>
<th ng-repeat="col in customFields" class="text-center nowrap">
<a ng-click="updateOrded(col.key)" href=""><i class="fa fa-caret-up"></i></a>{{col.label | translate}}<a ng-click="updateOrded('-' + col.key)" href=""><i class="fa fa-caret-down"></i></a>
</th>
<th class="text-center nowrap"><strong>{{'Delete' | translate}}</strong></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="person in persons | splittedFilter:query | orderBy:personsOrder">
<td class="text-right">{{$index+1}}</td>
<td>
<span><strong><a ui-sref="person.info({id: person._id})"><span>{{person.name}}</span>&nbsp;<span>{{person.surname}}</span></a></strong></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 ng-repeat="col in customFields" class="text-center">
<span ng-if="col.type == 'boolean'">
<button class="btn btn-link" ng-click="setAttribute(person, col.key, !person[col.key])"><span class="fa fa-lg {{(person[col.key]) && 'fa-check-circle text-success' || 'fa-times-circle text-danger'}}"></span></button>
</span>
<span ng-if="col.type != 'boolean'">
{{person[col.key]}}
</span>
</td>
<td class="text-center">
<button ng-click="remove(person._id)" type="button" class="btn btn-link fa fa-trash fa-lg"></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View file

@ -1,13 +1,6 @@
<!-- show details of an Event -->
<div class="container">
<div eventman-message="eventman-message" control="message"></div>
<div class="container">
<div class="row">
<div class="col-md-7 col-xs-7 vcenter">
<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.
@ -15,8 +8,24 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="panel panel-info table-striped top5">
<div class="panel-heading">{{'Join this event' | translate}}</div>
<div class="panel panel-primary table-striped top5">
<div class="panel-heading">
<h1>
{{event.title}}<span ng-if="!ticket._id"> - {{'join this event' | translate}}</span><span ng-if="ticket._id"> - {{'your ticket' | translate}}</span>
&nbsp;<button ng-click="$state.go('event.view', {id: event._id})" class="btn btn-success" ng-if="event._id && !hasPermission('event|update')">
<span class="fa fa-calendar vcenter"></span>
{{'Event details' | translate}}
</button>
&nbsp;<button ng-click="$state.go('event.edit', {id: event._id})" class="btn btn-success" ng-if="event._id && hasPermission('event|update')">
<span class="fa fa-gear vcenter"></span>
{{'Edit event' | translate}}
</button>
&nbsp;<button ng-click="$state.go('event.tickets', {id: event._id})" class="btn btn-success" ng-if="event._id && hasPermission('event:tickets-all|read')">
<span class="fa fa-ticket vcenter"></span>
{{'Tickets' | translate}}
</button>
</h1>
</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}}
@ -45,17 +54,16 @@
<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>
<button ng-click="guiOptions.dangerousActionsEnabled = !guiOptions.dangerousActionsEnabled" class="btn btn-warning">
<span class="fa fa-exclamation-triangle vcenter"></span>
{{'Toggle dangerous actions' | translate}}
</button>
&nbsp;
<button ng-disabled="!guiOptions.dangerousActionsEnabled" ng-click="toggleCancelledTicket({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

@ -0,0 +1,45 @@
<!-- show the list of Tickets -->
<div class="container">
<div class="panel panel-primary table-striped top5">
<div class="panel-heading">
<h1>
{{'All Tickets' | translate}}
<div class="label label-info vcenter">{{'Tickets:' | translate}} {{tickets.length || 0}}</div>
</h1>
</div>
<div class="panel-body">
<form class="form-inline">
<div class="form-group">
<label for="query-tickets">{{'Search:' | translate}}</label>
<input eventman-focus type="text" id="query-tickets" class="form-control" placeholder="{{'Name or email' | translate}}" ng-model="query" ng-model-options="{debounce: 600}">
</div>
</form>
<table class="table table-striped">
<thead>
<tr>
<th class="text-right nowrap">#</th>
<th class="nowrap"><a ng-click="updateOrded('name')" href=""><i class="fa fa-caret-up"></i></a>{{'Name' | translate}}<a ng-click="updateOrded('-name')" href=""><i class="fa fa-caret-down"></i></a> <a ng-click="updateOrded('surname')" href=""><i class="fa fa-caret-up"></i></a>{{'Surname' | translate}}<a ng-click="updateOrded('-surname')" href=""><i class="fa fa-caret-down"></i></a> <a ng-click="updateOrded('email')" href=""><i class="fa fa-caret-up"></i></a>{{'Email' | translate}}<a ng-click="updateOrded('-email')" href=""><i class="fa fa-caret-down"></i></a></th>
<th class="text-center nowrap"><strong>{{'Event' | translate}}</strong></th>
<th class="text-center nowrap"><strong>{{'Attended' | translate}}</strong></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="ticket in tickets | splittedFilter:query | orderBy:ticketsOrderProp">
<td class="text-right">{{$index+1}}</td>
<td>
<span><strong><a ui-sref="event.ticket.edit({id: ticket.event_id, ticket_id: ticket._id})"><span>{{ticket.name}}</span>&nbsp;<span>{{ticket.surname}}</span></a></strong></span><span ng-if="ticket.email">&nbsp;&lt;{{ticket.email}}&gt;</span>
<p ng-if="ticket.company || ticket.job_title"><i ng-if="ticket.job_title">{{ticket.job_title}}</i><span ng-if="ticket.company && ticket.job_title">&nbsp;@&nbsp;</span><i ng-if="ticket.company">{{ticket.company}}</i></p>
</td>
<td class="text-center">
<a ui-sref="event.view({id: ticket.event_id})">{{ticket.event_title}}</a>
</td>
<td class="text-center">
<span class="fa fa-lg {{(ticket.attended) && 'fa-check-circle text-success' || 'fa-times-circle text-danger'}}"></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View file

@ -0,0 +1,74 @@
<!-- show details of a User -->
<div class="container">
<div class="panel panel-primary table-striped">
<div class="panel-heading">
<h1>
{{user.username}}
<span ng-if="user.email">&nbsp;&lt;{{user.email}}&gt;</span>
&nbsp;-&nbsp;{{'update user information' | translate}}
</h1>
</div>
<div class="panel-body">
<form method="POST">
<div class="input-group input-group-lg top10">
<span class="input-group-addon min150">{{'Email' | translate}}</span>
<input type="email" id="new-email" name="new-email" ng-model="updateUserInfo.email" class="form-control">
</div>
<div class="input-group input-group-lg top10">
<span class="input-group-addon min150">{{'Old password' | translate}}</span>
<input type="password" id="old-password" name="old-password" ng-model="updateUserInfo.old_password" class="form-control">
<span class="input-group-addon min150">{{'New password' | translate}}</span>
<input type="password" id="new-password" name="new-password" ng-model="updateUserInfo.new_password" class="form-control">
</div>
<button type="submit" ng-click="updateUser()" class="btn btn-success top10">{{'update' | translate}}</button>
</form>
</div>
</div>
<div class="panel panel-primary table-striped top5">
<div class="panel-heading"><h1>{{'Tickets' | translate}}</h1></div>
<div class="panel-body">
<form class="form-inline">
<div class="form-group">
<label for="query-tickets">{{'Search:' | translate}}</label>
<input eventman-focus type="text" id="query-tickets" class="form-control" placeholder="{{'Event title' | translate}}" ng-model="query" ng-model-options="{debounce: 600}">
</div>
<div class="form-group">
<label for="tickets-order">{{'Sort by:' | translate}}</label>
<select id="tickets-order" class="form-control" ng-model="ticketsOrderProp">
<option value="title">{{'Title' | translate}}</option>
<option value="-title">{{'Title (descending)' | translate}}</option>
</select>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>{{'Ticket' | translate}}</th>
<th>{{'Event' | translate}}</th>
<th class="text-center">{{'Attended' | translate}}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="ticket in (user.tickets || []) | splittedFilter:query | orderBy:ticketsOrderProp">
<td>
<strong>
<a ui-sref="event.ticket.edit({id: ticket.event_id, ticket_id: ticket._id})">
<span ng-if="ticket.name || ticket.surname || ticket.email">
{{ticket.name}} {{ticket.surname}}<span ng-if="ticket.email">&nbsp;&lt;{{ticket.email}}&gt;</span>
</span>
<span ng-if="!(ticket.name || ticket.surname || ticket.email)">{{ticket.event_title}}</span>
</a>
</strong>
</td>
<td><strong>{{ticket.event_title}}</strong></td>
<td class="text-center">
<span class="fa fa-lg {{(ticket.attended) && 'fa-check-circle text-success' || 'fa-times-circle text-danger'}}"></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View file

@ -0,0 +1,2 @@
<!-- main view for User -->
<div ui-view></div>

View file

@ -0,0 +1,56 @@
<!-- show the list of Users -->
<div class="container">
<div class="panel panel-primary table-striped top5">
<div class="panel-heading">
<h1>
{{'Users' | translate}}
<button ng-click="$state.go('login')" class="btn btn-success">
<span class="fa fa-plus-circle vcenter"></span>
{{'Add user' | translate}}
</button>
</h1>
</div>
<div class="panel-body">
<form class="form-inline">
<div class="form-group">
<label for="query-users">{{'Search:' | translate}}</label>
<input userman-focus type="text" id="query-users" class="form-control" placeholder="{{'Username or email' | translate}}" ng-model="query" ng-model-options="{debounce: 600}">
</div>
<div class="form-group">
<label for="users-order">Sort by:</label>
<select id="users-order" class="form-control" ng-model="usersOrderProp">
<option value="username">{{'Username' | translate}}</option>
<option value="-username">{{'Username (descending)' | translate}}</option>
<option value="email">{{'Email' | translate}}</option>
<option value="-email">{{'Email (descending)' | translate}}</option>
</select>
</div>
</form>
<div ng-include=" 'modal-confirm-action.html' " class="hidden"></div>
<table class="table table-striped">
<thead>
<tr>
<th><strong>{{'User' | translate}}</strong></th>
<th><strong>{{'Actions' | translate}}</strong></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in users | splittedFilter:query | orderBy:usersOrderProp">
<td>
<span>
<strong>
<a ui-sref="user.edit({id: user._id})">{{user.username}}</a><span ng-if="user.email && user.email != user.username"> &lt;{{user.email}}&gt;</a>
</strong>
</span>
</td>
<td>
<button ng-if="hasPermission('user|delete')" ng-click="deleteUser(user._id)" type="button" class="btn btn-link fa fa-trash fa-lg" title="{{'Delete' | translate}}"></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View file

@ -1,9 +1,9 @@
"""Event Man(ager) database backend
"""EventMan(ager) database backend
Classes and functions used to manage events and attendees database.
Copyright 2015 Davide Alberani <da@erlug.linux.it>
RaspiBO <info@raspibo.org>
Copyright 2015-2016 Davide Alberani <da@erlug.linux.it>
RaspiBO <info@raspibo.org>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -24,7 +24,7 @@ re_objectid = re.compile(r'[0-9a-f]{24}')
_force_conversion = {
'seq_hex': str,
'persons.seq_hex': str
'tickets.seq_hex': str
}
@ -43,13 +43,6 @@ def convert_obj(obj):
return ObjectId(obj)
except:
pass
try:
i_obj = int(obj)
if i_obj > 2**64 - 1:
return obj
return i_obj
except:
pass
return obj

Binary file not shown.

View file

@ -1,9 +1,9 @@
#!/usr/bin/env python
"""print_label.py - print a label with the name, the company and the person_id (in a barcode) of an attendee
"""print_label.py - print a label with the name, the company and SEQ_HEX (in a barcode) of an attendee
Copyright 2015 Emiliano Mattioli <oloturia AT gmail.com>
Davide Alberani <da@erlug.linux.it>
RaspiBO <info@raspibo.org>
Copyright 2015-2016 Emiliano Mattioli <oloturia AT gmail.com>
Davide Alberani <da@erlug.linux.it>
RaspiBO <info@raspibo.org>
Licensed under the Apache License 2.0
"""

View file

@ -0,0 +1,5 @@
attends.d triggers
==================
Put here the scripts that you want to run when a person is marked as an attendee.

View file

@ -0,0 +1,5 @@
create_ticket_in_event.d triggers
=================================
Put here the scripts that you want to run when a ticket is created.

View file

@ -0,0 +1,5 @@
delete_ticket_in_event.d triggers
=================================
Put here the scripts that you want to run when a ticket is deleted.

View file

@ -0,0 +1,5 @@
update_ticket_in_event.d triggers
=================================
Put here the scripts that you want to run when a ticket is updated.

View file

@ -1,7 +1,7 @@
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
As of July 2016, EventMan(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.
@ -13,9 +13,9 @@ Definitions
- **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
- **ticket**: an entry in the list of persons registered at an event (one ticket, one registered person)
- **user**: a logged in user of the EventMan(ager) web interface (not the same as "person")
- **trigger**: an action that will cause the execution of some scripts
Paths
@ -33,12 +33,10 @@ These are the paths you see in the browser (AngularJS does client-side routing:
- /#/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
- /#/users - the list of users
- /#/user/:user\_id/edit - edit an existing user (contains the list of events the user registered for)
- /#/import/persons - form used to import persons in bulk
- /#/login - login form
- /#/login - login and new user forms
- /logout - when visited, the user is logged out
@ -52,37 +50,24 @@ The paths used to communicate with the Tornado web server:
- /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
- /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
- /events/:event\_id/tickets GET - return the complete list of tickets of the event
- /events/:event\_id/tickets POST - add a new ticket to this event
- /events/:event\_id/tickets/:ticket\_id GET - return a ticket (e.g.: name, surname, ticket ID, ...)
- /events/:event\_id/tickets/:ticket\_id PUT - update a ticket (e.g.: if the ticket attended)
- /events/:event\_id/tickets/:ticket\_id DELETE - remove the entry from the list of registered tickets
- /users GET - list of users
- /users POST - create a new user
- /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
- /settings GET - settings to customize the GUI (logo, extra columns for events and tickets lists)
- /info GET - information about the current user
- /ebcsvpersons POST - csv file upload to import persons
- /login POST - log a user in
- /logout GET - 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, 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.
Also, remember that most of the paths can take query parameters that will be used as a filter, like GET /events/:event\_id/tickets?name=Mario
Permissions
@ -106,24 +91,24 @@ 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\_ticket\_in\_event**: executed every time a ticket 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\_ticket\_in\_event and attends will receive these information:
- via *environment*:
- NAME
- SURNAME
- EMAIL
- COMPANY
- JOB
- PERSON\_ID
- TICKET\_ID
- EVENT\_ID
- EVENT\_TITLE
- SEQ
- 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
- dictionary **old** with the old data of the ticket
- dictionary **new** with the new data of the ticket
- dictionary **event** with the event information
- boolean **merged**, true if the data was updated
@ -133,14 +118,12 @@ 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 native ObjectId.
events collection
-----------------
Stores information about events and persons registered for a given event.
Please notice that information about a person registered for a given event is solely taken from the event.persons entry, and not to the relative entry in the persons collection. This may change in the future (to integrate missing information), but in general it is correct that, editing (or deleting) a person, older information about the partecipation to an event is not changed.
Stores information about events and tickets.
Main field:
@ -149,30 +132,24 @@ Main field:
- begin-time
- end-date
- end-time
- persons - a list of information about registered persons (each entry is a ticket)
- persons.$.\_id
- persons.$.person\_id
- persons.$.attended
- persons.$.name
- persons.$.surname
- persons.$.email
- persons.$.company
- persons.$.job
- persons.$.ebqrcode
- persons.$.seq
- persons.$.seq\_hex
persons collection
------------------
Basic information about a person:
- persons.name
- persons.surname
- persons.email
- persons.company
- persons.job
- summary
- description
- where
- group\_id
- tickets - a list of information about tickets (each entry is a ticket)
- tickets.$.\_id
- tickets.$.ticket\_id
- tickets.$.attended
- tickets.$.name
- tickets.$.surname
- tickets.$.email
- tickets.$.company
- tickets.$.job
- tickets.$.ebqrcode
- tickets.$.seq
- tickets.$.seq\_hex
Notice that all the fields used to identiy a person (name, surname, email) depends on how you've edited the event's form.
users collection
----------------
@ -184,6 +161,51 @@ To generate the hash, use:
print utils.hash\_password('MyVerySecretPassword')
Code layout
===========
The code is so divided:
+- eventman_server.py - the Tornado Web server
+- backend.py - stuff to interact with MongoDB
+- utils.py - utilities
+- angular_app/ - the client-side web application
| |
| +- *.html - AngularJS templates
| +- Gruntfile.js - Grunt file to extract i18n strings
| +- js/*.js - AngularJS code
| |
| +- app.js - main application and routing
| +- controllers.js - controllers of the templates
| +- services.js - interaction with the web server
| +- directives.js - stuff to interact with the DOM
| +- filters.js - filtering data
| +- i18n.js - i18n
+- data/
| |
| +- triggers/
| |
| +- triggers-available/ - various trigger scripts
| +- triggers/ enabled trigger scripts
| |
| +- attends.d/ - scripts to be executed when a person is marked as an attendee
| +- create_ticket_in_event.d/ - scripts that are run when a ticket is created
| +- update_ticket_in_event.d/ - scripts that are run when a ticket is updated
| +- delete_ticket_in_event.d/ - scripts that are run when a ticket is deleted
+- ssl/ - put here your eventman_cert.pem and eventman_key.pem certs
+- static/
| |
| +- js/ - every third-party libraries (plus eventman.js with some small utils)
| +- css/ - third-party CSS (plus eventman.css)
| +- fonts/ - third-party fonts
| +- images/ - third-party images
| +- i18n/ - i18n files
+- templates/ - Tornado Web templates (not used)
+- tests/ - eeeehhhh
Most of the time you have to edit something in angular\_app/js/ (for the logic; especially controllers.js and services.js), angular\_app/*.html (for the presentation) or eventman\_server.py for the backend.
Coding style and conventions
============================

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python
"""Event Man(ager)
"""EventMan(ager)
Your friendly manager of attendees at an event.
@ -46,9 +46,12 @@ API_VERSION = '1.0'
re_env_key = re.compile('[^A-Z_]+')
re_slashes = re.compile(r'//+')
# Keep track of WebSocket connections.
_ws_clients = {}
def authenticated(method):
"""Decorator to handle authentication."""
"""Decorator to handle forced authentication."""
original_wrapper = tornado.web.authenticated(method)
@tornado.web.functools.wraps(method)
def my_wrapper(self, *args, **kwargs):
@ -85,10 +88,11 @@ class BaseHandler(tornado.web.RequestHandler):
"""Base class for request handlers."""
permissions = {
'event|read': True,
'event:tickets|all': True,
'event:tickets|read': True,
'event:tickets|create': True,
'event:tickets|update': True,
'event:tickets-all|create': True,
'events|read': True,
'persons|create': True,
'users|create': True
}
@ -124,6 +128,8 @@ class BaseHandler(tornado.web.RequestHandler):
'true': True
}
_re_split_salt = re.compile(r'\$(?P<salt>.+)\$(?P<hash>.+)')
def write_error(self, status_code, **kwargs):
"""Default error handler."""
if isinstance(kwargs.get('exc_info', (None, None))[1], BaseException):
@ -200,8 +206,40 @@ class BaseHandler(tornado.web.RequestHandler):
return collection_permission(permission)
return False
def user_authorized(self, username, password):
"""Check if a combination of username/password is valid.
:param username: username or email
:type username: str
:param password: password
:type password: str
:returns: tuple like (bool_user_is_authorized, dict_user_info)
:rtype: dict"""
query = [{'username': username}, {'email': username}]
res = self.db.query('users', query)
if not res:
return (False, {})
user = res[0]
db_password = user.get('password') or ''
if not db_password:
return (False, {})
match = self._re_split_salt.match(db_password)
if not match:
return (False, {})
salt = match.group('salt')
if utils.hash_password(password, salt=salt) == db_password:
return (True, user)
return (False, {})
def build_error(self, message='', status=400):
"""Build and write an error message."""
"""Build and write an error message.
:param message: textual message
:type message: str
:param status: HTTP status code
:type status: int
"""
self.set_status(status)
self.write({'error': True, 'message': message})
@ -217,16 +255,12 @@ class RootHandler(BaseHandler):
angular_app_path = os.path.join(os.path.dirname(__file__), "angular_app")
@gen.coroutine
@authenticated
def get(self, *args, **kwargs):
# serve the ./angular_app/index.html file
with open(self.angular_app_path + "/index.html", 'r') as fd:
self.write(fd.read())
# Keep track of WebSocket connections.
_ws_clients = {}
class CollectionHandler(BaseHandler):
"""Base class for handlers that need to interact with the database backend.
@ -297,7 +331,7 @@ class CollectionHandler(BaseHandler):
return filtered
def _clean_dict(self, data):
"""Filter a dictionary (in place) to remove unwanted keywords.
"""Filter a dictionary (in place) to remove unwanted keywords in db queries.
:param data: dictionary to clean
:type data: dict"""
@ -399,6 +433,7 @@ class CollectionHandler(BaseHandler):
output = self.apply_filter(output, 'get_%s' % resource)
self.write(output)
return
return self.build_error(status=404, message='unable to access resource: %s' % resource)
if id_ is not None:
permission = '%s|%s' % (self.document, crud_method)
if not self.has_permission(permission):
@ -415,7 +450,8 @@ class CollectionHandler(BaseHandler):
newData = self.apply_filter(newData, '%s_all' % method)
self.write(newData)
# PUT (update an existing document) is handled by the POST (create a new document) method
# PUT (update an existing document) is handled by the POST (create a new document) method;
# in subclasses you can always separate sub-resources handlers like handle_post_tickets and handle_put_tickets
put = post
@gen.coroutine
@ -430,6 +466,7 @@ class CollectionHandler(BaseHandler):
if method and callable(method):
self.write(method(id_, resource_id, **kwargs))
return
return self.build_error(status=404, message='unable to access resource: %s' % resource)
if id_:
permission = '%s|delete' % self.document
if not self.has_permission(permission):
@ -503,7 +540,11 @@ class CollectionHandler(BaseHandler):
def build_ws_url(self, path, proto='ws', host=None):
"""Return a WebSocket url from a path."""
return 'ws://127.0.0.1:%s/ws/%s' % (self.listen_port + 1, path)
try:
args = '?uuid=%s' % self.get_argument('uuid')
except:
args = ''
return 'ws://127.0.0.1:%s/ws/%s%s' % (self.listen_port + 1, path, args)
@gen.coroutine
def send_ws_message(self, path, message):
@ -522,61 +563,35 @@ class CollectionHandler(BaseHandler):
self.logger.error('Error yielding WebSocket message: %s', e)
class PersonsHandler(CollectionHandler):
"""Handle requests for Persons."""
document = 'person'
collection = 'persons'
def handle_get_events(self, id_, resource_id=None, **kwargs):
# Get a list of events attended by this person.
# Inside the data of each event, a 'person_data' dictionary is
# created, duplicating the entry for the current person (so that
# there's no need to parse the 'persons' list on the client).
#
# If resource_id is given, only the specified event is considered.
#
# If the 'all' parameter is given, every event (also unattended ones) is returned.
args = self.request.arguments
query = {}
if id_ and not self.tobool(args.get('all')):
query = {'persons.person_id': id_}
if resource_id:
query['_id'] = resource_id
events = self.db.query('events', query)
for event in events:
person_data = {}
for persons in event.get('persons') or []:
if str(persons.get('person_id')) == id_:
person_data = persons
break
if 'persons' in event:
del event['persons']
event['person_data'] = person_data
if resource_id and events:
return events[0]
return {'events': events}
class EventsHandler(CollectionHandler):
"""Handle requests for Events."""
document = 'event'
collection = 'events'
def filter_get(self, output):
if not self.has_permission('persons-all|read'):
if 'persons' in output:
output['persons'] = []
if not self.has_permission('tickets-all|read'):
if 'tickets' in output:
output['tickets'] = []
return output
def filter_get_all(self, output):
if not self.has_permission('persons-all|read'):
if not self.has_permission('tickets-all|read'):
for event in output.get('events') or []:
if 'persons' in event:
event['persons'] = []
if 'tickets' in event:
event['tickets'] = []
return output
def filter_input_post(self, data):
# Auto-generate the group_id, if missing.
if 'group_id' not in data:
data['group_id'] = self.gen_id()
return data
filter_input_post_all = filter_input_post
filter_input_put = filter_input_post
def filter_input_post_tickets(self, data):
# Avoid users to be able to auto-update their 'attendee' status.
if not self.has_permission('event|update'):
if 'attended' in data:
del data['attended']
@ -584,134 +599,180 @@ class EventsHandler(CollectionHandler):
filter_input_put_tickets = filter_input_post_tickets
def _get_person_data(self, person_id_or_query, persons):
"""Filter a list of persons returning the first item with a given person_id
or which set of keys specified in a dictionary match their respective values."""
for person in persons:
if isinstance(person_id_or_query, dict):
if all(person.get(k) == v for k, v in person_id_or_query.iteritems()):
return person
else:
if str(person.get('person_id')) == person_id_or_query:
return person
return {}
def handle_get_persons(self, id_, resource_id=None, match_query=None):
# Return every person registered at this event, or the information
# about a specific person.
query = {'_id': id_}
event = self.db.query('events', query)[0]
if match_query is None:
match_query = resource_id
if resource_id:
return {'person': self._get_person_data(match_query, event.get('persons') or [])}
persons = self._filter_results(event.get('persons') or [], self.arguments)
def handle_get_group_persons(self, id_, resource_id=None):
persons = []
this_query = {'_id': id_}
this_event = self.db.query('events', this_query)[0]
group_id = this_event.get('group_id')
if group_id is None:
return {'persons': persons}
this_persons = [p for p in (this_event.get('tickets') or []) if not p.get('cancelled')]
this_emails = filter(None, [p.get('email') for p in this_persons])
all_query = {'group_id': group_id}
events = self.db.query('events', all_query)
for event in events:
if id_ is not None and str(event.get('_id')) == id_:
continue
persons += [p for p in (event.get('tickets') or []) if p.get('email') and p.get('email') not in this_emails]
return {'persons': persons}
def handle_get_tickets(self, id_, resource_id=None):
if resource_id is None and not self.has_permission('event:tickets|all'):
return self.build_error(status=401, message='insufficient permissions: event:tickets|all')
return self.handle_get_persons(id_, resource_id, {'_id': resource_id})
def _get_ticket_data(self, ticket_id_or_query, tickets):
"""Filter a list of tickets returning the first item with a given _id
or which set of keys specified in a dictionary match their respective values."""
for ticket in tickets:
if isinstance(ticket_id_or_query, dict):
if all(ticket.get(k) == v for k, v in ticket_id_or_query.iteritems()):
return ticket
else:
if str(ticket.get('_id')) == ticket_id_or_query:
return ticket
return {}
def handle_post_persons(self, id_, person_id, data):
# Add a person to the list of persons registered at this event.
def handle_get_tickets(self, id_, resource_id=None):
# Return every ticket registered at this event, or the information
# about a specific ticket.
query = {'_id': id_}
event = self.db.query('events', query)[0]
if resource_id:
return {'ticket': self._get_ticket_data(resource_id, event.get('tickets') or [])}
tickets = self._filter_results(event.get('tickets') or [], self.arguments)
return {'tickets': tickets}
def handle_post_tickets(self, id_, resource_id, data):
uuid, arguments = self.uuid_arguments
self._clean_dict(data)
data['seq'] = self.get_next_seq('event_%s_persons' % id_)
data['seq'] = self.get_next_seq('event_%s_tickets' % id_)
data['seq_hex'] = '%06X' % data['seq']
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']
data['_id'] = ticket_id = self.gen_id()
ret = {'action': 'add', 'ticket': data, 'uuid': uuid}
merged, doc = self.db.update('events',
{'_id': id_},
{'tickets': data},
operation='appendUnique',
create=False)
if doc:
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',
{'_id': id_},
{'persons': data},
operation='appendUnique',
create=False)
ticket = self._get_ticket_data(ticket_id, doc.get('tickets') or [])
env = self._dict2env(ticket)
env.update({'PERSON_ID': ticket_id, 'TICKED_ID': ticket_id, 'EVENT_ID': id_,
'EVENT_TITLE': doc.get('title', ''), 'WEB_USER': self.current_user,
'WEB_REMOTE_IP': self.request.remote_ip})
stdin_data = {'new': ticket,
'event': doc,
'merged': merged
}
self.run_triggers('create_ticket_in_event', stdin_data=stdin_data, env=env)
return ret
handle_post_tickets = handle_post_persons
def handle_put_persons(self, id_, person_id, data, ticket=False):
# Update an existing entry for a person registered at this event.
def handle_put_tickets(self, id_, ticket_id, data):
# Update an existing entry for a ticket registered at this event.
self._clean_dict(data)
uuid, arguments = self.uuid_arguments
query = dict([('persons.%s' % k, v) for k, v in arguments.iteritems()])
query = dict([('tickets.%s' % k, v) for k, v in arguments.iteritems()])
query['_id'] = id_
if ticket:
query['persons._id'] = person_id
person_query = {'_id': person_id}
elif person_id is not None:
query['persons.person_id'] = person_id
person_query = person_id
if ticket_id is not None:
query['tickets._id'] = ticket_id
ticket_query = {'_id': ticket_id}
else:
person_query = self.arguments
old_person_data = {}
ticket_query = self.arguments
old_ticket_data = {}
current_event = self.db.query(self.collection, query)
if current_event:
current_event = current_event[0]
else:
current_event = {}
old_person_data = self._get_person_data(person_query,
current_event.get('persons') or [])
old_ticket_data = self._get_ticket_data(ticket_query,
current_event.get('tickets') or [])
merged, doc = self.db.update('events', query,
data, updateList='persons', create=False)
new_person_data = self._get_person_data(person_query,
doc.get('persons') or [])
env = self._dict2env(new_person_data)
# always takes the person_id from the new person (it may have
# been a ticket_id).
person_id = str(new_person_data.get('person_id'))
env.update({'PERSON_ID': person_id, 'EVENT_ID': id_,
data, updateList='tickets', create=False)
new_ticket_data = self._get_ticket_data(ticket_query,
doc.get('tickets') or [])
env = self._dict2env(new_ticket_data)
# always takes the ticket_id from the new ticket
ticket_id = str(new_ticket_data.get('_id'))
env.update({'PERSON_ID': ticket_id, 'TICKED_ID': ticket_id, 'EVENT_ID': id_,
'EVENT_TITLE': doc.get('title', ''), 'WEB_USER': self.current_user,
'WEB_REMOTE_IP': self.request.remote_ip})
stdin_data = {'old': old_person_data,
'new': new_person_data,
stdin_data = {'old': old_ticket_data,
'new': new_ticket_data,
'event': doc,
'merged': merged
}
self.run_triggers('update_person_in_event', stdin_data=stdin_data, env=env)
if old_person_data and old_person_data.get('attended') != new_person_data.get('attended'):
if new_person_data.get('attended'):
self.run_triggers('update_ticket_in_event', stdin_data=stdin_data, env=env)
if old_ticket_data and old_ticket_data.get('attended') != new_ticket_data.get('attended'):
if new_ticket_data.get('attended'):
self.run_triggers('attends', stdin_data=stdin_data, env=env)
ret = {'action': 'update', 'person_id': person_id, 'person': new_person_data, 'uuid': uuid}
if old_person_data != new_person_data:
ret = {'action': 'update', '_id': ticket_id, 'ticket': new_ticket_data, 'uuid': uuid}
if old_ticket_data != new_ticket_data:
self.send_ws_message('event/%s/tickets/updates' % id_, json.dumps(ret))
return ret
def handle_put_tickets(self, id_, person_id, data):
return self.handle_put_persons(id_, person_id, data, True)
def handle_delete_persons(self, id_, person_id):
# Remove a specific person from the list of persons registered at this event.
def handle_delete_tickets(self, id_, ticket_id):
# Remove a specific ticket from the list of tickets registered at this event.
uuid, arguments = self.uuid_arguments
doc = self.db.query('events',
{'_id': id_, 'persons.person_id': person_id})
ret = {'action': 'delete', 'person_id': person_id, 'uuid': uuid}
{'_id': id_, 'tickets._id': ticket_id})
ret = {'action': 'delete', '_id': ticket_id, 'uuid': uuid}
if doc:
merged, doc = self.db.update('events',
ticket = self._get_ticket_data(ticket_id, doc[0].get('tickets') or [])
merged, rdoc = self.db.update('events',
{'_id': id_},
{'persons': {'person_id': person_id}},
{'tickets': {'_id': ticket_id}},
operation='delete',
create=False)
self.send_ws_message('event/%s/tickets/updates' % id_, json.dumps(ret))
env = self._dict2env(ticket)
env.update({'PERSON_ID': ticket_id, 'TICKED_ID': ticket_id, 'EVENT_ID': id_,
'EVENT_TITLE': rdoc.get('title', ''), 'WEB_USER': self.current_user,
'WEB_REMOTE_IP': self.request.remote_ip})
stdin_data = {'old': ticket,
'event': rdoc,
'merged': merged
}
self.run_triggers('delete_ticket_in_event', stdin_data=stdin_data, env=env)
return ret
handle_delete_tickets = handle_delete_persons
class UsersHandler(CollectionHandler):
"""Handle requests for Users."""
document = 'user'
collection = 'users'
def filter_get(self, data):
if 'password' in data:
del data['password']
if '_id' in data:
# Also add a 'tickets' list with all the tickets created by this user
tickets = []
events = self.db.query('events', {'tickets.created_by': data['_id']})
for event in events:
event_title = event.get('title') or ''
event_id = str(event.get('_id'))
evt_tickets = self._filter_results(event.get('tickets') or [], {'created_by': data['_id']})
for evt_ticket in evt_tickets:
evt_ticket['event_title'] = event_title
evt_ticket['event_id'] = event_id
tickets.extend(evt_tickets)
data['tickets'] = tickets
return data
def filter_get_all(self, data):
if 'users' not in data:
return data
for user in data['users']:
if 'password' in user:
del user['password']
return data
@gen.coroutine
@authenticated
def get(self, id_=None, resource=None, resource_id=None, acl=True, **kwargs):
if id_ is not None:
if (self.has_permission('user|read') or str(self.current_user_info.get('_id')) == id_):
acl = False
super(UsersHandler, self).get(id_, resource, resource_id, acl=acl, **kwargs)
def filter_input_post_all(self, data):
username = (data.get('username') or '').strip()
password = (data.get('password') or '').strip()
@ -724,9 +785,34 @@ class UsersHandler(CollectionHandler):
return {'username': username, 'password': utils.hash_password(password),
'email': email, '_id': self.gen_id()}
def filter_input_put(self, data):
old_pwd = data.get('old_password')
new_pwd = data.get('new_password')
if old_pwd is not None:
del data['old_password']
if new_pwd is not None:
del data['new_password']
authorized, user = self.user_authorized(data['username'], old_pwd)
if not (self.has_permission('user|update') or (authorized and self.current_user == data['username'])):
raise InputException('not authorized to change password')
data['password'] = utils.hash_password(new_pwd)
if '_id' in data:
# Avoid overriding _id
del data['_id']
return data
@gen.coroutine
@authenticated
def put(self, id_=None, resource=None, resource_id=None, **kwargs):
if id_ is None:
return self.build_error(status=404, message='unable to access the resource')
if not (self.has_permission('user|update') or str(self.current_user_info.get('_id')) == id_):
return self.build_error(status=401, message='insufficient permissions: user|update or current user')
super(UsersHandler, self).put(id_, resource, resource_id, **kwargs)
class EbCSVImportPersonsHandler(BaseHandler):
"""Importer for CSV files exported from eventbrite."""
"""Importer for CSV files exported from Eventbrite."""
csvRemap = {
'Nome evento': 'event_title',
'ID evento': 'event_id',
@ -756,11 +842,8 @@ class EbCSVImportPersonsHandler(BaseHandler):
'Email': 'email',
'Attendee #': 'attendee_nr',
'Barcode #': 'ebqrcode',
'Company': 'company',
'Company': 'company'
}
# Only these information are stored in the person collection.
keepPersonData = ('name', 'surname', 'email', 'name_title', 'name_suffix',
'company', 'job_title')
@gen.coroutine
@authenticated
@ -768,11 +851,13 @@ class EbCSVImportPersonsHandler(BaseHandler):
# import a CSV list of persons
event_handler = EventsHandler(self.application, self.request)
event_handler.db = self.db
targetEvent = None
event_id = None
try:
targetEvent = self.get_body_argument('targetEvent')
event_id = self.get_body_argument('targetEvent')
except:
pass
if event_id is None:
return self.build_error('invalid event')
reply = dict(total=0, valid=0, merged=0, new_in_event=0)
for fieldname, contents in self.request.files.iteritems():
for content in contents:
@ -781,25 +866,10 @@ class EbCSVImportPersonsHandler(BaseHandler):
reply['total'] += parseStats['total']
reply['valid'] += parseStats['valid']
for person in persons:
person_data = dict([(k, person[k]) for k in self.keepPersonData
if k in person])
merged, stored_person = self.db.update('persons',
[('email', 'name', 'surname')],
person_data)
if merged:
reply['merged'] += 1
if targetEvent and stored_person:
event_id = targetEvent
person_id = stored_person['_id']
registered_data = {
'person_id': person_id,
'attended': False,
'from_file': filename}
person.update(registered_data)
if not self.db.query('events',
{'_id': event_id, 'persons.person_id': person_id}):
event_handler.handle_post_persons(event_id, person_id, person)
reply['new_in_event'] += 1
person['attended'] = False
person['from_file'] = filename
event_handler.handle_post_persons(event_id, None, person)
reply['new_in_event'] += 1
self.write(reply)
@ -807,87 +877,72 @@ class SettingsHandler(BaseHandler):
"""Handle requests for Settings."""
@gen.coroutine
@authenticated
def get(self, **kwds):
def get(self, **kwargs):
query = self.arguments_tobool()
settings = self.db.query('settings', query)
self.write({'settings': settings})
class InfoHandler(BaseHandler):
"""Handle requests for Info."""
"""Handle requests for information about the logged in user."""
@gen.coroutine
@authenticated
def get(self, **kwds):
def get(self, **kwargs):
info = {}
user_info = self.current_user_info
if user_info:
info['user'] = user_info
info['authentication_required'] = self.authentication
self.write({'info': info})
class WebSocketEventUpdatesHandler(tornado.websocket.WebSocketHandler):
"""Manage websockets."""
"""Manage WebSockets."""
def _clean_url(self, url):
return re_slashes.sub('/', url)
url = re_slashes.sub('/', url)
ridx = url.rfind('?')
if ridx != -1:
url = url[:ridx]
return url
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))
def open(self, event_id, *args, **kwargs):
self.uuid = self.get_argument('uuid')
url = self._clean_url(self.request.uri)
logging.debug('WebSocketEventUpdatesHandler.on_open event_id:%s url:%s' % (event_id, url))
_ws_clients.setdefault(url, {})
if self.uuid not in _ws_clients[url]:
_ws_clients[url][self.uuid] = self
logging.debug('WebSocketEventUpdatesHandler.on_open %s clients connected' % len(_ws_clients[url]))
def on_message(self, message):
logging.debug('WebSocketEventUpdatesHandler.on_message')
url = self._clean_url(self.request.uri)
logging.debug('WebSocketEventUpdatesHandler.on_message url:%s' % url)
count = 0
for client in _ws_clients.get(self._clean_url(self.request.uri), []):
if client == self:
_to_delete = set()
for uuid, client in _ws_clients.get(url, {}).iteritems():
try:
client.write_message(message)
except:
_to_delete.add(uuid)
continue
client.write_message(message)
count += 1
for uuid in _to_delete:
try:
del _ws_clients[url][uuid]
except KeyError:
pass
logging.debug('WebSocketEventUpdatesHandler.on_message sent message to %d clients' % count)
def on_close(self):
logging.debug('WebSocketEventUpdatesHandler.on_close')
try:
if self in _ws_clients.get(self._clean_url(self.request.uri), []):
_ws_clients[self._clean_url(self.request.uri)].remove(self)
except Exception as e:
logging.warn('WebSocketEventUpdatesHandler.on_close error closing websocket: %s', str(e))
class LoginHandler(BaseHandler):
class LoginHandler(RootHandler):
"""Handle user authentication requests."""
re_split_salt = re.compile(r'\$(?P<salt>.+)\$(?P<hash>.+)')
@gen.coroutine
def get(self, **kwds):
def get(self, **kwargs):
# show the login page
if self.is_api():
self.set_status(401)
self.write({'error': True,
'message': 'authentication required'})
else:
with open(self.angular_app_path + "/login.html", 'r') as fd:
self.write(fd.read())
def _authorize(self, username, password, email=None):
"""Return True is this username/password is valid."""
query = [{'username': username}]
if email is not None:
query.append({'email': email})
res = self.db.query('users', query)
if not res:
return False
user = res[0]
db_password = user.get('password') or ''
if not db_password:
return False
match = self.re_split_salt.match(db_password)
if not match:
return False
salt = match.group('salt')
if utils.hash_password(password, salt=salt) == db_password:
return True
return False
@gen.coroutine
def post(self, *args, **kwargs):
@ -903,7 +958,9 @@ class LoginHandler(BaseHandler):
self.set_status(401)
self.write({'error': True, 'message': 'missing username or password'})
return
if self._authorize(username, password):
authorized, user = self.user_authorized(username, password)
if authorized and user.get('username'):
username = user['username']
logging.info('successful login for user %s' % username)
self.set_secure_cookie("user", username)
self.write({'error': False, 'message': 'successful login'})
@ -916,7 +973,7 @@ class LoginHandler(BaseHandler):
class LogoutHandler(BaseHandler):
"""Handle user logout requests."""
@gen.coroutine
def get(self, **kwds):
def get(self, **kwargs):
# log the user out
logging.info('logout')
self.logout()
@ -950,10 +1007,14 @@ def run():
if options.debug:
logger.setLevel(logging.DEBUG)
ssl_options = {}
if os.path.isfile(options.ssl_key) and os.path.isfile(options.ssl_cert):
ssl_options = dict(certfile=options.ssl_cert, keyfile=options.ssl_key)
# database backend connector
db_connector = backend.EventManDB(url=options.mongo_url, dbName=options.db_name)
init_params = dict(db=db_connector, data_dir=options.data_dir, listen_port=options.port,
authentication=options.authentication, logger=logger)
authentication=options.authentication, logger=logger, ssl_options=ssl_options)
# If not present, we store a user 'admin' with password 'eventman' into the database.
if not db_connector.query('users', {'username': 'admin'}):
@ -972,12 +1033,9 @@ def run():
{'setting': 'server_cookie_secret', 'cookie_secret': cookie_secret})
_ws_handler = (r"/ws/+event/+(?P<event_id>[\w\d_-]+)/+tickets/+updates/?", WebSocketEventUpdatesHandler)
_persons_path = r"/persons/?(?P<id_>[\w\d_-]+)?/?(?P<resource>[\w\d_-]+)?/?(?P<resource_id>[\w\d_-]+)?"
_events_path = r"/events/?(?P<id_>[\w\d_-]+)?/?(?P<resource>[\w\d_-]+)?/?(?P<resource_id>[\w\d_-]+)?"
_users_path = r"/users/?(?P<id_>[\w\d_-]+)?/?(?P<resource>[\w\d_-]+)?/?(?P<resource_id>[\w\d_-]+)?"
application = tornado.web.Application([
(_persons_path, PersonsHandler, init_params),
(r'/v%s%s' % (API_VERSION, _persons_path), PersonsHandler, init_params),
(_events_path, EventsHandler, init_params),
(r'/v%s%s' % (API_VERSION, _events_path), EventsHandler, init_params),
(_users_path, UsersHandler, init_params),
@ -998,9 +1056,6 @@ def run():
cookie_secret='__COOKIE_SECRET__',
login_url='/login',
debug=options.debug)
ssl_options = {}
if os.path.isfile(options.ssl_key) and os.path.isfile(options.ssl_cert):
ssl_options = dict(certfile=options.ssl_cert, keyfile=options.ssl_key)
http_server = tornado.httpserver.HTTPServer(application, ssl_options=ssl_options or None)
logger.info('Start serving on %s://%s:%d', 'https' if ssl_options else 'http',
options.address if options.address else '127.0.0.1',
@ -1011,9 +1066,12 @@ def run():
ws_application = tornado.web.Application([_ws_handler], debug=options.debug)
ws_http_server = tornado.httpserver.HTTPServer(ws_application)
ws_http_server.listen(options.port+1, address='127.0.0.1')
logger.debug('Starting WebSocket on ws://127.0.0.1:%d', options.port+1)
logger.debug('Starting WebSocket on %s://127.0.0.1:%d', 'wss' if ssl_options else 'ws', options.port+1)
tornado.ioloop.IOLoop.instance().start()
if __name__ == '__main__':
run()
try:
run()
except KeyboardInterrupt:
print('Stop server')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View file

@ -8,11 +8,7 @@ body { padding-top: 70px; }
padding-bottom: 0px;
}
a:focus a:hover {
color: #23527c;
}
a:hover {
a:focus, a:hover {
color: #23527c;
}

View file

@ -1,9 +1,9 @@
"""Event Man(ager) utils
"""EventMan(ager) utils
Miscellaneous utilities.
Copyright 2015 Davide Alberani <da@erlug.linux.it>
RaspiBO <info@raspibo.org>
Copyright 2015-2016 Davide Alberani <da@erlug.linux.it>
RaspiBO <info@raspibo.org>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.