svv/items.py
author Tero Marttila <terom@fixme.fi>
Mon, 10 Jan 2011 17:04:15 +0200
changeset 48 06fa83c8c0bb
child 53 06dad873204d
permissions -rw-r--r--
items: Inventory management
# 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"Laitejuttu", return_url=None, delete_action=None) :
        """
            Render <form> HTML

                return_url          - URL for reset button
                delete_action       - (optional) URL to delete-item option
        """

        return (
            html.form(action=action, method='POST')(
                html.fieldset(
                    html.legend(legend),

                    html.ol(
                        self.render_form_field('item_parent', u"Case", 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,
                        ),
                    ),
                ),
            ),

            html.form(action=delete_action, method='POST')(
                html.fieldset(
                    html.legend(u"Poistaminen"),

                    html.input(type='hidden', name='items', value=self.item_id),

                    html.ol(
                        html.li(
                            html.input(type='submit', name='delete', value=u"Poista"),
                        )
                    )
                ),
            ) if delete_action 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 render_items_list (self, name, items) :
        """
            Render list of items to delete.
        """

        def render_items (items) :
            return (
                html.ul(
                    render_item(item) for item in items
                ) if items else None
            )


        def render_item (item) :
            return html.li(
                html.input(type='hidden', name='items', value=item.id),
                item.name,
                render_items(item.children),
            )

        return html.div(class_='value')(
            render_items(items),
        )

    def render (self, action, return_url) :
        """
            Render form with list of target items, and a confirm button
        """

        return html.form(action=action, method='POST')(
            html.fieldset(
                html.legend(u"Poistettavat laitteet"),

                html.ol(
                    self.render_form_field('items', u"Poistettavat laitteet", u"Kaikki listatut laitteet poistetaan inventaarista", (
                        self.render_items_list('items', self.items)
                    )),

                    html.li(
                        self.render_submit_button(u"Varmista", 'confirm'),
                        
                        self.render_reset_button(u"Peruuta", return_url),
                    ),
                )
            )
        )

    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_content (self, id) :
        """
            View item's info
        """

        return (
            html.h1(u"Laite #%d" % self.item.id),

            self.form.render(
                action          = self.url_for(ItemView, id=id), 
                return_url      = self.url_for(InventoryView),
                delete_action   = self.url_for(DeleteItemView),
            ),
        )

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", delete=True)
        )

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 :
            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(action=self.url_for(DeleteItemView), return_url=self.url_for(InventoryView))
        )

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 html.table(
            html.caption("Kalustolistaus"),
        
            html.thead(
                html.tr(
                    html.th(title) for title in (
                        u"#ID",
                        u"Case",
                        u"Nimi",
                        u"Kuvaus",
                        u"Määrä",
                    )
                ),
            ),

            html.tbody(
                html.tr(id=('item-%d' % item.id))(
                    html.td(
                        html.a(href=self.url_for(ItemView, id=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
                    ),

                    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
            )
        )

    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(),
        )