Code

- queries on a per-user basis, and public queries (sf "bug" 891798 :)
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 26 Mar 2004 04:50:51 +0000 (04:50 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 26 Mar 2004 04:50:51 +0000 (04:50 +0000)
- EditAction was confused about who "self" was
- Edit collision detection was broken for index-page edits

git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@2204 57a73879-2fb5-44c3-a270-3262357dd7e2

CHANGES.txt
doc/upgrading.txt
doc/user_guide.txt
roundup/cgi/actions.py
roundup/cgi/templating.py
templates/classic/dbinit.py
templates/classic/html/_generic.collision.html
templates/classic/html/page.html
templates/classic/html/query.edit.html [new file with mode: 0644]
templates/classic/html/user.item.html

index b00dbb9c70dd08e556c0913377aee4acd9922a90..952d337e62e659167e1fd89131ed8f1303c613c2 100644 (file)
@@ -10,11 +10,14 @@ Feature:
 - added Reject exception which may be raised by auditors. This is trapped
   by mailgw and may be used to veto creation of file attachments or
   messages. (sf bug 700265)
+- queries on a per-user basis, and public queries (sf "bug" 891798 :)
 
 Fixed:
 - Boolean HTML templating was broken
 - Link HTML templating field() was broken
 - Fix reporting of test inclusion in postgresql test
+- EditAction was confused about who "self" was
+- Edit collision detection was broken for index-page edits
 
 
 2004-03-24 0.7.0b1
index dbd01ad2cb40363c7ed751a85cf86cf3ed1fed8f..ec390de2ceae25a9d0e336fd43222f0cc8a02029 100644 (file)
@@ -9,36 +9,76 @@ accordingly. Note that there is information about upgrade procedures in the
 .. contents::
 
 
-
-Migrating from 0.7 to 0.8
+Migrating from 0.6 to 0.7
 =========================
 
-0.8.0 Added Dispatcher role
----------------------------
+0.7.0 Saving and sharing of user queries
+----------------------------------------
 
-A new config option has been added. There is a 'role' that can be filled, 
-that of the 'dispatcher'. This person acts as a central sentinel for issues
-coming into the system. You can configure it so that all e-mail error messages
-get bounced to them, them and the user in question, or just the user (default).
+Due to popular demand, the user query saving mechanisms have been
+overhauled. This means that queries remember the user that created them
+and they may be marked as being private for a particular user.
 
-To toggle these switches, look at the new classic and minimal config.py's,
-specifically:
+You *are not required* to make these changes. You only need to make them
+if you wish to use the new query editing features. It's highly
+recommended, as the effort is minimal.
 
+1. You will need to edit your tracker's ``dbinit.py`` to change the way
+   queries are stored. Change the lines::
 
-# The 'dispatcher' is a role that can get notified of new items to the database.
-DISPATCHER_EMAIL = ADMIN_EMAIL
+      query = Class(db, "query",
+                      klass=String(),     name=String(),
+                      url=String())
+      query.setkey("name")
 
-...
+   to::
 
+      query = Class(db, "query",
+                      klass=String(),     name=String(),
+                      url=String(),       private_for=Link('user'))
 
-# Send error messages to the dispatcher, user, or both?
-# If 'dispatcher', error message notifications will only be sent to the dispatcher.
-# If 'user',       error message notifications will only be sent to the user.
-# If 'both',       error message notifications will be sent to both individuals.
-ERROR_MESSAGES_TO = 'user'
+   That is, add the "private_for" property, and remove the line that says
+   ``query.setkey("name")``. The latter is the most important edit here.
+
+2. You will also need to copy the ``query.edit.html`` template page from the
+   ``templates/classic/html/`` directory of the source to your tracker's
+   ``html`` directory.
+
+3. Once you've done that, edit the tracker's ``page.html`` template to
+   change::
+
+    <td rowspan="2" valign="top" class="sidebar">
+     <p class="classblock" tal:condition="request/user/queries">
+      <b>Your Queries</b><br>
+      <tal:block tal:repeat="qs request/user/queries">
+
+   to::
+
+    <td rowspan="2" valign="top" class="sidebar">
+     <p class="classblock">
+      <b>Your Queries</b> (<a href="query?@template=edit">edit</a>)<br>
+      <tal:block tal:repeat="qs request/user/queries">
+
+   That is, you're removing the ``tal:condition`` and adding a link to the
+   new edit page.
+
+4. You might also wish to remove the redundant query editing section from the
+   ``user.item.html`` page.
+
+
+0.7.0 Added Dispatcher role
+---------------------------
+
+A new config option has been added that specifies the email address of
+a "dispatcher" role.  This email address acts as a central sentinel for
+issues coming into the system. You can configure it so that all e-mail
+error messages get bounced to them, them and the user in question, or
+just the user (default).
+
+To toggle these switches, add the "DISPATCHER_EMAIL" and
+"ERROR_MESSAGES_TO" configuration values to your tracker's ``config.py``.
+See the `customisation documentation`_ for how to use them.
 
-Migrating from 0.6 to 0.7
-=========================
 
 0.7.0 Added CSV export action
 -----------------------------
index cf33487a7457abff6fb8632d3c2ee7eff558d8bb..19f44141e43237f5e723e8881c5e5ceb8c9d7fae 100644 (file)
@@ -2,7 +2,7 @@
 User Guide
 ==========
 
-:Version: $Revision: 1.25 $
+:Version: $Revision: 1.26 $
 
 .. contents::
 
@@ -245,6 +245,24 @@ See `entering values in your tracker`_ for an explanation of what you
 may type into the search form.
 
 
+Saving queries
+~~~~~~~~~~~~~~
+
+You may save queries in the tracker by giving the query a name. Each user
+may only have one query with a given name - if a subsequent search is
+performed with the same query name supplied, then it will edit the
+existing query of the same name.
+
+Queries may be marked as "private". These queries are only visible to the
+user that created them. If they're not marked "private" then all other
+users may include the query in their list of "Your Queries". Marking it as
+private at a later date does not affect users already using the query, nor
+does deleting the query.
+
+If a user subsequently creates or edits a public query, a new personal
+version of that query is made, with the same editing rules as described
+above.
+
 
 Under the covers
 ~~~~~~~~~~~~~~~~
index 529bfaca2d97ba2e9b57c2945bb16fd57edd8efa..481a971045f59c91f71de0060edbd92001c20635 100755 (executable)
@@ -1,4 +1,4 @@
-#$Id: actions.py,v 1.16 2004-03-26 00:46:33 richard Exp $
+#$Id: actions.py,v 1.17 2004-03-26 04:50:50 richard Exp $
 
 import re, cgi, StringIO, urllib, Cookie, time, random
 
@@ -134,14 +134,36 @@ class SearchAction(Action):
             # query string.
             url = req.indexargs_href('', {})[1:]
 
-            # handle editing an existing query
-            try:
-                qid = self.db.query.lookup(queryname)
-                self.db.query.set(qid, klass=self.classname, url=url)
-            except KeyError:
-                # create a query
-                qid = self.db.query.create(name=queryname,
-                    klass=self.classname, url=url)
+            key = self.db.query.getkey()
+            if key:
+                # edit the old way, only one query per name
+                try:
+                    qid = self.db.query.lookup(queryname)
+                    self.db.query.set(qid, klass=self.classname, url=url)
+                except KeyError:
+                    # create a query
+                    qid = self.db.query.create(name=queryname,
+                        klass=self.classname, url=url)
+            else:
+                # edit the new way, query name not a key any more
+                # see if we match an existing private query
+                uid = self.db.getuid()
+                qids = self.db.query.filter({}, {'name': queryname,
+                        'private_for': uid})
+                if not qids:
+                    # ok, so there's not a private query for the current user
+                    # - see if there's a public one created by them
+                    qids = self.db.query.filter({}, {'name': queryname,
+                        'private_for': -1, 'creator': uid})
+
+                if qids:
+                    # edit query
+                    qid = qids[0]
+                    self.db.query.set(qid, klass=self.classname, url=url)
+                else:
+                    # create a query
+                    qid = self.db.query.create(name=queryname,
+                        klass=self.classname, url=url, private_for=uid)
 
             # and add it to the user's query multilink
             queries = self.db.user.get(self.userid, 'queries')
@@ -435,23 +457,21 @@ class _EditAction(Action):
         return cl.create(**props)
 
 class EditItemAction(_EditAction):
-    def lastUserActivity(self):
+    def lastUserActvity(self):
         if self.form.has_key(':lastactivity'):
-            return date.Date(self.form[':lastactivity'].value)
+            user_actvity = date.Date(self.form[':lastactivity'].value)
         elif self.form.has_key('@lastactivity'):
-            return date.Date(self.form['@lastactivity'].value)
+            user_actvity = date.Date(self.form['@lastactivity'].value)
         else:
             return None
 
     def lastNodeActivity(self):
         cl = getattr(self.client.db, self.classname)
-        return cl.get(self.nodeid, 'activity')
+        node_activity = cl.get(self.nodeid, 'activity')
 
-    def detectCollision(self, userActivity, nodeActivity):
-        # Result from lastUserActivity may be None. If it is, assume there's no
-        # conflict, or at least not one we can detect.
-        if userActivity:
-            return userActivity < nodeActivity
+    def detectCollision(self, user_actvity, node_activity):
+        if user_activity:
+            return user_activity < node_activity
 
     def handleCollision(self):
         self.client.template = 'collision'
@@ -462,7 +482,9 @@ class EditItemAction(_EditAction):
         See parsePropsFromForm and _editnodes for special variables.
 
         """
-        if self.detectCollision(self.lastUserActivity(), self.lastNodeActivity()):
+        user_activity = self.lastUserActvity()
+        if user_activity and self.detectCollision(user_activity,
+                self.lastNodeActivity()):
             self.handleCollision()
             return
 
@@ -488,7 +510,7 @@ class EditItemAction(_EditAction):
         url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
             urllib.quote(self.template))
         if self.nodeid is None:
-            req = templating.HTMLRequest(self)
+            req = templating.HTMLRequest(self.client)
             url += '&' + req.indexargs_href('', {})[1:]
         raise Redirect, url
 
index ec223d9c68d27c0269fd0eee61085163f0ec2e2a..da9d29ed579e292334225f065566822de3556401 100644 (file)
@@ -124,7 +124,7 @@ class Templates:
                 raise
 
         if self.templates.has_key(src) and \
-                stime < self.templates[src].mtime:
+                stime <= self.templates[src].mtime:
             # compiled template is up to date
             return self.templates[src]
 
@@ -134,7 +134,7 @@ class Templates:
         content_type = mimetypes.guess_type(filename)[0] or 'text/html'
         pt.pt_edit(open(src).read(), content_type)
         pt.id = filename
-        pt.mtime = time.time()
+        pt.mtime = stime
         return pt
 
     def __getitem__(self, name):
@@ -195,6 +195,7 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
              'tracker': client.instance,
              'utils': utils(client),
              'templates': Templates(client.instance.config.TEMPLATES),
+             'template': self,
         }
         # add in the item if there is one
         if client.nodeid:
@@ -644,6 +645,10 @@ class HTMLItem(HTMLInputMixin, HTMLPermissions):
     def designator(self):
         """Return this item's designator (classname + id)."""
         return '%s%s'%(self._classname, self._nodeid)
+
+    def is_retired(self):
+        """Is this item retired?"""
+        return self._klass.is_retired(self._nodeid)
     
     def submit(self, label="Submit Changes"):
         """Generate a submit button.
@@ -1505,6 +1510,7 @@ class MultilinkHTMLProperty(HTMLProperty):
         ''' Support the "in" operator. We have to make sure the passed-in
             value is a string first, not a HTMLProperty.
         '''
+        print (self, value, self._value)
         return str(value) in self._value
 
     def reverse(self):
index 2e8c4741194b725576d5ed678038480b2e487828..f38dcabcf010f95e1a2da2393c27f508f54a6209 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: dbinit.py,v 1.6 2004-03-15 05:51:23 richard Exp $
+# $Id: dbinit.py,v 1.7 2004-03-26 04:50:50 richard Exp $
 
 import os
 
@@ -54,8 +54,7 @@ def open(name=None):
     
     query = Class(db, "query",
                     klass=String(),     name=String(),
-                    url=String())
-    query.setkey("name")
+                    url=String(),       private_for=Link('user'))
 
     # add any additional database schema configuration here
     
index fd2cc2b23d6a710cc22ad8b61dc7c5f3d2664061..34dbece1b2692e0d2e4b7a6ae7d6b48caf3289b0 100644 (file)
@@ -8,4 +8,5 @@
     editing. Please <a tal:attributes="href context/designator">reload</a>
     the node and review your edits.
   </td>
-</tal:block>
\ No newline at end of file
+</tal:block>
+
index 916d35f96804e7224144b33b503e4e0c485a131b..cf35212cff1e59512795c645844d266bd9dcb23f 100644 (file)
@@ -25,8 +25,8 @@
 
 <tr>
  <td rowspan="2" valign="top" class="sidebar">
-  <p class="classblock" tal:condition="request/user/queries">
-   <b>Your Queries</b><br>
+  <p class="classblock">
+   <b>Your Queries</b> (<a href="query?@template=edit">edit</a>)<br>
    <tal:block tal:repeat="qs request/user/queries">
     <a tal:attributes="href string:${qs/klass}?${qs/url}"
        tal:content="qs/name">link</a><br>
          tal:attributes="value name;
                          checked python:name == group_on">
 </td>
+<!-- SHA: 9defd15b86478f539e44f06b9548340e239d7320 -->
diff --git a/templates/classic/html/query.edit.html b/templates/classic/html/query.edit.html
new file mode 100644 (file)
index 0000000..c37a89f
--- /dev/null
@@ -0,0 +1,105 @@
+<!-- dollarId: user.item,v 1.7 2002/08/16 04:29:04 richard Exp dollar-->
+<tal:block metal:use-macro="templates/page/macros/icing">
+<title metal:fill-slot="head_title"> 
+<span tal:replace="config/TRACKER_NAME" />: "Your Queries" Editing
+</title> 
+<span metal:fill-slot="body_title" tal:omit-tag="python:1">
+ "Your Queries" Editing
+</span>
+
+<td class="content" metal:fill-slot="content">
+
+<span tal:condition="not:context/is_edit_ok">
+You are not allowed to edit queries.
+</span>
+
+<script language="javascript">
+// This exists solely because I can't figure how to get the & into an
+// attributes TALES expression, and so it keeps getting quoted.
+function retire(qid) {
+    window.location = 'query'+qid+'?@action=retire&@template=edit';
+}
+</script>
+
+<form method="POST" onSubmit="return submit_once()" action="query"
+      enctype="multipart/form-data" tal:condition="context/is_edit_ok">
+
+<table class="list" width="100%"
+       tal:define="uid request/user/id; mine request/user/queries">
+
+<tr><th>Query</th>
+    <th>Include in "Your Queries"</th>
+    <th>Edit</th>
+    <th>Private to you?</th>
+    <th>&nbsp;</th>
+</tr>
+
+<tr tal:repeat="query mine">
+ <tal:block condition="query/is_retired">
+
+ <td><a tal:attributes="href string:${query/klass}?${query/url}"
+        tal:content="query/name">query</a></td>
+
+ <td metal:define-macro="include">
+  <select tal:condition="python:query.id not in mine"
+          tal:attributes="name string:user${uid}@add@queries">
+    <option value="">leave out</option>
+    <option tal:attributes="value query/id">include</option>
+  </select>
+  <select tal:condition="python:query.id in mine"
+          tal:attributes="name string:user${uid}@remove@queries">
+    <option value="">leave in</option>
+    <option tal:attributes="value query/id">remove</option>
+  </select>
+ </td>
+
+ <td colspan="3">[query is retired]</td>
+
+ <!-- <td> maybe offer "restore" some day </td> -->
+ </tal:block>
+</tr>
+
+<tr tal:define="queries python:db.query.filter(filterspec={'private_for':uid})"
+     tal:repeat="query queries">
+ <td><a tal:attributes="href string:${query/klass}?${query/url}"
+        tal:content="query/name">query</a></td>
+
+ <td metal:use-macro="template/macros/include" />
+
+ <td><a tal:attributes="href string:query${query/id}">edit</a></td>
+
+ <td>
+  <select tal:attributes="name string:query${query/id}@private_for">
+   <option tal:attributes="selected python:query.private_for == uid;
+           value uid">yes</option>
+   <option tal:attributes="selected python:query.private_for == None"
+           value="-1">no</option>
+  </select>
+ </td>
+
+ <td>
+  <input type="button" value="Delete"
+  tal:attributes="onClick python:'''retire('%s')'''%query.id">
+  </td>
+</tr>
+
+<tr tal:define="queries python:db.query.filter(filterspec={'private_for':None})"
+     tal:repeat="query queries">
+ <td><a tal:attributes="href string:${query/klass}?${query/url}"
+        tal:content="query/name">query</a></td>
+
+ <td metal:use-macro="template/macros/include" />
+ <td colspan="3">[not yours to edit]</td>
+</tr>
+
+<tr><td colspan="5">
+        <input type="hidden" name="@action" value="edit">
+        <input type="hidden" name="@template" value="edit">
+        <input type="submit" value="Save Selection">
+</td></tr>
+
+</table>
+
+</form>
+</td>
+</tal:block>
index 2410b2cb4b9ffc2b5787ac405964ffb865b2311c..dae4b7356095b9e0c60a7ade2bbd77ba3322259d 100644 (file)
@@ -78,21 +78,6 @@ You are not allowed to view this page.
 </table>
 </form>
 
-<table class="otherinfo" tal:condition="python:context.queries and context.is_view_ok()">
- <tr><th colspan="3" class="header">Queries</th></tr>
- <tr><th>Name</th><th colspan="2">Actions</th></tr>
- <tr tal:repeat="query context/queries">
-  <td><a tal:attributes="href string:query${query/id}"
-         tal:content="query/name"></a></td>
-  <td>
-   <a tal:attributes="href string:${query/klass}?${query/url}">display</a>   
-  </td>
-  <td>
-   <a tal:attributes="href string:?@remove@queries=${query/id}&@action=edit">remove</a>
-  </td>
- </tr>
-</table>
-
 <table class="form" tal:condition="context/is_only_view_ok">
  <tr>
   <th colspan=2 class="header" tal:content="context/realname">realname</th>