changeset 7936:a9b136565838

feat: issue1525113 - notation to filter by logged-in user At long last (almost 18 years) this patch lands. It allows sharing of queries that want to use the currently logged in user (i.e. I or me). By replacing an id number for the user by '@current_user' in the query you can share the query for "my issues" where 'my' is the logged in user not the person who created the query. Updated the templates to use this. Updated upgrading.py for directions on using it. RDBMS and anydbm both work. Also expressions using it (e.g. not @current_user) work and are tested. Test code done. I am not sure what the change to templating.py does. I am following the original patch and have built a test case to hit the if clause. But the rest of the test doesn't actualy provide the props I need. If I knew what that code was supposed to do there I would create a real test.
author John Rouillard <rouilj@ieee.org>
date Mon, 06 May 2024 00:49:43 -0400
parents f670446b5e50
children 22354d7fc94a
files CHANGES.txt doc/upgrading.txt roundup/backends/back_anydbm.py roundup/backends/rdbms_common.py roundup/cgi/templating.py share/roundup/templates/classic/html/issue.search.html share/roundup/templates/devel/html/bug.search.html share/roundup/templates/devel/html/task.search.html share/roundup/templates/responsive/html/bug.search.html share/roundup/templates/responsive/html/task.search.html test/db_test_base.py test/test_templating.py
diffstat 12 files changed, 162 insertions(+), 21 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.txt	Sun May 05 20:57:11 2024 -0400
+++ b/CHANGES.txt	Mon May 06 00:49:43 2024 -0400
@@ -203,6 +203,13 @@
   (e.g. description of what a priority or status means) without being
   able to select the property in the classhelper.  Good for adding help
   for Link properties. (John Rouilllard)
+- issue1525113 - notation to filter by logged-in user. Use
+  @current_user with properties that are a Link to the 'user' class to
+  match the currently logged in user. Allows sharing of queries like
+  "Issues I created" or "Issues I am assigned to" by removing the
+  hard coded user id number and replacing it with the current user's
+  id. Tracker templates updated to use it. (John Rouillard from a
+  patch by Jon C. Thomason)
 
 2023-07-13 2.3.0
 
--- a/doc/upgrading.txt	Sun May 05 20:57:11 2024 -0400
+++ b/doc/upgrading.txt	Mon May 06 00:49:43 2024 -0400
@@ -293,6 +293,43 @@
 It is also missing the ``solves`` field which is added to match the
 schema.
 
+Use @current_user in Searches (optional)
+----------------------------------------
+
+You can create queries like: "My issues" by searching the ``creator``
+property of issues for your id number. Similarly you can search for
+"Issues assigned to me" by searching on the ``assignedto`` property.
+
+Queries in Roundup can be shared between users. However queries like
+these can be shared. However for any user but they will only find
+issues created by/assigned to the user who created the query.
+
+This release allows you to search Links to the User class by
+specifying ``@current_user``. This token searches for the currently
+log in user. It makes searches like the above usable when shared.
+
+This only works for properties that are a Link to the user
+class. E.G. creator, actor, assignedto. It does not yet work for
+MultiLink properties (like nosy).
+
+As an example this can be deployed to the classic tracker's issue
+search template (issue.search.html), by replacing::
+
+    <option metal:fill-slot="extra_options" i18n:translate=""
+            tal:attributes="value request/user/id">created by
+            me</option>
+
+with::
+
+    <option metal:fill-slot="extra_options" value="@current_user"
+            tal:attributes="selected python:value == '@current_user'"
+            i18n:translate="">created by me</option>
+
+There are three places where ``value request/user/id`` is used in the
+classic template. Your template may have more.
+`Details can be found in issue1525113
+ <https://issues.roundup-tracker.org/issue1525113>`_.
+
 New PostgreSQL Settings (optional)
 ----------------------------------
 
--- a/roundup/backends/back_anydbm.py	Sun May 05 20:57:11 2024 -0400
+++ b/roundup/backends/back_anydbm.py	Mon May 06 00:49:43 2024 -0400
@@ -1522,6 +1522,11 @@
         if not self.key:
             raise TypeError('No key property set for '
                             'class %s' % self.classname)
+
+        # special notation for looking up the current database user
+        if keyvalue == '@current_user' and self.classname == 'user':
+            keyvalue = self.db.user.get(self.db.getuid(), self.key)
+
         cldb = self.db.getclassdb(self.classname)
         try:
             for nodeid in self.getnodeids(cldb):
@@ -1763,6 +1768,9 @@
                 if isinstance(propclass, hyperdb.Link):
                     if not isinstance(v, list):
                         v = [v]
