diff doc/rest.txt @ 7801:af898d1d66dc

doc: run sphinx-lint over docs. Pointed out mutiple use of `x` where it should be ``x``. Also trailing whitespace and lines that are too long. Replaced all tabs by spaces. Also fixed spelling error while I was there. Fixed broken internal link.
author John Rouillard <rouilj@ieee.org>
date Wed, 13 Mar 2024 00:51:09 -0400
parents 835b248bf9fd
children be6cb2e0d471
line wrap: on
line diff
--- a/doc/rest.txt	Tue Mar 12 11:52:17 2024 -0400
+++ b/doc/rest.txt	Wed Mar 13 00:51:09 2024 -0400
@@ -1,7 +1,7 @@
 .. meta::
     :description:
         Documentation on the RESTful interface to the Roundup Issue
-	Tracker. Enable REST access, endpoints, methods,
+        Tracker. Enable REST access, endpoints, methods,
         authentication, discovery.
 
 .. index:: pair: api; Representational state transfer
@@ -259,7 +259,7 @@
 require that REST be enabled. These requests do not make any changes
 or get any information from the database. As a result they are
 available to the anonymous user and any authenticated user. The user
-does not need to have `Rest Access` permissions. Also these requests
+does not need to have ``Rest Access`` permissions. Also these requests
 bypass CSRF checks except for the Origin header check which is always
 run for preflight requests.
 
@@ -271,9 +271,9 @@
 The following CORS preflight headers are usually added automatically by
 the browser and must all be present:
 
-* `Access-Control-Request-Headers`
-* `Access-Control-Request-Method`
-* `Origin`
+* ``Access-Control-Request-Headers``
+* ``Access-Control-Request-Method``
+* ``Origin``
 
 The headers of the 204 response depend on the
 ``allowed_api_origins`` setting. If a ``*`` is included as the
@@ -283,19 +283,19 @@
 
 All 204 responses will include the headers:
 
-* `Access-Control-Allow-Origin`
-* `Access-Control-Allow-Headers`
-* `Access-Control-Allow-Methods`
-* `Access-Control-Max-Age: 86400`
+* ``Access-Control-Allow-Origin``
+* ``Access-Control-Allow-Headers``
+* ``Access-Control-Allow-Methods``
+* ``Access-Control-Max-Age: 86400``
 
 If the client's ORIGIN header matches an entry besides ``*`` in the
 ``allowed_api_origins`` it will also include:
 
-* `Access-Control-Allow-Credentials: true`
+* ``Access-Control-Allow-Credentials: true``
 
 permitting the client to log in and perform authenticated operations.
-  
-If the endpoint accepts the PATCH verb the header `Accept-Patch` with
+
+If the endpoint accepts the PATCH verb the header ``Accept-Patch`` with
 valid mime types (usually `application/x-www-form-urlencoded,
 multipart/form-data`) will be included.
 
@@ -327,7 +327,7 @@
 
    rest.dicttoxml = dtox
 
-.. _interfaces.py: customizing.html#interfaces-py-hooking-into-the-core-of-roundup 
+.. _interfaces.py: customizing.html#interfaces-py-hooking-into-the-core-of-roundup
 
 The rest interface accepts the http accept header and can include
 ``q`` values to specify the preferred mechanism. This is the preferred
