[Checkins] SVN: Sandbox/luciano/kirbi/ first signs of life from book leases

Luciano Ramalho luciano at ramalho.org
Tue Aug 21 03:57:43 EDT 2007


Log message for revision 79062:
  first signs of life from book leases
  

Changed:
  U   Sandbox/luciano/kirbi/README.txt
  U   Sandbox/luciano/kirbi/src/kirbi/README.txt
  U   Sandbox/luciano/kirbi/src/kirbi/app.py
  U   Sandbox/luciano/kirbi/src/kirbi/book_templates/index.pt
  U   Sandbox/luciano/kirbi/src/kirbi/collection.py
  U   Sandbox/luciano/kirbi/src/kirbi/collection_templates/index.pt
  U   Sandbox/luciano/kirbi/src/kirbi/interfaces.py
  U   Sandbox/luciano/kirbi/src/kirbi/item.py
  U   Sandbox/luciano/kirbi/src/kirbi/pac.py
  U   Sandbox/luciano/kirbi/src/kirbi/static/master.css

-=-
Modified: Sandbox/luciano/kirbi/README.txt
===================================================================
--- Sandbox/luciano/kirbi/README.txt	2007-08-21 03:29:18 UTC (rev 79061)
+++ Sandbox/luciano/kirbi/README.txt	2007-08-21 07:57:42 UTC (rev 79062)
@@ -1,165 +1 @@
-=============================
-Kirbi: a P2P library manager
-=============================
-
-Kirbi is a sample application to test Grok and help programmers learn
-how to use it to build a complete app.
-
-Kirbi also aims to be useful and not just a sample. It is a system to allow
-friends and colleagues to share their books and DVDs without losing track
-of them.
-
-Use cases
-===========
-
-Done
------
-
-* Add books to the public catalog via a Web form or XML-RPC
-
-* Allow searches to the public catalog
-
-* Add books by entering just the ISBN, and letting Kirbi fetch the book data
-  from Amazon.com
-
-* User self-registration
-
-* User catalogs own collection
-
-To Do
-------
-
-* User invites friends to share specific collections
-
-* User requests to borrow an item
-
-* User approves the loan of an item
-
-* User tracks lent items
-
-* User tracks borrowed items
-
-* Add books by entering title words or author names, and letting Kirbi fetch
-  some likely candidates from Amazon.com
-
-* User adds book which belongs to a friend, who maybe a current user or someone
-  to be invited (recovering a lost book is a very strong motivation to join!)
-
-Features
-==============
-
-This is a list of other use cases, organized by view.
-
-app/index
--------------
-
-* implement recent additions
-
-pac/index
--------------
-
-For each item listed:
-
-* button: "i own it/add to my collection"
-
-* button: "borrow"
-
-* list: owners
-
-* display: rating
-
-book/index
-------------
-
-* button: "i own it/add to my collection"
-
-* button: "borrow"
-
-* list: owners
-
-* button: "recommend" (to a friend/to yourself also?)
-
-* display: rating
-
-* control: "rate it"
-
-* list: reviews
-
-* button: "review"
-
-* button: rate review
-
-* display: tags
-
-* control: "tag"
-
-* button: "liberate"
-
-user/menu
--------------
-
-(currently this is part of app/master and not a separate view)
-
-* link: invite
-
-* link: preferences
-
-user/index
-------------
-
-(currently this is part of collection/index and not a separate view)
-
-* list: lease requests/due
-
-* list: borrow requests/due
-
-* list: recent invitations (pending/accepted)
-
-* list: recomendations for you
-
-lease/borrow
---------------
-
-* list: owners of copies of that book (items of that manifestation)
-
-* list: alternative manifestations based on OCLC xISBN service
-
-* control: duration needed
-
-* text area: suggested time/place for pickup
-
-lease/lend
----------------
-
-* control: duration approved
-
-* text area: edit suggested time/place for pickup
-
-* control: date when available
-
-* button: approve
-
-* text line: reason for denial
-
-* button: deny
-
-new views
--------------
-
-* manage invitations
-
-* preferences (at least passwd change; default lease time; create
-    alternative collecions; privacy)
-
-
-Other tasks
-===========
-
-* Refactor kirbifetch to allow pluggable metadata sources, instead of
-  relying on Amazon.com exclusively
-
-* Increase test coverage
-
-* Packaging (buildout, eggification)
-
-* AJAXification using same framework used for Grok Admin UI
+See ./src/kirbi/README.txt
\ No newline at end of file

