initial commit
This commit is contained in:
commit
30673a1f68
17 changed files with 1526 additions and 0 deletions
18
README.md
Normal file
18
README.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# ibt2
|
||||
|
||||
> I'll be there, 2
|
||||
|
||||
## Build Setup
|
||||
|
||||
``` bash
|
||||
# install dependencies
|
||||
npm install
|
||||
|
||||
# serve with hot reload at localhost:8080
|
||||
npm run dev
|
||||
|
||||
# build for production with minification
|
||||
npm run build
|
||||
```
|
||||
|
||||
For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
|
6
config/dev.env.js
Normal file
6
config/dev.env.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
var merge = require('webpack-merge')
|
||||
var prodEnv = require('./prod.env')
|
||||
|
||||
module.exports = merge(prodEnv, {
|
||||
NODE_ENV: '"development"'
|
||||
})
|
32
config/index.js
Normal file
32
config/index.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
// see http://vuejs-templates.github.io/webpack for documentation.
|
||||
var path = require('path')
|
||||
|
||||
module.exports = {
|
||||
build: {
|
||||
env: require('./prod.env'),
|
||||
index: path.resolve(__dirname, '../dist/index.html'),
|
||||
assetsRoot: path.resolve(__dirname, '../dist'),
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
productionSourceMap: true,
|
||||
// Gzip off by default as many popular static hosts such as
|
||||
// Surge or Netlify already gzip all static assets for you.
|
||||
// Before setting to `true`, make sure to:
|
||||
// npm install --save-dev compression-webpack-plugin
|
||||
productionGzip: false,
|
||||
productionGzipExtensions: ['js', 'css']
|
||||
},
|
||||
dev: {
|
||||
env: require('./dev.env'),
|
||||
port: 8080,
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
proxyTable: {},
|
||||
// CSS Sourcemaps off by default because relative paths are "buggy"
|
||||
// with this option, according to the CSS-Loader README
|
||||
// (https://github.com/webpack/css-loader#sourcemaps)
|
||||
// In our experience, they generally work as expected,
|
||||
// just be aware of this issue when enabling this option.
|
||||
cssSourceMap: false
|
||||
}
|
||||
}
|
3
config/prod.env.js
Normal file
3
config/prod.env.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
705
ibt2.py
Executable file
705
ibt2.py
Executable file
|
@ -0,0 +1,705 @@
|
|||
#!/usr/bin/env python
|
||||
"""I'll Be There 2 (ibt2) - an oversimplified attendees registration system.
|
||||
|
||||
Copyright 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.
|
||||
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
|
||||
import re
|
||||
import string
|
||||
import logging
|
||||
import datetime
|
||||
from operator import itemgetter
|
||||
import itertools
|
||||
|
||||
import tornado.httpserver
|
||||
import tornado.ioloop
|
||||
import tornado.options
|
||||
from tornado.options import define, options
|
||||
import tornado.web
|
||||
from tornado import gen, escape
|
||||
|
||||
import utils
|
||||
import monco
|
||||
|
||||
ENCODING = 'utf-8'
|
||||
PROCESS_TIMEOUT = 60
|
||||
|
||||
API_VERSION = '1.0'
|
||||
|
||||
re_env_key = re.compile('[^A-Z_]+')
|
||||
re_slashes = re.compile(r'//+')
|
||||
|
||||
|
||||
class BaseException(Exception):
|
||||
"""Base class for ibt2 custom exceptions.
|
||||
|
||||
:param message: text message
|
||||
:type message: str
|
||||
:param status: numeric http status code
|
||||
:type status: int"""
|
||||
def __init__(self, message, status=400):
|
||||
super(BaseException, self).__init__(message)
|
||||
self.message = message
|
||||
self.status = status
|
||||
|
||||
|
||||
class InputException(BaseException):
|
||||
"""Exception raised by errors in input handling."""
|
||||
pass
|
||||
|
||||
|
||||
class BaseHandler(tornado.web.RequestHandler):
|
||||
"""Base class for request handlers."""
|
||||
permissions = {
|
||||
'day|read': True,
|
||||
'day:groups|read': True,
|
||||
'day:groups|create': True,
|
||||
'day:groups|update': True,
|
||||
'day:groups-all|read': True,
|
||||
'day:groups-all|create': True,
|
||||
'days|read': True,
|
||||
'days|create': True,
|
||||
'users|create': True
|
||||
}
|
||||
|
||||
# Cache currently connected users.
|
||||
_users_cache = {}
|
||||
|
||||
# A property to access the first value of each argument.
|
||||
arguments = property(lambda self: dict([(k, v[0])
|
||||
for k, v in self.request.arguments.iteritems()]))
|
||||
|
||||
_bool_convert = {
|
||||
'0': False,
|
||||
'n': False,
|
||||
'f': False,
|
||||
'no': False,
|
||||
'off': False,
|
||||
'false': False,
|
||||
'1': True,
|
||||
'y': True,
|
||||
't': True,
|
||||
'on': True,
|
||||
'yes': True,
|
||||
'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):
|
||||
exc = kwargs['exc_info'][1]
|
||||
status_code = exc.status
|
||||
message = exc.message
|
||||
else:
|
||||
message = 'internal error'
|
||||
self.build_error(message, status=status_code)
|
||||
|
||||
def is_api(self):
|
||||
"""Return True if the path is from an API call."""
|
||||
return self.request.path.startswith('/v%s' % API_VERSION)
|
||||
|
||||
def tobool(self, obj):
|
||||
"""Convert some textual values to boolean."""
|
||||
if isinstance(obj, (list, tuple)):
|
||||
obj = obj[0]
|
||||
if isinstance(obj, (str, unicode)):
|
||||
obj = obj.lower()
|
||||
return self._bool_convert.get(obj, obj)
|
||||
|
||||
def arguments_tobool(self):
|
||||
"""Return a dictionary of arguments, converted to booleans where possible."""
|
||||
return dict([(k, self.tobool(v)) for k, v in self.arguments.iteritems()])
|
||||
|
||||
def initialize(self, **kwargs):
|
||||
"""Add every passed (key, value) as attributes of the instance."""
|
||||
for key, value in kwargs.iteritems():
|
||||
setattr(self, key, value)
|
||||
|
||||
@property
|
||||
def current_user(self):
|
||||
"""Retrieve current user name from the secure cookie."""
|
||||
return self.get_secure_cookie("user")
|
||||
|
||||
@property
|
||||
def current_user_info(self):
|
||||
"""Information about the current user, including their permissions."""
|
||||
current_user = self.current_user
|
||||
if current_user in self._users_cache:
|
||||
return self._users_cache[current_user]
|
||||
permissions = set([k for (k, v) in self.permissions.iteritems() if v is True])
|
||||
user_info = {'permissions': permissions}
|
||||
if current_user:
|
||||
user_info['username'] = current_user
|
||||
res = self.db.query('users', {'username': current_user})
|
||||
if res:
|
||||
user = res[0]
|
||||
user_info = user
|
||||
permissions.update(set(user.get('permissions') or []))
|
||||
user_info['permissions'] = permissions
|
||||
self._users_cache[current_user] = user_info
|
||||
return user_info
|
||||
|
||||
def has_permission(self, permission):
|
||||
"""Check permissions of the current user.
|
||||
|
||||
:param permission: the permission to check
|
||||
:type permission: str
|
||||
|
||||
:returns: True if the user is allowed to perform the action or False
|
||||
:rtype: bool
|
||||
"""
|
||||
user_info = self.current_user_info or {}
|
||||
user_permissions = user_info.get('permissions') or []
|
||||
global_permission = '%s|all' % permission.split('|')[0]
|
||||
if 'admin|all' in user_permissions or global_permission in user_permissions or permission in user_permissions:
|
||||
return True
|
||||
collection_permission = self.permissions.get(permission)
|
||||
if isinstance(collection_permission, bool):
|
||||
return collection_permission
|
||||
if callable(collection_permission):
|
||||
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.
|
||||
|
||||
: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})
|
||||
|
||||
def logout(self):
|
||||
"""Remove the secure cookie used fro authentication."""
|
||||
if self.current_user in self._users_cache:
|
||||
del self._users_cache[self.current_user]
|
||||
self.clear_cookie("user")
|
||||
|
||||
|
||||
class RootHandler(BaseHandler):
|
||||
"""Handler for the / path."""
|
||||
app_path = os.path.join(os.path.dirname(__file__), "dist")
|
||||
|
||||
@gen.coroutine
|
||||
def get(self, *args, **kwargs):
|
||||
# serve the ./app/index.html file
|
||||
with open(self.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)
|
||||
document = None
|
||||
collection = None
|
||||
|
||||
# set of documents used to store incremental sequences
|
||||
counters_collection = 'counters'
|
||||
|
||||
_id_chars = string.ascii_lowercase + string.digits
|
||||
|
||||
def _filter_results(self, results, params):
|
||||
"""Filter a list using keys and values from a dictionary.
|
||||
|
||||
:param results: the list to be filtered
|
||||
:type results: list
|
||||
:param params: a dictionary of items that must all be present in an original list item to be included in the return
|
||||
:type params: dict
|
||||
|
||||
:returns: list of items that have all the keys with the same values as params
|
||||
:rtype: list"""
|
||||
if not params:
|
||||
return results
|
||||
params = monco.convert(params)
|
||||
filtered = []
|
||||
for result in results:
|
||||
add = True
|
||||
for key, value in params.iteritems():
|
||||
if key not in result or result[key] != value:
|
||||
add = False
|
||||
break
|
||||
if add:
|
||||
filtered.append(result)
|
||||
return filtered
|
||||
|
||||
def _clean_dict(self, data):
|
||||
"""Filter a dictionary (in place) to remove unwanted keywords in db queries.
|
||||
|
||||
:param data: dictionary to clean
|
||||
:type data: dict"""
|
||||
if isinstance(data, dict):
|
||||
for key in data.keys():
|
||||
if isinstance(key, (str, unicode)) and key.startswith('$'):
|
||||
del data[key]
|
||||
return data
|
||||
|
||||
def apply_filter(self, data, filter_name):
|
||||
"""Apply a filter to the data.
|
||||
|
||||
:param data: the data to filter
|
||||
:returns: the modified (possibly also in place) data
|
||||
"""
|
||||
filter_method = getattr(self, 'filter_%s' % filter_name, None)
|
||||
if filter_method is not None:
|
||||
data = filter_method(data)
|
||||
return data
|
||||
|
||||
@gen.coroutine
|
||||
def get(self, id_=None, resource=None, resource_id=None, acl=True, **kwargs):
|
||||
if resource:
|
||||
# Handle access to sub-resources.
|
||||
permission = '%s:%s%s|read' % (self.document, resource, '-all' if resource_id is None else '')
|
||||
if acl and not self.has_permission(permission):
|
||||
return self.build_error(status=401, message='insufficient permissions: %s' % permission)
|
||||
handler = getattr(self, 'handle_get_%s' % resource, None)
|
||||
if callable(handler):
|
||||
output = handler(id_, resource_id, **kwargs) or {}
|
||||
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:
|
||||
# read a single document
|
||||
permission = '%s|read' % self.document
|
||||
if acl and not self.has_permission(permission):
|
||||
return self.build_error(status=401, message='insufficient permissions: %s' % permission)
|
||||
handler = getattr(self, 'handle_get', None)
|
||||
if callable(handler):
|
||||
output = handler(id_, **kwargs) or {}
|
||||
else:
|
||||
output = self.db.get(self.collection, id_)
|
||||
output = self.apply_filter(output, 'get')
|
||||
self.write(output)
|
||||
else:
|
||||
# return an object containing the list of all objects in the collection.
|
||||
# Please, never return JSON lists that are not encapsulated into an object,
|
||||
# to avoid XSS vulnerabilities.
|
||||
permission = '%s|read' % self.collection
|
||||
if acl and not self.has_permission(permission):
|
||||
return self.build_error(status=401, message='insufficient permissions: %s' % permission)
|
||||
output = {self.collection: self.db.query(self.collection, self.arguments)}
|
||||
output = self.apply_filter(output, 'get_all')
|
||||
self.write(output)
|
||||
|
||||
@gen.coroutine
|
||||
def post(self, id_=None, resource=None, resource_id=None, **kwargs):
|
||||
data = escape.json_decode(self.request.body or '{}')
|
||||
self._clean_dict(data)
|
||||
method = self.request.method.lower()
|
||||
crud_method = 'create' if method == 'post' else 'update'
|
||||
now = datetime.datetime.now()
|
||||
user_info = self.current_user_info
|
||||
user_id = user_info.get('_id')
|
||||
if crud_method == 'create':
|
||||
data['created_by'] = user_id
|
||||
data['created_at'] = now
|
||||
data['updated_by'] = user_id
|
||||
data['updated_at'] = now
|
||||
if resource:
|
||||
permission = '%s:%s%s|%s' % (self.document, resource, '-all' if resource_id is None else '', crud_method)
|
||||
if not self.has_permission(permission):
|
||||
return self.build_error(status=401, message='insufficient permissions: %s' % permission)
|
||||
# Handle access to sub-resources.
|
||||
handler = getattr(self, 'handle_%s_%s' % (method, resource), None)
|
||||
if handler and callable(handler):
|
||||
data = self.apply_filter(data, 'input_%s_%s' % (method, resource))
|
||||
output = handler(id_, resource_id, data, **kwargs)
|
||||
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):
|
||||
return self.build_error(status=401, message='insufficient permissions: %s' % permission)
|
||||
data = self.apply_filter(data, 'input_%s' % method)
|
||||
merged, newData = self.db.update(self.collection, id_, data)
|
||||
newData = self.apply_filter(newData, method)
|
||||
else:
|
||||
permission = '%s|%s' % (self.collection, crud_method)
|
||||
if not self.has_permission(permission):
|
||||
return self.build_error(status=401, message='insufficient permissions: %s' % permission)
|
||||
data = self.apply_filter(data, 'input_%s_all' % method)
|
||||
newData = self.db.add(self.collection, data)
|
||||
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;
|
||||
# in subclasses you can always separate sub-resources handlers like handle_post_tickets and handle_put_tickets
|
||||
put = post
|
||||
|
||||
@gen.coroutine
|
||||
def delete(self, id_=None, resource=None, resource_id=None, **kwargs):
|
||||
if resource:
|
||||
# Handle access to sub-resources.
|
||||
permission = '%s:%s%s|delete' % (self.document, resource, '-all' if resource_id is None else '')
|
||||
if not self.has_permission(permission):
|
||||
return self.build_error(status=401, message='insufficient permissions: %s' % permission)
|
||||
method = getattr(self, 'handle_delete_%s' % resource, None)
|
||||
if method and callable(method):
|
||||
output = method(id_, resource_id, **kwargs)
|
||||
self.write(output)
|
||||
return
|
||||
return self.build_error(status=404, message='unable to access resource: %s' % resource)
|
||||
if id_ is not None:
|
||||
permission = '%s|delete' % self.document
|
||||
if not self.has_permission(permission):
|
||||
return self.build_error(status=401, message='insufficient permissions: %s' % permission)
|
||||
howMany = self.db.delete(self.collection, id_)
|
||||
else:
|
||||
self.write({'success': False})
|
||||
self.write({'success': True})
|
||||
|
||||
|
||||
class AttendeesHandler(CollectionHandler):
|
||||
document = 'attendee'
|
||||
collection = 'attendees'
|
||||
|
||||
@gen.coroutine
|
||||
def get(self, id_=None, **kwargs):
|
||||
if id_:
|
||||
output = self.db.getOne(self.collection, {'_id': id_})
|
||||
else:
|
||||
output = {self.collection: self.db.query(self.collection, self.arguments)}
|
||||
self.write(output)
|
||||
|
||||
@gen.coroutine
|
||||
def post(self, **kwargs):
|
||||
data = escape.json_decode(self.request.body or '{}')
|
||||
self._clean_dict(data)
|
||||
user_info = self.current_user_info
|
||||
user_id = user_info.get('_id')
|
||||
now = datetime.datetime.now()
|
||||
data['created_by'] = user_id
|
||||
data['created_at'] = now
|
||||
data['updated_by'] = user_id
|
||||
data['updated_at'] = now
|
||||
doc = self.db.add(self.collection, data)
|
||||
doc = self.apply_filter(doc, 'create')
|
||||
self.write(doc)
|
||||
|
||||
@gen.coroutine
|
||||
def put(self, id_, **kwargs):
|
||||
data = escape.json_decode(self.request.body or '{}')
|
||||
self._clean_dict(data)
|
||||
user_info = self.current_user_info
|
||||
user_id = user_info.get('_id')
|
||||
now = datetime.datetime.now()
|
||||
data['updated_by'] = user_id
|
||||
data['updated_at'] = now
|
||||
merged, doc = self.db.update(self.collection, {'_id': id_}, data)
|
||||
doc = self.apply_filter(doc, 'update')
|
||||
self.write(doc)
|
||||
|
||||
@gen.coroutine
|
||||
def delete(self, id_=None, **kwargs):
|
||||
if id_ is not None:
|
||||
howMany = self.db.delete(self.collection, id_)
|
||||
self.write({'success': True, 'deleted entries': howMany.get('n')})
|
||||
else:
|
||||
self.write({'success': False})
|
||||
|
||||
|
||||
class DaysHandler(CollectionHandler):
|
||||
"""Handle requests for Days."""
|
||||
|
||||
def _summarize(self, days):
|
||||
res = []
|
||||
for day in days:
|
||||
print day['day'], [x['group'] for x in day.get('groups')]
|
||||
res.append({'day': day['day'], 'groups_count': len(day.get('groups', []))})
|
||||
return res
|
||||
|
||||
@gen.coroutine
|
||||
def get(self, day=None, **kwargs):
|
||||
params = self.arguments
|
||||
summary = params.get('summary', False)
|
||||
if summary:
|
||||
del params['summary']
|
||||
start = params.get('start')
|
||||
if start:
|
||||
del params['start']
|
||||
end = params.get('end')
|
||||
if end:
|
||||
del params['end']
|
||||
if day:
|
||||
params['day'] = day
|
||||
else:
|
||||
if start:
|
||||
params['day'] = {'$gte': start}
|
||||
if end:
|
||||
if 'day' not in params:
|
||||
params['day'] = {}
|
||||
if end.count('-') == 0:
|
||||
end += '-13'
|
||||
elif end.count('-') == 1:
|
||||
end += '-31'
|
||||
params['day'].update({'$lte': end})
|
||||
res = self.db.query('attendees', params)
|
||||
days = []
|
||||
for d, dayItems in itertools.groupby(sorted(res, key=itemgetter('day')), key=itemgetter('day')):
|
||||
dayData = {'day': d, 'groups': []}
|
||||
for group, attendees in itertools.groupby(sorted(dayItems, key=itemgetter('group')), key=itemgetter('group')):
|
||||
attendees = sorted(attendees, key=itemgetter('_id'))
|
||||
dayData['groups'].append({'group': group, 'attendees': attendees})
|
||||
days.append(dayData)
|
||||
if summary:
|
||||
days = self._summarize(days)
|
||||
if not day:
|
||||
self.write({'days': days})
|
||||
elif days:
|
||||
self.write(days[0])
|
||||
else:
|
||||
self.write({})
|
||||
|
||||
|
||||
class UsersHandler(CollectionHandler):
|
||||
"""Handle requests for Users."""
|
||||
document = 'user'
|
||||
collection = 'users'
|
||||
|
||||
def filter_get(self, data):
|
||||
if 'password' in data:
|
||||
del data['password']
|
||||
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
|
||||
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()
|
||||
email = (data.get('email') or '').strip()
|
||||
if not (username and password):
|
||||
raise InputException('missing username or password')
|
||||
res = self.db.query('users', {'username': username})
|
||||
if res:
|
||||
raise InputException('username already exists')
|
||||
return {'username': username, 'password': utils.hash_password(password),
|
||||
'email': email}
|
||||
|
||||
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 (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
|
||||
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 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 SettingsHandler(BaseHandler):
|
||||
"""Handle requests for Settings."""
|
||||
@gen.coroutine
|
||||
def get(self, **kwargs):
|
||||
query = self.arguments_tobool()
|
||||
settings = self.db.query('settings', query)
|
||||
self.write({'settings': settings})
|
||||
|
||||
|
||||
class LoginHandler(RootHandler):
|
||||
"""Handle user authentication requests."""
|
||||
|
||||
@gen.coroutine
|
||||
def get(self, **kwargs):
|
||||
# show the login page
|
||||
if self.is_api():
|
||||
self.set_status(401)
|
||||
self.write({'error': True,
|
||||
'message': 'authentication required'})
|
||||
|
||||
@gen.coroutine
|
||||
def post(self, *args, **kwargs):
|
||||
# authenticate a user
|
||||
try:
|
||||
password = self.get_body_argument('password')
|
||||
username = self.get_body_argument('username')
|
||||
except tornado.web.MissingArgumentError:
|
||||
data = escape.json_decode(self.request.body or '{}')
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
if not (username and password):
|
||||
self.set_status(401)
|
||||
self.write({'error': True, 'message': 'missing username or password'})
|
||||
return
|
||||
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'})
|
||||
return
|
||||
logging.info('login failed for user %s' % username)
|
||||
self.set_status(401)
|
||||
self.write({'error': True, 'message': 'wrong username and password'})
|
||||
|
||||
|
||||
class LogoutHandler(BaseHandler):
|
||||
"""Handle user logout requests."""
|
||||
@gen.coroutine
|
||||
def get(self, **kwargs):
|
||||
# log the user out
|
||||
logging.info('logout')
|
||||
self.logout()
|
||||
self.write({'error': False, 'message': 'logged out'})
|
||||
|
||||
|
||||
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=3000, help="run on the given port", type=int)
|
||||
define("address", default='', help="bind the server at the given address", type=str)
|
||||
define("data_dir", default=os.path.join(os.path.dirname(__file__), "data"),
|
||||
help="specify the directory used to store the data")
|
||||
define("ssl_cert", default=os.path.join(os.path.dirname(__file__), 'ssl', 'ibt2_cert.pem'),
|
||||
help="specify the SSL certificate to use for secure connections")
|
||||
define("ssl_key", default=os.path.join(os.path.dirname(__file__), 'ssl', 'ibt2_key.pem'),
|
||||
help="specify the SSL private key to use for secure connections")
|
||||
define("mongo_url", default=None,
|
||||
help="URL to MongoDB server", type=str)
|
||||
define("db_name", default='ibt2',
|
||||
help="Name of the MongoDB database to use", type=str)
|
||||
define("authentication", default=False, help="if set to true, authentication is required")
|
||||
define("debug", default=False, help="run in debug mode")
|
||||
define("config", help="read configuration file",
|
||||
callback=lambda path: tornado.options.parse_config_file(path, final=False))
|
||||
tornado.options.parse_command_line()
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
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 = monco.Monco(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, ssl_options=ssl_options)
|
||||
|
||||
# If not present, we store a user 'admin' with password 'ibt2' into the database.
|
||||
if not db_connector.query('users', {'username': 'admin'}):
|
||||
db_connector.add('users',
|
||||
{'username': 'admin', 'password': utils.hash_password('ibt2'),
|
||||
'isAdmin': True})
|
||||
|
||||
# If present, use the cookie_secret stored into the database.
|
||||
cookie_secret = db_connector.query('settings', {'setting': 'server_cookie_secret'})
|
||||
if cookie_secret:
|
||||
cookie_secret = cookie_secret[0]['cookie_secret']
|
||||
else:
|
||||
# the salt guarantees its uniqueness
|
||||
cookie_secret = utils.hash_password('__COOKIE_SECRET__')
|
||||
db_connector.add('settings',
|
||||
{'setting': 'server_cookie_secret', 'cookie_secret': cookie_secret})
|
||||
|
||||
_days_path = r"/days/?(?P<day>[\d_-]+)?"
|
||||
_attendees_path = r"/days/(?P<day_id>[\d_-]+)/groups/(?P<group_id>[\w\d_\ -]+)/attendees/?(?P<attendee_id>[\w\d_\ -]+)?"
|
||||
_users_path = r"/users/?(?P<id_>[\w\d_-]+)?/?(?P<resource>[\w\d_-]+)?/?(?P<resource_id>[\w\d_-]+)?"
|
||||
_attendees_path = r"/attendees/?(?P<id_>[\w\d_-]+)?"
|
||||
application = tornado.web.Application([
|
||||
(_attendees_path, AttendeesHandler, init_params),
|
||||
(r'/v%s%s' % (API_VERSION, _attendees_path), AttendeesHandler, init_params),
|
||||
(_days_path, DaysHandler, init_params),
|
||||
(r'/v%s%s' % (API_VERSION, _days_path), DaysHandler, init_params),
|
||||
(_users_path, UsersHandler, init_params),
|
||||
(r'/v%s%s' % (API_VERSION, _users_path), UsersHandler, init_params),
|
||||
(r"/(?:index.html)?", RootHandler, init_params),
|
||||
(r"/settings", SettingsHandler, init_params),
|
||||
(r'/login', LoginHandler, init_params),
|
||||
(r'/v%s/login' % API_VERSION, LoginHandler, init_params),
|
||||
(r'/logout', LogoutHandler),
|
||||
(r'/v%s/logout' % API_VERSION, LogoutHandler),
|
||||
(r'/?(.*)', tornado.web.StaticFileHandler, {"path": "dist"})
|
||||
],
|
||||
static_path=os.path.join(os.path.dirname(__file__), "dist/static"),
|
||||
cookie_secret='__COOKIE_SECRET__',
|
||||
login_url='/login',
|
||||
debug=options.debug)
|
||||
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',
|
||||
options.port)
|
||||
http_server.listen(options.port, options.address)
|
||||
tornado.ioloop.IOLoop.instance().start()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
run()
|
||||
except KeyboardInterrupt:
|
||||
print('Stop server')
|
12
index.html
Normal file
12
index.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>ibt2</title>
|
||||
<link href="http://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
285
monco.py
Normal file
285
monco.py
Normal file
|
@ -0,0 +1,285 @@
|
|||
"""Monco: a MongoDB database backend
|
||||
|
||||
Classes and functions used to issue queries to a MongoDB database.
|
||||
|
||||
Copyright 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.
|
||||
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 re
|
||||
import pymongo
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
re_objectid = re.compile(r'[0-9a-f]{24}')
|
||||
|
||||
_force_conversion = {
|
||||
'_id': ObjectId
|
||||
}
|
||||
|
||||
|
||||
def convert_obj(obj):
|
||||
"""Convert an object in a format suitable to be stored in MongoDB.
|
||||
|
||||
:param obj: object to convert
|
||||
|
||||
:returns: object that can be stored in MongoDB.
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
if isinstance(obj, bool):
|
||||
return obj
|
||||
try:
|
||||
if re_objectid.match(obj):
|
||||
return ObjectId(obj)
|
||||
except:
|
||||
pass
|
||||
return obj
|
||||
|
||||
|
||||
def convert(seq):
|
||||
"""Convert an object to a format suitable to be stored in MongoDB,
|
||||
descending lists, tuples and dictionaries (a copy is returned).
|
||||
|
||||
:param seq: sequence or object to convert
|
||||
|
||||
:returns: object that can be stored in MongoDB.
|
||||
"""
|
||||
if isinstance(seq, dict):
|
||||
d = {}
|
||||
for key, item in seq.iteritems():
|
||||
if key in _force_conversion:
|
||||
try:
|
||||
d[key] = _force_conversion[key](item)
|
||||
except:
|
||||
d[key] = item
|
||||
else:
|
||||
d[key] = convert(item)
|
||||
return d
|
||||
if isinstance(seq, (list, tuple)):
|
||||
return [convert(x) for x in seq]
|
||||
return convert_obj(seq)
|
||||
|
||||
|
||||
class MoncoError(Exception):
|
||||
"""Base class for Monco exceptions."""
|
||||
pass
|
||||
|
||||
|
||||
class MoncoConnection(MoncoError):
|
||||
"""Monco exceptions raise when a connection problem occurs."""
|
||||
pass
|
||||
|
||||
|
||||
class Monco(object):
|
||||
"""MongoDB connector."""
|
||||
db = None
|
||||
connection = None
|
||||
|
||||
# map operations on lists of items.
|
||||
_operations = {
|
||||
'update': '$set',
|
||||
'append': '$push',
|
||||
'appendUnique': '$addToSet',
|
||||
'delete': '$pull',
|
||||
'increment': '$inc'
|
||||
}
|
||||
|
||||
def __init__(self, dbName, url=None):
|
||||
"""Initialize the instance, connecting to the database.
|
||||
|
||||
:param dbName: name of the database
|
||||
:type dbName: str (or None to use the dbName passed at initialization)
|
||||
: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, dbName=None, url=None):
|
||||
"""Connect to the database.
|
||||
|
||||
:param dbName: name of the database
|
||||
:type dbName: str (or None to use the dbName passed at initialization)
|
||||
:param url: URL of the database
|
||||
:type url: str (or None to connect to localhost)
|
||||
|
||||
:returns: the database we're connected to
|
||||
:rtype: :class:`~pymongo.database.Database`
|
||||
"""
|
||||
if self.db is not None:
|
||||
return self.db
|
||||
if url:
|
||||
self._url = url
|
||||
if dbName:
|
||||
self._dbName = dbName
|
||||
if not self._dbName:
|
||||
raise MoncoConnection('no database name specified')
|
||||
self.connection = pymongo.MongoClient(self._url)
|
||||
self.db = self.connection[self._dbName]
|
||||
return self.db
|
||||
|
||||
def getOne(self, collection, query=None):
|
||||
"""Get a single document with the specified `query`.
|
||||
|
||||
:param collection: search the document in this collection
|
||||
:type collection: str
|
||||
:param query: query to filter the documents
|
||||
:type query: dict or None
|
||||
|
||||
:returns: the first document matching the query
|
||||
:rtype: dict
|
||||
"""
|
||||
results = self.query(collection, convert(query))
|
||||
return results and results[0] or {}
|
||||
|
||||
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`
|
||||
|
||||
:returns: the document with the given `_id`
|
||||
:rtype: dict
|
||||
"""
|
||||
return self.getOne(collection, {'_id': _id})
|
||||
|
||||
def query(self, collection, query=None, condition='or'):
|
||||
"""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
|
||||
|
||||
:returns: list of matching documents
|
||||
:rtype: list
|
||||
"""
|
||||
db = self.connect()
|
||||
query = convert(query or {})
|
||||
if isinstance(query, (list, tuple)):
|
||||
query = {'$%s' % condition: query}
|
||||
return list(db[collection].find(query))
|
||||
|
||||
def add(self, collection, data, _id=None):
|
||||
"""Insert a new document.
|
||||
|
||||
:param collection: insert the document in this collection
|
||||
:type collection: str
|
||||
:param data: the document to store
|
||||
:type data: dict
|
||||
:param _id: the _id of the document to store; if None, it's generated
|
||||
:type _id: object
|
||||
|
||||
:returns: the document, as created in the database
|
||||
:rtype: dict
|
||||
"""
|
||||
db = self.connect()
|
||||
data = convert(data)
|
||||
if _id is not None:
|
||||
data['_id'] = _id
|
||||
_id = db[collection].insert(data)
|
||||
return self.get(collection, _id)
|
||||
|
||||
def insertOne(self, collection, data):
|
||||
"""Insert a document, avoiding duplicates.
|
||||
|
||||
:param collection: update a document in this collection
|
||||
:type collection: str
|
||||
:param data: the document information to store
|
||||
:type data: dict
|
||||
|
||||
:returns: True if the document was already present
|
||||
:rtype: bool
|
||||
"""
|
||||
db = self.connect()
|
||||
data = convert(data)
|
||||
ret = db[collection].update(data, {'$set': data}, upsert=True)
|
||||
return ret['updatedExisting']
|
||||
|
||||
def _buildSearchPattern(self, data, searchBy):
|
||||
"""Return an OR condition."""
|
||||
_or = []
|
||||
for searchPattern in searchBy:
|
||||
try:
|
||||
_or.append(dict([(k, data[k]) for k in searchPattern if k in data]))
|
||||
except KeyError:
|
||||
continue
|
||||
return _or
|
||||
|
||||
def update(self, collection, _id_or_query, data, operation='update',
|
||||
updateList=None, create=True):
|
||||
"""Update an existing document or create it, if requested.
|
||||
_id_or_query can be an ID, a dict representing a query or a list of tuples.
|
||||
In the latter case, the tuples are put in OR; a tuple match if all of its
|
||||
items from 'data' are contained in the document.
|
||||
|
||||
:param collection: update a document in this collection
|
||||
:type collection: str
|
||||
:param _id_or_query: ID of the document to be updated, or a query or a list of attributes in the data that must match
|
||||
:type _id_or_query: str or :class:`~bson.objectid.ObjectId` or iterable
|
||||
:param data: the updated information to store
|
||||
:type data: dict
|
||||
:param operation: operation used to update the document or a portion of it, like a list (update, append, appendUnique, delete, increment)
|
||||
:type operation: str
|
||||
:param updateList: if set, it's considered the name of a list (the first matching element will be updated)
|
||||
:type updateList: str
|
||||
:param create: if True, the document is created if no document matches
|
||||
:type create: bool
|
||||
|
||||
:returns: a boolean (True if an existing document was updated) and the document after the update
|
||||
:rtype: tuple of (bool, dict)
|
||||
"""
|
||||
db = self.connect()
|
||||
data = convert(data or {})
|
||||
_id_or_query = convert(_id_or_query)
|
||||
if isinstance(_id_or_query, (list, tuple)):
|
||||
_id_or_query = {'$or': self._buildSearchPattern(data, _id_or_query)}
|
||||
elif not isinstance(_id_or_query, dict):
|
||||
_id_or_query = {'_id': _id_or_query}
|
||||
if '_id' in data:
|
||||
del data['_id']
|
||||
operator = self._operations.get(operation)
|
||||
if updateList:
|
||||
newData = {}
|
||||
for key, value in data.iteritems():
|
||||
newData['%s.$.%s' % (updateList, key)] = value
|
||||
data = newData
|
||||
res = db[collection].find_and_modify(query=_id_or_query,
|
||||
update={operator: data}, full_response=True, new=True, upsert=create)
|
||||
lastErrorObject = res.get('lastErrorObject') or {}
|
||||
return lastErrorObject.get('updatedExisting', False), res.get('value') or {}
|
||||
|
||||
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
|
||||
|
||||
:returns: how many documents were removed
|
||||
:rtype: int
|
||||
"""
|
||||
if not _id_or_query and not force:
|
||||
return
|
||||
db = self.connect()
|
||||
if not isinstance(_id_or_query, dict):
|
||||
_id_or_query = {'_id': _id_or_query}
|
||||
_id_or_query = convert(_id_or_query)
|
||||
return db[collection].remove(_id_or_query)
|
||||
|
59
package.json
Normal file
59
package.json
Normal file
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "ibt2",
|
||||
"version": "1.0.0",
|
||||
"description": "I'll be there, 2",
|
||||
"author": "Davide Alberani <da@erlug.linux.it>",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "node build/dev-server.js",
|
||||
"pydev": "python ibt2.py --debug --db_name=ibt2_test",
|
||||
"watch": "watch 'npm run build' src/",
|
||||
"build": "node build/build.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel": "^6.5.2",
|
||||
"jquery": "^3.1.1",
|
||||
"material-ui-vue": "^0.1.4",
|
||||
"materialize-css": "^0.97.8",
|
||||
"vue": "^2.1.6",
|
||||
"vue-loader": "^10.0.2",
|
||||
"vue-resource": "^1.0.3",
|
||||
"vuejs-datepicker": "^0.4.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^6.4.0",
|
||||
"babel-core": "^6.0.0",
|
||||
"babel-loader": "^6.0.0",
|
||||
"babel-plugin-transform-runtime": "^6.0.0",
|
||||
"babel-preset-es2015": "^6.0.0",
|
||||
"babel-preset-stage-2": "^6.0.0",
|
||||
"babel-register": "^6.0.0",
|
||||
"chalk": "^1.1.3",
|
||||
"connect-history-api-fallback": "^1.1.0",
|
||||
"css-loader": "^0.25.0",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"express": "^4.13.3",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
"file-loader": "^0.9.0",
|
||||
"function-bind": "^1.0.2",
|
||||
"html-webpack-plugin": "^2.8.1",
|
||||
"http-proxy-middleware": "^0.17.2",
|
||||
"json-loader": "^0.5.4",
|
||||
"semver": "^5.3.0",
|
||||
"opn": "^4.0.2",
|
||||
"ora": "^0.3.0",
|
||||
"shelljs": "^0.7.4",
|
||||
"url-loader": "^0.5.7",
|
||||
"vue-loader": "^10.0.0",
|
||||
"vue-style-loader": "^1.0.0",
|
||||
"vue-template-compiler": "^2.1.0",
|
||||
"webpack": "^1.13.2",
|
||||
"webpack-dev-middleware": "^1.8.3",
|
||||
"webpack-hot-middleware": "^2.12.2",
|
||||
"webpack-merge": "^0.14.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
}
|
133
src/App.vue
Normal file
133
src/App.vue
Normal file
|
@ -0,0 +1,133 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<div id="datepicker">
|
||||
<datepicker :value="state.date" :inline="true" v-on:selected="getDay"></datepicker>
|
||||
</div>
|
||||
<div id="panel">
|
||||
<ul class="groups">
|
||||
<li v-for="group in state.day.groups || []">
|
||||
<div>{{ group.group }}</div>
|
||||
<ul class="attendees">
|
||||
<li v-for="attendee in group.attendees || []">
|
||||
{{attendee.name}}
|
||||
</li>
|
||||
<li class="add-attendee">add: <input v-on:keyup.enter="addAttendee(group.group, newAttendee)" v-model="newAttendee" /></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="add-group"><input v-model="newGroup" /></a>
|
||||
<ul v-if="newGroup">
|
||||
<li class="add-attendee">add: <input v-on:keyup.enter="addAttendee(newGroup, newAttendee)" v-model="newAttendee" /></li>
|
||||
</ul>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Datepicker from 'vuejs-datepicker';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
state: {
|
||||
date: new Date(),
|
||||
day: {},
|
||||
},
|
||||
newAttendee: null,
|
||||
newGroup: null
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate: function() {
|
||||
this.daysUrl = this.$resource('days{/day}');
|
||||
this.attendeesUrl = this.$resource('attendees{/id}');
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
var ym = this.dateToString(this.state.date, true);
|
||||
this.getSummary({start: ym, end: ym});
|
||||
this.getDay(this.state.date);
|
||||
},
|
||||
|
||||
methods: {
|
||||
dateToString(date, excludeDay) {
|
||||
var year = '' + date.getFullYear();
|
||||
var month = '' + (date.getMonth() + 1);
|
||||
month = '00'.substring(0, 2 - month.length) + month;
|
||||
var ym = year + '-' + month;
|
||||
if (excludeDay) {
|
||||
return ym;
|
||||
}
|
||||
var day = '' + (date.getDate());
|
||||
day = '00'.substring(0, 2 - day.length) + day;
|
||||
return ym + '-' + day;
|
||||
},
|
||||
|
||||
getSummary(params) {
|
||||
console.log('getSummary');
|
||||
console.log(params);
|
||||
if (!params) {
|
||||
params = {};
|
||||
}
|
||||
params['summary'] = true;
|
||||
this.daysUrl.query(params).then((response) => {
|
||||
return response.json();
|
||||
}, (response) => {
|
||||
alert('failed get resource');
|
||||
}).then((json) => {
|
||||
console.log('summary data');
|
||||
console.log(json);
|
||||
});
|
||||
},
|
||||
|
||||
getDay(day) {
|
||||
console.log("getDay");
|
||||
console.log(day);
|
||||
if (day) {
|
||||
day = this.dateToString(day);
|
||||
} else {
|
||||
day = this.state.day.day;
|
||||
}
|
||||
this.daysUrl.get({day: day}).then((response) => {
|
||||
return response.json();
|
||||
}, (response) => {
|
||||
alert('failed get resource');
|
||||
}).then((json) => {
|
||||
console.log('day data');
|
||||
console.log(json);
|
||||
this.state.day = json;
|
||||
});
|
||||
},
|
||||
|
||||
addAttendee(group, newAttendee) {
|
||||
console.log(group);
|
||||
console.log(newAttendee);
|
||||
this.newAttendee = '';
|
||||
this.attendeesUrl.save({day: this.state.day.day, group: group, name: newAttendee}).then((response) => {
|
||||
return response.json();
|
||||
}, (response) => {
|
||||
alert('failed get resource');
|
||||
}).then((json) => {
|
||||
console.log('attendee data');
|
||||
console.log(json);
|
||||
this.getDay();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
Datepicker
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
margin-top: 60px;
|
||||
}
|
||||
</style>
|
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
53
src/components/Hello.vue
Normal file
53
src/components/Hello.vue
Normal file
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
<h2>Essential Links</h2>
|
||||
<ul>
|
||||
<li><a href="https://vuejs.org" target="_blank">Core Docs</a></li>
|
||||
<li><a href="https://forum.vuejs.org" target="_blank">Forum</a></li>
|
||||
<li><a href="https://gitter.im/vuejs/vue" target="_blank">Gitter Chat</a></li>
|
||||
<li><a href="https://twitter.com/vuejs" target="_blank">Twitter</a></li>
|
||||
<br>
|
||||
<li><a href="http://vuejs-templates.github.io/webpack/" target="_blank">Docs for This Template</a></li>
|
||||
</ul>
|
||||
<h2>Ecosystem</h2>
|
||||
<ul>
|
||||
<li><a href="http://router.vuejs.org/" target="_blank">vue-router</a></li>
|
||||
<li><a href="http://vuex.vuejs.org/" target="_blank">vuex</a></li>
|
||||
<li><a href="http://vue-loader.vuejs.org/" target="_blank">vue-loader</a></li>
|
||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank">awesome-vue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'hello',
|
||||
data () {
|
||||
return {
|
||||
msg: 'Welcome to Your Vue.js App'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
h1, h2 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
22
src/main.js
Normal file
22
src/main.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
// The Vue build version to load with the `import` command
|
||||
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
|
||||
import Vue from 'vue'
|
||||
import App from './App'
|
||||
require("vue-resource")
|
||||
/*
|
||||
import 'jquery/dist/jquery.min.js'
|
||||
import 'materialize-css/bin/materialize.css'
|
||||
import 'materialize-css/bin/materialize.js'
|
||||
require("material-ui-vue")
|
||||
*/
|
||||
var VueResource = require("vue-resource");
|
||||
require("jquery");
|
||||
|
||||
Vue.use(VueResource);
|
||||
|
||||
/* eslint-disable no-new */
|
||||
new Vue({
|
||||
el: '#app',
|
||||
template: '<App/>',
|
||||
components: { App }
|
||||
})
|
0
static/.gitkeep
Normal file
0
static/.gitkeep
Normal file
136
tests/ibt2_tests.py
Executable file
136
tests/ibt2_tests.py
Executable file
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env python
|
||||
"""I'll Be There 2 (ibt2) - tests
|
||||
|
||||
Copyright 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.
|
||||
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 unittest
|
||||
import requests
|
||||
import monco
|
||||
|
||||
BASE_URL = 'http://localhost:3000/v1.0/'
|
||||
DB_NAME = 'ibt2_test'
|
||||
|
||||
def dictInDict(d, dContainer):
|
||||
for k, v in d.viewitems():
|
||||
if k not in dContainer:
|
||||
return False
|
||||
if v != dContainer[k]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class Ibt2Tests(unittest.TestCase):
|
||||
#@classmethod
|
||||
#def setUpClass(cls):
|
||||
def setUp(self):
|
||||
self.monco_conn = monco.Monco(dbName=DB_NAME)
|
||||
self.connection = self.monco_conn.connection
|
||||
self.db = self.monco_conn.db
|
||||
self.connection.drop_database(DB_NAME)
|
||||
|
||||
def tearDown(self):
|
||||
self.add_attendee({'day': '2017-01-15', 'name': 'A name', 'group': 'group A'})
|
||||
self.add_attendee({'day': '2017-01-16', 'name': 'A new name', 'group': 'group C'})
|
||||
self.add_attendee({'day': '2017-01-15', 'name': 'Another name', 'group': 'group A'})
|
||||
self.add_attendee({'day': '2017-01-15', 'name': 'Yet another name', 'group': 'group B'})
|
||||
|
||||
def add_attendee(self, attendee):
|
||||
r = requests.post('%sattendees' % BASE_URL, json=attendee)
|
||||
r.raise_for_status()
|
||||
return r
|
||||
|
||||
def test_add_attendee(self):
|
||||
# POST /attendees/ {name: 'A Name', day: '2017-01-15', group: 'A group'}
|
||||
# GET /attendees/:id
|
||||
attendee = {'name': 'A Name', 'day': '2017-01-15', 'group': 'A group'}
|
||||
r = self.add_attendee(attendee)
|
||||
rj = r.json()
|
||||
id_ = rj.get('_id')
|
||||
self.assertTrue(dictInDict(attendee, rj))
|
||||
r = requests.get(BASE_URL + 'attendees/' + id_)
|
||||
r.raise_for_status()
|
||||
rj = r.json()
|
||||
self.assertTrue(dictInDict(attendee, rj))
|
||||
|
||||
def test_put_attendee(self):
|
||||
# POST /attendees/ {name: 'A Name', day: '2017-01-15', group: 'A group'}
|
||||
# GET /attendees/:id
|
||||
attendee = {'name': 'A Name', 'day': '2017-01-15', 'group': 'A group'}
|
||||
r = self.add_attendee(attendee)
|
||||
update = {'notes': 'A note'}
|
||||
r = requests.post(BASE_URL + 'attendees', json=attendee)
|
||||
r.raise_for_status()
|
||||
id_ = r.json().get('_id')
|
||||
r = requests.put(BASE_URL + 'attendees/' + id_, json=update)
|
||||
r.raise_for_status()
|
||||
r = requests.get('%s%s/%s' % (BASE_URL, 'attendees', id_))
|
||||
r.raise_for_status()
|
||||
rj = r.json()
|
||||
final = attendee.copy()
|
||||
final.update(update)
|
||||
self.assertTrue(dictInDict(final, rj))
|
||||
|
||||
def test_delete_attendee(self):
|
||||
# POST /attendees/ {name: 'A Name', day: '2017-01-15', group: 'A group'}
|
||||
# GET /attendees/:id
|
||||
attendee = {'name': 'A Name', 'day': '2017-01-15', 'group': 'A group'}
|
||||
r = self.add_attendee(attendee)
|
||||
id_ = r.json().get('_id')
|
||||
r = requests.delete(BASE_URL + 'attendees/' + id_)
|
||||
r.raise_for_status()
|
||||
self.assertTrue(r.json().get('success'))
|
||||
|
||||
def test_get_days(self):
|
||||
self.add_attendee({'day': '2017-01-15', 'name': 'A name', 'group': 'group A'})
|
||||
self.add_attendee({'day': '2017-01-16', 'name': 'A new name', 'group': 'group C'})
|
||||
self.add_attendee({'day': '2017-01-15', 'name': 'Another name', 'group': 'group A'})
|
||||
self.add_attendee({'day': '2017-01-15', 'name': 'Yet another name', 'group': 'group B'})
|
||||
r = requests.get(BASE_URL + 'days')
|
||||
r.raise_for_status()
|
||||
rj = r.json()
|
||||
self.assertEqual([x.get('day') for x in rj['days']], ['2017-01-15', '2017-01-16'])
|
||||
self.assertEqual([x.get('group') for x in rj['days'][0]['groups']], ['group A', 'group B'])
|
||||
self.assertTrue(len(rj['days'][0]['groups'][0]['attendees']) == 2)
|
||||
self.assertTrue(len(rj['days'][0]['groups'][1]['attendees']) == 1)
|
||||
self.assertEqual([x.get('group') for x in rj['days'][1]['groups']], ['group C'])
|
||||
self.assertTrue(len(rj['days'][1]['groups'][0]['attendees']) == 1)
|
||||
|
||||
def test_get_days_summary(self):
|
||||
self.add_attendee({'day': '2017-01-15', 'name': 'A name', 'group': 'group A'})
|
||||
self.add_attendee({'day': '2017-01-16', 'name': 'A new name', 'group': 'group C'})
|
||||
self.add_attendee({'day': '2017-01-15', 'name': 'Another name', 'group': 'group A'})
|
||||
self.add_attendee({'day': '2017-01-15', 'name': 'Yet another name', 'group': 'group B'})
|
||||
r = requests.get(BASE_URL + 'days?summary=1')
|
||||
r.raise_for_status()
|
||||
rj = r.json()
|
||||
self.assertEqual(rj,
|
||||
{"days": [{"groups_count": 2, "day": "2017-01-15"}, {"groups_count": 1, "day": "2017-01-16"}]})
|
||||
|
||||
def _test_post_day_group(self):
|
||||
# POST /days/ {day: '2017-01-04'}
|
||||
# GET /days/2017-01-04
|
||||
day = '2017-01-15'
|
||||
query = {'day': day, 'groups': [{'name': 'group1'}]}
|
||||
r = requests.post(BASE_URL + 'days', json=query)
|
||||
r.raise_for_status()
|
||||
rj = r.json()
|
||||
self.assertTrue(dictInDict(query, rj))
|
||||
r = requests.get('%s%s/%s' % (BASE_URL, 'days', day))
|
||||
r.raise_for_status()
|
||||
rj = r.json()
|
||||
self.assertTrue(dictInDict(query, rj))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
1
tests/monco.py
Symbolic link
1
tests/monco.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
../monco.py
|
BIN
tests/monco.pyc
Normal file
BIN
tests/monco.pyc
Normal file
Binary file not shown.
61
utils.py
Normal file
61
utils.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
"""ibt2 utils
|
||||
|
||||
Miscellaneous utilities.
|
||||
|
||||
Copyright 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.
|
||||
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 json
|
||||
import string
|
||||
import random
|
||||
import hashlib
|
||||
import datetime
|
||||
import StringIO
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
|
||||
def hash_password(password, salt=None):
|
||||
"""Hash a password.
|
||||
|
||||
:param password: the cleartext password
|
||||
:type password: str
|
||||
:param salt: the optional salt (randomly generated, if None)
|
||||
:type salt: str
|
||||
|
||||
:returns: the hashed password
|
||||
:rtype: str"""
|
||||
if salt is None:
|
||||
salt_pool = string.ascii_letters + string.digits
|
||||
salt = ''.join(random.choice(salt_pool) for x in xrange(32))
|
||||
hash_ = hashlib.sha512('%s%s' % (salt, password))
|
||||
return '$%s$%s' % (salt, hash_.hexdigest())
|
||||
|
||||
|
||||
class ImprovedEncoder(json.JSONEncoder):
|
||||
"""Enhance the default JSON encoder to serialize datetime and ObjectId instances."""
|
||||
def default(self, o):
|
||||
if isinstance(o, (datetime.datetime, datetime.date,
|
||||
datetime.time, datetime.timedelta, ObjectId)):
|
||||
try:
|
||||
return str(o)
|
||||
except:
|
||||
pass
|
||||
elif isinstance(o, set):
|
||||
return list(o)
|
||||
return json.JSONEncoder.default(self, o)
|
||||
|
||||
|
||||
# Inject our class as the default encoder.
|
||||
json._default_encoder = ImprovedEncoder()
|
||||
|
Loading…
Reference in a new issue