Commit 258b4a98 authored by Andreas Valder's avatar Andreas Valder

added module support and a basic api module

parent 3cabc9d6
from server import app
from modules.mod_api import api
app.register_blueprint(api)
from flask import Blueprint, jsonify
from server import app, query
import json
api = Blueprint('api', __name__, url_prefix='/api')
from . import autodoc
doc = autodoc.Autodoc(app)
from . import errorhandlers
from . import help
from . import auth
from . import public
from . import *
from server import dologin, ismod as session_ismod
from flask import make_response, request, session
def ismod():
return session_ismod()
@api.route('/moderator/login', methods=['POST', 'GET'])
@doc.doc()
def modlogin():
"""Returns a access token in a cookie used for all other moderator functions. Accepts *user* and *password* as post data"""
return jsonify(dologin(request.values['user'], request.values['password']))
@api.route('/moderator/logout', methods=['POST', 'GET'])
@doc.doc()
def modlogout():
"""Removes a moderator token in a cookie"""
session.pop('user', None)
return jsonify(True)
@api.route('/moderator/test')
@doc.doc()
def modtest():
"""Returns if you are logged in as moderator"""
return jsonify(ismod())
from operator import attrgetter, itemgetter
import os
import re
from collections import defaultdict
import sys
import inspect
from flask import current_app, render_template, render_template_string
from jinja2 import evalcontextfilter
try:
from flask import _app_ctx_stack as stack
except ImportError:
from flask import _request_ctx_stack as stack
if sys.version < '3':
get_function_code = attrgetter('func_code')
else:
get_function_code = attrgetter('__code__')
class Autodoc(object):
def __init__(self, app=None):
self.app = app
self.func_groups = defaultdict(set)
self.func_props = defaultdict()
self.immutable_props = ['rule', 'endpoint']
self.default_props = ['methods', 'docstring',
'args', 'defaults', 'location'] + self.immutable_props
self.func_locations = defaultdict(dict)
if app is not None:
self.init_app(app)
def init_app(self, app):
if hasattr(app, 'teardown_appcontext'):
app.teardown_appcontext(self.teardown)
else:
app.teardown_request(self.teardown)
self.add_custom_template_filters(app)
def teardown(self, exception):
ctx = stack.top
def add_custom_template_filters(self, app):
"""Add custom filters to jinja2 templating engine"""
self.add_custom_nl2br_filters(app)
def add_custom_nl2br_filters(self, app):
"""Add a custom filter nl2br to jinja2
Replaces all newline to <BR>
"""
_paragraph_re = re.compile(r'(?:\r\n|\r|\n){3,}')
@app.template_filter()
@evalcontextfilter
def nl2br(eval_ctx, value):
result = '\n\n'.join('%s' % p.replace('\n', '<br>\n')
for p in _paragraph_re.split(value))
return result
def doc(self, groups=None, set_location=True, **properties):
"""Add flask route to autodoc for automatic documentation
Any route decorated with this method will be added to the list of
routes to be documented by the generate() or html() methods.
By default, the route is added to the 'all' group.
By specifying group or groups argument, the route can be added to one
or multiple other groups as well, besides the 'all' group.
If set_location is True, the location of the function will be stored.
NOTE: this assumes that the decorator is placed just before the
function (in the normal way).
Custom parameters may also be passed in beyond groups, if they are
named something not already in the dict descibed in the docstring for
the generare() function, they will be added to the route's properties,
which can be accessed from the template.
If a parameter is passed in with a name that is already in the dict, but
not of a reserved name, the passed parameter overrides that dict value.
"""
def decorator(f):
# Get previous group list (if any)
if f in self.func_groups:
groupset = self.func_groups[f]
else:
groupset = set()
# Set group[s]
if type(groups) is list:
groupset.update(groups)
elif type(groups) is str:
groupset.add(groups)
groupset.add('all')
self.func_groups[f] = groupset
self.func_props[f] = properties
# Set location
if set_location:
caller_frame = inspect.stack()[1]
self.func_locations[f] = {
'filename': caller_frame[1],
'line': caller_frame[2],
}
return f
return decorator
def generate(self, groups='all', sort=None):
"""Return a list of dict describing the routes specified by the
doc() method
Each dict contains:
- methods: the set of allowed methods (ie ['GET', 'POST'])
- rule: relative url (ie '/user/<int:id>')
- endpoint: function name (ie 'show_user')
- doc: docstring of the function
- args: function arguments
- defaults: defaults values for the arguments
By specifying the group or groups arguments, only routes belonging to
those groups will be returned.
Routes are sorted alphabetically based on the rule.
"""
groups_to_generate = list()
if type(groups) is list:
groups_to_generate = groups
elif type(groups) is str:
groups_to_generate.append(groups)
links = []
for rule in current_app.url_map.iter_rules():
if rule.endpoint == 'static':
continue
func = current_app.view_functions[rule.endpoint]
arguments = rule.arguments if rule.arguments else ['None']
func_groups = self.func_groups[func]
func_props = self.func_props[func] if func in self.func_props \
else {}
location = self.func_locations.get(func, None)
if func_groups.intersection(groups_to_generate):
props = dict(
methods=rule.methods,
rule="%s" % rule,
endpoint=rule.endpoint,
docstring=func.__doc__,
args=arguments,
defaults=rule.defaults,
location=location,
)
for p in func_props:
if p not in self.immutable_props:
props[p] = func_props[p]
links.append(props)
if sort:
return sort(links)
else:
return sorted(links, key=itemgetter('rule'))
def html(self, groups='all', template=None, **context):
"""Return an html string of the routes specified by the doc() method
A template can be specified. A list of routes is available under the
'autodoc' value (refer to the documentation for the generate() for a
description of available values). If no template is specified, a
default template is used.
By specifying the group or groups arguments, only routes belonging to
those groups will be returned.
"""
context['autodoc'] = context['autodoc'] if 'autodoc' in context \
else self.generate(groups=groups)
context['defaults'] = context['defaults'] if 'defaults' in context \
else self.default_props
if template:
return render_template(template, **context)
else:
filename = os.path.join(
os.path.dirname(__file__),
'templates',
'autodoc_default.html'
)
with open(filename) as file:
content = file.read()
with current_app.app_context():
return render_template_string(content, **context)
The MIT License (MIT)
Copyright (c) 2013 Arnaud Coomans
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
\ No newline at end of file
from . import *
import traceback
@api.errorhandler(404)
def handle_not_found(e=None):
return 'Error', 404
@api.errorhandler(500)
@api.errorhandler(Exception)
def handle_internal_error(e):
if not e:
return 'Error', 505
else:
traceback.print_exc()
return 'Error', 500
from . import *
@api.route('/help')
@api.route('/')
@doc.doc()
def help():
"""Prints an API description """
return doc.html()
from . import *
from server import genlive
@api.route('/courses')
@doc.doc()
def courses():
"""Returns a list of all courses without the lectures, optionaly takes a moderator token as request parameter *token* """
return jsonify(query('SELECT title, description, handle, id, organizer, semester, short, subject FROM courses WHERE (? OR (visible AND listed)) ORDER BY lower(semester), lower(title)'), auth.ismod())
@api.route('/<int:id>')
@api.route('/<handle>')
@doc.doc()
def course(id=None, handle=None):
"""Returns data to a single course, including the lectures, optionaly takes a moderator token as request parameter *token* """
coursedata = query('SELECT title, description, handle, id, organizer, semester, short, subject FROM courses WHERE (? OR (visible AND listed)) AND (id = ? OR handle = ?) ORDER BY lower(semester), lower(title)', auth.ismod(), id, handle)
if len(coursedata) == 1:
coursedata[0]['lectures'] = query('SELECT id, title, time, comment, speaker, duration, place FROM lectures WHERE course_id = ? AND (visible or ?)', coursedata[0]['id'], auth.ismod())
for lecture in coursedata[0]['lectures']:
videos = query('''
SELECT videos.id, videos.comment, videos.file_size, videos.hash, videos.path, "formats" AS sep, formats.description, formats.resolution
FROM videos
JOIN lectures ON (videos.lecture_id = lectures.id)
JOIN formats ON (videos.video_format = formats.id)
JOIN courses ON (lectures.course_id = courses.id)
WHERE lectures.course_id= ? AND (? OR videos.visible)
ORDER BY lectures.time, formats.prio DESC
''', coursedata[0]['id'], auth.ismod())
livestreams = query('''SELECT streams.handle AS livehandle, streams.lecture_id, "formats" AS sep, formats.*
FROM streams
JOIN lectures ON lectures.id = streams.lecture_id
JOIN formats ON formats.keywords = "hls"
WHERE streams.active AND (? OR streams.visible) AND lectures.course_id = ?
''', auth.ismod(), coursedata[0]['id'])
videos += genlive(livestreams)
lecture['videos'] = videos
return jsonify(coursedata[0] if len(coursedata) == 1 else [])
<html>
<head>
<title>
{% if title is defined -%}
{{title}}
{% else -%}
Documentation
{% endif -%}
</title>
<style>
* {
margin: 0;
padding: 0;
font-family: Verdana, "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
}
body {
margin: 10px;
}
div.mapping {
margin: 20px 20px;
}
ul.methods:before { content: "Methods: "; }
ul.methods li {
display: inline;
list-style: none;
}
ul.methods li:after { content: ","; }
ul.methods li:last-child:after { content: ""; }
ul.arguments:before { content: "Arguments: "; }
ul.arguments li {
display: inline;
list-style: none;
}
ul.arguments .argument { font-style:italic }
ul.arguments .default:not(:empty):before { content: "("; }
ul.arguments .default:not(:empty):after { content: ")"; }
ul.arguments li:after { content: ","; }
ul.arguments li:last-child:after { content: ""; }
.docstring:before { content: "Description: "; }
</style>
</head>
<body>
<h1>
{% if title is defined -%}
{{title}}
{% else -%}
Documentation
{% endif -%}
</h1>
{% for doc in autodoc %}
<div class="mapping">
<a id="rule-{{doc.rule|urlencode}}" class="rule"><h2>{{doc.rule|escape}}</h2></a>
<ul class="methods">
{% for method in doc.methods -%}
{% if method == 'GET' and doc.args == ['None'] %}
<a href="{{doc.rule}}" class="getmethod">
<li class="method">{{method}}</li>
</a>
{% else %}
<li class="method">{{method}}</li>
{% endif %}
{% endfor %}
</ul>
<ul class="arguments">
{% for arg in doc.args %}
<li>
<span class="argument">{{arg}}</span>
<span class="default">{{doc.defaults[arg]}}</span>
</li>
{% endfor %}
</ul>
<p class="docstring">{{doc.docstring|urlize|nl2br}}</p>
</div>
{% endfor %}
</body>
</html>
......@@ -341,15 +341,11 @@ def check_mod(user, groups):
return True
return False
@app.route('/internal/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
userinfo, groups = ldapauth(request.form.get('user'), request.form.get('password'))
def dologin(username, password):
userinfo, groups = ldapauth(username, password)
user = userinfo.get('uid')
if not check_mod(user, groups):
flash('Login fehlgeschlagen!')
return make_response(render_template('login.html'), 403)
return False
session['user'] = userinfo
dbuser = query('SELECT * FROM users WHERE name = ?', user)
if not dbuser:
......@@ -358,7 +354,17 @@ def login():
session['user']['dbid'] = dbuser[0]['id']
session['_csrf_token'] = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(64))
session.permanent = True
return redirect(request.values.get('ref', url_for('index')))
return True
@app.route('/internal/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
if not dologin (request.form.get('user'), request.form.get('password')):
flash('Login fehlgeschlagen!')
return make_response(render_template('login.html'), 403)
else:
return redirect(request.values.get('ref', url_for('index')))
@app.route('/internal/logout', methods=['GET', 'POST'])
def logout():
......@@ -490,3 +496,5 @@ import chapters
import icalexport
import livestreams
import cutprogress
import modules
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment