diff options
Diffstat (limited to 'app/views')
-rw-r--r-- | app/views/__init__.py | 61 | ||||
-rw-r--r-- | app/views/categories.py | 31 | ||||
-rw-r--r-- | app/views/consts.py | 174 | ||||
-rw-r--r-- | app/views/expenses.py | 224 | ||||
-rw-r--r-- | app/views/stats.py | 56 | ||||
-rw-r--r-- | app/views/user.py | 89 |
6 files changed, 0 insertions, 635 deletions
diff --git a/app/views/__init__.py b/app/views/__init__.py deleted file mode 100644 index 6b432e8..0000000 --- a/app/views/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -from flask import render_template, request, url_for -import flask - -from .. import app, db - -# -# Some general imports -# -from ..login import current_user, login_required -from ..utils import today, templated, redirect, assert_authorisation -from flask import Blueprint, flash - -__all__ = [ - 'db', 'app', - 'current_user', 'login_required', - 'assert_authorisation', 'templated', 'today', 'redirect', - 'Blueprint', 'flash', - 'request', 'url_for' -] - -# check for mobile visitors -mobile_checks = ['J2ME', 'Opera Mini'] - -app.add_template_global(zip) -app.add_template_global(current_user) - -@app.before_request -def handle_mobile(): - ua = request.environ.get('HTTP_USER_AGENT', '') - - flask.g.is_mobile = any((x in ua) for x in mobile_checks) - -@app.template_filter('static_url') -def static_url(s, **kwargs): - return url_for('static', filename=s, **kwargs) - -@app.template_filter('eur') -def eur(s): - return ('%s EUR' % s) - -@app.template_filter('date') -def format_date(s, format='%Y/%m'): - if hasattr(s, 'date'): - return s.date.strftime(format) - else: - return s.strftime(format) - -@app.errorhandler(404) -def page_not_found (error): - return render_template('404.jinja', page = request.path), 404 - -# Now import the views -from . import categories, consts, expenses, user, stats - -app.register_blueprint(expenses.mod) -app.register_blueprint(user.mod, url_prefix='/user') -app.register_blueprint(consts.mod, url_prefix='/const') -app.register_blueprint(categories.mod, url_prefix='/cat') -app.register_blueprint(stats.mod, url_prefix='/stats') - -app.add_url_rule('/', endpoint = 'index', build_only = True) diff --git a/app/views/categories.py b/app/views/categories.py deleted file mode 100644 index 47379ba..0000000 --- a/app/views/categories.py +++ /dev/null @@ -1,31 +0,0 @@ -from . import Blueprint, db, \ - current_user, login_required, \ - templated, redirect, request - -from ..model import Category - -mod = Blueprint('categories', __name__) - -@mod.route('/', methods=('GET', 'POST')) -@login_required -@templated -def manage(): - """Workhorse: List and edit/create. For historic reasons, - everything is done in JavaScript. - - NB: No deletion possible yet. - """ - if request.method == 'GET': - categories = Category.of(current_user).order_by(Category.name).all() - - return { 'cats' : categories } - else: - for id, name in request.form.items(): - if id.startswith('n-'): - db.session.add(Category(name = name, user = current_user)) - else: - Category.get(id).name = name - - db.session.commit() - - return redirect('.manage') diff --git a/app/views/consts.py b/app/views/consts.py deleted file mode 100644 index 5d6598d..0000000 --- a/app/views/consts.py +++ /dev/null @@ -1,174 +0,0 @@ -# -*- coding: utf-8 -*- -from . import Blueprint, flash, db, \ - current_user, login_required, \ - assert_authorisation, templated, redirect, request, \ - today - -from ..model import Category, ConstExpense -from .. import forms as F - -from sqlalchemy import sql -from functools import partial - -mod = Blueprint('consts', __name__) -assert_authorisation = partial(assert_authorisation, ConstExpense.get) - -def one_year(d): - """Returns the date d', such that [d, d'] spans exactly one year. - In effect, this is d + 11 months (NB: not 12!)""" - if d.month == 1: - return d.replace(month = 12) - else: - return d.replace(month = d.month - 1, year = d.year + 1) - -# -# Form -# -class ConstForm(F.Form): - start = F.MonthField('Beginn', F.req, - default=lambda: today()) - - end = F.MonthField('Ende', F.req, - default=lambda: one_year(today()), - description='(einschließlich)') - - months = F.IntegerField('Zahlungsrythmus', F.req, - description='Monate') - - expense = F.DecimalField('Betrag', F.req, - description='EUR', - places=2) - - description = F.StringField('Beschreibung', F.req) - - category = F.QuerySelectField('Kategorie', - get_label='name', - get_pk=lambda c: c.id) - - prev = F.QuerySelectField('Vorgänger', - get_label='description', - allow_blank=True, - get_pk=lambda p: p.id) - - def __init__(self, cur=None, obj=None): - obj = cur if obj is None else obj - super().__init__(obj=obj) - self.category.query = Category.of(current_user).order_by(Category.name) - - # init prev_list - CE = ConstExpense - - filter = (CE.next == None) - - if cur and cur.id is not None: # not empty - filter = sql.or_(CE.next == cur, filter) - filter = sql.and_(filter, CE.id != cur.id) - - self.prev.query = CE.of(current_user).filter(filter).order_by(CE.description) - -# -# Views -# -@mod.route('/') -@login_required -@templated -def list (): - """List all constant expenses.""" - d = today().replace(day = 1) - - expenses = ConstExpense.of(current_user).order_by(ConstExpense.description).all() - - current = [] - old = [] - future = [] - last_month = [] - - for e in expenses: - if e.start <= d: - if e.end >= d: - current.append(e) - else: - if (d.month == 1 and e.end.month == 12 and e.end.year == d.year - 1) \ - or (e.end.year == d.year and e.end.month == d.month - 1): - last_month.append(e) - else: - old.append(e) - else: - future.append(e) - - return { 'current': current, 'old': old, 'future': future, 'last_month': last_month } - - -@mod.route('/<int:id>') -@login_required -@assert_authorisation('id') -@templated -def show(id): - """Show a specific constant expense.""" - return { 'exp': ConstExpense.get(id) } - - -@mod.route('/edit/<int:id>', methods=('GET', 'POST')) -@login_required -@assert_authorisation('id') -@templated -def edit(id): - """Edit a specific constant expense. This includes deletion.""" - exp = ConstExpense.get(id) - form = ConstForm(exp) - - if form.is_submitted(): - if 'deleteB' in request.form: - db.session.delete(exp) - db.session.commit() - return redirect('.list') - - elif form.flash_validate(): # change - form.populate_obj(exp) - db.session.commit() - flash("Eintrag geändert.") - return redirect('.show', id = id) - - return { 'form': form } - - -@mod.route('/add/', methods=('GET', 'POST')) -@login_required -@templated -def add(): - """Add a new constant expense.""" - exp = ConstExpense() - - form = ConstForm() - - if form.validate_on_submit(): - form.populate_obj(exp) - exp.user = current_user - db.session.add(exp) - db.session.commit() - flash("Eintrag hinzugefügt.") - return redirect('.show', id = exp.id) - - return { 'form': form } - - -@mod.route('/add/from/<int:other>') -@login_required -@assert_authorisation('other') -@templated('.add') -def add_from(other): - """Copy `other` and create a new expense based on it.""" - exp = ConstExpense() # needed to initialize 'CE.next' - - other = ConstExpense.get(other) - - # get form with data from other - form = ConstForm(obj = other) - - # replace some fields to be more meaningful - start = max(form.end.data, today()) - form.start.data = start - form.end.data = one_year(start) - if not other.next: form.prev.data = other - - return { 'form': form } diff --git a/app/views/expenses.py b/app/views/expenses.py deleted file mode 100644 index 90c8ffd..0000000 --- a/app/views/expenses.py +++ /dev/null @@ -1,224 +0,0 @@ -# -*- coding: utf-8 -*- -from . import Blueprint, flash, db, \ - current_user, login_required, \ - assert_authorisation, templated, redirect, request, url_for, today - -from flask import Markup - -from ..model import Category, SingleExpense, CatExpense, MonthExpense -from .. import forms as F - -import datetime -from sqlalchemy import func -from functools import partial - -assert_authorisation = partial(assert_authorisation, SingleExpense.get) -mod = Blueprint('expenses', __name__) - -# -# Form -# -class ExpenseForm(F.Form): - date = F.DateField('Datum', F.req, - default=lambda: today()) - - expense = F.DecimalField('Betrag', F.req, - description='EUR', - places=2) - - description = F.StringField('Beschreibung') - - category = F.QuerySelectField('Kategorie', - get_label='name', - get_pk=lambda c: c.id) - - def __init__(self, obj = None, description_req = True): - super().__init__(obj = obj) - self.category.query = Category.of(current_user).order_by(Category.name) - - if description_req: - self.description.validators.extend(F.req) - -# -# Utilities -# -def calc_month_exp(year, month): - """Returns the `MonthExpense` for the given month.""" - ssum = func.sum(SingleExpense.expense) - query = SingleExpense.of_month(current_user, month, year) - - result = query.group_by(SingleExpense.category_id).\ - values(SingleExpense.category_id, ssum) - - exps = [CatExpense(Category.get(c), s, query.filter(SingleExpense.category_id == c)) for c,s in result] - - return MonthExpense(current_user, datetime.date(year, month, 1), exps) - - -def pie_data(exp): - """Generates the dictionary needed to show the pie diagram. - The resulting dict is category → sum of expenses. - """ - expenses = {} - for c in exp.catexps: - expenses[c.cat.name] = float(c.sum) - - for c in Category.of(current_user).order_by(Category.name).all(): - yield (c.name, expenses.get(c.name, 0.0)) - - -def calc_month_and_pie(year, month): - exp = calc_month_exp(year,month) - pie = pie_data(exp) - return (exp, dict(pie)) - - -def entry_flash(msg, exp): - """When changing/adding an entry, a message is shown.""" - url = url_for('.edit', id = exp.id) - link = "<a href=\"%s\">%s</a>" % (url, exp.description) - flash(Markup(msg % link)) - -DATE_FORMAT='%Y%m%d' -def parse_date(value): - try: - dt = datetime.datetime.strptime(value, DATE_FORMAT) - except ValueError: - return today() - else: - return dt.date() - -def gen_date(value): - return value.strftime(DATE_FORMAT) - -# -# Template additions -# -@mod.app_template_filter() -def prev_date(exp): - if exp.date.month == 1: - return exp.date.replace(year = exp.date.year - 1, month = 12) - else: - return exp.date.replace(month = exp.date.month - 1) - - -@mod.app_template_filter() -def next_date(exp): - if exp.date.month == 12: - return exp.date.replace(year = exp.date.year + 1, month = 1) - else: - return exp.date.replace(month = exp.date.month + 1) - - -@mod.app_template_test('last_date') -def is_last(exp): - return exp.date >= today().replace(day = 1) - -# -# Views -# -@mod.route('/') -@login_required -@templated -def show(): - """Show this and the last month.""" - d = today() - - first, pfirst = calc_month_and_pie(d.year, d.month) - if d.month == 1: - second, psecond = calc_month_and_pie(d.year - 1, 12) - else: - second, psecond = calc_month_and_pie(d.year, d.month - 1) - - return { 'exps' : [first, second], 'pies': [pfirst, psecond] } - - -@mod.route('/<int(fixed_digits=4):year>/<int(fixed_digits=2):month>') -@login_required -@templated('.show') -def show_date(year, month): - """Show the expenses of the specified month.""" - c,p = calc_month_and_pie(year, month) - return { 'exps' : [c], 'pies' : [p] } - -# shortcut to allow calling the above route, when year/month is a string -mod.add_url_rule('/<path:p>', endpoint = 'show_date_str', build_only = True) - - -@mod.route('/edit/<int:id>', methods=('GET', 'POST')) -@login_required -@assert_authorisation('id') -@templated -def edit(id): - """Edit a single expense, given by `id`.""" - exp = SingleExpense.get(id) - form = ExpenseForm(exp) - - if form.is_submitted(): - if 'deleteB' in request.form: - db.session.delete(exp) - - elif form.flash_validate(): # change - form.populate_obj(exp) - - else: - return { 'form': form } - - db.session.commit() - entry_flash("Eintrag %s geändert.", exp) - return redirect('index') - - return { 'form': form } - - -@mod.route('/add', methods=('GET', 'POST')) -@login_required -@templated -def add(): - """Add a new expense.""" - form = ExpenseForm(description_req=False) - - if request.method == 'GET' and 'date' in request.args: - form.date.data = parse_date(request.args['date']) - - if form.validate_on_submit(): - if not form.description.data.strip(): - form.description.data = form.category.data.name - - exp = SingleExpense() - - form.populate_obj(exp) - exp.user = current_user - - db.session.add(exp) - db.session.commit() - - entry_flash("Neuer Eintrag %s hinzugefügt.", exp) - - return redirect('.add', date = gen_date(exp.date) if exp.date != today() else None) - - return { 'form': form } - -@mod.route('/search', methods=('POST', 'GET')) -@login_required -@templated -def search(): - try: - query = request.form['search'].strip() - except KeyError: - flash("Ungültige Suchanfrage") - return redirect('index') - - if not query: - flash("Leere Suche") - return redirect('index') - - exps = SingleExpense.of(current_user).filter(SingleExpense.description.ilike(query))\ - .order_by(SingleExpense.year.desc(), SingleExpense.month, SingleExpense.day, SingleExpense.description)\ - .all() - - if not exps: - flash("Keine Ergebnisse") - return redirect('index') - - return { 'exps': exps } diff --git a/app/views/stats.py b/app/views/stats.py deleted file mode 100644 index 9ff81a1..0000000 --- a/app/views/stats.py +++ /dev/null @@ -1,56 +0,0 @@ -from . import Blueprint, flash, db, \ - current_user, login_required, \ - assert_authorisation, templated, redirect, request, \ - today - -from .. import forms as F -from ..model import ConstExpense, SingleExpense -import sqlalchemy as sql -import calendar -from collections import defaultdict -from datetime import date -from flask import jsonify - -mod = Blueprint('stats', __name__) - -def next_date(d): - if d.month == 12: - return d.replace(year = d.year + 1, month = 1) - else: - return d.replace(month = d.month + 1) - -def date_to_ms(d): - return calendar.timegm(d.timetuple()) * 1000 - -@mod.route('/_const/<int(fixed_digits=4):year>/<int(fixed_digits=2):month>') -@login_required -@templated -def const_dialog(year,month): - consts = ConstExpense.of_month(current_user, month, year).order_by(ConstExpense.description) - - return { 'consts': consts } - - -@mod.route('/') -@login_required -@templated -def show(): - # easy way: fetch them all and then do some computation - consts = defaultdict(int) - t = today().replace(day = 1) - for e in ConstExpense.of(current_user): - cur = e.start - end = min(e.end, t) - while cur <= end: - consts[date_to_ms(cur)] += e.monthly - cur = next_date(cur) - - consts = list(sorted(consts.items())) - - expQuery = SingleExpense.of(current_user)\ - .group_by(SingleExpense.year, SingleExpense.month)\ - .values(SingleExpense.year, SingleExpense.month, sql.func.sum(SingleExpense.expense)) - - expenses = list(sorted((date_to_ms(date(year,month,1)), exp) for (year, month, exp) in expQuery)) - - return { 'consts': consts, 'expenses' : expenses } diff --git a/app/views/user.py b/app/views/user.py deleted file mode 100644 index 9b75af8..0000000 --- a/app/views/user.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- encoding: utf-8 -*- -from . import Blueprint, flash, db, \ - current_user, login_required, \ - templated, redirect, request, url_for - -from .. import forms as F -from ..login import login_user, logout_user, login_manager, User - -import flask - -mod = Blueprint('user', __name__) - -# -# Forms -# -class LoginForm(F.Form): - username = F.StringField('Username', F.req) - pwd = F.PasswordField('Passwort', F.req) - remember = F.BooleanField('Eingeloggt bleiben?') - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.user = None - - def validate(self): - rv = super().validate() - if not rv: - return False - - user = User.get_by(name = self.username.data) - if user is None or not user.check_password(self.pwd.data): - return False - - self.user = user - return True - - -class ChangePwdForm(F.Form): - old = F.PasswordField('Passwort', F.req) - new = F.PasswordField('Neues Passwort', F.req + [F.validators.EqualTo('confirm', 'Passwörter stimmen nicht überein')]) - confirm = F.PasswordField('Wdh. neues Passwort', F.req) - - def validate_old(self, field): - if not current_user.check_password(field.data): - raise F.ValidationError("Falsches Passwort") - - @property - def newpwd(self): - return self.new.data - -# -# Views -# -@mod.route('/login', methods=('GET', 'POST')) -@templated -def login(): - """Log the user in.""" - form = LoginForm(flash="Login fehlgeschlagen!") - if form.validate_on_submit(): - login_user(form.user, remember=form.remember.data) - # we explicitly need flask's variant as we redirect to a URI - return flask.redirect(request.args.get('next') or url_for('index')) - return { 'form': form } - - -@mod.route('/logout') -def logout(): - """Log the user out.""" - logout_user() - return redirect('.login') - - -@mod.route('/cpw', methods=('GET', 'POST')) -@login_required -@templated -def cpw(): - """Change the password of the user.""" - form = ChangePwdForm() - - if form.validate_on_submit(): - current_user.set_password(form.newpwd) - db.session.commit() - flash("Passwort geändert.") - return redirect('index') - - return { 'form': form } - -# set this, so the user is redirected to the correct view, when not logged in -login_manager.login_view = 'user.login' |