Merge pull request #5 from alberanid/master

documentation
This commit is contained in:
Davide Alberani 2015-03-22 11:09:04 +01:00
commit 821c6784e9
11 changed files with 282 additions and 92 deletions

View file

@ -1,7 +1,7 @@
Event Man(ager)
===============
Manage attendants at an event.
Your friendly manager of attendees at an event.
Notice
======
@ -10,11 +10,50 @@ No, this project is not ready, yet.
I'll let you know when I'm finished experimenting with it and you can contribute.
See the DEVELOPMENT.md file for more information about how to contribute.
Technological stack
===================
- [AngularJS](https://angularjs.org/) for the webApp
- [Bootstrap](http://getbootstrap.com/) (plus [jQuery](https://jquery.com/)) for the eye-candy
- [Tornado web](http://www.tornadoweb.org/) as web server
- [MongoDB](https://www.mongodb.org/) to store the data
The web part is incuded; you need to install Tornado, MongoDB and the pymongo module on your system (no configuration needed).
Coding style and conventions
============================
It's enough to be consistent within the document you're editing.
I suggest four spaces instead of tabs for all the code: Python (**mandatory**), JavaScript, HTML and CSS.
Python code documented following the [Sphinx](http://sphinx-doc.org/) syntax.
Install and run
===============
wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py
sudo pip install tornado
sudo pip install pymongo
cd
git clone https://github.com/raspibo/eventman
cd eventman
./eventman_server.py --debug
Open browser and navigate to: http://localhost:5242/
License and copyright
=====================
Copyright 2015 Davide Alberani <da@erlug.linux.it>
Copyright 2015 Davide Alberani <da@erlug.linux.it>
RaspiBO <info@raspibo.org>
Licensed under the Apache License, Version 2.0 (the "License");
@ -27,22 +66,3 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Install and run
===============
wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py
sudo pip install tornado
cd
git clone https://github.com/raspibo/eventman
cd eventman
./eventman_server.py
Open browser and navigate to: http://localhost:5242/

View file

@ -1,3 +1,4 @@
<!-- show details of a single Event (editing also take place here) -->
<div class="container-fluid">
<form ng-model="eventdetails" ng-submit="save()">
<div class="input-group input-group-lg">

View file

@ -1,28 +1,25 @@
<div class="container-fluid">
<!-- show a list of Events -->
<div class="container-fluid">
<div class="row">
<div class="col-md-10">
Search: <input ng-model="query">
Sort by:
<select ng-model="orderProp">
<option value="title">Alphabetical</option>
<option value="begin-datetime">Date</option>
</select>
</div>
<div class="col-md-10">
Search: <input ng-model="query">
Sort by:
<select ng-model="orderProp">
<option value="title">Alphabetical</option>
<option value="begin-datetime">Date</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-10">
<ul class="events">
<li ng-repeat="event in events | filter:query | orderBy:orderProp">
<span><a href="/#/events/{{event._id}}">{{event.title}}</a></span>
<p>{{event['begin-datetime']}}</p>
</li>
</ul>
</div>
<div class="col-md-10">
<ul class="events">
<li ng-repeat="event in events | filter:query | orderBy:orderProp">
<span><a href="/#/events/{{event._id}}">{{event.title}}</a></span>
<p>{{event['begin-datetime']}}</p>
</li>
</ul>
</div>
</div>
</div>
</div>

View file

@ -1,6 +1,7 @@
<!doctype html>
<html ng-app="eventManApp">
<head>
<title>Event Man(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">
@ -10,11 +11,25 @@
<script src="/js/app.js"></script>
<script src="/js/services.js"></script>
<script src="/js/controllers.js"></script>
<title>Event Man(ager)</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/bootstrap-theme.min.css" rel="stylesheet">
<link href="/static/css/eventman.css" rel="stylesheet">
</head>
<!--
Copyright 2015 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.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<body>
<div class="main-header" ng-controller="navigation as n">
@ -24,6 +39,8 @@
<input type="button" id="persons-button" ng-click="n.go('/new-person')" class="btn btn-link" value="Add Persons" />
</div>
<!-- all the magic takes place here: the content inside the next div
changes accordingly to the location you're visiting -->
<div ng-view></div>
<div class="main-footer">

23
angular_app/js/app.js vendored
View file

@ -1,3 +1,21 @@
'use strict';
/*
Copyright 2015 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.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* Register our fantastic app. */
var eventManApp = angular.module('eventManApp', [
'ngRoute',
'eventManServices',
@ -5,6 +23,7 @@ var eventManApp = angular.module('eventManApp', [
]);
/* Directive that can be used to make an input field react to the press of Enter. */
eventManApp.directive('ngEnter', function () {
return function (scope, element, attrs) {
element.bind("keydown keypress", function (event) {
@ -19,6 +38,7 @@ eventManApp.directive('ngEnter', function () {
});
/* Configure the routes. */
eventManApp.config(['$routeProvider',
function($routeProvider) {
$routeProvider.
@ -49,5 +69,6 @@ eventManApp.config(['$routeProvider',
otherwise({
redirectTo: '/events'
});
}]);
}
]);

View file

@ -1,9 +1,12 @@
'use strict';
/* Controllers */
/* Controllers; their method are available where specified with the ng-controller
* directive or for a given route (see app.js). They use some services to
* connect to the backend (see services.js). */
var eventManControllers = angular.module('eventManControllers', []);
/* A controller that can be used to navigate. */
eventManControllers.controller('navigation', ['$location',
function ($location) {
this.go = function(url) {
@ -26,11 +29,12 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', 'Event', '$routePa
if ($routeParams.id) {
$scope.event = Event.get($routeParams);
}
// store a new Event or update an existing one
$scope.save = function() {
if ($scope.event.id === undefined) {
Event.save($scope.event);
$scope.event = Event.save($scope.event);
} else {
Event.update($scope.event);
$scope.event = Event.update($scope.event);
}
};
}]
@ -50,11 +54,13 @@ eventManControllers.controller('PersonDetailsCtrl', ['$scope', 'Person', '$route
if ($routeParams.id) {
$scope.person = Person.get($routeParams);
}
// store a new Person or update an existing one
$scope.save = function() {
$scope.save = function() {
if ($scope.person.id === undefined) {
Person.save($scope.person);
$scope.person = Person.save($scope.person);
} else {
Person.update($scope.person);
$scope.person = Person.update($scope.person);
}
};
}]

View file

@ -1,5 +1,9 @@
'use strict';
/* Services that are used to interact with the backend. */
var eventManServices = angular.module('eventManServices', ['ngResource']);
eventManServices.factory('Event', ['$resource',
function($resource) {
return $resource('events/:id', {id: '@_id'}, {

View file

@ -1,3 +1,4 @@
<!-- show details of a single Person (editing also take place here) -->
<div class="container-fluid">
<form ng-model="persondetails" ng-submit="save()">
<div class="input-group input-group-lg">

View file

@ -1,28 +1,25 @@
<div class="container-fluid">
<!-- show a list of Persons -->
<div class="container-fluid">
<div class="row">
<div class="col-md-10">
Search: <input ng-model="query">
Sort by:
<select ng-model="orderProp">
<option value="name">Alphabetical</option>
<option value="id">ID</option>
</select>
</div>
<div class="col-md-10">
Search: <input ng-model="query">
Sort by:
<select ng-model="orderProp">
<option value="name">Alphabetical</option>
<option value="id">ID</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-10">
<ul class="persons">
<li ng-repeat="person in persons | filter:query | orderBy:orderProp">
<a href="/#/persons/{{person._id}}"><span>{{person.name}}</span>&nbsp;<span>{{person.surname}}</span></a>
<p>{{person.email}}</p>
</li>
</ul>
</div>
<div class="col-md-10">
<ul class="persons">
<li ng-repeat="person in persons | filter:query | orderBy:orderProp">
<a href="/#/persons/{{person._id}}"><span>{{person.name}}</span>&nbsp;<span>{{person.surname}}</span></a>
<p>{{person.email}}</p>
</li>
</ul>
</div>
</div>
</div>
</div>

View file

@ -1,6 +1,19 @@
"""Event Man(ager) backend
"""Event Man(ager) database backend
Classes and functions used to manage events and attendants.
Classes and functions used to manage events and attendees database.
Copyright 2015 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.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import pymongo
@ -8,15 +21,29 @@ from bson.objectid import ObjectId
class EventManDB(object):
"""MongoDB connector."""
db = None
connection = None
def __init__(self, url=None, dbName='eventman'):
"""Initialize the instance, connecting to the database.
:param url: URL of the database
:type url: str (or None to connect to localhost)
"""
self._url = url
self._dbName = dbName
self.connect(url)
def connect(self, url=None, dbName=None):
"""Connect to the database.
:param url: URL of the database
:type url: str (or None to connect to localhost)
:return: the database we're connected to
:rtype: :class:`~pymongo.database.Database`
"""
if self.db is not None:
return self.db
if url:
@ -28,12 +55,32 @@ class EventManDB(object):
return self.db
def get(self, collection, _id):
"""Get a single document with the specified `_id`.
:param collection: search the document in this collection
:type collection: str
:param _id: unique ID of the document
:type _id: str or :class:`~bson.objectid.ObjectId`
:return: the document with the given `_id`
:rtype: dict
"""
if not isinstance(_id, ObjectId):
_id = ObjectId(_id)
results = self.query(collection, {'_id': _id})
return results and results[0] or {}
def query(self, collection, query=None):
"""Get multiple documents matching a query.
:param collection: search for documents in this collection
:type collection: str
:param query: search for documents with those attributes
:type query: dict or None
:return: list of matching documents
:rtype: list
"""
db = self.connect()
query = query or {}
if'_id' in query and not isinstance(query['_id'], ObjectId):
@ -44,11 +91,33 @@ class EventManDB(object):
return results
def add(self, collection, data):
"""Insert a new document.
:param collection: insert the document in this collection
:type collection: str
:param data: the document to store
:type data: dict
:return: the document, as created in the database
:rtype: dict
"""
db = self.connect()
_id = db[collection].insert(data)
return self.get(collection, _id)
def update(self, collection, _id, data):
"""Update an existing document.
:param collection: update a document in this collection
:type collection: str
:param _id: unique ID of the document to be updatd
:type _id: str or :class:`~bson.objectid.ObjectId`
:param data: the updated information to store
:type data: dict
:return: the document, after the update
:rtype: dict
"""
db = self.connect()
data = data or {}
if '_id' in data:
@ -56,3 +125,20 @@ class EventManDB(object):
db[collection].update({'_id': ObjectId(_id)}, {'$set': data})
return self.get(collection, _id)
def delete(self, collection, _id_or_query=None, force=False):
"""Remove one or more documents from a collection.
:param collection: search the documents in this collection
:type collection: str
:param _id_or_query: unique ID of the document or query to match multiple documents
:type _id_or_query: str or :class:`~bson.objectid.ObjectId` or dict
:param force: force the deletion of all documents, when `_id_or_query` is empty
:type force: bool
"""
if not _id_or_query and not force:
return
db = self.connect()
if not isinstance(_id_or_query, (ObjectId, dict)):
_id_or_query = ObjectId(_id_or_query)
db[collection].remove(_id_or_query)

View file

@ -1,7 +1,20 @@
#!/usr/bin/env python
"""Event Man(ager)
Your friendly manager of attendants at a conference.
Your friendly manager of attendees at an event.
Copyright 2015 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.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import os
@ -18,58 +31,84 @@ from tornado import gen, escape
import backend
class BaseHandler(tornado.web.RequestHandler):
def initialize(self, **kwargs):
for key, value in kwargs.iteritems():
setattr(self, key, value)
class RootHandler(BaseHandler):
angular_app_path = os.path.join(os.path.dirname(__file__), "angular_app")
@gen.coroutine
def get(self):
with open(self.angular_app_path + "/index.html", 'r') as fd:
self.write(fd.read())
class ImprovedEncoder(json.JSONEncoder):
"""Enhance the default JSON encoder to serialize datetime objects."""
def default(self, o):
if isinstance(o, datetime.datetime):
if isinstance(o, (datetime.datetime, datetime.date,
datetime.time, datetime.timedelta)):
return str(o)
return json.JSONEncoder.default(self, o)
json._default_encoder = ImprovedEncoder()
class BaseHandler(tornado.web.RequestHandler):
"""Base class for request handlers."""
def initialize(self, **kwargs):
"""Add every passed (key, value) as attributes of the instance."""
for key, value in kwargs.iteritems():
setattr(self, key, value)
class RootHandler(BaseHandler):
"""Handler for the / path."""
angular_app_path = os.path.join(os.path.dirname(__file__), "angular_app")
@gen.coroutine
def get(self):
# serve the ./angular_app/index.html file
with open(self.angular_app_path + "/index.html", 'r') as fd:
self.write(fd.read())
class CollectionHandler(BaseHandler):
"""Base class for handlers that need to interact with the database backend.
Introduce basic CRUD operations."""
# set of documents we're managing (a collection in MongoDB or a table in a SQL database)
collection = None
@gen.coroutine
def get(self, id_=None):
if id_ is not None:
# read a single document
self.write(self.db.get(self.collection, id_))
else:
# return an object containing the list of all objects in the collection;
# e.g.: {'events': [{'_id': 'obj1-id, ...}, {'_id': 'obj2-id, ...}, ...]}
# Please, never return JSON lists that are not encapsulated in an object,
# to avoid XSS vulnerabilities.
self.write({self.collection: self.db.query(self.collection)})
@gen.coroutine
def post(self, id_=None, **kwargs):
data = escape.json_decode(self.request.body or {})
if id_ is None:
# insert a new document
newData = self.db.add(self.collection, data)
else:
# update an existing document
newData = self.db.update(self.collection, id_, data)
self.write(newData)
# PUT is handled by the POST method
put = post
class PersonsHandler(CollectionHandler):
"""Handle requests for Persons."""
collection = 'persons'
class EventsHandler(CollectionHandler):
"""Handle requests for Events."""
collection = 'events'
def main():
def run():
"""Run the Tornado web application."""
# command line arguments; can also be written in a configuration file,
# specified with the --config argument.
define("port", default=5242, help="run on the given port", type=int)
define("data", default=os.path.join(os.path.dirname(__file__), "data"),
help="specify the directory used to store the data")
@ -82,6 +121,7 @@ def main():
callback=lambda path: tornado.options.parse_config_file(path, final=False))
tornado.options.parse_command_line()
# database backend connector
db_connector = backend.EventManDB(url=options.mongodbURL, dbName=options.dbName)
init_params = dict(db=db_connector)
@ -100,5 +140,5 @@ def main():
if __name__ == '__main__':
main()
run()