Modified: Sandbox/luciano/kirbi/src/kirbi/README.txt
===================================================================
--- Sandbox/luciano/kirbi/src/kirbi/README.txt	2007-08-21 03:29:18 UTC (rev 79061)
+++ Sandbox/luciano/kirbi/src/kirbi/README.txt	2007-08-21 07:57:42 UTC (rev 79062)
@@ -19,19 +19,16 @@
 
 * Allow searches to the public catalog
 
-To Do
-------
-
 * Add books by entering just the ISBN, and letting Kirbi fetch the book data
-  from Amazon.com (under development)
+  from Amazon.com
 
-* Add books by entering title words or author names, and letting Kirbi fetch
-  some likely candidates from Amazon.com
-  
 * User self-registration
 
-* User catalogs own collections
+* User catalogs own collection
 
+To Do
+------
+
 * User invites friends to share specific collections
 
 * User requests to borrow an item
@@ -42,14 +39,125 @@
 
 * User tracks borrowed items
 
+* Add books by entering title words or author names, and letting Kirbi fetch
+  some likely candidates from Amazon.com
 
+* User adds book which belongs to a friend, who maybe a current user or someone
+  to be invited (recovering a lost book is a very strong motivation to join!)
+
+Features
+==============
+
+This is a list of other use cases, organized by view.
+
+app/index
+-------------
+
+* implement recent additions
+
+pac/index
+-------------
+
+For each item listed:
+
+* button: "i own it/add to my collection"
+
+* button: "borrow"
+
+* list: owners
+
+* display: rating
+
+book/index
+------------
+
+* button: "i own it/add to my collection"
+
+* button: "borrow"
+
+* list: owners
+
+* button: "recommend" (to a friend/to yourself also?)
+
+* display: rating
+
+* control: "rate it"
+
+* list: reviews
+
+* button: "review"
+
+* button: rate review
+
+* display: tags
+
+* control: "tag"
+
+* button: "liberate"
+
+user/menu
+-------------
+
+(currently this is part of app/master and not a separate view)
+
+* link: invite
+
+* link: preferences
+
+user/index
+------------
+
+(currently this is part of collection/index and not a separate view)
+
+* list: lease requests/due
+
+* list: borrow requests/due
+
+* list: recent invitations (pending/accepted)
+
+* list: recomendations for you
+
+item/borrow
+--------------
+
+* list: alternative manifestations based on OCLC xISBN service
+
+* control: duration needed
+
+* text area: suggested time/place for pickup
+
+item/lend
+---------------
+
+* control: duration approved
+
+* text area: edit suggested time/place for pickup
+
+* control: date when available
+
+* button: approve
+
+* text line: reason for denial
+
+* button: deny
+
+new views
+-------------
+
+* manage invitations
+
+* preferences (at least passwd change; default lease time; create
+    alternative collecions; privacy)
+
+
 Other tasks
 ===========
 
-* Add tests
+* Refactor kirbifetch to allow pluggable metadata sources, instead of
+  relying on Amazon.com exclusively
 
+* Increase test coverage
+
 * Packaging (buildout, eggification)
 
-* AJAXification using some framework (whatever Uli chooses for the Grok Admin
-  UI, currently MojiKit or KSS)
-
+* AJAXification using same framework used for Grok Admin UI

