summaryrefslogtreecommitdiff
path: root/app
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
parent6f6c8af2a55fabb69372e3fc4e8504167805d018 (diff)
downloadkosten-81493afa53a1a1d5ff4b417d05febf9f9e2a172b.tar.gz
kosten-81493afa53a1a1d5ff4b417d05febf9f9e2a172b.tar.bz2
kosten-81493afa53a1a1d5ff4b417d05febf9f9e2a172b.zip
Restructure
Diffstat (limited to 'app')
-rw-r--r--app/__init__.py32
-rw-r--r--app/forms.py59
-rw-r--r--app/login.py21
-rw-r--r--app/model.py205
-rw-r--r--app/utils.py117
-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
11 files changed, 0 insertions, 1069 deletions
diff --git a/app/__init__.py b/app/__init__.py
deleted file mode 100644
index 7c6408a..0000000
--- a/app/__init__.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from flask import Flask
-
-# create app
-app = Flask('kosten', instance_relative_config = True)
-
-# force autoescape in all files
-app.jinja_env.autoescape = True
-
-app.jinja_env.lstrip_blocks = True
-app.jinja_env.trim_blocks = True
-
-# load config
-app.config.from_object('settings')
-app.config.from_pyfile('settings.py', silent = True)
-
-from .model import db
-from .login import login_manager
-from . import views
-
-# commands
-@app.cli.command()
-def create():
- db.create_all()
-
-@app.cli.command()
-def drop():
- db.drop_all()
-
-@app.cli.command()
-def compile():
- """Compiles all templates."""
- app.jinja_env.compile_templates('comp', zip = None)
diff --git a/app/forms.py b/app/forms.py
deleted file mode 100644
index b7cbebf..0000000
--- a/app/forms.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# -*- encoding: utf-8 -*-
-import flask
-from flask_wtf import FlaskForm
-from wtforms.fields import BooleanField, StringField, HiddenField, PasswordField, DecimalField as WTFDecimalField, DateField as HTML4DateField
-from wtforms.fields.html5 import DateField, IntegerField
-from wtforms import validators, ValidationError, Form as WTForm
-
-from wtforms.ext.sqlalchemy.fields import QuerySelectField
-
-from . import app
-
-@app.template_test('hidden')
-def is_hidden_field(f):
- return isinstance(f, HiddenField)
-
-class DecimalField(WTFDecimalField):
- def __init__(self, *args, **kwargs):
- render_kw = kwargs.setdefault('render_kw', dict())
- render_kw.setdefault('inputmethod', 'decimal')
-
- super().__init__(*args, **kwargs)
-
- def process_formdata(self, valuelist):
- if valuelist:
- value = valuelist[0].replace(',','.')
- super().process_formdata([value])
-
-class MonthField(HTML4DateField):
- def __init__(self, label, validators, format='%m.%Y', **kwargs):
- super().__init__(label, validators, format, **kwargs)
-
-req = [validators.input_required()]
-
-class Form(FlaskForm):
- class Meta:
- locales = ['de_DE', 'de']
-
- def __init__ (self, *args, **kwargs):
- self._msg = kwargs.pop('flash', "Fehler im Formular!")
- super().__init__(*args, **kwargs)
-
- def flash(self):
- flask.flash(self._msg, 'error')
-
- def flash_validate (self):
- if not self.validate():
- self.flash()
- return False
-
- return True
-
- def validate_on_submit (self):
- return self.is_submitted() and self.flash_validate()
-
- def _get_translations(self):
- # use WTForms builtin translation support instead of the flask-babael
- # stuff added by flask-wtf
- # FIXME: remove this, if flask-babel is used in the app
- return WTForm._get_translations(self)
diff --git a/app/login.py b/app/login.py
deleted file mode 100644
index 850cc8a..0000000
--- a/app/login.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from flask_login import LoginManager, UserMixin
-from passlib.apps import custom_app_context as pwd_context
-from . import app, model
-
-# just for exporting
-from flask_login import login_user, logout_user, login_required, current_user
-
-login_manager = LoginManager()
-login_manager.init_app(app)
-login_manager.login_message = "Bitte einloggen!"
-
-class User (model.User, UserMixin):
- def check_password(self, pwd):
- return pwd_context.verify(pwd, self.pwd)
-
- def set_password(self, pwd):
- self.pwd = pwd_context.encrypt(pwd)
-
-@login_manager.user_loader
-def load_user(id):
- return User.get(id)
diff --git a/app/model.py b/app/model.py
deleted file mode 100644
index 4663685..0000000
--- a/app/model.py
+++ /dev/null
@@ -1,205 +0,0 @@
-from flask_sqlalchemy import SQLAlchemy
-from sqlalchemy import sql
-from sqlalchemy.ext.declarative import declared_attr
-
-import datetime
-import decimal
-from functools import partial
-from collections import namedtuple
-
-from . import app
-
-db = SQLAlchemy(app)
-
-__all__ = ['db', \
- 'Category', 'SingleExpense', 'ConstExpense', \
- 'CatExpense', 'MonthExpense']
-
-Column = db.Column
-ReqColumn = partial(Column, nullable = False)
-ExpNum = db.Numeric(scale = 2, precision = 10)
-
-def to_exp(d):
- """Converts decimal into expense"""
- return d.quantize(decimal.Decimal('.01'), rounding = decimal.ROUND_UP)
-
-#
-# Database Entities
-#
-class Model (db.Model):
- """Abstract base class for all models.
- Adds an id PK and several convenience accessors.
- """
-
- __abstract__ = True
-
- id = Column(db.Integer, primary_key=True)
-
- @declared_attr
- def __tablename__ (cls):
- return cls.__name__.lower()
-
- @classmethod
- def get_by (cls, *args, **kwargs):
- return cls.query.filter_by(*args, **kwargs).first()
-
- @classmethod
- def get_by_or_404 (cls, *args, **kwargs):
- return cls.query.filter_by(*args, **kwargs).first_or_404()
-
- @classmethod
- def get (cls, *args, **kwargs):
- return cls.query.get(*args, **kwargs)
-
- @classmethod
- def get_or_404 (cls, *args, **kwargs):
- return cls.query.get_or_404(*args, **kwargs)
-
-
-class User (Model):
- # NB: This is abstract, the flesh is added in login.py
-
- __abstract__ = True
- name = ReqColumn(db.Unicode(50), unique = True)
- pwd = ReqColumn(db.Unicode(255))
- description = Column(db.Unicode(100))
-
-
-class UserModel (Model):
- """Abstract base class for tables that have a user column."""
- __abstract__ = True
-
- @declared_attr
- def user_id(cls):
- return ReqColumn(db.Integer, db.ForeignKey('user.id'))
-
- @declared_attr
- def user(cls):
- return db.relationship('User')
-
- @classmethod
- def of (cls, user):
- return cls.query.filter_by(user = user)
-
-
-class Category (UserModel):
- name = ReqColumn(db.Unicode(50))
- parent_id = Column(db.Integer, db.ForeignKey('category.id'))
-
- children = db.relationship('Category',
- backref=db.backref('parent', remote_side="Category.id"))
-
- def __init__ (self, name, user, parent_id = None):
- Model.__init__(self)
- self.name = name
- self.user = user
- self.parent_id = parent_id
-
- def __repr__ (self):
- if self.parent:
- return '<Category "%s" of "%s">' % (self.name, self.parent.name)
- else:
- return '<Category "%s">' % self.name
-
-
-class CategoryModel (db.Model):
- """Abstract base class for expenses: Adds the common fields
- and establishes the connection to `Category`.
- """
- __abstract__ = True
-
- @declared_attr
- def category_id(cls):
- return ReqColumn(db.Integer, db.ForeignKey(Category.id))
-
- @declared_attr
- def category(cls):
- return db.relationship(Category, innerjoin = True)
-
-
-class SingleExpense (UserModel, CategoryModel):
- description = Column(db.Unicode(50))
- expense = ReqColumn(ExpNum)
- year = ReqColumn(db.Integer)
- month = ReqColumn(db.SmallInteger)
- day = ReqColumn(db.SmallInteger)
-
- @classmethod
- def of_month (cls, user, month, year):
- return cls.of(user).filter_by(month = month, year = year)
-
- @property
- def date (self):
- return datetime.date(self.year, self.month, self.day)
-
- @date.setter
- def date (self, d):
- self.year = d.year
- self.month = d.month
- self.day = d.day
-
-
-class ConstExpense (UserModel, CategoryModel):
- description = Column(db.Unicode(50))
- expense = ReqColumn(ExpNum)
- months = ReqColumn(db.SmallInteger)
- start = ReqColumn(db.Date, index = True)
- end = ReqColumn(db.Date, index = True)
- prev_id = Column(db.Integer, db.ForeignKey('constexpense.id'))
-
- prev = db.relationship('ConstExpense', remote_side = 'ConstExpense.id', uselist = False,
- backref=db.backref('next', uselist = False))
-
- @property
- def monthly(self):
- return to_exp(self.expense / self.months)
-
- @classmethod
- def of_month (cls, user, month, year):
- d = datetime.date(year, month, 1)
- return cls.of(user).filter(sql.between(d, cls.start, cls.end))
-
-#
-# Work entities (not stored in DB)
-#
-class CatExpense (namedtuple('CatExpense', 'cat sum exps')):
- __slots__ = ()
-
- @property
- def all (self):
- return self.exps.order_by(SingleExpense.day).all()
-
-class MonthExpense (namedtuple('MonthExpense', 'user date catexps')):
-
- def __init__ (self, *args, **kwargs):
- self._consts = None
-
- @property
- def consts (self):
- if self._consts is None:
- self._consts = ConstExpense.of_month(self.user, self.date.month, self.date.year).all()
-
- return self._consts
-
- @property
- def constsum (self):
- s = sum(c.monthly for c in self.consts)
- return s or 0
-
- @property
- def sum (self):
- return self.constsum + sum(x.sum for x in self.catexps)
-
- @property
- def all (self):
- return SingleExpense.of_month(self.user, self.date.month, self.date.year).order_by(SingleExpense.day).all()
-
- def __str__ (self):
- return '<MonthExpense (user: %s) of "%s": %s>' % (self.user.name, self.date, self.sum)
-
-#
-# Extra indices have to be here
-#
-
-db.Index('idx_single_date', SingleExpense.user_id, SingleExpense.year, SingleExpense.month)
-db.Index('idx_start_end', ConstExpense.user_id, ConstExpense.start, ConstExpense.end)
diff --git a/app/utils.py b/app/utils.py
deleted file mode 100644
index 73f2b51..0000000
--- a/app/utils.py
+++ /dev/null
@@ -1,117 +0,0 @@
-from functools import wraps
-from flask import flash, request, render_template, url_for
-from flask import redirect as _redirect
-
-from .login import current_user
-
-import datetime
-today = datetime.date.today
-
-def _gen_tpl(endpoint):
- return endpoint.replace('.', '/') + '.jinja'
-
-def templated(template=None):
- """Marks a view as being rendered by a template. The view then shall
- return a dictionary holding the parameters for the template. Ig this
- is not the case, the response is returned unchanged. This is needed
- to support `redirect` and similar.
-
- The correct template is deducted as:
- - when passed nothing: the name of the view
- - when passed a string '.bla', the endpoint 'bla' in the current
- blueprint
- - when passed any other string: this string (VERBATIM!)
-
- Except for the last case, the hierarchy of blueprint and view is taken
- as directories in the template directory. And '.jinja' is appended.
-
- If the first argument is a function, this is taken as 'None' to allow:
- >>> @templated
- ... def foo():
- ... ...
-
- (else it would have to be ``@templated()``).
- """
-
- fun = None
- if template is not None and callable(template):
- # a function was passed in
- fun = template
- template = None
-
- def decorator(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if template is None:
- template_name = _gen_tpl(request.endpoint)
- elif template[0] == '.' and request.blueprint is not None:
- template_name = _gen_tpl(request.blueprint + template)
- else:
- template_name = template
-
- 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
-
- if fun is None:
- return decorator
- else:
- return decorator(fun)
-
-def redirect (target, **kwargs):
- """Convenience wrapper for `flask.redirect`. It applies `url_for`
- on the target, which also gets passed all arguments.
-
- Special argument '_code' to set the HTTP-Code.
- """
- code = kwargs.pop('_code', None)
- url = url_for(target, **kwargs)
-
- if code is None:
- return _redirect(url)
- else:
- return _redirect(url, code)
-
-def assert_authorisation(constructor, param):
- """Asserts that the current user has the right to load some specific data.
-
- This is done by using the argument with keyword `param` and pass it
- to `constructor`. If the resulting object has an attribute `user_id`,
- this is checked to be equal to `current_user.id`.
-
- Usage example::
-
- @route('/job/<int:id>')
- @assert_authorisation(Job, 'id')
- def show_job(id):
- # this is only executed if Job(id).user_id == current_user.id
-
- """
- def decorator(f):
- @wraps(f)
- def decorated_function(*args, **kwargs):
- p = kwargs.get(param, None)
-
- if p is None:
- raise TypeError("Keyword %s expected but not received." % param)
-
- obj = constructor(p)
- if obj is None:
- flash("Eintrag existiert nicht!", 'error')
- return redirect('index')
-
- if not hasattr(obj, 'user_id'):
- return f(*args, **kwargs)
-
- # explicitly use user_id to avoid having to load the user object
- if obj.user_id != current_user.id:
- flash("Nicht erlaubte Operation!", 'error')
- return redirect('index')
- else:
- return f(*args, **kwargs)
- return decorated_function
- return decorator
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