+                    if propclass.classname == 'user' and '@current_user' in v:
+                        cu = self.db.getuid()
+                        v = [x if x != "@current_user" else cu for x in v]
                     l.append((LINK, k, v))
                 elif isinstance(propclass, hyperdb.Multilink):
                     # If it's a reverse multilink, we've already
--- a/roundup/backends/rdbms_common.py	Sun May 05 20:57:11 2024 -0400
+++ b/roundup/backends/rdbms_common.py	Mon May 06 00:49:43 2024 -0400
@@ -2313,6 +2313,10 @@
             raise TypeError('No key property set for class %s' %
                             self.classname)
 
+        # special notation for looking up the current database user
+        if keyvalue == '@current_user' and self.classname == 'user':
+            keyvalue = self.db.user.get(self.db.getuid(), self.key)
+
         # use the arg to handle any odd database type conversion (hello,
         # sqlite)
         sql = "select id from _%s where _%s=%s and __retired__=%s" % (
@@ -2566,6 +2570,10 @@
         """
         pln = proptree.parent.uniqname
         prp = proptree.name
+
+        if proptree.classname == 'user' and '@current_user' in v:
+            cu = self.db.getuid()
+            v = [x if x != "@current_user" else cu for x in v]
         try:
             opcodes = [int(x) for x in v]
             if min(opcodes) >= -1:
@@ -2843,6 +2851,9 @@
                                 where.append(w)
                                 args += arg
                         else:
+                            if v == '@current_user' and \
+                               propclass.classname == 'user':
+                                v = self.db.getuid()
                             if v in ('-1', None):
                                 v = None
                                 where.append('_%s._%s is NULL' % (pln, k))
--- a/roundup/cgi/templating.py	Sun May 05 20:57:11 2024 -0400
+++ b/roundup/cgi/templating.py	Mon May 06 00:49:43 2024 -0400
@@ -564,10 +564,16 @@
             else:
                 l.append(item)
                 continue
+
         # if fail_ok, ignore lookup error
         # otherwise entry must be existing object id rather than key value
         if fail_ok:
             l.append(entry)
+        elif entry == '@current_user' and prop.classname == 'user':
+            # as a special case, '@current_user' means the currently
+            # logged-in user
+            l.append(entry)
+
     return l
 
 
--- a/share/roundup/templates/classic/html/issue.search.html	Sun May 05 20:57:11 2024 -0400
+++ b/share/roundup/templates/classic/html/issue.search.html	Mon May 06 00:49:43 2024 -0400
@@ -86,8 +86,9 @@
     tal:condition="db/user/is_view_ok">
   <th i18n:translate="">Creator:</th>
   <td metal:use-macro="search_select">
-    <option metal:fill-slot="extra_options" i18n:translate=""
-            tal:attributes="value request/user/id">created by me</option>
+    <option metal:fill-slot="extra_options" value="@current_user"
+	    tal:attributes="selected python:value == '@current_user'"
+            i18n:translate="">created by me</option>
   </td>
   <td metal:use-macro="column_input"></td>
   <td metal:use-macro="sort_input"></td>
@@ -108,8 +109,9 @@
     tal:condition="db/user/is_view_ok">
   <th i18n:translate="">Actor:</th>
   <td metal:use-macro="search_select">
-    <option metal:fill-slot="extra_options" i18n:translate=""
-            tal:attributes="value request/user/id">done by me</option>
+    <option metal:fill-slot="extra_options" value="@current_user"
+	    tal:attributes="selected python:value == '@current_user'"
+            i18n:translate="">done by me</option>
   </td>
   <td metal:use-macro="column_input"></td>
   <td metal:use-macro="sort_input"></td>
@@ -153,7 +155,8 @@
   <th i18n:translate="">Assigned to:</th>
   <td metal:use-macro="search_select">
     <tal:block metal:fill-slot="extra_options">
-      <option tal:attributes="value request/user/id"
+      <option value="@current_user"
+       tal:attributes="selected python:value == '@current_user'"
        i18n:translate="">assigned to me</option>
       <option value="-1" tal:attributes="selected python:value == '-1'"
        i18n:translate="">unassigned</option>
--- a/share/roundup/templates/devel/html/bug.search.html	Sun May 05 20:57:11 2024 -0400
+++ b/share/roundup/templates/devel/html/bug.search.html	Mon May 06 00:49:43 2024 -0400
@@ -73,7 +73,9 @@
   <th i18n:translate="">Creator:</th>
   <td metal:use-macro="search_input">
     <option metal:fill-slot="extra_options" i18n:translate=""
-            tal:attributes="value request/user/id">created by me</option>
+            value="@current_user"
+	    tal:attributes="selected python:value == '@current_user'">
+      created by me</option>
   </td>
   <td metal:use-macro="column_input"></td>
   <td metal:use-macro="sort_input"></td>
@@ -94,7 +96,9 @@
   <th i18n:translate="">Last actor:</th>
   <td metal:use-macro="search_input">
     <option metal:fill-slot="extra_options" i18n:translate=""
-            tal:attributes="value request/user/id">done by me</option>
+            value="@current_user"
+	    tal:attributes="selected python:value == '@current_user'">
+            done by me</option>
   </td>
   <td metal:use-macro="column_input"></td>
   <td metal:use-macro="sort_input"></td>
@@ -177,8 +181,9 @@
   <th i18n:translate="">Assigned to:</th>
   <td metal:use-macro="search_input">
     <tal:block metal:fill-slot="extra_options">
-      <option tal:attributes="value request/user/id"
-       i18n:translate="">assigned to me</option>
+      <option value="@current_user"
+	      tal:attributes="selected python:value == '@current_user'"
+	      i18n:translate="">assigned to me</option>
       <option value="-1" tal:attributes="selected python:value == '-1'"
        i18n:translate="">unassigned</option>
     </tal:block>
--- a/share/roundup/templates/devel/html/task.search.html	Sun May 05 20:57:11 2024 -0400
+++ b/share/roundup/templates/devel/html/task.search.html	Mon May 06 00:49:43 2024 -0400
@@ -73,7 +73,9 @@
   <th i18n:translate="">Creator:</th>
   <td metal:use-macro="search_input">
     <option metal:fill-slot="extra_options" i18n:translate=""
-            tal:attributes="value request/user/id">created by me</option>
+            value="@current_user"
+	    tal:attributes="selected python:value == '@current_user'">
+      created by me</option>
   </td>
   <td metal:use-macro="column_input"></td>
   <td metal:use-macro="sort_input"></td>
@@ -94,7 +96,9 @@
   <th i18n:translate="">Last actor:</th>
   <td metal:use-macro="search_input">
     <option metal:fill-slot="extra_options" i18n:translate=""
-            tal:attributes="value request/user/id">done by me</option>
+            value="@current_user"
+	    tal:attributes="selected python:value == '@current_user'">
+      done by me</option>
   </td>
   <td metal:use-macro="column_input"></td>
   <td metal:use-macro="sort_input"></td>
@@ -177,8 +181,9 @@
   <th i18n:translate="">Assigned to:</th>
   <td metal:use-macro="search_input">
     <tal:block metal:fill-slot="extra_options">
-      <option tal:attributes="value request/user/id"
-       i18n:translate="">assigned to me</option>
+      <option value="@current_user"
+	      tal:attributes="selected python:value == '@current_user'"
+	      i18n:translate="">assigned to me</option>
       <option value="-1" tal:attributes="selected python:value == '-1'"
        i18n:translate="">unassigned</option>
     </tal:block>
--- a/share/roundup/templates/responsive/html/bug.search.html	Sun May 05 20:57:11 2024 -0400
+++ b/share/roundup/templates/responsive/html/bug.search.html	Mon May 06 00:49:43 2024 -0400
@@ -73,7 +73,9 @@
   <th i18n:translate="">Creator:</th>
   <td metal:use-macro="search_input">
     <option metal:fill-slot="extra_options" i18n:translate=""
-            tal:attributes="value request/user/id">created by me</option>
+            value="@current_user"
+	    tal:attributes="selected python:value == '@current_user'">
+      created by me</option>
   </td>
   <td metal:use-macro="column_input"></td>
   <td metal:use-macro="sort_input"></td>
@@ -94,7 +96,9 @@
   <th i18n:translate="">Last actor:</th>
   <td metal:use-macro="search_input">
     <option metal:fill-slot="extra_options" i18n:translate=""
-            tal:attributes="value request/user/id">done by me</option>
+            value="@current_user"
+	    tal:attributes="selected python:value == '@current_user'">
+            done by me</option>
   </td>
   <td metal:use-macro="column_input"></td>
   <td metal:use-macro="sort_input"></td>
@@ -177,8 +181,9 @@
   <th i18n:translate="">Assigned to:</th>
   <td metal:use-macro="search_input">
     <tal:block metal:fill-slot="extra_options">
-      <option tal:attributes="value request/user/id"
-       i18n:translate="">assigned to me</option>
+      <option value="@current_user"
+	      tal:attributes="selected python:value == '@current_user'"
+	      i18n:translate="">assigned to me</option>
       <option value="-1" tal:attributes="selected python:value == '-1'"
        i18n:translate="">unassigned</option>
     </tal:block>
--- a/share/roundup/templates/responsive/html/task.search.html	Sun May 05 20:57:11 2024 -0400
+++ b/share/roundup/templates/responsive/html/task.search.html	Mon May 06 00:49:43 2024 -0400
@@ -73,7 +73,9 @@
   <th i18n:translate="">Creator:</th>
   <td metal:use-macro="search_input">
     <option metal:fill-slot="extra_options" i18n:translate=""
-            tal:attributes="value request/user/id">created by me</option>
+            value="@current_user"
+	    tal:attributes="selected python:value == '@current_user'">
+      created by me</option>
   </td>
   <td metal:use-macro="column_input"></td>
   <td metal:use-macro="sort_input"></td>
@@ -94,7 +96,9 @@
   <th i18n:translate="">Last actor:</th>
   <td metal:use-macro="search_input">
     <option metal:fill-slot="extra_options" i18n:translate=""
-            tal:attributes="value request/user/id">done by me</option>
+            value="@current_user"
+	    tal:attributes="selected python:value == '@current_user'">
+      done by me</option>
   </td>
   <td metal:use-macro="column_input"></td>
   <td metal:use-macro="sort_input"></td>
@@ -151,8 +155,9 @@
   <th i18n:translate="">Assigned to:</th>
   <td metal:use-macro="search_input">
     <tal:block metal:fill-slot="extra_options">
-      <option tal:attributes="value request/user/id"
-       i18n:translate="">assigned to me</option>
+      <option value="@current_user"
+	      tal:attributes="selected python:value == '@current_user'"
+	      i18n:translate="">assigned to me</option>
       <option value="-1" tal:attributes="selected python:value == '-1'"
        i18n:translate="">unassigned</option>
     </tal:block>
--- a/test/db_test_base.py	Sun May 05 20:57:11 2024 -0400
+++ b/test/db_test_base.py	Mon May 06 00:49:43 2024 -0400
@@ -202,6 +202,7 @@
             yield (filt_iter_list)
         return self.assertEqual, filter_test_iterator
 
+
     def filteringSetupTransitiveSearch(self, classname='issue'):
         u_m = {}
         k = 30
@@ -1513,6 +1514,50 @@
         # we should have the create and last set entries now
         self.assertEqual(jlen-1, len(self.db.getjournal('issue', id)))
 
+    def testCurrentUserLookup(self):
+        # admin is the default
+        f = self.db.user.lookup('@current_user')
+        self.assertEqual(f, "1")
+
+
+        self.db.journaltag = "fred"
+        f = self.db.user.lookup('@current_user')
+        self.assertEqual(f, "2")
+
+    def testCurrentUserIssueFilterLink(self):
+        # admin is the default user
+
+        for user in ['admin', 'fred']:
+            self.db.journaltag = user
+            for commit in (0,1):
+                nid = self.db.issue.create(
+                    title="spam %s %s" % (user, commit),
+                    status='1',
+                    nosy=['2'] if commit else ['1'])
+            self.db.commit()
+
+        self.db.journaltag = 'admin'
+        self.db.issue.set('3', status='2')
+
+        f = self.db.issue.filter(None, {"creator": '@current_user'})
+        self.assertEqual(f, ["1", "2"])
+
+        f = self.db.issue.filter(None, {"actor": '@current_user'})
+        self.assertEqual(f, ["1", "2", "3"])
+
+
+        self.db.journaltag = 'fred'
+        f = self.db.issue.filter(None, {"creator": '@current_user'})
+        self.assertEqual(f, ["3", "4"])
+
+        # check not @current_user
+        f = self.db.issue.filter(None, {"creator": ['@current_user', '-2']})
+        self.assertEqual(f, ["1", "2"])
+
+        # check different prop
+        f = self.db.issue.filter(None, {"actor": '@current_user'})
+        self.assertEqual(f, ["4"])
+
     def testIndexerSearching(self):
         f1 = self.db.file.create(content='hello', type="text/plain")
         # content='world' has the wrong content-type and won't be indexed
--- a/test/test_templating.py	Sun May 05 20:57:11 2024 -0400
+++ b/test/test_templating.py	Mon May 06 00:49:43 2024 -0400
@@ -145,6 +145,8 @@
                 return '1'
             if key == 'fail':
                 raise KeyError('fail')
+            if key == '@current_user':
+                raise KeyError('@current_user')
             return key
         db._db.classes = {'issue': MockNull(lookup=lookup)}
         prop = MockNull(classname='issue')
@@ -153,6 +155,8 @@
         self.assertEqual(lookupIds(db._db, prop, ['ok', 'fail'], 1),
             ['1', 'fail'])
         self.assertEqual(lookupIds(db._db, prop, ['ok', 'fail']), ['1'])
+        self.assertEqual(lookupIds(db._db, prop, ['ok', '@current_user']),
+                        ['1'])
 
     def test_lookupKeys(self):
         db = HTMLDatabase(self.client)

Roundup Issue Tracker: http://roundup-tracker.org/