Modified: Sandbox/luciano/kirbi/src/kirbi/app.py
===================================================================
--- Sandbox/luciano/kirbi/src/kirbi/app.py	2007-08-21 03:29:18 UTC (rev 79061)
+++ Sandbox/luciano/kirbi/src/kirbi/app.py	2007-08-21 07:57:42 UTC (rev 79062)
@@ -19,7 +19,7 @@
 from kirbi.pac import Pac
 from kirbi.book import Book
 from kirbi.collection import Collection
-from kirbi.interfaces import IItem, IUser
+from kirbi.interfaces import IItem, IUser, ILease
 from zope.interface import Interface, implements
 from zope.component import getSiteManager
 from zope.traversing import browser
@@ -102,7 +102,19 @@
     
     manifestation_id = index.Field()
     owner_login = index.Field()
+    
+class LeaseIndexes(grok.Indexes):
+    grok.site(Kirbi)
+    grok.context(ILease)
 
+    item_id = index.Field()
+    lender_login = index.Field()
+    borrower_login = index.Field()
+    getDue = index.Field()
+    request_date = index.Field()
+    status = index.Field()
+
+
 class Master(grok.View):
     """The master page template macro."""
     # register this view for all objects

Modified: Sandbox/luciano/kirbi/src/kirbi/book_templates/index.pt
===================================================================
--- Sandbox/luciano/kirbi/src/kirbi/book_templates/index.pt	2007-08-21 03:29:18 UTC (rev 79061)
+++ Sandbox/luciano/kirbi/src/kirbi/book_templates/index.pt	2007-08-21 07:57:42 UTC (rev 79062)
@@ -64,13 +64,23 @@
     </table>
 
     </div><!-- /details -->
+    
+    <div class="book_owners">
+        <h3>People who own this book</h3>
+        <tal:owner repeat="item context/getItems">
+            <a tal:attributes="href
+                string:${view/application_url}/u/${item/owner}/${item/item_id}/borrow"
+                tal:content="item/owner" />&nbsp;&nbsp;
+        </tal:owner>    
+        <p>(click on a name to borrow it from that person)</p>
+    </div>
 
-    <!-- XXX: only the site manager should be able to edit a book -->
+    <!-- XXX: only the site manager should be able to edit a book 
     <form class="search" tal:attributes="action python:view.url('edit')">
         <input type="submit" name="submit" value="edit">
     </form>
+    -->
 
-
   </div><!-- /content -->
   </body>
 </html>

Modified: Sandbox/luciano/kirbi/src/kirbi/collection.py
===================================================================
--- Sandbox/luciano/kirbi/src/kirbi/collection.py	2007-08-21 03:29:18 UTC (rev 79061)
+++ Sandbox/luciano/kirbi/src/kirbi/collection.py	2007-08-21 07:57:42 UTC (rev 79062)
@@ -22,7 +22,10 @@
 from kirbi.item import Item
 from kirbi.book import Book
 from zope.app.container.interfaces import INameChooser
+from zope.component import getUtility
+from zope.app.catalog.interfaces import ICatalog
 
