"""
The latest version of this package is available at:
<http://github.com/jantman/biweeklybudget>
################################################################################
Copyright 2017 Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>
This file is part of biweeklybudget, also known as biweeklybudget.
biweeklybudget is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
biweeklybudget is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with biweeklybudget. If not, see <http://www.gnu.org/licenses/>.
The Copyright and Authors attributions contained herein may not be removed or
otherwise altered, except to add the Author attribution of a contributor to
this work. (Additional Terms pursuant to Section 7b of the AGPL v3)
################################################################################
While not legally required, I sincerely request that anyone who finds
bugs please submit them at <https://github.com/jantman/biweeklybudget> or
to me via email, and that you send any contributions or improvements
either as a pull request on GitHub, or to me via email.
################################################################################
AUTHORS:
Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>
################################################################################
"""
import logging
from flask.views import MethodView
from flask import render_template, jsonify, request
from datatables import DataTable
from sqlalchemy import or_
from decimal import Decimal
from biweeklybudget.flaskapp.app import app
from biweeklybudget.db import db_session
from biweeklybudget.models.projects import Project, BoMItem
from biweeklybudget.flaskapp.views.searchableajaxview import SearchableAjaxView
from biweeklybudget.flaskapp.views.formhandlerview import FormHandlerView
logger = logging.getLogger(__name__)
[docs]class ProjectsView(MethodView):
"""
Render the GET /projects view using the ``projects.html`` template.
"""
[docs] def get(self):
total_active = Decimal('0.0')
remain_active = Decimal('0.0')
for p in db_session.query(Project).filter(
Project.is_active.__eq__(True)
).all():
total_active += p.total_cost
remain_active += p.remaining_cost
return render_template(
'projects.html',
total_active=total_active,
remain_active=remain_active
)
[docs]class ProjectsAjax(SearchableAjaxView):
"""
Handle GET /ajax/projects endpoint.
"""
[docs] def _filterhack(self, qs, s, args):
"""
DataTables 1.10.12 has built-in support for filtering based on a value
in a specific column; when this is done, the filter value is set in
``columns[N][search][value]`` where N is the column number. However,
the python datatables package used here only supports the global
``search[value]`` input, not the per-column one.
However, the DataTable search is implemented by passing a callable
to ``table.searchable()`` which takes two arguments, the current Query
that's being built, and the user's ``search[value]`` input; this must
then return a Query object with the search applied.
In python datatables 0.4.9, this code path is triggered on
``if callable(self.search_func) and search.get("value", None):``
As such, we can "trick" the table to use per-column searching (currently
only if global searching is not being used) by examining the per-column
search values in the request, and setting the search function to one
(this method) that uses those values instead of the global
``search[value]``.
:param qs: Query currently being built
:type qs: ``sqlalchemy.orm.query.Query``
:param s: user search value
:type s: str
:param args: args
:type args: dict
:return: Query with searching applied
:rtype: ``sqlalchemy.orm.query.Query``
"""
# search
if s != '' and s != 'FILTERHACK':
if len(s) < 3:
return qs
s = '%' + s + '%'
qs = qs.filter(or_(
Project.notes.like(s),
Project.name.like(s)
))
return qs
[docs] def get(self):
"""
Render and return JSON response for GET /ajax/projects
"""
args = request.args.to_dict()
args_dict = self._args_dict(args)
if self._have_column_search(args_dict) and args['search[value]'] == '':
args['search[value]'] = 'FILTERHACK'
table = DataTable(
args,
Project,
db_session.query(Project),
[
'name',
'total_cost',
'remaining_cost',
'is_active',
'notes'
]
)
table.add_data(
id=lambda o: o.id
)
if args['search[value]'] != '':
table.searchable(lambda qs, s: self._filterhack(qs, s, args_dict))
return jsonify(table.json())
[docs]class BoMItemView(MethodView):
"""
Render the GET /project/<int:project_id> view using the ``bomitem.html``
template.
"""
[docs] def get(self, project_id):
proj = db_session.query(Project).get(project_id)
return render_template(
'bomitem.html',
project_id=project_id,
project_name=proj.name,
project_notes=proj.notes,
remaining=proj.remaining_cost,
total=proj.total_cost
)
[docs]class BoMItemsAjax(SearchableAjaxView):
"""
Handle GET /ajax/projects/<int:project_id>/bom_items endpoint.
"""
[docs] def _filterhack(self, qs, s, args):
"""
DataTables 1.10.12 has built-in support for filtering based on a value
in a specific column; when this is done, the filter value is set in
``columns[N][search][value]`` where N is the column number. However,
the python datatables package used here only supports the global
``search[value]`` input, not the per-column one.
However, the DataTable search is implemented by passing a callable
to ``table.searchable()`` which takes two arguments, the current Query
that's being built, and the user's ``search[value]`` input; this must
then return a Query object with the search applied.
In python datatables 0.4.9, this code path is triggered on
``if callable(self.search_func) and search.get("value", None):``
As such, we can "trick" the table to use per-column searching (currently
only if global searching is not being used) by examining the per-column
search values in the request, and setting the search function to one
(this method) that uses those values instead of the global
``search[value]``.
:param qs: Query currently being built
:type qs: ``sqlalchemy.orm.query.Query``
:param s: user search value
:type s: str
:param args: args
:type args: dict
:return: Query with searching applied
:rtype: ``sqlalchemy.orm.query.Query``
"""
# search
if s != '' and s != 'FILTERHACK':
if len(s) < 3:
return qs
s = '%' + s + '%'
qs = qs.filter(or_(
BoMItem.notes.like(s),
BoMItem.name.like(s),
BoMItem.url.like(s)
))
return qs
[docs] def get(self, project_id):
"""
Render and return JSON response for
GET /ajax/projects/<int:project_id>/bom_items
"""
args = request.args.to_dict()
args_dict = self._args_dict(args)
if self._have_column_search(args_dict) and args['search[value]'] == '':
args['search[value]'] = 'FILTERHACK'
table = DataTable(
args,
BoMItem,
db_session.query(BoMItem).filter(
BoMItem.project_id.__eq__(project_id)
),
[
'name',
'quantity',
'unit_cost',
'is_active',
'notes',
'url',
'id'
]
)
table.add_data(
line_cost=lambda o: float(o.unit_cost) * o.quantity
)
if args['search[value]'] != '':
table.searchable(lambda qs, s: self._filterhack(qs, s, args_dict))
return jsonify(table.json())
[docs]class ProjectAjax(MethodView):
"""
Render the GET /ajax/projects/<int:project_id> JSON view.
"""
[docs] def get(self, project_id):
proj = db_session.query(Project).get(project_id)
d = proj.as_dict
d['total_cost'] = proj.total_cost
d['remaining_cost'] = proj.remaining_cost
return jsonify(d)
[docs]class BoMItemAjax(MethodView):
"""
Render the GET /ajax/projects/bom_item/<int:id> JSON view.
"""
[docs] def get(self, id):
return jsonify(db_session.query(BoMItem).get(id).as_dict)
app.add_url_rule('/projects', view_func=ProjectsView.as_view('projects'))
app.add_url_rule(
'/forms/projects',
view_func=ProjectsFormHandler.as_view('projects_form')
)
app.add_url_rule(
'/ajax/projects',
view_func=ProjectsAjax.as_view('projects_ajax')
)
app.add_url_rule(
'/projects/<int:project_id>',
view_func=BoMItemView.as_view('bom_item_view')
)
app.add_url_rule(
'/ajax/projects/<int:project_id>',
view_func=ProjectAjax.as_view('ajax_project_view')
)
app.add_url_rule(
'/ajax/projects/<int:project_id>/bom_items',
view_func=BoMItemsAjax.as_view('bom_items_ajax')
)
app.add_url_rule(
'/ajax/projects/bom_item/<int:id>',
view_func=BoMItemAjax.as_view('bom_item_ajax')
)
app.add_url_rule(
'/forms/bom_item',
view_func=BoMItemFormHandler.as_view('bom_item_form')
)