# coding: utf-8
from svv import database as db
from svv.controllers import PageHandler
from svv.html import tags as html
from svv.forms import BaseForm, FormError
import logging
log = logging.getLogger('svv.items')
class Item (object) :
"""
Data-mapping for the items table
"""
db.mapper(Item, db.items, properties=dict(
# forward ref to parent
parent = db.relation(Item, remote_side=db.items.c.id,
backref = db.backref('children', remote_side=db.items.c.parent_id)
),
))
class ItemForm (BaseForm) :
"""
Form for editing item
"""
def __init__ (self, session) :
"""
Use given database session for rendering form.
"""
super(ItemForm, self).__init__()
self.session = session
def defaults (self) :
self.item_id = None
self.item_name = None
self.item_detail = None
self.item_quantity = None
self.item_parent_id = None
def load (self, item) :
"""
Load info from existing item for editing
"""
self.item_id = item.id
self.item_name = item.name
self.item_detail = item.detail
self.item_quantity = item.quantity
self.item_parent_id = item.parent.id if item.parent else None
def render_parent_select (self, name, parent_id) :
"""
Render <select> for parent
"""
# existing items suitable as parents
# XXX: currently only top-level items, as a heuristic
parent_items = self.session.query(Item.id, Item.name).filter(Item.parent == None).all()
return (
# <select> with options and selected=
self.render_select_input(name, [(0, u"---")] + parent_items, parent_id)
)
def render (self, action, legend=u"Laitetiedot", return_url=None) :
"""
Render <form> HTML
return_url - URL for reset button
"""
return html.form(action=action, method='POST')(
html.fieldset(
html.legend(legend),
html.ol(
self.render_form_field('item_parent', u"Sijainti", u"Missä laite sijaitsee", (
self.render_parent_select('item_parent', self.item_parent_id)
)),
self.render_form_field('item_name', u"Nimi", u"Lyhyt nimi", (
self.render_text_input('item_name', self.item_name)
)),
self.render_form_field('item_detail', u"Kuvaus", u"Tarkempi kuvaus", (
self.render_text_input('item_detail', self.item_detail, multiline=True)
)),
self.render_form_field('item_quantity', u"Kappalemäärä", u"Jos on esim. useampi piuha/adapter, kirjaa niitten määrä; muuten jätä tyhjäksi", (
self.render_text_input('item_quantity', self.item_quantity)
)),
html.li(
self.render_submit_button(u"Tallenna"),
self.render_reset_button(u"Palaa inventaariin", return_url) if return_url else None,
),
),
),
)
def process (self, data) :
"""
Process incoming POST data
"""
# bind
self.data = data
self.do_delete = self.process_action_field('delete')
if not self.do_delete :
try :
# XXX: self.process_id_field?
self.item_parent_id = self.process_integer_field('item_parent')
except FormError, e :
self.fail_field(e, 'item_parent')
try :
self.item_name = self.process_text_field('item_name', required=True)
except FormError, e :
self.fail_field(e, 'item_name')
try :
self.item_detail = self.process_text_field('item_detail')
except FormError, e :
self.fail_field(e, 'item_detail')
try :
self.item_quantity = self.process_integer_field('item_quantity')
except FormError, e :
self.fail_field(e, 'item_quantity')
# ok?
return not self.errors
class DeleteItemForm (BaseForm) :
"""
Display a list of items to delete, and confirm
"""
def __init__ (self, session) :
"""
Use given database session for rendering form.
"""
super(DeleteItemForm, self).__init__()
self.session = session
# list of items to delete
self.items = []
def load (self, items) :
"""
Init set of items to delete
"""
self.items = items
def render_items_list (self, name, items, visible=None) :
"""
Render list of items to delete.
visible - display list of items in addition the rendering the <input>'s
"""
def render_items (items, with_list, with_input) :
""" Item <ul> or <input>s """
if not items :
# blank
return None
elif with_list :
# nested list
return html.ul(
render_item(item, True, with_input) for item in items
)
else :
# just the controls
return [render_item(item, False, with_input) for item in items]
def render_item (item, with_list, with_input) :
""" Item <li> or <input> """
if with_input :
# the form field
input = html.input(type='hidden', name='items', value=item.id),
else :
# no field
input = None
if with_list :
return html.li(
input,
item.name if visible else None,
# don't recurse inputs
render_items(item.children, with_list, with_input=False),
)
else :
# just the input
return input
## Render <div> or <input>s
if visible :
# <div> with nested <li>s
return html.div(class_='value')(
render_items(items, with_list=True, with_input=True),
)
else :
# just the <input>s
return render_items(items, with_list=False, with_input=True)
def render (self, legend=None, delete_action=None, confirm_action=None, return_url=None, item_listing=None) :
"""
Render form with list of target items, and a confirm button
legend - form title
delete_action - URL for initial confirm stage submit
confirm_action - URL for final delete stage submit
reutrn_url - URL for cancel button
item_listing - display recursive item listing for confirmation
Supply either delete_action or confirm_action, but not both
"""
action = delete_action or confirm_action
return html.form(action=action, method='POST')(
html.fieldset(
html.legend(legend),
# raw field with just the <input>s
(self.render_items_list('items', self.items, visible=False)) if not item_listing else None,
html.ol(
# full UI field
(self.render_form_field('items', u"Poistettavat laitteet", u"Kaikki listatut laitteet poistetaan inventaarista", (
self.render_items_list('items', self.items, visible=True)
)) if item_listing else None),
html.li(
self.render_submit_button(u"Poista") if delete_action else None,
self.render_submit_button(u"Varmista", 'confirm') if confirm_action else None,
self.render_reset_button(u"Peruuta", return_url) if return_url else None,
),
)
)
)
def process_items_list (self, name) :
"""
Uses the incoming list of id's to build the list of Item's to delete.
"""
# incoming ids
item_ids = self.process_list_field('items', type=int)
# look up
items = self.session.query(Item).filter(Item.id.in_(item_ids)).all()
# make sure they all exist
found = [item_id for item_id in item_ids if item_id in set(item.id for item in items)]
if set(found) != set(item_ids) :
raise FormError(name, found, "Some items were not found")
if not items :
raise FormError(name, [], "No items were given")
# ok
return items
def process (self, data) :
"""
Look up list of Item's to delete.
Returns True if the delete is confirmed, False otherwise.
"""
# bind
self.data = data
# load items
try :
self.items = self.process_items_list('items')
except FormError, e :
self.fail_field(e, 'items')
# confirm?
confirm = self.process_action_field('confirm')
return not self.errors and confirm
class ItemView (PageHandler) :
"""
Display/edit info for a single item
"""
def update (self, item, form) :
"""
Update item data from form
"""
# lookup
item.parent = self.session.query(Item).get(form.item_parent_id)
# modify
item.name = form.item_name
item.detail = form.item_detail
item.quantity = form.item_quantity
# update
self.session.commit()
def process (self, id) :
"""
Update item data if POST'd
"""
# db
self.session = self.app.session()
# item in concern
self.item = self.session.query(Item).get(id)
# form
self.form = ItemForm(self.session)
self.form.load(self.item)
# process?
if self.POST :
if self.form.process(self.POST) :
# update
self.update(self.item, self.form)
# ok, done with item, redirect to full list view...
return self.redirect_for(InventoryView, fragment=('item-%d' % self.item.id))
else :
# re-render form
return
def render_subitems_table (self, subitems) :
"""
Render listing of child items in this item
"""
return ItemTable(self).render(subitems, parent=self.item)
def render_delete_form (self) :
"""
Render delete form for this item
"""
form = DeleteItemForm(self.session)
form.load([self.item])
return form.render(
legend = u"Poistaminen",
delete_action = self.url_for(DeleteItemView),
item_listing = False,
)
def render_content (self, id) :
"""
View item's info
"""
# ourselves
item = self.item
# items in this item
subitems = item.children
return (
html.h1("(#%d) %s" % (item.id, item.name)),
html.h3(
html.raw(u"Sijaitsee %s:ssa" % (
html.a(href=self.url_for(ItemView, id=item.parent.id))(item.parent.name)
)),
) if item.parent else None,
self.render_subitems_table(subitems) if subitems else None,
self.form.render(
action = self.url_for(ItemView, id=id),
return_url = self.url_for(InventoryView),
),
# delete form
self.render_delete_form(),
)
class NewItemView (PageHandler) :
"""
Create new item
"""
def create (self, form) :
"""
Create and return new item from ItemForm data.
"""
item = Item()
# validate and lookup parent
item.parent = self.session.query(Item).get(form.item_parent_id)
# store
item.name = form.item_name
item.detail = form.item_detail
item.quantity = form.item_quantity
# insert
self.session.add(item)
self.session.commit()
# ok
return item
def process (self) :
# db
self.session = self.app.session()
self.form = ItemForm(self.session)
# load POST
if self.POST :
if self.form.process(self.POST) :
# create
item = self.create(self.form)
# redirect to full list view...
return self.redirect_for(InventoryView, fragment=('item-%d' % item.id))
else :
# fail, just render...
return
else :
# render blank form
self.form.defaults()
def render_content (self) :
"""
Render the proecss()'d form
"""
return (
self.form.render(action=self.url_for(NewItemView), legend=u"Lisää uusi")
)
class DeleteItemView (PageHandler) :
"""
Confirm deletion of items.
"""
def delete (self, form) :
"""
Delete items from form
"""
# list of root Item's to delete, along with children
items = form.items
for item in items :
# XXX: we shouldn't actually DELETE these; rather, mark them as removed
# messes up existing orders like this...
self.session.delete(item)
# ok
self.session.commit()
def process (self) :
# db
self.session = self.app.session()
# form
self.form = DeleteItemForm(self.session)
# process
if self.form.process(self.POST) :
# delete
self.delete(self.form)
# redirect back
return self.redirect_for(InventoryView)
else :
# render
pass
def render_content (self) :
return (
self.form.render(
legend = u"Poistettavat laitteet",
confirm_action = self.url_for(DeleteItemView),
return_url = self.url_for(InventoryView),
item_listing = True,
)
)
class ItemTable (object) :
"""
Table of items
"""
def __init__ (self, handler) :
"""
handler - the AppHandler we are running under, used for url_for etc.
"""
self.handler = handler
def url_for (self, *args, **kwargs) :
"""
Proxy to handler
"""
return self.handler.url_for(*args, **kwargs)
def render (self, items, parent=None) :
"""
Render table for given set of items. If parent is given, those items are assumed to be the children of that
item.
"""
return html.table(
html.caption(
u"Kalustolistaus",
(
html.a(href=self.url_for(ItemView, id=parent.id))(
parent.name
) if parent else None
),
),
html.thead(
html.tr(
html.th(title) for title in (
u"#ID",
u"Sijainti" if not parent else None,
u"Nimi",
u"Kuvaus",
u"Määrä",
) if title
),
),
html.tbody(
html.tr(id=('item-%d' % item.id))(
html.td(
html.a(href=self.url_for(InventoryView, fragment=('item-%d' % item.id)))(
u'#%d' % item.id
)
),
html.td(
html.a(href=self.url_for(ItemView, id=item.parent.id))(
item.parent.name
) if item.parent else None
) if not parent else None,
html.td(
html.a(href=self.url_for(ItemView, id=item.id))(
item.name
)
),
html.td(
item.detail
),
html.td(
"%d kpl" % item.quantity if item.quantity else None
),
) for item in items
)
)
class InventoryView (PageHandler) :
"""
Display overview of all items
"""
def process (self) :
# db
self.session = self.app.session()
def render_item_table (self) :
"""
Render HTML for full <table> of all items, sorted heirarchially (by parent)
"""
# listing of inventory items
items = self.session.query(Item).order_by(Item.parent).all()
return ItemTable(self).render(items)
def render_item_form (self) :
"""
Render ItemForm for creating a new item
"""
form = ItemForm(self.session)
form.defaults()
return form.render(action=self.url_for(NewItemView), legend=u"Lisää uusi")
def render_content (self) :
"""
Full listing of all inventory
"""
return (
html.h1(u"Inventaari"),
self.render_item_table(),
self.render_item_form(),
)