Benjamin Renard commited on 2014-01-12 00:33:07
Showing 19 changed files, with 772 additions and 0 deletions.
... | ... |
@@ -0,0 +1,24 @@ |
1 |
+Install |
|
2 |
+======= |
|
3 |
+ |
|
4 |
+Debian dependencies: |
|
5 |
+ |
|
6 |
+$ aptitude install python python-mako python-markupsafe python-paste python-pastedeploy python-pastescript \ |
|
7 |
+ python-weberror python-webhelpers python-webob |
|
8 |
+ |
|
9 |
+Non-Debian dependencies: |
|
10 |
+ |
|
11 |
+$ git clone git://gitorious.org/biryani/biryani.git biryani1 |
|
12 |
+$ cd biryani1 |
|
13 |
+$ git checkout biryani1 |
|
14 |
+$ python setup.py develop --no-deps --user |
|
15 |
+ |
|
16 |
+Install mycoserver Python egg (from mycoserver root directory): |
|
17 |
+ |
|
18 |
+$ python setup.py develop --no-deps --user |
|
19 |
+ |
|
20 |
+ |
|
21 |
+Start server |
|
22 |
+============ |
|
23 |
+ |
|
24 |
+$ paster serve --reload development.ini |
... | ... |
@@ -0,0 +1,53 @@ |
1 |
+# EesyVPN Web - Development environment configuration |
|
2 |
+# |
|
3 |
+# The %(here)s variable will be replaced with the parent directory of this file. |
|
4 |
+ |
|
5 |
+[DEFAULT] |
|
6 |
+debug = true |
|
7 |
+# Uncomment and replace with the address which should receive any error reports |
|
8 |
+#email_to = you@yourdomain.com |
|
9 |
+smtp_server = localhost |
|
10 |
+from_address = myco-server@localhost |
|
11 |
+dbpass = myP@ssw0rd |
|
12 |
+ |
|
13 |
+[server:main] |
|
14 |
+use = egg:Paste#http |
|
15 |
+host = 0.0.0.0 |
|
16 |
+port = 8765 |
|
17 |
+ |
|
18 |
+[app:main] |
|
19 |
+use = egg:MyCoServer |
|
20 |
+ |
|
21 |
+ |
|
22 |
+# Logging configuration |
|
23 |
+[loggers] |
|
24 |
+keys = root, mycoserver, mycoserver_router |
|
25 |
+ |
|
26 |
+[handlers] |
|
27 |
+keys = console |
|
28 |
+ |
|
29 |
+[formatters] |
|
30 |
+keys = generic |
|
31 |
+ |
|
32 |
+[logger_root] |
|
33 |
+level = DEBUG |
|
34 |
+handlers = console |
|
35 |
+ |
|
36 |
+[logger_mycoserver] |
|
37 |
+level = DEBUG |
|
38 |
+handlers = |
|
39 |
+qualname = mycoserver |
|
40 |
+ |
|
41 |
+[logger_mycoserver_router] |
|
42 |
+level = DEBUG |
|
43 |
+handlers = |
|
44 |
+qualname = mycoserver.router |
|
45 |
+ |
|
46 |
+[handler_console] |
|
47 |
+class = StreamHandler |
|
48 |
+args = (sys.stderr,) |
|
49 |
+formatter = generic |
|
50 |
+ |
|
51 |
+[formatter_generic] |
|
52 |
+format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s:%(funcName)s line %(lineno)d] %(message)s |
|
53 |
+datefmt = %H:%M:%S |
... | ... |
@@ -0,0 +1,57 @@ |
1 |
+# -*- coding: utf-8 -*- |
|
2 |
+ |
|
3 |
+ |
|
4 |
+"""Middleware initialization""" |
|
5 |
+ |
|
6 |
+ |
|
7 |
+import logging.config |
|
8 |
+import os |
|
9 |
+ |
|
10 |
+from paste.cascade import Cascade |
|
11 |
+from paste.urlparser import StaticURLParser |
|
12 |
+from weberror.errormiddleware import ErrorMiddleware |
|
13 |
+ |
|
14 |
+from . import configuration, context, controllers, templates |
|
15 |
+ |
|
16 |
+import db |
|
17 |
+ |
|
18 |
+ |
|
19 |
+def make_app(global_conf, **app_conf): |
|
20 |
+ """Create a WSGI application and return it |
|
21 |
+ |
|
22 |
+ ``global_conf`` |
|
23 |
+ The inherited configuration for this application. Normally from |
|
24 |
+ the [DEFAULT] section of the Paste ini file. |
|
25 |
+ |
|
26 |
+ ``app_conf`` |
|
27 |
+ The application's local configuration. Normally specified in |
|
28 |
+ the [app:<name>] section of the Paste ini file (where <name> |
|
29 |
+ defaults to main). |
|
30 |
+ """ |
|
31 |
+ logging.config.fileConfig(global_conf['__file__']) |
|
32 |
+ app_ctx = context.Context() |
|
33 |
+ app_ctx.conf = configuration.load_configuration(global_conf, app_conf) |
|
34 |
+ app_ctx.templates = templates.load_templates(app_ctx) |
|
35 |
+ app_ctx.db = db.DB( |
|
36 |
+ app_ctx.conf.get('dbhost','localhost'), |
|
37 |
+ app_ctx.conf.get('dbuser','myco'), |
|
38 |
+ app_ctx.conf.get('dbpass','password'), |
|
39 |
+ app_ctx.conf.get('dbname','myco'), |
|
40 |
+ ) |
|
41 |
+ if not app_ctx.db.connect(): |
|
42 |
+ logging.error('Failed to connect DB') |
|
43 |
+ app = controllers.make_router() |
|
44 |
+ app = context.make_add_context_to_request(app, app_ctx) |
|
45 |
+ if not app_ctx.conf['debug']: |
|
46 |
+ app = ErrorMiddleware( |
|
47 |
+ app, |
|
48 |
+ error_email=app_ctx.conf['email_to'], |
|
49 |
+ error_log=app_ctx.conf.get('error_log', None), |
|
50 |
+ error_message=app_ctx.conf.get('error_message', 'An internal server error occurred'), |
|
51 |
+ error_subject_prefix=app_ctx.conf.get('error_subject_prefix', 'Web application error: '), |
|
52 |
+ from_address=app_ctx.conf['from_address'], |
|
53 |
+ smtp_server=app_ctx.conf.get('smtp_server', 'localhost'), |
|
54 |
+ ) |
|
55 |
+ app = Cascade([StaticURLParser(os.path.join(app_ctx.conf['app_dir'], 'static')), app]) |
|
56 |
+ app.ctx = app_ctx |
|
57 |
+ return app |
... | ... |
@@ -0,0 +1,30 @@ |
1 |
+# -*- coding: utf-8 -*- |
|
2 |
+ |
|
3 |
+ |
|
4 |
+"""Paste INI configuration""" |
|
5 |
+ |
|
6 |
+ |
|
7 |
+import os |
|
8 |
+ |
|
9 |
+from biryani1 import strings |
|
10 |
+from biryani1.baseconv import (check, default, guess_bool, pipe, struct) |
|
11 |
+ |
|
12 |
+ |
|
13 |
+def load_configuration(global_conf, app_conf): |
|
14 |
+ """Build the application configuration dict.""" |
|
15 |
+ app_dir = os.path.dirname(os.path.abspath(__file__)) |
|
16 |
+ conf = {} |
|
17 |
+ conf.update(strings.deep_decode(global_conf)) |
|
18 |
+ conf.update(strings.deep_decode(app_conf)) |
|
19 |
+ conf.update(check(struct( |
|
20 |
+ { |
|
21 |
+ 'app_conf': default(app_conf), |
|
22 |
+ 'app_dir': default(app_dir), |
|
23 |
+ 'cache_dir': default(os.path.join(os.path.dirname(app_dir), 'cache')), |
|
24 |
+ 'debug': pipe(guess_bool, default(False)), |
|
25 |
+ 'global_conf': default(global_conf), |
|
26 |
+ }, |
|
27 |
+ default='drop', |
|
28 |
+ drop_none_values=False, |
|
29 |
+ ))(conf)) |
|
30 |
+ return conf |
... | ... |
@@ -0,0 +1,24 @@ |
1 |
+# -*- coding: utf-8 -*- |
|
2 |
+ |
|
3 |
+ |
|
4 |
+"""Context loaded and saved in WSGI requests""" |
|
5 |
+ |
|
6 |
+ |
|
7 |
+from webob.dec import wsgify |
|
8 |
+ |
|
9 |
+ |
|
10 |
+def make_add_context_to_request(app, app_ctx): |
|
11 |
+ """Return a WSGI middleware that adds context to requests.""" |
|
12 |
+ @wsgify |
|
13 |
+ def add_context_to_request(req): |
|
14 |
+ req.ctx = app_ctx |
|
15 |
+ req.ctx.req = req |
|
16 |
+ return req.get_response(app) |
|
17 |
+ return add_context_to_request |
|
18 |
+ |
|
19 |
+ |
|
20 |
+class Context(object): |
|
21 |
+ _ = lambda self, message: message |
|
22 |
+ conf = None |
|
23 |
+ templates = None |
|
24 |
+ db = None |
... | ... |
@@ -0,0 +1,70 @@ |
1 |
+# -*- coding: utf-8 -*- |
|
2 |
+ |
|
3 |
+ |
|
4 |
+import logging |
|
5 |
+ |
|
6 |
+from webob.dec import wsgify |
|
7 |
+ |
|
8 |
+from . import conv, router, templates, wsgi_helpers |
|
9 |
+ |
|
10 |
+import json |
|
11 |
+ |
|
12 |
+log = logging.getLogger(__name__) |
|
13 |
+ |
|
14 |
+@wsgify |
|
15 |
+def home(req): |
|
16 |
+ return templates.render(req.ctx, '/home.mako', data={}) |
|
17 |
+ |
|
18 |
+@wsgify |
|
19 |
+def login(req): |
|
20 |
+ params = req.params |
|
21 |
+ log.debug(u'params = {}'.format(params)) |
|
22 |
+ inputs = { |
|
23 |
+ 'email': params.get('email'), |
|
24 |
+ 'password': params.get('password'), |
|
25 |
+ } |
|
26 |
+ log.debug(u'inputs = {}'.format(inputs)) |
|
27 |
+ data, errors = conv.inputs_to_login_data(inputs) |
|
28 |
+ if errors is not None: |
|
29 |
+ return wsgi_helpers.bad_request(req.ctx, comment=errors) |
|
30 |
+ |
|
31 |
+ log.debug(u'data = {}'.format(data)) |
|
32 |
+ |
|
33 |
+ login_data=req.ctx.db.login(data['email'],data['password']) |
|
34 |
+ return wsgi_helpers.respond_json(req.ctx,login_data,headers=[('Access-Control-Allow-Origin','*')]) |
|
35 |
+ |
|
36 |
+@wsgify |
|
37 |
+def sync(req): |
|
38 |
+ params = req.params |
|
39 |
+ log.debug(u'params = {}'.format(params)) |
|
40 |
+ inputs = { |
|
41 |
+ 'email': params.get('email'), |
|
42 |
+ 'password': params.get('password'), |
|
43 |
+ 'groups': params.get('groups') |
|
44 |
+ } |
|
45 |
+ log.debug(u'inputs = {}'.format(inputs)) |
|
46 |
+ data, errors = conv.inputs_to_sync_data(inputs) |
|
47 |
+ if errors is not None or data['groups'] is None: |
|
48 |
+ return wsgi_helpers.bad_request(req.ctx, comment=errors) |
|
49 |
+ |
|
50 |
+ data['groups']=json.loads(data['groups']) |
|
51 |
+ |
|
52 |
+ log.debug(u'data = {}'.format(data)) |
|
53 |
+ |
|
54 |
+ login_data=req.ctx.db.login(data['email'],data['password']) |
|
55 |
+ if 'email' in login_data: |
|
56 |
+ ret=req.ctx.db.sync_group(data['email'],data['groups']) |
|
57 |
+ return wsgi_helpers.respond_json(req.ctx,ret,headers=[('Access-Control-Allow-Origin','*')]) |
|
58 |
+ else: |
|
59 |
+ return wsgi_helpers.respond_json( |
|
60 |
+ req.ctx, |
|
61 |
+ login_data, |
|
62 |
+ headers=[('Access-Control-Allow-Origin','*')] |
|
63 |
+ ) |
|
64 |
+ |
|
65 |
+def make_router(): |
|
66 |
+ return router.make_router( |
|
67 |
+ ('GET', '^/$', home), |
|
68 |
+ ('GET', '^/login$', login), |
|
69 |
+ ('GET', '^/sync$', sync), |
|
70 |
+ ) |
... | ... |
@@ -0,0 +1,23 @@ |
1 |
+# -*- coding: utf-8 -*- |
|
2 |
+ |
|
3 |
+ |
|
4 |
+from biryani1.baseconv import cleanup_line, empty_to_none, not_none, pipe, struct |
|
5 |
+ |
|
6 |
+inputs_to_login_data = struct( |
|
7 |
+ { |
|
8 |
+ 'email': pipe(cleanup_line, empty_to_none), |
|
9 |
+ 'password': pipe(cleanup_line, empty_to_none), |
|
10 |
+ }, |
|
11 |
+ default='drop', |
|
12 |
+ drop_none_values=False, |
|
13 |
+ ) |
|
14 |
+ |
|
15 |
+inputs_to_sync_data = struct( |
|
16 |
+ { |
|
17 |
+ 'email': pipe(cleanup_line, empty_to_none), |
|
18 |
+ 'password': pipe(cleanup_line, empty_to_none), |
|
19 |
+ 'groups': pipe(empty_to_none), |
|
20 |
+ }, |
|
21 |
+ default='drop', |
|
22 |
+ drop_none_values=False, |
|
23 |
+ ) |
... | ... |
@@ -0,0 +1,84 @@ |
1 |
+#!/usr/bin/python |
|
2 |
+# -*- coding: utf-8 -*- |
|
3 |
+ |
|
4 |
+import json |
|
5 |
+import logging |
|
6 |
+log = logging.getLogger(__name__) |
|
7 |
+import MySQLdb |
|
8 |
+ |
|
9 |
+class DB(object): |
|
10 |
+ |
|
11 |
+ def __init__(self,host,user,pwd,db): |
|
12 |
+ self.host = host |
|
13 |
+ self.user = user |
|
14 |
+ self.pwd = pwd |
|
15 |
+ self.db = db |
|
16 |
+ self.con = 0 |
|
17 |
+ |
|
18 |
+ def connect(self): |
|
19 |
+ if self.con == 0: |
|
20 |
+ try: |
|
21 |
+ con = MySQLdb.connect(self.host,self.user,self.pwd,self.db) |
|
22 |
+ self.con = con |
|
23 |
+ return True |
|
24 |
+ except Exception, e: |
|
25 |
+ log.fatal('Error connecting to database : %s' % e) |
|
26 |
+ return |
|
27 |
+ |
|
28 |
+ def do_sql(self,sql): |
|
29 |
+ try: |
|
30 |
+ c=self.con.cursor() |
|
31 |
+ c.execute(sql) |
|
32 |
+ self.con.commit() |
|
33 |
+ return c |
|
34 |
+ except Exception,e: |
|
35 |
+ log.error('Error executing request %s : %s' % (sql,e)) |
|
36 |
+ return False |
|
37 |
+ |
|
38 |
+ def select(self,sql): |
|
39 |
+ ret=self.do_sql(sql) |
|
40 |
+ if ret!=False: |
|
41 |
+ return ret.fetchall() |
|
42 |
+ return ret |
|
43 |
+ |
|
44 |
+ def login(self,email,password): |
|
45 |
+ ret=self.select("SELECT email,name,password FROM users WHERE email='%s' AND password='%s'" % (email,password)) |
|
46 |
+ log.debug(ret) |
|
47 |
+ if ret: |
|
48 |
+ if len(ret)==1: |
|
49 |
+ return { |
|
50 |
+ 'email': ret[0][0], |
|
51 |
+ 'name': ret[0][1] |
|
52 |
+ } |
|
53 |
+ elif len(ret)>=1: |
|
54 |
+ log.warning('Duplicate user %s in database' % email) |
|
55 |
+ elif ret==(): |
|
56 |
+ return { 'loginerror': 'Utilisateur inconnu' } |
|
57 |
+ return { 'loginerror': 'Erreur inconnu' } |
|
58 |
+ |
|
59 |
+ def sync_group(self,email,groups): |
|
60 |
+ db_groups=self.get_group(email) |
|
61 |
+ json_group=json.dumps(groups) |
|
62 |
+ if db_groups!=False: |
|
63 |
+ if db_groups=={}: |
|
64 |
+ if groups=={}: |
|
65 |
+ return {'groups': {}} |
|
66 |
+ else: |
|
67 |
+ if self.do_sql("INSERT INTO groups (email,groups) VALUES ('%s','%s')" % (email,json_group)): |
|
68 |
+ return {'groups': groups} |
|
69 |
+ elif groups=={}: |
|
70 |
+ return {'groups': db_groups} |
|
71 |
+ else: |
|
72 |
+ if self.do_sql("UPDATE groups SET groups='%s' WHERE email='%s'" % (json_group,email)): |
|
73 |
+ return {'groups': groups} |
|
74 |
+ return {'syncerror': 'Erreur inconnu'} |
|
75 |
+ |
|
76 |
+ def get_group(self,email): |
|
77 |
+ ret=self.select("SELECT groups FROM groups WHERE email='%s'" % email) |
|
78 |
+ if ret!=False: |
|
79 |
+ if len(ret)==1: |
|
80 |
+ return json.loads(ret[0][0]) |
|
81 |
+ else: |
|
82 |
+ return {} |
|
83 |
+ else: |
|
84 |
+ return False |
... | ... |
@@ -0,0 +1,50 @@ |
1 |
+# -*- coding: utf-8 -*- |
|
2 |
+ |
|
3 |
+ |
|
4 |
+"""Helpers for URLs""" |
|
5 |
+ |
|
6 |
+ |
|
7 |
+import logging |
|
8 |
+import re |
|
9 |
+ |
|
10 |
+from webob.dec import wsgify |
|
11 |
+ |
|
12 |
+from . import wsgi_helpers |
|
13 |
+ |
|
14 |
+ |
|
15 |
+log = logging.getLogger(__name__) |
|
16 |
+ |
|
17 |
+ |
|
18 |
+def make_router(*routings): |
|
19 |
+ """Return a WSGI application that dispatches requests to controllers.""" |
|
20 |
+ routes = [] |
|
21 |
+ for routing in routings: |
|
22 |
+ methods, regex, app = routing[:3] |
|
23 |
+ if isinstance(methods, basestring): |
|
24 |
+ methods = (methods,) |
|
25 |
+ vars = routing[3] if len(routing) >= 4 else {} |
|
26 |
+ routes.append((methods, re.compile(regex), app, vars)) |
|
27 |
+ |
|
28 |
+ @wsgify |
|
29 |
+ def router(req): |
|
30 |
+ """Dispatch request to controllers.""" |
|
31 |
+ split_path_info = req.path_info.split('/') |
|
32 |
+ assert not split_path_info[0], split_path_info |
|
33 |
+ for methods, regex, app, vars in routes: |
|
34 |
+ if methods is None or req.method in methods: |
|
35 |
+ match = regex.match(req.path_info) |
|
36 |
+ if match is not None: |
|
37 |
+ log.debug(u'URL path = {path} matched controller {controller}'.format( |
|
38 |
+ controller=app, path=req.path_info)) |
|
39 |
+ if getattr(req, 'urlvars', None) is None: |
|
40 |
+ req.urlvars = {} |
|
41 |
+ req.urlvars.update(dict( |
|
42 |
+ (name, value.decode('utf-8') if value is not None else None) |
|
43 |
+ for name, value in match.groupdict().iteritems() |
|
44 |
+ )) |
|
45 |
+ req.urlvars.update(vars) |
|
46 |
+ req.script_name += req.path_info[:match.end()] |
|
47 |
+ req.path_info = req.path_info[match.end():] |
|
48 |
+ return req.get_response(app) |
|
49 |
+ return wsgi_helpers.not_found(req.ctx) |
|
50 |
+ return router |
... | ... |
@@ -0,0 +1,44 @@ |
1 |
+# -*- coding: utf-8 -*- |
|
2 |
+ |
|
3 |
+ |
|
4 |
+"""Mako templates rendering""" |
|
5 |
+ |
|
6 |
+ |
|
7 |
+import json |
|
8 |
+import mako.lookup |
|
9 |
+import os |
|
10 |
+ |
|
11 |
+from . import helpers |
|
12 |
+ |
|
13 |
+ |
|
14 |
+js = lambda x: json.dumps(x, encoding='utf-8', ensure_ascii=False) |
|
15 |
+ |
|
16 |
+ |
|
17 |
+def load_templates(ctx): |
|
18 |
+ # Create the Mako TemplateLookup, with the default auto-escaping. |
|
19 |
+ return mako.lookup.TemplateLookup( |
|
20 |
+ default_filters=['h'], |
|
21 |
+ directories=[os.path.join(ctx.conf['app_dir'], 'templates')], |
|
22 |
+ input_encoding='utf-8', |
|
23 |
+ module_directory=os.path.join(ctx.conf['cache_dir'], 'templates'), |
|
24 |
+ ) |
|
25 |
+ |
|
26 |
+ |
|
27 |
+def render(ctx, template_path, **kw): |
|
28 |
+ return ctx.templates.get_template(template_path).render_unicode( |
|
29 |
+ ctx=ctx, |
|
30 |
+ helpers=helpers, |
|
31 |
+ js=js, |
|
32 |
+ N_=lambda message: message, |
|
33 |
+ req=ctx.req, |
|
34 |
+ **kw).strip() |
|
35 |
+ |
|
36 |
+ |
|
37 |
+def render_def(ctx, template_path, def_name, **kw): |
|
38 |
+ return ctx.templates.get_template(template_path).get_def(def_name).render_unicode( |
|
39 |
+ _=ctx.translator.ugettext, |
|
40 |
+ ctx=ctx, |
|
41 |
+ js=js, |
|
42 |
+ N_=lambda message: message, |
|
43 |
+ req=ctx.req, |
|
44 |
+ **kw).strip() |
... | ... |
@@ -0,0 +1,14 @@ |
1 |
+# -*- coding: utf-8 -*- |
|
2 |
+ |
|
3 |
+ |
|
4 |
+import os |
|
5 |
+import random |
|
6 |
+import datetime |
|
7 |
+ |
|
8 |
+ |
|
9 |
+def random_sequence(length): |
|
10 |
+ return [random.random() for idx in xrange(0, length)] |
|
11 |
+ |
|
12 |
+ |
|
13 |
+def relative_path(ctx, abs_path): |
|
14 |
+ return os.path.relpath(abs_path, ctx.req.path) |
... | ... |
@@ -0,0 +1,13 @@ |
1 |
+## -*- coding: utf-8 -*- |
|
2 |
+ |
|
3 |
+ |
|
4 |
+<%inherit file="/site.mako"/> |
|
5 |
+ |
|
6 |
+<%block name="title_content">MyCo</%block> |
|
7 |
+ |
|
8 |
+<%block name="body_content"> |
|
9 |
+<div class="hero-unit"> |
|
10 |
+ <h1>MyCo <small>Gérer vos déponses communes</small></h1> |
|
11 |
+ <p class="muted">Application mobile de gestion de vos dépenses communes.</p> |
|
12 |
+</div> |
|
13 |
+</%block> |
... | ... |
@@ -0,0 +1,23 @@ |
1 |
+## -*- coding: utf-8 -*- |
|
2 |
+ |
|
3 |
+ |
|
4 |
+<%inherit file="/site.mako"/> |
|
5 |
+ |
|
6 |
+ |
|
7 |
+<%block name="body_content"> |
|
8 |
+<div class="alert alert-block alert-error"> |
|
9 |
+ <h4 class="alert-heading">Error « ${title} »</h4> |
|
10 |
+ <p>${explanation}</p> |
|
11 |
+% if comment: |
|
12 |
+ <p>${comment}</p> |
|
13 |
+% endif |
|
14 |
+% if message: |
|
15 |
+ <p>${message}</p> |
|
16 |
+% endif |
|
17 |
+</div> |
|
18 |
+</%block> |
|
19 |
+ |
|
20 |
+ |
|
21 |
+<%block name="title_content"> |
|
22 |
+${title} - ${parent.title_content()} |
|
23 |
+</%block> |
... | ... |
@@ -0,0 +1,36 @@ |
1 |
+<!DOCTYPE html> |
|
2 |
+<html lang="fr"> |
|
3 |
+ <head> |
|
4 |
+ <meta charset="utf-8"> |
|
5 |
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
6 |
+ <title><%block name="title_content">MyCo</%block></title> |
|
7 |
+ <link rel="stylesheet" href="https://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css"> |
|
8 |
+ <link rel="stylesheet" href="https://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap-theme.min.css"> |
|
9 |
+ <link rel="stylesheet" href="${helpers.relative_path(ctx, '/css/style.css')}"> |
|
10 |
+ <!--[if lt IE 9]> |
|
11 |
+ <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> |
|
12 |
+ <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> |
|
13 |
+ <![endif]--> |
|
14 |
+ </head> |
|
15 |
+ <body> |
|
16 |
+ |
|
17 |
+ |
|
18 |
+<div class="navbar navbar-inverse navbar-fixed-top" role="navigation"> |
|
19 |
+ <div class="container"> |
|
20 |
+ <div class="navbar-header"> |
|
21 |
+ <a class="navbar-brand" href="index.html">MyCo</a> |
|
22 |
+ </div> |
|
23 |
+ </div> |
|
24 |
+</div> |
|
25 |
+ |
|
26 |
+<div class="container"> |
|
27 |
+ |
|
28 |
+<%block name="body_content"/> |
|
29 |
+ |
|
30 |
+</div> |
|
31 |
+ |
|
32 |
+<script src="https://code.jquery.com/jquery.js"></script> |
|
33 |
+<script src="https://netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script> |
|
34 |
+ |
|
35 |
+ </body> |
|
36 |
+</html> |
... | ... |
@@ -0,0 +1,170 @@ |
1 |
+# -*- coding: utf-8 -*- |
|
2 |
+ |
|
3 |
+ |
|
4 |
+import collections |
|
5 |
+import json |
|
6 |
+ |
|
7 |
+from markupsafe import Markup |
|
8 |
+from webhelpers.html import tags |
|
9 |
+import webob.dec |
|
10 |
+import webob.exc |
|
11 |
+ |
|
12 |
+from . import templates |
|
13 |
+ |
|
14 |
+ |
|
15 |
+N_ = lambda message: message |
|
16 |
+ |
|
17 |
+ |
|
18 |
+errors_explanation = { |
|
19 |
+ 400: N_("Request is faulty"), |
|
20 |
+ 401: N_("Access is restricted to authorized persons."), |
|
21 |
+ 403: N_("Access is forbidden."), |
|
22 |
+ 404: N_("The requested page was not found."), |
|
23 |
+ } |
|
24 |
+errors_message = { |
|
25 |
+ 401: N_("You must login to access this page."), |
|
26 |
+ } |
|
27 |
+errors_title = { |
|
28 |
+ 400: N_("Unable to Access"), |
|
29 |
+ 401: N_("Access Denied"), |
|
30 |
+ 403: N_("Access Denied"), |
|
31 |
+ 404: N_("Unable to Access"), |
|
32 |
+ } |
|
33 |
+ |
|
34 |
+ |
|
35 |
+def bad_request(ctx, **kw): |
|
36 |
+ return error(ctx, 400, **kw) |
|
37 |
+ |
|
38 |
+ |
|
39 |
+def discard_empty_items(data): |
|
40 |
+ if isinstance(data, collections.Mapping): |
|
41 |
+ # Use type(data) to keep OrderedDicts. |
|
42 |
+ data = type(data)( |
|
43 |
+ (name, discard_empty_items(value)) |
|
44 |
+ for name, value in data.iteritems() |
|
45 |
+ if value is not None |
|
46 |
+ ) |
|
47 |
+ return data |
|
48 |
+ |
|
49 |
+ |
|
50 |
+def error(ctx, code, **kw): |
|
51 |
+ response = webob.exc.status_map[code](headers=kw.pop('headers', None)) |
|
52 |
+ if code != 204: # No content |
|
53 |
+ body = kw.pop('body', None) |
|
54 |
+ if body is None: |
|
55 |
+ template_path = kw.pop('template_path', '/http-error.mako') |
|
56 |
+ explanation = kw.pop('explanation', None) |
|
57 |
+ if explanation is None: |
|
58 |
+ explanation = errors_explanation.get(code) |
|
59 |
+ explanation = ctx._(explanation) if explanation is not None else response.explanation |
|
60 |
+ message = kw.pop('message', None) |
|
61 |
+ if message is None: |
|
62 |
+ message = errors_message.get(code) |
|
63 |
+ if message is not None: |
|
64 |
+ message = ctx._(message) |
|
65 |
+ comment = kw.pop('comment', None) |
|
66 |
+ if isinstance(comment, dict): |
|
67 |
+ comment = tags.ul(u'{0}Â : {1}'.format(key, value) for key, value in comment.iteritems()) |
|
68 |
+ elif isinstance(comment, list): |
|
69 |
+ comment = tags.ul(comment) |
|
70 |
+ title = kw.pop('title', None) |
|
71 |
+ if title is None: |
|
72 |
+ title = errors_title.get(code) |
|
73 |
+ title = ctx._(title) if title is not None else response.status |
|
74 |
+ body = templates.render(ctx, template_path, |
|
75 |
+ comment=comment, |
|
76 |
+ explanation=explanation, |
|
77 |
+ message=message, |
|
78 |
+ response=response, |
|
79 |
+ title=title, |
|
80 |
+ **kw) |
|
81 |
+ response.body = body.encode('utf-8') if isinstance(body, unicode) else body |
|
82 |
+ return response |
|
83 |
+ |
|
84 |
+ |
|
85 |
+def forbidden(ctx, **kw): |
|
86 |
+ return error(ctx, 403, **kw) |
|
87 |
+ |
|
88 |
+ |
|
89 |
+def method_not_allowed(ctx, **kw): |
|
90 |
+ return error(ctx, 405, **kw) |
|
91 |
+ |
|
92 |
+ |
|
93 |
+def no_content(ctx, headers=None): |
|
94 |
+ return error(ctx, 204, headers=headers) |
|
95 |
+ |
|
96 |
+ |
|
97 |
+def not_found(ctx, **kw): |
|
98 |
+ return error(ctx, 404, **kw) |
|
99 |
+ |
|
100 |
+ |
|
101 |
+def redirect(ctx, code=302, location=None, **kw): |
|
102 |
+ assert location is not None |
|
103 |
+ location_str = location.encode('utf-8') if isinstance(location, unicode) else location |
|
104 |
+ response = webob.exc.status_map[code](headers=kw.pop('headers', None), location=location_str) |
|
105 |
+ body = kw.pop('body', None) |
|
106 |
+ if body is None: |
|
107 |
+ template_path = kw.pop('template_path', '/http-error.mako') |
|
108 |
+ explanation = kw.pop('explanation', None) |
|
109 |
+ if explanation is None: |
|
110 |
+ explanation = Markup(u'{0} <a href="{1}">{1}</a>.').format(ctx._(u"You'll be redirected to page"), location) |
|
111 |
+ message = kw.pop('message', None) |
|
112 |
+ if message is None: |
|
113 |
+ message = errors_message.get(code) |
|
114 |
+ if message is not None: |
|
115 |
+ message = ctx._(message) |
|
116 |
+ title = kw.pop('title', None) |
|
117 |
+ if title is None: |
|
118 |
+ title = ctx._("Redirection in progress...") |
|
119 |
+ body = templates.render(ctx, template_path, |
|
120 |
+ comment=kw.pop('comment', None), |
|
121 |
+ explanation=explanation, |
|
122 |
+ message=message, |
|
123 |
+ response=response, |
|
124 |
+ title=title, |
|
125 |
+ **kw) |
|
126 |
+ response.body = body.encode('utf-8') if isinstance(body, unicode) else body |
|
127 |
+ return response |
|
128 |
+ |
|
129 |
+ |
|
130 |
+def respond_json(ctx, data, code=None, headers=None, jsonp=None): |
|
131 |
+ """Return a JSON response. |
|
132 |
+ |
|
133 |
+ This function is optimized for JSON following |
|
134 |
+ `Google JSON Style Guide <http://google-styleguide.googlecode.com/svn/trunk/jsoncstyleguide.xml>`_, but will handle |
|
135 |
+ any JSON except for HTTP errors. |
|
136 |
+ """ |
|
137 |
+ if isinstance(data, collections.Mapping): |
|
138 |
+ # Remove null properties as recommended by Google JSON Style Guide. |
|
139 |
+ data = discard_empty_items(data) |
|
140 |
+ error = data.get('error') |
|
141 |
+ else: |
|
142 |
+ error = None |
|
143 |
+ if headers is None: |
|
144 |
+ headers = [] |
|
145 |
+ if jsonp: |
|
146 |
+ headers.append(('Content-Type', 'application/javascript; charset=utf-8')) |
|
147 |
+ else: |
|
148 |
+ headers.append(('Content-Type', 'application/json; charset=utf-8')) |
|
149 |
+ if error: |
|
150 |
+ code = code or error['code'] |
|
151 |
+ assert isinstance(code, int) |
|
152 |
+ response = webob.exc.status_map[code](headers=headers) |
|
153 |
+ if error.get('code') is None: |
|
154 |
+ error['code'] = code |
|
155 |
+ if error.get('message') is None: |
|
156 |
+ error['message'] = response.title |
|
157 |
+ else: |
|
158 |
+ response = ctx.req.response |
|
159 |
+ if code is not None: |
|
160 |
+ response.status = code |
|
161 |
+ response.headers.update(headers) |
|
162 |
+ text = unicode(json.dumps(data, encoding='utf-8', ensure_ascii=False, indent=2, sort_keys=True)) |
|
163 |
+ if jsonp: |
|
164 |
+ text = u'{0}({1})'.format(jsonp, text) |
|
165 |
+ response.text = text |
|
166 |
+ return response |
|
167 |
+ |
|
168 |
+ |
|
169 |
+def unauthorized(ctx, **kw): |
|
170 |
+ return error(ctx, 401, **kw) |
... | ... |
@@ -0,0 +1,40 @@ |
1 |
+#!/usr/bin/env python |
|
2 |
+# -*- coding: utf-8 -*- |
|
3 |
+ |
|
4 |
+ |
|
5 |
+"""MyCO Server web application.""" |
|
6 |
+ |
|
7 |
+ |
|
8 |
+from setuptools import setup, find_packages |
|
9 |
+ |
|
10 |
+ |
|
11 |
+doc_lines = __doc__.split('\n') |
|
12 |
+ |
|
13 |
+ |
|
14 |
+setup( |
|
15 |
+ author=u'Benjamin Renard', |
|
16 |
+ author_email=u'brenard@zionetrix.net', |
|
17 |
+ description=doc_lines[0], |
|
18 |
+ entry_points=""" |
|
19 |
+ [paste.app_factory] |
|
20 |
+ main = mycoserver.application:make_app |
|
21 |
+ """, |
|
22 |
+ include_package_data=True, |
|
23 |
+ install_requires=[ |
|
24 |
+ 'Biryani1 >= 0.9dev', |
|
25 |
+ 'MarkupSafe >= 0.15', |
|
26 |
+ 'WebError >= 0.10', |
|
27 |
+ 'WebHelpers >= 1.3', |
|
28 |
+ 'WebOb >= 1.1', |
|
29 |
+ ], |
|
30 |
+# keywords='', |
|
31 |
+# license=u'http://www.fsf.org/licensing/licenses/agpl-3.0.html', |
|
32 |
+ long_description='\n'.join(doc_lines[2:]), |
|
33 |
+ name=u'MyCoServer', |
|
34 |
+ packages=find_packages(), |
|
35 |
+ paster_plugins=['PasteScript'], |
|
36 |
+ setup_requires=['PasteScript >= 1.6.3'], |
|
37 |
+# url=u'', |
|
38 |
+ version='0.1', |
|
39 |
+ zip_safe=False, |
|
40 |
+ ) |
|
0 | 41 |