+
 class Collection(grok.Container):
     """A collection of items (books, disks etc.) belonging to one user.
     
@@ -60,7 +63,23 @@
         cover_name = 'covers/large/'+book.__name__+'.jpg'
         return self.static.get(cover_name,
                                self.static['covers/small-placeholder.jpg'])()
+    
+    def yourRequests(self):
+        catalog = getUtility(ICatalog)
+        res = catalog.searchResults(
+                    borrower_login=(self.context.__name__,
+                                    self.context.__name__)
+                    )
+        return ['%s->%s' % (r.lender_login, r.item_id) for r in res]
 
+    def othersRequests(self):
+        catalog = getUtility(ICatalog)
+        res = catalog.searchResults(
+                    lender_login=(self.context.__name__,
+                                    self.context.__name__)
+                    )
+        return ['%s->%s' % (r.lender_login, r.item_id) for r in res]
+
 class AddFromPac(grok.View):
     def render(self,manifestation_id,camefrom):
         pac = grok.getSite()['pac']

Modified: Sandbox/luciano/kirbi/src/kirbi/collection_templates/index.pt
===================================================================
--- Sandbox/luciano/kirbi/src/kirbi/collection_templates/index.pt	2007-08-21 03:29:18 UTC (rev 79061)
+++ Sandbox/luciano/kirbi/src/kirbi/collection_templates/index.pt	2007-08-21 07:57:42 UTC (rev 79062)
@@ -14,26 +14,37 @@
   </span>
   <div metal:fill-slot="content">
 
-    <h3 tal:content="view/results_title">999 items matched the query</h3>
-    <table tal:condition="view/results">
-        <tr tal:repeat="item view/results">
-            <th align="right" tal:content="repeat/item/number" />
-            <td align="center">
-                <img class="cover"
-                     tal:attributes="src python:view.coverUrl(item)"
-                     height="53" />
-            </td>
-            <td>
-                <dl>
-                    <dt><a tal:attributes="href python:view.url(item)"
-                        tal:content="item/filing_title">title goes here</a>
-                    </dt>
-                    <dd tal:content="item/creatorsLine">
-                    </dd>
-                </dl>
-            </td>
-        </tr>
-    </table>
+    <table><tr>
+    <td>
+        <h3 tal:content="view/results_title">999 items matched the query</h3>
+        <table tal:condition="view/results">
+            <tr tal:repeat="item view/results">
+                <th align="right" tal:content="repeat/item/number" />
+                <td align="center">
+                    <img class="cover"
+                         tal:attributes="src python:view.coverUrl(item)"
+                         height="53" />
+                </td>
+                <td>
+                    <dl>
+                        <dt><a tal:attributes="href python:view.url(item)"
+                            tal:content="item/filing_title">title goes here</a>
+                        </dt>
+                        <dd tal:content="item/creatorsLine">
+                        </dd>
+                    </dl>
+                </td>
+            </tr>
+        </table>
+    <td><div class="leases">
+        <h2>Leases</h2>
+        <h3>Your requests</h3>
+            <span tal:replace="view/yourRequests|string:(none)" />
+        <h3>Other's requests</h3>
+            <span tal:replace="view/othersRequests|string:(none)" />
+        </div>
+    </td>
+    </tr></table>
 
   </div>
 </body>

Modified: Sandbox/luciano/kirbi/src/kirbi/interfaces.py
===================================================================
--- Sandbox/luciano/kirbi/src/kirbi/interfaces.py	2007-08-21 03:29:18 UTC (rev 79061)
+++ Sandbox/luciano/kirbi/src/kirbi/interfaces.py	2007-08-21 07:57:42 UTC (rev 79062)
@@ -145,38 +145,22 @@
 class ILease(Interface):
     """A book lease."""
 
-    copy_id = schema.TextLine(title=u"Copy id",
-                    description=u"The id of the copy being lent.",
-                    required=True)
+    item_id = Attribute(u"The id of the copy being lent.")
 
-    # Note: the lender_id can usually be obtained from the copy, however if a
-    # copy is given to a new owner, the lease history would become incomplete.
-    lender_id = schema.Text(title=u"Lender",
-                            description=(u"Lender login."),
-                            required=True)
+    # Note: the lender_login can usually be obtained from the item.__parent__,
+    # however if an item is given to a new owner, the lease history would
+    # become incomplete.
+    lender_login = Attribute(u"Lender login.")
 
-    borrower_id = schema.Text(title=u"Borrower",
-                            description=(u"Borrower login."),
-                            required=True)
+    borrower_login = Attribute(u"Borrower login.")
 
-    #XXX: This should be filled automatically.
-    request_date = schema.Date(title=u"Request date",
-                    description=u"When the lease was requested.",
-                    required=False)
+    request_date = Attribute(u"When the lease was requested.")
 
-    delivery_date = schema.Date(title=u"Delivery date",
-                    description=u"When the copy was delivered to the borrower.",
-                    required=False)
+    getDue = Attribute(u"Calculated due date.")
 
-    due_date = schema.Date(title=u"Due date",
-                description=u"When the copy should be returned to the lender.",
-                required=False)
+    # XXX: This should have a vocabulary
+    duration = Attribute(u"One of: minute week month quarter semester year")
+    
+    # XXX: This should have a vocabulary
+    status = Attribute(u"One of: pending approved denied")
 
-    return_date = schema.Date(title=u"Returnd date",
-                    description=u"When the copy was returned to the lender.",
-                    required=False)
-
-    @invariant
-    def dueAfterDelivery(lease):
-        if not (lease.due_date > lease.delivery_date):
-            raise Invalid(u'The due date must be after the delivery date.')

Modified: Sandbox/luciano/kirbi/src/kirbi/item.py
===================================================================
--- Sandbox/luciano/kirbi/src/kirbi/item.py	2007-08-21 03:29:18 UTC (rev 79061)
+++ Sandbox/luciano/kirbi/src/kirbi/item.py	2007-08-21 07:57:42 UTC (rev 79062)
@@ -15,11 +15,13 @@
 """
 
 import grok
