From 1ec270de4390f215f874e8fad23736ce978c1bbd Mon Sep 17 00:00:00 2001 From: Alexander Sulfrian Date: Sun, 10 Jan 2016 05:08:36 +0100 Subject: Use sqlalchemy, flask-migrate, flask-login and flask-script No peewee anymore. All dependencies are available as debian packages now. --- utils/__init__.py | 0 utils/apimixin.py | 85 +++++++++++++++++++++++++++++++++++++++++++++++++ utils/filters.py | 39 +++++++++++++++++++++++ utils/forms.py | 55 ++++++++++++++++++++++++++++++++ utils/login.py | 40 +++++++++++++++++++++++ utils/pagination.py | 18 +++++++++++ utils/request.py | 25 +++++++++++++++ utils/viewdecorators.py | 21 ++++++++++++ utils/widgets.py | 21 ++++++++++++ 9 files changed, 304 insertions(+) create mode 100644 utils/__init__.py create mode 100644 utils/apimixin.py create mode 100644 utils/filters.py create mode 100644 utils/forms.py create mode 100644 utils/login.py create mode 100644 utils/pagination.py create mode 100644 utils/request.py create mode 100644 utils/viewdecorators.py create mode 100644 utils/widgets.py (limited to 'utils') diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/apimixin.py b/utils/apimixin.py new file mode 100644 index 0000000..b48fd60 --- /dev/null +++ b/utils/apimixin.py @@ -0,0 +1,85 @@ +from sqlalchemy import event, Column, String +from sqlalchemy.orm.session import Session + + +class APIMixin(object): + api_id = Column(String(255), nullable=False) + + def get_api_id(self): + if self.api_id is None: + self.create_api_object() + + return self.api_id + + def before_created(self): + if self.api_id is None: + self.create_api_object() + + def after_deleted(self): + if self.api_id is not None: + self.remove_api_object() + self.api_id = None + + def create_api_object(self): + raise NotImplementedError + + def remove_api_object(self): + raise NotImplementedError + + def after_commit(self): + pass + + +class SessionHelper(object): + + def __init__(self): + self.new = set() + self.dirty = set() + self.deleted = set() + + event.listen(Session, 'before_flush', self.before_flush) + event.listen(Session, 'after_flush', self.after_flush) + event.listen(Session, 'after_commit', self.after_commit) + event.listen(Session, 'after_rollback', self.after_rollback) + + def before_flush(self, session, flush_context, instances): + for obj in session.new: + if isinstance(obj, APIMixin): + obj.before_created() + + def after_flush(self, session, flush_context): + self.new.update( + obj for obj in session.new + if isinstance(obj, APIMixin)) + + self.dirty.update( + obj for obj in session.dirty + if isinstance(obj, APIMixin)) + + self.deleted.update( + obj for obj in session.deleted + if isinstance(obj, APIMixin)) + + def after_commit(self, session): + for obj in self.new: + obj.after_commit() + self.new.clear() + + for obj in self.dirty: + obj.after_commit() + self.dirty.clear() + + for obj in self.deleted: + obj.after_deleted() + self.deleted.clear() + + def after_rollback(self, session): + self.dirty.clear() + self.deleted.clear() + + for obj in self.new: + obj.after_deleted() + self.new.clear() + + +helper = SessionHelper() diff --git a/utils/filters.py b/utils/filters.py new file mode 100644 index 0000000..eb0e1c8 --- /dev/null +++ b/utils/filters.py @@ -0,0 +1,39 @@ +from app import app +from jinja2 import contextfilter +from jinja2.filters import make_attrgetter + + +@app.template_filter('selectattr') +@contextfilter +def do_selectattr(*args, **kwargs): + return _select_or_reject(args, kwargs, lambda x: x) + + +@app.template_filter('rejectattr') +@contextfilter +def do_rejectattr(*args, **kwargs): + return _select_or_reject(args, kwargs, lambda x: not x) + + +def _select_or_reject(args, kwargs, modfunc): + context = args[0] + seq = args[1] + + try: + attr = args[2] + except LookupError: + raise FilterArgumentError('Missing parameter for attribute name') + transfunc = make_attrgetter(context.environment, attr) + + try: + name = args[3] + args = args[4:] + func = lambda item: context.environment.call_test( + name, item, args, kwargs) + except LookupError: + func = bool + + if seq: + for item in seq: + if modfunc(func(transfunc(item))): + yield item diff --git a/utils/forms.py b/utils/forms.py new file mode 100644 index 0000000..a6ff4de --- /dev/null +++ b/utils/forms.py @@ -0,0 +1,55 @@ +from flask import request, url_for, redirect +from urlparse import urlparse, urljoin +from wtforms import Field, HiddenField, ValidationError + +from widgets import Static + + +class Unique(object): + """ validator that checks field uniqueness """ + def __init__(self, model, field, message=None): + self.model = model + self.field = field + if not message: + message = u'This element already exists.' + self.message = message + + def __call__(self, form, field): + if self.model.query.filter(self.field == field.data).count() > 0: + raise ValidationError(self.message) + + +class ReadonlyField(Field): + widget = Static() + + def process_formdata(self, _): + pass + + +class RedirectMixin(object): + next = HiddenField() + + def __init__(self, *args, **kwargs): + super(RedirectMixin, self).__init__(*args, **kwargs) + if not self.next.data: + self.next.data = self._get_redirect_target() or '' + + def _get_redirect_target(self): + for target in request.args.get('next'), request.referrer: + if not target: + continue + if self._is_safe_url(target): + return target + + def _is_safe_url(self, target): + ref_url = urlparse(request.host_url) + test_url = urlparse(urljoin(request.host_url, target)) + return test_url.scheme in ('http', 'https') and \ + ref_url.netloc == test_url.netloc + + def redirect(self, endpoint='index', **values): + if self._is_safe_url(self.next.data): + return redirect(self.next.data) + + target = self._get_redirect_target() + return redirect(target or url_for(endpoint, **values)) diff --git a/utils/login.py b/utils/login.py new file mode 100644 index 0000000..e6c8f21 --- /dev/null +++ b/utils/login.py @@ -0,0 +1,40 @@ +import ldap +from functools import reduce + + +def user_cls(login): + def decorator(cls): + login.user_loader(lambda uid: cls.query.get(uid)) + return cls + return decorator + + +def _format_dn(attr, base_dn=None): + attr = [attr] + if base_dn is not None: + attr.extend(base_dn) + + return ','.join(['%s=%s' % (key, ldap.dn.escape_dn_chars(value)) + for (key, value) in attr]) + + +def auth(config, model, username, password): + ldap.protocol_version = 3 + l = ldap.initialize(config['host']) + l.set_option(ldap.OPT_X_TLS_DEMAND, True) + try: + user_dn = _format_dn(('uid', username), config['base_dn']) + l.simple_bind_s(user_dn, password) + except ldap.INVALID_CREDENTIALS: + return None + + user = model.query.filter_by(name=username).first() + if user is None: + user_data = l.search_s(user_dn, ldap.SCOPE_BASE) + if len(user_data) != 1: + return None + + (dn, user_data) = user_data[0] + user = model.create(name=username, email=user_data['mail'][0]) + + return user diff --git a/utils/pagination.py b/utils/pagination.py new file mode 100644 index 0000000..8d2cb60 --- /dev/null +++ b/utils/pagination.py @@ -0,0 +1,18 @@ +from flask import url_for, request +from app import app + + +def url_for_other_page(page): + args = request.view_args.copy() + args['page'] = page + return url_for(request.endpoint, **args) +app.jinja_env.globals['url_for_other_page'] = url_for_other_page + + +# @app.context_processor +# def register_method(): +# def url_for_other_page(page): +# args = request.view_args.copy() +# args['page'] = page +# return url_for(request.endpoint, **args) +# return dict(url_for_other_page=url_for_other_page) diff --git a/utils/request.py b/utils/request.py new file mode 100644 index 0000000..8d36ed6 --- /dev/null +++ b/utils/request.py @@ -0,0 +1,25 @@ +from flask import g, request + +from app import app + + +def after_this_request(f): + """ + Decorator to execute methods after the request is handled, to + modify the response before sending back to the client. This could + be used to set cookies. + """ + + if not hasattr(g, 'after_request_callbacks'): + g.after_request_callbacks = [] + + g.after_request_callbacks.append(f) + return f + + +@app.after_request +def call_after_request_callbacks(response): + for callback in getattr(g, 'after_request_callbacks', ()): + callback(response) + + return response diff --git a/utils/viewdecorators.py b/utils/viewdecorators.py new file mode 100644 index 0000000..8f96c07 --- /dev/null +++ b/utils/viewdecorators.py @@ -0,0 +1,21 @@ +from functools import wraps +from flask import render_template, request + + +# using http://flask.pocoo.org/docs/patterns/viewdecorators/ +def templated(template=None): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + template_name = template + if template_name is None: + template_name = request.endpoint \ + .replace('.', '/') + '.html' + ctx = f(*args, **kwargs) + if ctx is None: + ctx = {} + elif not isinstance(ctx, dict): + return ctx + return render_template(template_name, **ctx) + return decorated_function + return decorator diff --git a/utils/widgets.py b/utils/widgets.py new file mode 100644 index 0000000..3e5d2b8 --- /dev/null +++ b/utils/widgets.py @@ -0,0 +1,21 @@ +import wtforms.widgets.core + + +class Static(object): + def __call__(self, field, **kwargs): + kwargs.setdefault('id', field.id) + if kwargs['class_'] == 'form-control': + kwargs['class_'] = 'form-control-static' + html = ['

' % wtforms.widgets.core.html_params(**kwargs), field.data,'

'] + return wtforms.widgets.core.HTMLString(''.join(html)) + + +class TextArea(wtforms.widgets.core.TextArea): + def __init__(self, **kwargs): + self.kwargs = kwargs + + def __call__(self, field, **kwargs): + for arg in self.kwargs: + if arg not in kwargs: + kwargs[arg] = self.kwargs[arg] + return super(TextArea, self).__call__(field, **kwargs) -- cgit v1.2.3-1-g7c22