summaryrefslogtreecommitdiff
path: root/app/views
diff options
context:
space:
mode:
authorRené 'Necoro' Neumann <necoro@necoro.eu>2020-07-23 00:28:47 +0200
committerRené 'Necoro' Neumann <necoro@necoro.eu>2020-07-23 00:28:47 +0200
commit81493afa53a1a1d5ff4b417d05febf9f9e2a172b (patch)
tree00de0a1bb7c386cff4203aa7b0789569e75347bb /app/views
parent6f6c8af2a55fabb69372e3fc4e8504167805d018 (diff)
downloadkosten-81493afa53a1a1d5ff4b417d05febf9f9e2a172b.tar.gz
kosten-81493afa53a1a1d5ff4b417d05febf9f9e2a172b.tar.bz2
kosten-81493afa53a1a1d5ff4b417d05febf9f9e2a172b.zip
Restructure
Diffstat (limited to 'app/views')
-rw-r--r--app/views/__init__.py61
-rw-r--r--app/views/categories.py31
-rw-r--r--app/views/consts.py174
-rw-r--r--app/views/expenses.py224
-rw-r--r--app/views/stats.py56
-rw-r--r--app/views/user.py89
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'