-from interfaces import IItem, IBook
+from interfaces import IItem, IBook, ILease
 from zope.interface import Interface, implements, invariant
 from zope import schema
-from datetime import datetime
+from datetime import datetime, timedelta
+from zope.app.container.interfaces import INameChooser
 
+
 class Item(grok.Container):
     """An exemplar of a book.
 
@@ -59,5 +61,113 @@
     def __getattr__(self,name):
         # XXX: Martijn Faassen sugests a refactoring here, implementing
         # the Item->Manifestation relationship not using this sort of
-        # dynamically hacked inheritance but in as an association
+        # dynamically hacked inheritance but as an association
         return getattr(self.manifestation, name)
+    
+    def addLease(self, lease):
+        name = INameChooser(self).chooseName(lease.borrower_login, lease)
+        self[name] = lease
+        return lease.__name__
+
+    
+class Borrow(grok.View):
+    # control: duration needed
+    # text area: suggested time/place for pickup
+    grok.context(Item)
+    
+    
+    def __init__(self, context, request):
+        super(Borrow, self).__init__(context, request)
+    
+        self.form_title = u'Borrow item'
+        self.borrow_from = u'%s (%s)' % (context.__parent__.title,
+                                         context.__parent__.__name__)
+
+    def getDurations(self):
+        return u'minute week month quarter semester year'.split()
+
+    def coverUrl(self):
+        cover_name = 'covers/large/'+self.context.manifestation_id+'.jpg'
+        return self.static.get(cover_name,
+                               self.static['covers/small-placeholder.jpg'])()
+    
+    def update(self, duration=None, pickup=''):
+        if duration is not None:
+            lease = Lease(self.context.__name__, self.context.__parent__.__name__,
+                          self.request.principal.id, duration, pickup)
+            
+            self.context.addLease(lease)
+    
+            self.redirect(self.url(self.context))
+            
+class Lease(grok.Model):
+    """A book lease.
+    
+    >>> start = datetime(2007,1,1,0,0,0)
+    >>> start.isoformat()
+    '2007-01-01T00:00:00'
+
+    >>> Lease.calculateDue(start,u'minute').isoformat()
+    '2007-01-01T00:01:00'
+    >>> Lease.calculateDue(start,u'hour').isoformat()
+    '2007-01-01T01:00:00'
+    >>> Lease.calculateDue(start,u'day').isoformat()
+    '2007-01-02T00:00:00'
+    >>> Lease.calculateDue(start,u'week').isoformat()
+    '2007-01-08T00:00:00'
+    >>> Lease.calculateDue(start,u'month').isoformat()
+    '2007-02-01T00:00:00'
+    >>> Lease.calculateDue(start,u'quarter').isoformat()
+    '2007-04-01T00:00:00'
+    >>> Lease.calculateDue(start,u'semester').isoformat()
+    '2007-07-01T00:00:00'
+    >>> Lease.calculateDue(start,u'year').isoformat()
+    '2008-01-01T00:00:00'
+        
+    Test year wrap-around:
+
+    >>> start = datetime(2007,12,1,0,0,0)
+    >>> start.isoformat()
+    '2007-12-01T00:00:00'
+    >>> Lease.calculateDue(start,u'month').isoformat()
+    '2008-01-01T00:00:00'
+    >>> Lease.calculateDue(start,u'quarter').isoformat()
+    '2008-03-01T00:00:00'
+    >>> Lease.calculateDue(start,u'semester').isoformat()
+    '2008-06-01T00:00:00'
+    
+    """
+    implements(ILease)
+
+    def __init__(self, item_id, lender_login, borrower_login, duration, pickup):
+        super(Lease, self).__init__()
+
+        self.item_id = item_id
+        self.lender_login = lender_login
+        self.borrower_login = borrower_login
+        self.duration = duration
+        self.pickup = pickup
+        self.request_date = datetime.now()
+        self.status = u'pending' # One of: pending approved denied
+
+    def getDue(self):
+        return Lease.calculateDue(self.request_date, self.duration)
+
+    @staticmethod
+    def calculateDue(fromDateTime, interval):
+        MONTHLY_INTERVALS = {u'month':1, u'quarter':3, u'semester':6}
+        if interval in [u'minute',u'hour',u'day',u'week']:
+            interval = timedelta(**{interval+u's':1})
+            return fromDateTime + interval
+        elif interval in MONTHLY_INTERVALS:
+            months = MONTHLY_INTERVALS[interval]    
+            if fromDateTime.month < (13-months):
+                return fromDateTime.replace(month=fromDateTime.month+months)
+            else:
+                return fromDateTime.replace(year=fromDateTime.year+1,
+                                            month=fromDateTime.month-(12-months))
+        elif interval == u'year':
+            return fromDateTime.replace(year=fromDateTime.year+1)
+        else:
+            raise ValueError, u'Unknown interval: "%s"' % interval
+                

Modified: Sandbox/luciano/kirbi/src/kirbi/pac.py
===================================================================
--- Sandbox/luciano/kirbi/src/kirbi/pac.py	2007-08-21 03:29:18 UTC (rev 79061)
+++ Sandbox/luciano/kirbi/src/kirbi/pac.py	2007-08-21 07:57:42 UTC (rev 79062)
@@ -136,6 +136,9 @@
                     self.results_title = u'"%s" is not a valid query' % query
                     self.results = []
                     return
+            # XXX: remove Items from the result; like Martijn said
+            # Items and Books must be refactored to become less symbiotic
+            results = (r for r in results if not hasattr(r,'manifestation_id'))
             # Note: to sort the results, we must cast the result iterable
             # to a list, which can be very expensive
             results = list(results)

Modified: Sandbox/luciano/kirbi/src/kirbi/static/master.css
===================================================================
--- Sandbox/luciano/kirbi/src/kirbi/static/master.css	2007-08-21 03:29:18 UTC (rev 79061)
+++ Sandbox/luciano/kirbi/src/kirbi/static/master.css	2007-08-21 07:57:42 UTC (rev 79062)
@@ -1,5 +1,7 @@
 body {
     margin: 0px;
+    font-family: sans-serif;
+    font-size: 11pt;
 }
 
 a:link {
@@ -173,4 +175,9 @@
  
 .listing th {
     text-align: center;
-}
\ No newline at end of file
+}
+
+div.leases {
+    background-color: #eef;
+    padding: 10px;
+}



More information about the Checkins mailing list