svv/items.py
author Tero Marttila <terom@fixme.fi>
Mon, 10 Jan 2011 17:51:08 +0200
changeset 53 06dad873204d
parent 48 06fa83c8c0bb
child 55 433842c04ab1
permissions -rw-r--r--
items: use DeleteItemForm for ItemView as well
# 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"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,
                    ),
                ),
            ),
        )

    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, visible) :
            if not items :
                # blank
                return None

            elif visible :
                # nested list
                return html.ul(
                    render_item(item, True) for item in items
                )

            else :
                # just the controls
                return [render_item(item, False) for item in items]

        def render_item (item, visible) :
            # the form field
            field = html.input(type='hidden', name='items', value=item.id),

            if visible :
                return html.li(
                    field,

                    item.name if visible else None,

                    render_items(item.children, visible),
                )
            else :
                # just the input
                return field
        
        if visible :
            # div with nested <li>s
            return html.div(class_='value')(
                render_items(items, True),
            )
        else :
            # just the <input>s
            return render_items(items, False)

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

                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 
                        
                        # raw field with just the <input>s
                        (self.render_items_list('items', self.items, visible=False))
                    ),

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