From 0567318344330295512176569a84afc9748d79c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20=27Necoro=27=20Neumann?= Date: Thu, 11 Apr 2013 01:14:49 +0200 Subject: First part of the transition to flask --- __init__.py | 2 +- app/__init__.py | 13 ++++ app/model.py | 167 ++++++++++++++++++++++++++++++++++++++++++++++++ app/views.py | 32 ++++++++++ index.py | 75 ++++------------------ model.py | 175 --------------------------------------------------- settings.py | 11 ++++ templates/menu.jinja | 7 +++ templates/menu.mako | 8 --- templates/root.jinja | 56 +++++++++++++++++ templates/root.mako | 56 ----------------- 11 files changed, 298 insertions(+), 304 deletions(-) create mode 100644 app/__init__.py create mode 100644 app/model.py create mode 100644 app/views.py delete mode 100644 model.py create mode 100644 settings.py create mode 100644 templates/menu.jinja delete mode 100644 templates/menu.mako create mode 100644 templates/root.jinja delete mode 100644 templates/root.mako diff --git a/__init__.py b/__init__.py index c3189d1..d099b92 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1 @@ -from index import app +from app import app diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..6ba7025 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,13 @@ +from flask import Flask + +# create app +app = Flask("kosten") + +# force autoescape in all files +app.jinja_env.autoescape = True + +# load config +app.config.from_pyfile("settings.py") + +from .model import db +from . import views diff --git a/app/model.py b/app/model.py new file mode 100644 index 0000000..92b3710 --- /dev/null +++ b/app/model.py @@ -0,0 +1,167 @@ +from flask.ext.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"] + +ReqColumn = partial(db.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__ = True + + id = db.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 Category (Model): + name = ReqColumn(db.Unicode(50), unique = True) + parent_id = db.Column(db.Integer, db.ForeignKey('category.id')) + + children = db.relationship('Category', + backref=db.backref('parent', remote_side="Category.id")) + + def __init__ (self, name, parent_id = None): + Model.__init__(self) + self.name = name + self.parent_id = parent_id + + def __repr__ (self): + if self.parent: + return '' % (self.name, self.parent.name) + else: + return '' % self.name + +class Expense (Model): + __abstract__ = True + + description = db.Column(db.Unicode(50)) + expense = ReqColumn(ExpNum) + + @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 (Expense): + year = ReqColumn(db.Integer) + month = ReqColumn(db.SmallInteger) + day = ReqColumn(db.SmallInteger) + + @classmethod + def of_month (cls, month, year): + comp = sql.and_( + cls.month == month, + cls.year == year) + + return cls.query.filter(comp) + + @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 (Expense): + months = ReqColumn(db.SmallInteger) + start = ReqColumn(db.Date, index = True) + end = ReqColumn(db.Date, index = True) + prev_id = db.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, month, year): + d = datetime.date(year, month, 1) + return cls.query.filter(sql.between(d, cls.start, cls.end)) + +# +# Work entities (not stored in DB) +# +class CatExpense (namedtuple('CatExpense', 'cat expense exps')): + __slots__ = () + + @property + def all (self): + return self.exps.order_by(SingleExpense.day).all() + +class MonthExpense (namedtuple('MonthExpense', 'date catexps')): + + def __init__ (self, *args, **kwargs): + self._consts = None + super(MonthExpense, self).__init__(*args, **kwargs) + + @property + def consts (self): + if self._consts is None: + self._consts = ConstExpense.of_month(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.expense for x in self.catexps) + + @property + def all (self): + return SingleExpense.of_month(self.date.month, self.date.year).order_by(SingleExpense.day).all() + + def __str__ (self): + return '' % (self.date, self.sum) + +# +# Extra indizes have to be here +# + +db.Index('idx_single_date', SingleExpense.year, SingleExpense.month) +db.Index('idx_start_end', ConstExpense.start, ConstExpense.end) diff --git a/app/views.py b/app/views.py new file mode 100644 index 0000000..7450946 --- /dev/null +++ b/app/views.py @@ -0,0 +1,32 @@ +from flask import render_template, request, url_for +import flask + +from . import app, db + +# check for mobile visitors +mobile_checks = ["J2ME", "Opera Mini"] + +@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): + return url_for("static", filename=s) + + +#@app.errorhandler(404) +#def page_not_found (error): +# print request.path + +@app.route("/") +@app.route("/index") +def index(): + return render_template("root.jinja") + +@app.route("/add") +def addExp(): + return render_template("root.jinja") diff --git a/index.py b/index.py index d4ec756..c3ad9fa 100755 --- a/index.py +++ b/index.py @@ -1,69 +1,16 @@ #!/usr/bin/python +import sys -import web -import controller -import model +from app import app, db -# -# URL Mappings -# -urls = ( - "/add/?", controller.Add, - "/edit/(\d+)", controller.Edit, - "/const/?", controller.Const, - "/const/(\d+)", controller.Const, - "/const/add/?", controller.ConstAdd, - "/const/add/from/(\d+)", controller.ConstAdd, - "/const/edit/(\d+)", controller.ConstEdit, - "/categories", controller.Cat, - "/(\d\d\d\d)/(\d\d?)/?", controller.Show, - "/", controller.Show, - "/(.*)", controller.FourOhFour - ) -# -# ORM -# -def handle_sql(handler): - web.ctx.orm = model.Session() +try: + cmd = sys.argv[1] +except IndexError: + cmd = None - try: - try: - h = handler() - except web.HTTPError: - web.ctx.orm.commit() - raise - except: - web.ctx.orm.rollback() - raise - else: - web.ctx.orm.commit() - return h - finally: - model.Session.remove() - -# -# Check for mobile (at least somewhat) -# -mobile_checks = ["J2ME", "Opera Mini"] -def handle_mobile(): - ua = web.ctx.env.get("HTTP_USER_AGENT", "") - - web.ctx.is_mobile = any((x in ua) for x in mobile_checks) - -# -# The App -# -app = web.application(urls, globals()) -app.notfound = controller.FourOhFour.catch - -# add processors -app.add_processor(handle_sql) -app.add_processor(web.loadhook(handle_mobile)) - -# debug for the moment -web.config.debug = True - -# -# And go! -if __name__ == "__main__": +if cmd == "create": + db.create_all() +elif cmd == "drop": + db.drop_all() +else: app.run() diff --git a/model.py b/model.py deleted file mode 100644 index 7a07ff0..0000000 --- a/model.py +++ /dev/null @@ -1,175 +0,0 @@ -from sqlalchemy import types as T -from sqlalchemy import sql, Index, Column, ForeignKey, create_engine -from sqlalchemy.orm import relationship, backref, scoped_session, sessionmaker,\ - column_property -from sqlalchemy.ext.declarative import declarative_base, declared_attr -from sqlalchemy.ext.hybrid import hybrid_property - -import datetime -import decimal -from functools import partial -from collections import namedtuple - -__all__ = ["Category", "SingleExpense", "ConstExpense", "CatExpense", "MonthExpense", - "Session"] - -# -# DB Setup -# - -engine = create_engine("sqlite:///test.sqlite") -engine.echo = True - -Session = scoped_session(sessionmaker(bind=engine)) - -# -# Global definitions -# - -class Base(object): - @declared_attr - def __tablename__ (cls): - return cls.__name__.lower() - - id = Column(T.Integer, primary_key=True) - - query = Session.query_property() - - @classmethod - def get_by (cls, *args, **kwargs): - return cls.query.filter_by(*args, **kwargs).first() - - @classmethod - def get (cls, *args, **kwargs): - return cls.query.get(*args, **kwargs) - - -Base = declarative_base(cls=Base) - -ReqColumn = partial(Column, nullable = False) -ExpNum = T.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 Category (Base): - name = ReqColumn(T.Unicode(50), unique = True) - parent_id = Column(T.Integer, ForeignKey('category.id')) - - children = relationship('Category', - backref=backref('parent', remote_side="Category.id")) - - def __repr__ (self): - if self.parent: - return '' % (self.name, self.parent.name) - else: - return '' % self.name - -class Expense (Base): - __abstract__ = True - - description = Column(T.Unicode(50)) - expense = ReqColumn(ExpNum) - - @declared_attr - def category_id(cls): - return ReqColumn(T.Integer, ForeignKey(Category.id)) - - @declared_attr - def category(cls): - return relationship(Category, innerjoin = True) - -class SingleExpense (Expense): - year = ReqColumn(T.Integer) - month = ReqColumn(T.SmallInteger) - day = ReqColumn(T.SmallInteger) - - @classmethod - def of_month (cls, month, year): - comp = sql.and_( - cls.month == month, - cls.year == year) - - return cls.query.filter(comp) - - @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 (Expense): - months = ReqColumn(T.SmallInteger) - start = ReqColumn(T.Date, index = True) - end = ReqColumn(T.Date, index = True) - prev_id = Column(T.Integer, ForeignKey('constexpense.id')) - - prev = relationship('ConstExpense', remote_side = "ConstExpense.id", uselist = False, - backref=backref('next', uselist = False)) - - @property - def monthly(self): - return to_exp(self.expense / self.months) - - @classmethod - def of_month (cls, month, year): - d = datetime.date(year, month, 1) - return cls.query.filter(sql.between(d, cls.start, cls.end)) - -# -# Work entities (not stored in DB) -# -class CatExpense (namedtuple('CatExpense', 'cat expense exps')): - __slots__ = () - - @property - def all (self): - return self.exps.order_by(SingleExpense.day).all() - -class MonthExpense (namedtuple('MonthExpense', 'date catexps')): - - def __init__ (self, *args, **kwargs): - self._consts = None - super(MonthExpense, self).__init__(*args, **kwargs) - - @property - def consts (self): - if self._consts is None: - self._consts = ConstExpense.of_month(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.expense for x in self.catexps) - - @property - def all (self): - return SingleExpense.of_month(self.date.month, self.date.year).order_by(SingleExpense.day).all() - - def __str__ (self): - return '' % (self.date, self.sum) - -# -# Extra indizes have to be here -# - -Index('idx_single_date', SingleExpense.year, SingleExpense.month) -Index('idx_start_end', ConstExpense.start, ConstExpense.end) - -if __name__ == "__main__": - Base.metadata.create_all(engine) diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..0effb96 --- /dev/null +++ b/settings.py @@ -0,0 +1,11 @@ +import os + +# set this to False on productive systems +DEBUG = True + +# just generate a new secret key on each startup +SECRET_KEY = os.urandom(42) + +# database +SQLALCHEMY_DATABASE_URI = "sqlite:///test.sqlite" +SQLALCHEMY_ECHO = True diff --git a/templates/menu.jinja b/templates/menu.jinja new file mode 100644 index 0000000..b96d542 --- /dev/null +++ b/templates/menu.jinja @@ -0,0 +1,7 @@ +{% set menu = [ + ("index", "Kosten"), + ("addExp", "Neu"),] %} + {# ("/const", "Konstante Kosten"), + ("/categories", "Kategorien") + ] +#} diff --git a/templates/menu.mako b/templates/menu.mako deleted file mode 100644 index 98e5904..0000000 --- a/templates/menu.mako +++ /dev/null @@ -1,8 +0,0 @@ -<%! - menu = [ - ("/", "Kosten"), - ("/add", "Neu"), - ("/const", "Konstante Kosten"), - ("/categories", "Kategorien") - ] -%> diff --git a/templates/root.jinja b/templates/root.jinja new file mode 100644 index 0000000..7debb6a --- /dev/null +++ b/templates/root.jinja @@ -0,0 +1,56 @@ + + + + + {% if not g.is_mobile %} + {% block js %} + + + {% endblock %} + {% endif %} + + {% block title -%}Kostenverwaltung{%- endblock %} + {% block style %} + + {% endblock %} + + + +
+ + + +
+ +
+
+ {% block content %}{% endblock %} +
+
+
+ + + diff --git a/templates/root.mako b/templates/root.mako deleted file mode 100644 index 19af96e..0000000 --- a/templates/root.mako +++ /dev/null @@ -1,56 +0,0 @@ - - - - - % if not w.ctx.is_mobile: - <%block name="js"> - - - - % endif - - <%block name="title">Kostenverwaltung</%block> - <%block name="style"> - - - - - -
- - - -
- -
-
- ${next.body()} -
-
-
- - - -- cgit v1.2.3