@@ -380,7 +380,7 @@
       "collection": [
             { "link": "url to item",
               "id": "internal identifier for item" },
-	    { "link": "url to second item",
+            { "link": "url to second item",
               "id": "id item 2" },
       ... ]
       "@links": {
@@ -395,7 +395,7 @@
   }
 
 available meta data is described in the documentation for the
-collections endpoint. 
+collections endpoint.
 
 The ``link`` fields implement `HATEOS`_ by supplying a url for the
 resource represented by that object. The "link" parameter with the
@@ -429,8 +429,8 @@
       "link": "link to retrieve item",
       "attributes": {
           "title": "title of issue",
-          "nosy": [ 
-	            { "link": "url for user4",
+          "nosy": [
+                    { "link": "url for user4",
                       "id": "4" }
           ],
 
@@ -482,7 +482,7 @@
 ``/data/issue/42/title``.
 
 
-All the links mentioned in the following support the http method ``GET``. 
+All the links mentioned in the following support the http method ``GET``.
 Results of a ``GET`` request will always return the results as a
 dictionary with the entry ``data`` referring to the returned data.
 
@@ -498,20 +498,20 @@
 https://.../rest/data/issue returns::
 
     {
-	"data": {
-	    "collection": [
-		{
-		    "id": "1",
-		    "link": "https://.../rest/data/issue/1"
-		},
-		{
-		    "id": "100",
-		    "link": "https://.../rest/data/issue/100"
-		}
-	...
-	    ],
-	    "@total_size": 171
-	}
+        "data": {
+            "collection": [
+                {
+                    "id": "1",
+                    "link": "https://.../rest/data/issue/1"
+                },
+                {
+                    "id": "100",
+                    "link": "https://.../rest/data/issue/100"
+                }
+        ...
+            ],
+            "@total_size": 171
+        }
     }
 
 Collection endpoints support a number of features as seen in the next
@@ -572,14 +572,14 @@
 ``title=Something`` (or in long form title~=Something) will find all
 issues with "Something" or "someThing", etc. in the title.
 
-Changing the search to ``title:=Something`` (note the `:`) performs an
+Changing the search to ``title:=Something`` (note the ``:``) performs an
 exact case-sensitive string match for exactly one word ``Something``
 with a capital ``S``. Another example is:
 ``title:=test+that+nosy+actually+works.`` where the + signs are spaces
 in the string. Replacing ``+`` with the `URL encoding`_ for space
 ``%20`` will also work. Note that you must match the spaces when
-performing exact matches. So `title:=test++that+nosy+actually+works.``
-matches the word ``test`` with two spaces bewteen ``test`` and
+performing exact matches. So ``title:=test++that+nosy+actually+works.``
+matches the word ``test`` with two spaces between ``test`` and
 ``that`` in the title.
 
 To make this clear, searching
@@ -641,7 +641,7 @@
 
 Collection endpoints support pagination. This is controlled by query
 parameters ``@page_size`` and ``@page_index`` (Note the use of the
-leading `@` to make the parameters distinguishable from field names.)
+leading ``@`` to make the parameters distinguishable from field names.)
 
 .. list-table:: Query Parameters Examples
   :header-rows: 1
@@ -661,25 +661,25 @@
 links along side the collection data. This looks like::
 
   { "data":
-    { 
-       "collection": { ... }, 
+    {
+       "collection": { ... },
        "@total_size": 222,
        "@links": {
-	   "self": [
-	       {
-		   "uri":
-	   "https://.../rest/data/issue?@page_index=1&@fields=status&@page_size=5",
-		   "rel": "self"
-	       }
-	   ],
-	   "next": [
-	       {
-		   "uri":
-	   "https://.../rest/data/issue?@page_index=2&@fields=status&@page_size=5",
-		   "rel": "next"
-	       }
-	   ]
-	 }
+           "self": [
+               {
+                   "uri":
+           "https://.../rest/data/issue?@page_index=1&@fields=status&@page_size=5",
+                   "rel": "self"
+               }
+           ],
+           "next": [
+               {
+                   "uri":
+           "https://.../rest/data/issue?@page_index=2&@fields=status&@page_size=5",
+                   "rel": "next"
+               }
+           ]
+         }
       }
   }
 
@@ -747,17 +747,17 @@
 
   {
       "data": {
-	  "collection": [
-	      {
-		  "link": "https://.../rest/data/issue/1",
-		  "title": "Welcome to the tracker START HERE",
-		  "id": "1",
-		  "status": {
-		      "link": "https://.../rest/data/status/1",
-		      "id": "1",
-		      "name": "new"
-		  }
-	      },
+          "collection": [
+              {
+                  "link": "https://.../rest/data/issue/1",
+                  "title": "Welcome to the tracker START HERE",
+                  "id": "1",
+                  "status": {
+                      "link": "https://.../rest/data/status/1",
+                      "id": "1",
+                      "name": "new"
+                  }
+              },
     ...
   }
 
@@ -767,15 +767,15 @@
 
   {
       "data": {
-	  "collection": [
-	      {
-		  "link": "https://.../rest/data/issue/1",
-		  "id": "1",
-		  "status": {
-		      "link": "https://.../rest/data/status/1",
-		      "id": "1"
-		  }
-	      },
+          "collection": [
+              {
+                  "link": "https://.../rest/data/issue/1",
+                  "id": "1",
+                  "status": {
+                      "link": "https://.../rest/data/status/1",
+                      "id": "1"
+                  }
+              },
      ...
   }
 
@@ -801,7 +801,7 @@
         "id": "11",
         "type": "msg",
         "link": "https://.../demo/rest/data/msg/11",
-	"attributes": {
+        "attributes": {
             "author": {
                 "id": "5",
                 "link": "https://.../demo/rest/data/user/5"
@@ -858,7 +858,7 @@
        veritatis et used voluptas I elit, a The...",
    "date": "2017-10-30.00:53:15",
    ...
-   
+
 Lines are wrapped for display, content value is one really long
 line. If the data is not utf-8 compatible, you will get a link.
 
@@ -897,7 +897,6 @@
 
   "content": "file11 is not text, retrieve using binary_content
               property. mdsum: bd990c0f8833dd991daf610b81b62316",
-	       
 
 You can use the `binary_content property`_ described below to
 retrieve an encoded copy of the data.
@@ -1097,42 +1096,42 @@
 
   {
       "data": {
-	  "type": "issue",
-	  "@etag": "\"f15e6942f00a41960de45f9413684591\"",
-	  "link": "https://.../rest/data/issue/23",
-	  "attributes": {
-	      "keyword": [],
-	      "messages": [
-		  {
-		      "link": "https://.../rest/data/msg/375",
-		      "id": "375"
-		  },
-		  {
-		      "link": "https://.../rest/data/msg/376",
-		      "id": "376"
-		  },
-		  ...
-	      ],
-	      "files": [],
-	      "status": {
-		  "link": "https://.../rest/data/status/2",
-		  "id": "2"
-	      },
-	      "title": "This is a title title",
-	      "superseder": [],
-	      "nosy": [
-		  {
-		      "link": "https://.../rest/data/user/4",
-		      "id": "4"
-		  },
-		  {
-		      "link": "https://.../rest/data/user/5",
-		      "id": "5"
-		  }
-	      ],
-	      "assignedto": null,
-	  },
-	  "id": "23"
+          "type": "issue",
+          "@etag": "\"f15e6942f00a41960de45f9413684591\"",
+          "link": "https://.../rest/data/issue/23",
+          "attributes": {
+              "keyword": [],
+              "messages": [
+                  {
+                      "link": "https://.../rest/data/msg/375",
+                      "id": "375"
+                  },
+                  {
+                      "link": "https://.../rest/data/msg/376",
+                      "id": "376"
+                  },
+                  ...
+              ],
+              "files": [],
+              "status": {
+                  "link": "https://.../rest/data/status/2",
+                  "id": "2"
+              },
+              "title": "This is a title title",
+              "superseder": [],
+              "nosy": [
+                  {
+                      "link": "https://.../rest/data/user/4",
+                      "id": "4"
+                  },
+                  {
+                      "link": "https://.../rest/data/user/5",
+                      "id": "5"
+                  }
+              ],
+              "assignedto": null,
+          },
+          "id": "23"
       }
   }
 
@@ -1198,7 +1197,7 @@
           "link": "https://.../demo/rest/data/file/12"
        }
   }
- 
+
 
 Other Supported Methods for Items
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1220,15 +1219,15 @@
 
   {
       "data": {
-	  "attribute": {
-	      "nosy": [
-		  "1",
-		  "5"
-	      ]
-	  },
-	  "type": "issue",
-	  "link": "https://example.com/demo/rest/data/issue/23",
-	  "id": "23"
+          "attribute": {
+              "nosy": [
+                  "1",
+                  "5"
+              ]
+          },
+          "type": "issue",
+          "link": "https://example.com/demo/rest/data/issue/23",
+          "id": "23"
       }
   }
 
@@ -1240,13 +1239,13 @@
 
   {
       "data": {
-	  "attribute": {
-	      "title": "This is now my title"
-	  },
-	  "type": "issue",
-	  "link":
+          "attribute": {
+              "title": "This is now my title"
+          },
+          "type": "issue",
+          "link":
       "https://.../demo/rest/data/issue/23",
-	  "id": "23"
+          "id": "23"
       }
   }
 
@@ -1266,19 +1265,19 @@
 which returns both title and nosy attributes::
 
     {
-	"data": {
-	    "attribute": {
-		"title": "This is now my new title",
-		"nosy": [
-		    "4",
-		    "5"
-		]
-	    },
-	    "type": "issue",
-	    "link":
-	    "https://.../demo/rest/data/issue/23",
-	    "id": "23"
-	}
+        "data": {
+            "attribute": {
+                "title": "This is now my new title",
+                "nosy": [
+                    "4",
+                    "5"
+                ]
+            },
+            "type": "issue",
+            "link":
+            "https://.../demo/rest/data/issue/23",
+            "id": "23"
+        }
     }
 
 Note that mixing url query parameters with payload submission doesn't
@@ -1332,15 +1331,15 @@
 
   {
       "data": {
-	  "attribute": {
-	      "nosy": [
-		  "3",
-		  "4"
-	      ]
-	  },
-	  "type": "issue",
-	  "link": "https://.../rest/data/issue/23",
-	  "id": "23"
+          "attribute": {
+              "nosy": [
+                  "3",
+                  "4"
+              ]
+          },
+          "type": "issue",
+          "link": "https://.../rest/data/issue/23",
+          "id": "23"
       }
   }
 
@@ -1366,11 +1365,11 @@
 
   {
       "data": {
-	  "link": "https://.../rest/data/issue/22/title",
-	  "data": "I need Broken PC",
-	  "type": "<class 'str'>",
-	  "id": "22",
-	  "@etag": "\"370510512b2d8fc3f98aac3d762cc7b1\""
+          "link": "https://.../rest/data/issue/22/title",
+          "data": "I need Broken PC",
+          "type": "<class 'str'>",
+          "id": "22",
+          "@etag": "\"370510512b2d8fc3f98aac3d762cc7b1\""
       }
   }
 
@@ -1394,7 +1393,7 @@
               reprehenderit\neu to quisquam velit, passage, was or...",
             "@etag": "\"584f82231079e349031bbb853747df1c\""
     }
-  }		
+  }
 
 (the content property is wrapped for display, it is one long line.)
 
@@ -1497,7 +1496,7 @@
         >>> import requests
         >>> u = 'http://user:password@tracker.example.com/demo/rest/data/'
         >>> s = requests.session()
-	>>> session.auth = ('admin', 'admin')
+        >>> session.auth = ('admin', 'admin')
         >>> r = s.get(u + 'issue/42/title')
         >>> if r.status_code != 200:
         ...     print("Failed: %s: %s" % (r.status_code, r.reason))
@@ -1519,7 +1518,7 @@
         >>> print("ETag: %s" % etag)
         >>> etag = r.json()['data']['@etag']
         >>> print("@etag: %s" % etag)
-        >>> h = {'If-Match': etag, 
+        >>> h = {'If-Match': etag,
         ...   'X-Requested-With': 'rest',
         ...   'Referer': 'http://tracker.example.com/demo/'}
         >>> d = {'@op:'action', '@action_name':'retire'}
@@ -1541,32 +1540,32 @@
 
        curl -s -u admin:admin \
         -H "Referer: https://tracker.example.com/demo/" \
-	-H "X-requested-with: rest"  \
-	-H "Content-Type: application/json" \
-	https://tracker.example.com/demo/rest/data/status/1
+        -H "X-requested-with: rest"  \
+        -H "Content-Type: application/json" \
+        https://tracker.example.com/demo/rest/data/status/1
 
 to get the etag manually. Then insert the etag in the If-Match header
 for this retire example::
 
      curl -s -u admin:admin \
         -H "Referer: https://tracker.example.com/demo/" \
-	-H "X-requested-with: rest"  \
-	-H "Content-Type: application/json" \
-	-H 'If-Match: "a502faf4d6b8e3897c4ecd66b5597571"' \
-	--data-raw '{ "@op":"action", "@action_name": "retire" }'\
-	-X PATCH \
-	https://tracker.example.com/demo/rest/data/status/1
+        -H "X-requested-with: rest"  \
+        -H "Content-Type: application/json" \
+        -H 'If-Match: "a502faf4d6b8e3897c4ecd66b5597571"' \
+        --data-raw '{ "@op":"action", "@action_name": "retire" }'\
+        -X PATCH \
+        https://tracker.example.com/demo/rest/data/status/1
 
 and restore::
 
      curl -s -u admin:admin \
         -H "Referer: https://tracker.example.com/demo/" \
-	-H "X-requested-with: rest"  \
-	-H "Content-Type: application/json" \
-	-H 'If-Match: "a502faf4d6b8e3897c4ecd66b5597571"' \
-	--data-raw '{ "@op":"action", "@action_name": "restore" }'\
-	-X PATCH \
-	https://tracker.example.com/demo/rest/data/status/1
+        -H "X-requested-with: rest"  \
+        -H "Content-Type: application/json" \
+        -H 'If-Match: "a502faf4d6b8e3897c4ecd66b5597571"' \
+        --data-raw '{ "@op":"action", "@action_name": "restore" }'\
+        -X PATCH \
+        https://tracker.example.com/demo/rest/data/status/1
 
 
 Searches and selection
@@ -1585,18 +1584,18 @@
 selectize.js (and jquery) code similar to::
 
     $('#superseder').selectize({
-	valueField: 'id',
-	labelField: 'title',
-	searchField: 'title', ...
-	load: function(query, callback) {
-		if (!query.length) return callback();
-		$.ajax({
-			url: '.../rest/data/issue?@verbose=2&title='
-			    + encodeURIComponent(query),
-			type: 'GET',
-			error: function() {callback();},
-			success: function(res) {
-			  callback(res.data.collection);}
+        valueField: 'id',
+        labelField: 'title',
+        searchField: 'title', ...
+        load: function(query, callback) {
+                if (!query.length) return callback();
+                $.ajax({
+                        url: '.../rest/data/issue?@verbose=2&title='
+                            + encodeURIComponent(query),
+                        type: 'GET',
+                        error: function() {callback();},
+                        success: function(res) {
+                          callback(res.data.collection);}
 
 Sets up a box that a user can type the word "request" into. Then
 selectize.js will use that word to generate an ajax request with the
@@ -1609,14 +1608,14 @@
     "@total_size": 440,
     "collection": [
       {
-	  "link": ".../rest/data/issue/8",
-	  "id": "8",
-	  "title": "Request for Power plugs"
+          "link": ".../rest/data/issue/8",
+          "id": "8",
+          "title": "Request for Power plugs"
       },
       {
-	  "link": ".../rest/data/issue/27",
-	  "id": "27",
-	  "title": "Request for foo"
+          "link": ".../rest/data/issue/27",
+          "id": "27",
+          "title": "Request for foo"
       },
   ...
 
@@ -1684,11 +1683,11 @@
 
     class RestfulInstance:
 
-	@Routing.route("/summary2")
-	@_data_decorator
-	def summary2(self, input):
-	    result = { "hello": "world" }
-	    return 200, result
+        @Routing.route("/summary2")
+        @_data_decorator
+        def summary2(self, input):
+            result = { "hello": "world" }
+            return 200, result
 
 will make a new endpoint .../rest/summary2 that you can test with::
 
@@ -1704,19 +1703,19 @@
     # handle more endpoints
         @Routing.route("/data/<:class_name>/@schema", 'GET')
         def get_element_schema(self, class_name, input):
-	    result = { "schema": {} }
-	    uid = self.db.getuid ()
-	    if not self.db.security.hasPermission('View', uid, class_name) :
-		raise Unauthorised('Permission to view %s denied' % class_name)
+            result = { "schema": {} }
+            uid = self.db.getuid ()
+            if not self.db.security.hasPermission('View', uid, class_name) :
+                raise Unauthorised('Permission to view %s denied' % class_name)
 
-	    class_obj = self.db.getclass(class_name)
-	    props = class_obj.getprops(protected=False)
-	    schema = result['schema']
+            class_obj = self.db.getclass(class_name)
+            props = class_obj.getprops(protected=False)
+            schema = result['schema']
 
-	    for prop in props:
-		schema[prop] = { "type": repr(class_obj.properties[prop]) }
+            for prop in props:
+                schema[prop] = { "type": repr(class_obj.properties[prop]) }
 
-	    return result
+            return result
 
 ..
   the # comment in the example is needed to preserve indention under Class.
@@ -1724,21 +1723,21 @@
 returns some data about the class::
 
     $ curl -X GET .../rest/data/issue/@schema
-    {   
-	"schema": {
-	    "keyword": {
-		"type": "<roundup.hyperdb.Multilink to \"keyword\">"
-	    },
-	    "title": {
-		"type": "<roundup.hyperdb.String>"
-	    },
-	    "files": {
-		"type": "<roundup.hyperdb.Multilink to \"file\">"
-	    },
-	    "status": {
-		"type": "<roundup.hyperdb.Link to \"status\">"
-	    }, ...
-	}
+    {
+        "schema": {
+            "keyword": {
+                "type": "<roundup.hyperdb.Multilink to \"keyword\">"
+            },
+            "title": {
+                "type": "<roundup.hyperdb.String>"
+            },
+            "files": {
+                "type": "<roundup.hyperdb.Multilink to \"file\">"
+            },
+            "status": {
+                "type": "<roundup.hyperdb.Link to \"status\">"
+            }, ...
+        }
     }
 
 
@@ -1799,7 +1798,8 @@
 permissions scheme. For example access to a user's roles should be
 limited to the user (read only) and an admin.  If you have customised
 your schema to implement `Restricting the list of
-users that are assignable to a task <customizing.html#restricting-the-list-of-users-that-are-assignable-to-a-task>`__
+users that are assignable to a task
+<customizing.html#restricting-the-list-of-users-that-are-assignable-to-a-task>`__
 so that only users with a
 Developer role are allowed to be assigned to an issue, a rest end
 point must be added to provide a view that exposes users with this
@@ -1808,7 +1808,7 @@
 Using the normal ``/data/user?roles=Developer`` will return all the
 users in the system unless you are an admin user because most users
 can't see the roles. Building on the `Adding new rest endpoints`_
-section this code adds a new endpoint `/data/@permission/Developer`
+section this code adds a new endpoint ``/data/@permission/Developer``
 that returns a list of users with the developer role::
 
     from roundup.rest import Routing, RestfulInstance
@@ -1816,83 +1816,83 @@
 
     class RestfulInstance(object):
 
-	@Routing.route("/data/@permission/Developer")
-	def get_role_Developer(self, input):
-	    '''An endpoint to return a list of users with Developer
-	       role who can be assigned to an issue.
+        @Routing.route("/data/@permission/Developer")
+        def get_role_Developer(self, input):
+            '''An endpoint to return a list of users with Developer
+               role who can be assigned to an issue.
 
-	       It ignores attempt to search by any property except
-	       username and realname. It also ignores the whole @fields
-	       specification if it specifies a property the user
-	       can't view. Other @ query params (e.g. @page... and
-	       @verbose) are supported.
+               It ignores attempt to search by any property except
+               username and realname. It also ignores the whole @fields
+               specification if it specifies a property the user
+               can't view. Other @ query params (e.g. @page... and
+               @verbose) are supported.
 
-	       It assumes admin access rights so that the roles property
-	       of the user can be searched. This is needed if the roles
-	       property is not searchable/viewable by normal users. A user
-	       who can search roles can identify users with the admin
-	       role. So it does not respond the same as a rest/data/users
-	       search by a non-admin user.
-	    '''
-	    # get real user id
-	    realuid=self.db.getuid()
+               It assumes admin access rights so that the roles property
+               of the user can be searched. This is needed if the roles
+               property is not searchable/viewable by normal users. A user
+               who can search roles can identify users with the admin
+               role. So it does not respond the same as a rest/data/users
+               search by a non-admin user.
+            '''
+            # get real user id
+            realuid=self.db.getuid()
 
-	    def allowed_field(fs):
-		if fs.name in ['username', 'realname' ]:
-		    # only allow search matches for these fields
-		    return True
-		elif fs.name in [ '@fields' ]:
-		    for prop in fs.value.split(','):
-			# if any property is unviewable to user, remove
-			# @field entry. If they can't see it for the admin
-			# user, don't let them see it for any user.
-			if not self.db.security.hasPermission(
-				'View', realuid, 'user', property=prop,
-				itemid='1'):
-			    return False
-		    return True
-		elif fs.name.startswith("@"):
-		    # allow @page..., @verbose etc. 
-		    return True
+            def allowed_field(fs):
+                if fs.name in ['username', 'realname' ]:
+                    # only allow search matches for these fields
+                    return True
+                elif fs.name in [ '@fields' ]:
+                    for prop in fs.value.split(','):
+                        # if any property is unviewable to user, remove
+                        # @field entry. If they can't see it for the admin
+                        # user, don't let them see it for any user.
+                        if not self.db.security.hasPermission(
+                                'View', realuid, 'user', property=prop,
+                                itemid='1'):
+                            return False
+                    return True
+                elif fs.name.startswith("@"):
+                    # allow @page..., @verbose etc.
+                    return True
 
-		# deny all other url parmeters
-		return False
+                # deny all other url parmeters
+                return False
 
-	    # Cleanup input.list to prevent user from probing roles
-	    # or viewing things the user should not be able to view.
-	    input.list[:] = [ fs for fs in input.list 
-			      if allowed_field(fs) ]
+            # Cleanup input.list to prevent user from probing roles
+            # or viewing things the user should not be able to view.
+            input.list[:] = [ fs for fs in input.list
+                              if allowed_field(fs) ]
 
-	    # Add the role filter required to implement the permission
-	    # search
-	    input.list.append(MiniFieldStorage("roles", "Developer"))
+            # Add the role filter required to implement the permission
+            # search
+            input.list.append(MiniFieldStorage("roles", "Developer"))
 
-	    # change user to acquire permission to search roles
-	    self.db.setCurrentUser('admin') 
+            # change user to acquire permission to search roles
+            self.db.setCurrentUser('admin')
 
-	    # Once we have cleaned up the request, pass it to
-	    # get_collection as though /rest/data/users?... has been called
-	    # to get @verbose and other args supported.
-	    return self.get_collection('user', input)
+            # Once we have cleaned up the request, pass it to
+            # get_collection as though /rest/data/users?... has been called
+            # to get @verbose and other args supported.
+            return self.get_collection('user', input)
 
 Calling this with::
 
    curl 'http://example.com/demo/rest/data/@permission/Developer?@fields=realname&roles=Users&@verbose=2'
-   
+
 produces output similar to::
 
     {
-	"data": {
-	    "collection": [
-		{
-		    "username": "agent",
-		    "link": http://example.com/demo/rest/data/user/4",
+        "data": {
+            "collection": [
+                {
+                    "username": "agent",
+                    "link": http://example.com/demo/rest/data/user/4",
                     "realname": "James Bond",
-		    "id": "4"
-		}
-	    ],
-	    "@total_size": 1
-	}
+                    "id": "4"
+                }
+            ],
+            "@total_size": 1
+        }
     }
 
 assuming user 4 is the only user with the Developer role. Note that
@@ -1940,8 +1940,8 @@
 Create role
 ~~~~~~~~~~~
 
-Adding this snippet of code to the tracker's ``schema.py`` should create a role with the
-proper authorization::
+Adding this snippet of code to the tracker's ``schema.py`` should
+create a role with the proper authorization::
 
    db.security.addRole(name="User:timelog",
          description="allow a user to create and append timelogs")
@@ -2074,8 +2074,8 @@
             secret = self.db.config.WEB_JWT_SECRET
             myjwt = jwt.encode(claim, secret, algorithm='HS256')
 
-	    # if jwt.__version__ >= 2.0.0 jwt.encode() returns string
-	    # not byte. So do not use b2s() with newer versions of pyjwt.
+            # if jwt.__version__ >= 2.0.0 jwt.encode() returns string
+            # not byte. So do not use b2s() with newer versions of pyjwt.
             result = {"jwt": b2s(myjwt),
                      }
 
@@ -2143,7 +2143,7 @@
       https://.../demo/rest/JWT/validate?JWT=eyJ0eXAiOiJK...XxMDb-Q3oCnMpyhxPXMAk
 
 (note no login is required) which returns::
-  
+
   {
     "data": {
        "user": "3",
@@ -2155,7 +2155,7 @@
        "iat": 1569542404,
        "exp": 1569546004
      }
-  }				
+  }
 
 
 There is an issue for `thoughts on JWT credentials`_ that you can view
@@ -2212,13 +2212,13 @@
             class_obj = self.db.getclass('user')
             node = class_obj.getnode(uid)
 
-	    # set value to 0 to use WEB_API_CALLS_PER_INTERVAL
-	    user_calls = node.__getattr__('rate_limit_calls')
-	    # set to 0 to use WEB_API_INTERVAL_IN_SEC
-	    user_interval = node.__getattr__('rate_limit_interval')
-	    
+            # set value to 0 to use WEB_API_CALLS_PER_INTERVAL
+            user_calls = node.__getattr__('rate_limit_calls')
+            # set to 0 to use WEB_API_INTERVAL_IN_SEC
+            user_interval = node.__getattr__('rate_limit_interval')
+
             return RateLimit(user_calls or calls,
-	       	   timedelta(seconds=(user_interval or interval)))
+                   timedelta(seconds=(user_interval or interval)))
         else:
             # disable rate limiting if either parameter is 0
             return None

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