diff --git a/coaster/sqlalchemy/mixins.py b/coaster/sqlalchemy/mixins.py
index d2858c7..cfcbae8 100644
--- a/coaster/sqlalchemy/mixins.py
+++ b/coaster/sqlalchemy/mixins.py
@@ -22,7 +22,9 @@ Mixin classes must always appear *before* ``db.Model`` in your model's base clas
 
 from __future__ import absolute_import
 import six
+import six.moves.collections_abc as abc
 
+from collections import namedtuple
 import uuid as uuid_
 
 from sqlalchemy import (
@@ -71,6 +73,7 @@ __all__ = [
     'IdMixin',
     'TimestampMixin',
     'PermissionMixin',
+    'UrlDict',
     'UrlForMixin',
     'NoIdMixin',
     'BaseMixin',
@@ -276,34 +279,94 @@ class PermissionMixin(object):
         )
 
 
+UrlEndpointData = namedtuple(
+    'UrlEndpointData',
+    ['endpoint', 'paramattrs', 'external', 'roles', 'requires_kwargs'],
+)
+
+
+class UrlDictStub(object):
+    """
+    Dictionary-based access to URLs for a model instance, used by :class:`UrlForMixin`.
+    Proxies to :meth:`UrlForMixin.url_for` for keyword-based lookup. Uses
+    :attr:`UrlForMixin.url_for_endpoints` for enumeration, but with URLs limited to
+    those available under current roles.
+    """
+
+    def __get__(self, obj, cls=None):
+        if obj is None:
+            return self  # pragma: no cover
+        return UrlDict(obj)
+
+
+class UrlDict(abc.Mapping):
+    """
+    Provides dictionary access to an object's URLs.
+    """
+
+    def __init__(self, obj):
+        self.obj = obj
+
+    def __getitem__(self, key):
+        try:
+            return self.obj.url_for(key, _external=True)
+        except BuildError:
+            raise KeyError(key)
+
+    def __len__(self):
+        return len(self.obj.url_for_endpoints[None]) + (
+            len(self.obj.url_for_endpoints.get(current_app._get_current_object(), {}))
+            if current_app
+            else 0
+        )
+
+    def __iter__(self):
+        # 1. Iterate through all actions available to the None app and to current_app
+        # 2. If the action requires specific roles, confirm overlap with current_roles
+        # 3. Confirm the action does not require additional parameters
+        # 4. Yield whatever passes the tests
+        current_roles = self.obj.current_roles
+        for app, app_actions in self.obj.url_for_endpoints.items():
+            if app is None or (
+                current_app and app is current_app._get_current_object()
+            ):
+                for action, epdata in app_actions.items():
+                    if not epdata.requires_kwargs and (
+                        epdata.roles is None or epdata.roles.intersection(current_roles)
+                    ):
+                        yield action
+
+
 class UrlForMixin(object):
     """
     Provides a :meth:`url_for` method used by BaseMixin-derived classes
     """
 
-    #: Mapping of {app: {action: (endpoint, {param: attr}, _external)}},
-    #: where attr is a string or tuple of strings. The same action can point to
-    #: different endpoints in different apps. The app may also be None as fallback.
-    #: Each subclass will get its own dictionary. This particular dictionary is only
-    #: used as an inherited fallback.
+    #: Mapping of {app: {action: UrlEndpointData}}, where attr is a string or tuple of
+    #: strings. The same action can point to different endpoints in different apps. The
+    #: app may also be None as fallback. Each subclass will get its own dictionary.
+    #: This particular dictionary is only used as an inherited fallback.
     url_for_endpoints = {None: {}}
     #: Mapping of {app: {action: (classview, attr)}}
     view_for_endpoints = {}
 
+    #: Dictionary of URLs available on this object
+    urls = UrlDictStub()
+
     def url_for(self, action='view', **kwargs):
         """
         Return public URL to this instance for a given action (default 'view')
         """
         app = current_app._get_current_object() if current_app else None
         if app is not None and action in self.url_for_endpoints.get(app, {}):
-            endpoint, paramattrs, _external = self.url_for_endpoints[app][action]
+            epdata = self.url_for_endpoints[app][action]
         else:
             try:
-                endpoint, paramattrs, _external = self.url_for_endpoints[None][action]
+                epdata = self.url_for_endpoints[None][action]
             except KeyError:
                 raise BuildError(action, kwargs, 'GET')
         params = {}
-        for param, attr in list(paramattrs.items()):
+        for param, attr in list(epdata.paramattrs.items()):
             if isinstance(attr, tuple):
                 # attr is a tuple containing:
                 # 1. ('parent', 'name') --> self.parent.name
@@ -320,12 +383,12 @@ class UrlForMixin(object):
                 params[param] = attr(self)
             else:
                 params[param] = getattr(self, attr)
-        if _external is not None:
-            params['_external'] = _external
+        if epdata.external is not None:
+            params['_external'] = epdata.external
         params.update(kwargs)  # Let kwargs override params
 
         # url_for from flask
-        return url_for(endpoint, **params)
+        return url_for(epdata.endpoint, **params)
 
     @property
     def absolute_url(self):
@@ -340,30 +403,67 @@ class UrlForMixin(object):
     ):
         """
         View decorator that registers the view as a :meth:`url_for` target.
+
+        :param str _action: Action to register a URL under
+        :param str _endpoint: View endpoint name to pass to Flask's ``url_for``
+        :param _app: The app to register this action on (if your repo has multiple apps)
+        :param _external: If `True`, URLs are assumed to be external-facing by default
+        :param dict paramattrs: Mapping of URL parameter to attribute on the object
         """
 
         def decorator(f):
-            if 'url_for_endpoints' not in cls.__dict__:
-                cls.url_for_endpoints = {
-                    None: {}
-                }  # Stick it into the class with the first endpoint
-            cls.url_for_endpoints.setdefault(_app, {})
-
-            for keyword in paramattrs:
-                if (
-                    isinstance(paramattrs[keyword], six.string_types)
-                    and '.' in paramattrs[keyword]
-                ):
-                    paramattrs[keyword] = tuple(paramattrs[keyword].split('.'))
-            cls.url_for_endpoints[_app][_action] = (
-                _endpoint or f.__name__,
-                paramattrs,
-                _external,
+            cls.register_endpoint(
+                action=_action,
+                endpoint=_endpoint or f.__name__,
+                app=_app,
+                external=_external,
+                paramattrs=paramattrs,
             )
             return f
 
         return decorator
 
+    @classmethod
+    def register_endpoint(
+        cls, action, endpoint, app=None, external=None, roles=None, paramattrs=None
+    ):
+        """
+        Helper method for registering an endopint to a :meth:`url_for` action.
+
+        :param view_func: View handler to be registered
+        :param str action: Action to register a URL under
+        :param str endpoint: View endpoint name to pass to Flask's ``url_for``
+        :param app: Flask app (default: `None`)
+        :param external: If `True`, URLs are assumed to be external-facing by default
+        :param roles: Roles to which this URL is available, required by :class:`UrlDict`
+        :param dict paramattrs: Mapping of URL parameter to attribute on the object
+        """
+
+        if 'url_for_endpoints' not in cls.__dict__:
+            cls.url_for_endpoints = {
+                None: {}
+            }  # Stick it into the class with the first endpoint
+        cls.url_for_endpoints.setdefault(app, {})
+
+        for keyword in paramattrs:
+            if (
+                isinstance(paramattrs[keyword], six.string_types)
+                and '.' in paramattrs[keyword]
+            ):
+                paramattrs[keyword] = tuple(paramattrs[keyword].split('.'))
+        requires_kwargs = False
+        for attrs in paramattrs.values():
+            if isinstance(attrs, tuple) and attrs[0].startswith('**'):
+                requires_kwargs = True
+                break
+        cls.url_for_endpoints[app][action] = UrlEndpointData(
+            endpoint=endpoint,
+            paramattrs=paramattrs if paramattrs is not None else {},
+            external=external,
+            roles=roles,
+            requires_kwargs=requires_kwargs,
+        )
+
     @classmethod
     def register_view_for(cls, app, action, classview, attr):
         """
diff --git a/coaster/views/classview.py b/coaster/views/classview.py
index a5de3b5..39af1c9 100644
--- a/coaster/views/classview.py
+++ b/coaster/views/classview.py
@@ -669,7 +669,7 @@ class UrlForView(object):
             params = {
                 v: cls.route_model_map[v] for v in rulevars if v in cls.route_model_map
             }
-            # Hook up is_url_for with the view function's name, endpoint name and
+            # Register endpoint with the view function's name, endpoint name and
             # parameters. Register the view for a specific app, unless we're in a
             # Blueprint, in which case it's not an app.
             # FIXME: The behaviour of a Blueprint + multi-app combo is unknown and needs
@@ -680,9 +680,13 @@ class UrlForView(object):
             else:
                 prefix = ''
                 reg_app = app
-            cls.model.is_url_for(
-                view_func.__name__, prefix + endpoint, _app=reg_app, **params
-            )(view_func)
+            cls.model.register_endpoint(
+                action=view_func.__name__,
+                endpoint=prefix + endpoint,
+                app=reg_app,
+                roles=getattr(view_func, 'requires_roles', None),
+                paramattrs=params,
+            )
             cls.model.register_view_for(
                 app=reg_app,
                 action=view_func.__name__,

Turn url_for from a callable to a dynamic dictionary
It should be possible to get URLs in all these ways:

1. [x] `obj.url_for()` / `obj.url_for('edit')` (current)
2. [x] ~`obj.url_for['view']`~ `obj.urls['view']` (new)
3. [x] ~`dict(obj.url_for)`~ `dict(obj.urls)` to get all URLs
4. [x] ~`obj.current_access()['url_for']['view']`~ `obj.current_access()['urls']['view']` for JSON serialisation. Ideally, this should limit the listed URLs to the ones that the current role has access to, except we don't define roles when declaring view endpoints. That will require retrofitting.
5. [ ] `obj.current_access(profiles=('first', 'second'))` to limit JSON serialisation to the views defined in the 'first' profile. Once again, there is no definition for views available in a given profile.
diff --git a/tests/test_annotations.py b/tests/test_sqlalchemy_annotations.py
similarity index 100%
rename from tests/test_annotations.py
rename to tests/test_sqlalchemy_annotations.py
diff --git a/tests/test_sqlalchemy_coordinates.py b/tests/test_sqlalchemy_coordinates.py
index 50c145a..a3d8bde 100644
--- a/tests/test_sqlalchemy_coordinates.py
+++ b/tests/test_sqlalchemy_coordinates.py
@@ -7,7 +7,7 @@ import unittest
 from coaster.db import db
 from coaster.sqlalchemy import BaseMixin, CoordinatesMixin
 
-from .test_models import app2
+from .test_sqlalchemy_models import app2
 
 
 class CoordinatesData(BaseMixin, CoordinatesMixin, db.Model):
diff --git a/tests/test_markdowncolumn.py b/tests/test_sqlalchemy_markdowncolumn.py
similarity index 98%
rename from tests/test_markdowncolumn.py
rename to tests/test_sqlalchemy_markdowncolumn.py
index cc1f41a..0af5f25 100644
--- a/tests/test_markdowncolumn.py
+++ b/tests/test_sqlalchemy_markdowncolumn.py
@@ -8,7 +8,7 @@ from coaster.db import db
 from coaster.gfm import markdown
 from coaster.sqlalchemy import BaseMixin, MarkdownColumn
 
-from .test_models import app1, app2
+from .test_sqlalchemy_models import app1, app2
 
 
 class MarkdownData(BaseMixin, db.Model):
diff --git a/tests/test_models.py b/tests/test_sqlalchemy_models.py
similarity index 100%
rename from tests/test_models.py
rename to tests/test_sqlalchemy_models.py
diff --git a/tests/test_roles.py b/tests/test_sqlalchemy_roles.py
similarity index 100%
rename from tests/test_roles.py
rename to tests/test_sqlalchemy_roles.py
diff --git a/tests/test_url_for.py b/tests/test_url_for.py
index 0f3b916..ffdc3e2 100644
--- a/tests/test_url_for.py
+++ b/tests/test_url_for.py
@@ -9,7 +9,7 @@ from werkzeug.routing import BuildError
 
 from coaster.db import db
 
-from .test_models import Container, NamedDocument, ScopedNamedDocument
+from .test_sqlalchemy_models import Container, NamedDocument, ScopedNamedDocument
 
 # --- Test setup --------------------------------------------------------------
 
@@ -42,7 +42,7 @@ def doc_upper(doc):
     return u'{} {}'.format('upper', doc)
 
 
-# The unusual parameter `other='@other.name'` requires an explanation.
+# The unusual parameter `other='**other.name'` requires an explanation.
 # The first `other` refers to `<other>` in the URL. The second refers
 # to the parameter given to `NamedDocument.url_for` in the test below.
 @app1.route('/<doc>/with/<other>')
@@ -202,6 +202,29 @@ class TestUrlFor(TestUrlForBase):
         # url_for is given an object and extracts an attribute from it
         assert doc1.url_for('with', other=doc2) == '/document1/with/document2'
 
+    def test_url_dict(self):
+        """URLs to views are available from a .urls dictionary-like object."""
+        doc1 = NamedDocument(name=u'document1', title=u"Document")
+        self.session.add(doc1)
+        self.session.commit()
+
+        assert doc1.urls != {}
+        assert doc1.urls['view'] == 'http://localhost/document1'
+        with self.assertRaises(KeyError):
+            doc1.urls['random']
+
+        # The len() count includes the doc_with view, but it is excluded from actual
+        # enumeration because it requires additional keyword parameters, which cannot
+        # be passed in with dictionary access.
+        assert len(doc1.urls) == 6
+        assert dict(doc1.urls) == {
+            'app_only': 'http://localhost/document1/app_only',
+            'edit': 'http://localhost/document1/edit',
+            'per_app': 'http://localhost/document1/app1',
+            'upper': 'http://localhost/DOCUMENT1/upper',
+            'view': 'http://localhost/document1',
+        }
+
 
 class TestUrlFor2(TestUrlForBase):
     app = app2
@@ -224,3 +247,20 @@ class TestUrlFor2(TestUrlForBase):
         # This action is not available in this app
         with self.assertRaises(BuildError):
             doc1.url_for('app_only')
+
+    def test_url_dict_app_only(self):
+        """The urls dictionary only includes available URLs."""
+        doc1 = NamedDocument(name=u'document1', title=u"Document 1")
+        self.session.add(doc1)
+        self.session.commit()
+
+        assert 'app_only' not in doc1.urls
+        assert 'per_app' in doc1.urls
+        assert doc1.urls['per_app'] == 'http://localhost/document1/app2'
+
+        # Note: attempting to cast `urls` into a dict will break as there are
+        # URLs registered with `is_url_for` with no app specified (meaning they are
+        # supposedly available to all apps) but with no @app.route registration. This
+        # will cause BuildError while attempting to retrieve the value. However, casting
+        # to a list/set of just the keys will work.
+        assert set(doc1.urls) == {'edit', 'upper', 'per_app', 'view'}
diff --git a/tests/test_utils_markdown.py b/tests/test_utils_markdown.py
index 0d2c9b2..11c4984 100644
--- a/tests/test_utils_markdown.py
+++ b/tests/test_utils_markdown.py
@@ -70,7 +70,7 @@ It has a newline break here.</p>
 </ol>
 <p>A naked link: <a href="https://www.example.com/" rel="nofollow">https://www.example.com/</a></p>
 <p>An <a href="https://www.example.com/" rel="nofollow">inline link</a></p>
-<div class="codehilite"><pre><span></span><code><span class="hll"><span class="k">def</span> <span class="nf">foo</span><span class="p">():</span>
+<div class="highlight"><pre><span></span><code><span class="hll"><span class="k">def</span> <span class="nf">foo</span><span class="p">():</span>
 </span>    <span class="k">return</span> <span class="s1">'https://www.example.com/'</span>
 </code></pre></div>
 
@@ -189,11 +189,13 @@ if six.PY2:
         sample_output.replace('<pre><code>', '<pre>')
         .replace('</code></pre>', '</pre>')
         .replace('<pre><span></span><code>', '<pre><span></span>')
+        .replace('<div class="highlight">', '<div class="codehilite">')
     )
     sample_output_html = (
         sample_output_html.replace('<pre><code>', '<pre>')
         .replace('</code></pre>', '</pre>')
         .replace('<pre><span></span><code>', '<pre><span></span>')
+        .replace('<div class="highlight">', '<div class="codehilite">')
     )
 
 
diff --git a/tests/test_views_classview.py b/tests/test_views_classview.py
index a0bc53f..83602a7 100644
--- a/tests/test_views_classview.py
+++ b/tests/test_views_classview.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 
 from __future__ import absolute_import, unicode_literals
+import six.moves.collections_abc as abc
 
 import unittest
 
@@ -27,7 +28,16 @@ from coaster.views import (
     viewdata,
 )
 
+
+class JsonEncoder(json.JSONEncoder):
+    def default(self, o):
+        if isinstance(o, abc.Mapping):
+            return dict(o)
+        return super(JsonEncoder, self).default(o)
+
+
 app = Flask(__name__)
+app.json_encoder = JsonEncoder
 app.testing = True
 app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
@@ -750,3 +760,23 @@ class TestClassView(unittest.TestCase):
         # safety checks, and will need amending then.
         assert ModelDocumentView(obj=doc1) != ScopedDocumentView(obj=doc1)
         assert ScopedDocumentView(obj=doc1) != ModelDocumentView(obj=doc1)
+
+    def test_url_dict_current_roles(self):
+        doc1 = ViewDocument(name='test1', title="Test 1")
+        assert set(doc1.urls) == {
+            'view',  # From ModelDocumentView
+            'edit',  # From ModelDocumentView
+            'by_perm',  # From GatedDocumentView (`urls` can't handle permission gating)
+        }
+        # Adding acess permissions changes the URLs available
+        add_auth_attribute('user', 'this-is-the-owner')  # See ViewDocument.permissions
+        assert set(doc1.urls) == {
+            # From ModelDocumentView
+            'view',
+            'edit',
+            # From GatedDocumentView
+            'by_perm',
+            'by_role_perm',
+            'by_perm_role',
+            'by_role',
+        }
diff --git a/tests/test_views_loadmodels.py b/tests/test_views_loadmodels.py
index 2b05783..2f4902c 100644
--- a/tests/test_views_loadmodels.py
+++ b/tests/test_views_loadmodels.py
@@ -14,7 +14,7 @@ from coaster.db import db
 from coaster.sqlalchemy import BaseMixin, BaseNameMixin, BaseScopedIdMixin
 from coaster.views import load_model, load_models
 
-from .test_models import (
+from .test_sqlalchemy_models import (
     Container,
     IdNamedDocument,
     NamedDocument,

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
3.6
test_requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alembic==1.7.7
attrs==22.2.0
bcrypt==4.0.1
bleach==4.1.0
blinker==1.5
certifi==2021.5.30
charset-normalizer==2.0.12
click==8.0.4
-e git+https://github.com/hasgeek/coaster.git@5b2770b457d9d26093ab0d255265a9889adddc06#egg=coaster
coverage==6.2
coveralls==3.3.1
dataclasses==0.8
docflow==0.3.3
docopt==0.6.2
filelock==3.4.1
Flask==2.0.3
Flask-Assets==2.1.0
Flask-Migrate==4.0.1
Flask-Script==2.0.6
Flask-SQLAlchemy==2.5.1
furl==2.1.4
greenlet==2.0.2
html2text==2020.1.16
html5lib==1.1
idna==3.10
importlib-metadata==4.8.3
importlib-resources==5.4.0
iniconfig==1.1.1
iso8601==1.1.0
isoweek==1.3.3
itsdangerous==2.0.1
Jinja2==3.0.3
joblib==1.1.1
Mako==1.1.6
Markdown==3.3.7
MarkupSafe==2.0.1
nltk==3.6.7
orderedmultidict==1.0.1
packaging==21.3
pluggy==1.0.0
psycopg2==2.7.7
py==1.11.0
PyExecJS==1.5.1
Pygments==2.14.0
pymdown-extensions==9.1
pyparsing==3.1.4
pytest==7.0.1
pytz==2025.2
regex==2023.8.8
requests==2.27.1
requests-file==2.1.0
semantic-version==2.10.0
shortuuid==1.0.13
simplejson==3.20.1
six==1.17.0
SQLAlchemy==1.4.54
SQLAlchemy-Utils==0.41.1
tldextract==3.1.2
tomli==1.2.3
tqdm==4.64.1
typing_extensions==4.1.1
UgliPyJS==0.2.5
Unidecode==1.3.8
urllib3==1.26.20
webassets==2.0
webencodings==0.5.1
Werkzeug==2.0.3
zipp==3.6.0

name: coaster
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alembic==1.7.7
      - attrs==22.2.0
      - bcrypt==4.0.1
      - bleach==4.1.0
      - blinker==1.5
      - charset-normalizer==2.0.12
      - click==8.0.4
      - coverage==6.2
      - coveralls==3.3.1
      - dataclasses==0.8
      - docflow==0.3.3
      - docopt==0.6.2
      - filelock==3.4.1
      - flask==2.0.3
      - flask-assets==2.1.0
      - flask-migrate==4.0.1
      - flask-script==2.0.6
      - flask-sqlalchemy==2.5.1
      - furl==2.1.4
      - greenlet==2.0.2
      - html2text==2020.1.16
      - html5lib==1.1
      - idna==3.10
      - importlib-metadata==4.8.3
      - importlib-resources==5.4.0
      - iniconfig==1.1.1
      - iso8601==1.1.0
      - isoweek==1.3.3
      - itsdangerous==2.0.1
      - jinja2==3.0.3
      - joblib==1.1.1
      - mako==1.1.6
      - markdown==3.3.7
      - markupsafe==2.0.1
      - nltk==3.6.7
      - orderedmultidict==1.0.1
      - packaging==21.3
      - pluggy==1.0.0
      - psycopg2==2.7.7
      - py==1.11.0
      - pyexecjs==1.5.1
      - pygments==2.14.0
      - pymdown-extensions==9.1
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytz==2025.2
      - regex==2023.8.8
      - requests==2.27.1
      - requests-file==2.1.0
      - semantic-version==2.10.0
      - shortuuid==1.0.13
      - simplejson==3.20.1
      - six==1.17.0
      - sqlalchemy==1.4.54
      - sqlalchemy-utils==0.41.1
      - tldextract==3.1.2
      - tomli==1.2.3
      - tqdm==4.64.1
      - typing-extensions==4.1.1
      - uglipyjs==0.2.5
      - unidecode==1.3.8
      - urllib3==1.26.20
      - webassets==2.0
      - webencodings==0.5.1
      - werkzeug==2.0.3
      - zipp==3.6.0
prefix: /opt/conda/envs/coaster

tests/test_url_for.py::TestUrlFor::test_url_dict tests/test_url_for.py::TestUrlFor2::test_url_dict_app_only tests/test_views_classview.py::TestClassView::test_url_dict_current_roles
tests/test_sqlalchemy_annotations.py::TestCoasterAnnotations::test_immutable_foreignkey tests/test_sqlalchemy_annotations.py::TestCoasterAnnotations::test_postload_immutability tests/test_sqlalchemy_coordinates.py::TestCoordinatesColumn::test_column_set_value tests/test_sqlalchemy_coordinates.py::TestCoordinatesColumn::test_columns_created tests/test_sqlalchemy_coordinates.py::TestCoordinatesColumn::test_columns_when_missing tests/test_sqlalchemy_coordinates.py::TestCoordinatesColumn::test_columns_when_null tests/test_sqlalchemy_markdowncolumn.py::TestMarkdownColumn2::test_does_not_render_on_load tests/test_sqlalchemy_markdowncolumn.py::TestMarkdownColumn2::test_markdown_column tests/test_sqlalchemy_markdowncolumn.py::TestMarkdownColumn2::test_raw_value tests/test_sqlalchemy_models.py::TestCoasterModels::test_uuid_buid_suuid tests/test_sqlalchemy_models.py::TestCoasterModels::test_uuid_url_id tests/test_sqlalchemy_models.py::TestCoasterModels::test_uuid_url_id_name_suuid tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_auto_init_default tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_container tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_failsafe_add tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_failsafe_add_existing tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_failsafe_add_fail tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_failsafe_add_silent_fail tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_has_timestamps tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_id_named tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_jsondict tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_named tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_named_auto tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_named_blank_allowed tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_named_blank_disallowed tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_parent_child_primary tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_parent_child_primary_sql_validator tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_query tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_reserved_name tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_scoped_id tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_scoped_id_auto tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_scoped_id_named tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_scoped_id_without_parent tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_scoped_named tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_scoped_named_short_title tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_scoped_named_without_parent tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_timestamp tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_timestamp_aware_is_aware tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_timestamp_naive_is_naive tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_title_for_name tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_unlimited_name_title tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_unnamed tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_url_for_fail tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_urltype tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_urltype_empty tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_urltype_invalid tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_urltype_invalid_scheme_custom tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_urltype_invalid_scheme_default tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_urltype_invalid_without_host tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_urltype_invalid_without_scheme tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_urltype_optional_host tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_urltype_optional_scheme tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_urltype_optional_scheme_host tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_uuid_buid_suuid tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_uuid_default tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_uuid_key tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_uuid_url_id tests/test_sqlalchemy_models.py::TestCoasterModelsPG::test_uuid_url_id_name_suuid tests/test_utils_markdown.py::TestMarkdown::test_sample_markdown tests/test_utils_markdown.py::TestMarkdown::test_sample_markdown_html tests/test_views_loadmodels.py::TestLoadModels2::test_callable_document tests/test_views_loadmodels.py::TestLoadModels2::test_container tests/test_views_loadmodels.py::TestLoadModels2::test_direct_permissions tests/test_views_loadmodels.py::TestLoadModels2::test_dotted_document tests/test_views_loadmodels.py::TestLoadModels2::test_id_named_document tests/test_views_loadmodels.py::TestLoadModels2::test_inherited_permissions tests/test_views_loadmodels.py::TestLoadModels2::test_load_user_to_g tests/test_views_loadmodels.py::TestLoadModels2::test_loadmodel_permissions tests/test_views_loadmodels.py::TestLoadModels2::test_named_document tests/test_views_loadmodels.py::TestLoadModels2::test_redirect_document tests/test_views_loadmodels.py::TestLoadModels2::test_scoped_id_document tests/test_views_loadmodels.py::TestLoadModels2::test_scoped_id_named_document tests/test_views_loadmodels.py::TestLoadModels2::test_scoped_named_document tests/test_views_loadmodels.py::TestLoadModels2::test_single_model_in_loadmodels tests/test_views_loadmodels.py::TestLoadModels2::test_unmutated_inherited_permissions
tests/test_sqlalchemy_annotations.py::TestCoasterAnnotations::test_annotation_in_annotations tests/test_sqlalchemy_annotations.py::TestCoasterAnnotations::test_attr_in_annotations tests/test_sqlalchemy_annotations.py::TestCoasterAnnotations::test_base_attrs_in_annotations tests/test_sqlalchemy_annotations.py::TestCoasterAnnotations::test_has_annotations tests/test_sqlalchemy_annotations.py::TestCoasterAnnotations::test_immutable_relationship tests/test_sqlalchemy_annotations.py::TestCoasterAnnotations::test_init_immutability tests/test_sqlalchemy_annotations.py::TestCoasterAnnotations::test_polymorphic_annotations tests/test_sqlalchemy_annotations.py::TestCoasterAnnotations::test_polymorphic_immutable tests/test_sqlalchemy_annotations.py::TestCoasterAnnotations::test_postinit_immutability tests/test_sqlalchemy_annotations.py::TestCoasterAnnotations::test_synonym_annotation tests/test_sqlalchemy_markdowncolumn.py::TestMarkdownColumn::test_does_not_render_on_load tests/test_sqlalchemy_markdowncolumn.py::TestMarkdownColumn::test_markdown_column tests/test_sqlalchemy_markdowncolumn.py::TestMarkdownColumn::test_raw_value tests/test_sqlalchemy_models.py::TestCoasterModels::test_auto_init_default tests/test_sqlalchemy_models.py::TestCoasterModels::test_container tests/test_sqlalchemy_models.py::TestCoasterModels::test_failsafe_add tests/test_sqlalchemy_models.py::TestCoasterModels::test_failsafe_add_existing tests/test_sqlalchemy_models.py::TestCoasterModels::test_failsafe_add_fail tests/test_sqlalchemy_models.py::TestCoasterModels::test_failsafe_add_silent_fail tests/test_sqlalchemy_models.py::TestCoasterModels::test_has_timestamps tests/test_sqlalchemy_models.py::TestCoasterModels::test_id_named tests/test_sqlalchemy_models.py::TestCoasterModels::test_jsondict tests/test_sqlalchemy_models.py::TestCoasterModels::test_named tests/test_sqlalchemy_models.py::TestCoasterModels::test_named_auto tests/test_sqlalchemy_models.py::TestCoasterModels::test_named_blank_allowed tests/test_sqlalchemy_models.py::TestCoasterModels::test_named_blank_disallowed tests/test_sqlalchemy_models.py::TestCoasterModels::test_parent_child_primary tests/test_sqlalchemy_models.py::TestCoasterModels::test_query tests/test_sqlalchemy_models.py::TestCoasterModels::test_reserved_name tests/test_sqlalchemy_models.py::TestCoasterModels::test_scoped_id tests/test_sqlalchemy_models.py::TestCoasterModels::test_scoped_id_auto tests/test_sqlalchemy_models.py::TestCoasterModels::test_scoped_id_named tests/test_sqlalchemy_models.py::TestCoasterModels::test_scoped_id_without_parent tests/test_sqlalchemy_models.py::TestCoasterModels::test_scoped_named tests/test_sqlalchemy_models.py::TestCoasterModels::test_scoped_named_short_title tests/test_sqlalchemy_models.py::TestCoasterModels::test_scoped_named_without_parent tests/test_sqlalchemy_models.py::TestCoasterModels::test_timestamp tests/test_sqlalchemy_models.py::TestCoasterModels::test_title_for_name tests/test_sqlalchemy_models.py::TestCoasterModels::test_unlimited_name_title tests/test_sqlalchemy_models.py::TestCoasterModels::test_unnamed tests/test_sqlalchemy_models.py::TestCoasterModels::test_url_for_fail tests/test_sqlalchemy_models.py::TestCoasterModels::test_urltype tests/test_sqlalchemy_models.py::TestCoasterModels::test_urltype_empty tests/test_sqlalchemy_models.py::TestCoasterModels::test_urltype_invalid tests/test_sqlalchemy_models.py::TestCoasterModels::test_urltype_invalid_scheme_custom tests/test_sqlalchemy_models.py::TestCoasterModels::test_urltype_invalid_scheme_default tests/test_sqlalchemy_models.py::TestCoasterModels::test_urltype_invalid_without_host tests/test_sqlalchemy_models.py::TestCoasterModels::test_urltype_invalid_without_scheme tests/test_sqlalchemy_models.py::TestCoasterModels::test_urltype_optional_host tests/test_sqlalchemy_models.py::TestCoasterModels::test_urltype_optional_scheme tests/test_sqlalchemy_models.py::TestCoasterModels::test_urltype_optional_scheme_host tests/test_sqlalchemy_models.py::TestCoasterModels::test_uuid_default tests/test_sqlalchemy_models.py::TestCoasterModels::test_uuid_key tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_access_for_all tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_access_for_roles_and_actor_or_anchors tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_access_for_syntax tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_actors_with tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_actors_with_invalid tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_attr_dict_access tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_autorole_dict tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_bad_decorator tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_base_is_clean tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_basemixin_roles tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_cascading_datasets tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_collection_relationship tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_current_access tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_current_roles tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_dictionary_comparison tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_diff_roles tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_diff_roles_single_model_dataset tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_dynamic_association_proxy tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_method_call tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_no_write tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_role_dict tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_role_grant tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_role_grant_synonyms tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_roles_for_actor tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_roles_for_anon tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_roles_for_owner tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_scalar_relationship tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_uuidmixin_roles tests/test_sqlalchemy_roles.py::TestCoasterRoles::test_write_without_read tests/test_sqlalchemy_roles.py::TestLazyRoleSet::test_initial tests/test_sqlalchemy_roles.py::TestLazyRoleSet::test_inspectable_lazyroleset tests/test_sqlalchemy_roles.py::TestLazyRoleSet::test_lazyroleset tests/test_sqlalchemy_roles.py::TestLazyRoleSet::test_set_add_remove_discard tests/test_sqlalchemy_roles.py::TestLazyRoleSet::test_set_operations tests/test_url_for.py::TestUrlFor::test_absolute_url tests/test_url_for.py::TestUrlFor::test_absolute_url_in_access_proxy tests/test_url_for.py::TestUrlFor::test_absolute_url_missing tests/test_url_for.py::TestUrlFor::test_app_only tests/test_url_for.py::TestUrlFor::test_class_has_url_for tests/test_url_for.py::TestUrlFor::test_linked_doc tests/test_url_for.py::TestUrlFor::test_per_app tests/test_url_for.py::TestUrlFor::test_url_for tests/test_url_for.py::TestUrlFor2::test_app_only tests/test_url_for.py::TestUrlFor2::test_per_app tests/test_utils_markdown.py::TestMarkdown::test_empty_markdown tests/test_utils_markdown.py::TestMarkdown::test_html_markdown tests/test_utils_markdown.py::TestMarkdown::test_markdown tests/test_utils_markdown.py::TestMarkdown::test_text_markdown tests/test_views_classview.py::TestClassView::test_added_routes tests/test_views_classview.py::TestClassView::test_callable_view tests/test_views_classview.py::TestClassView::test_cant_route_missing_method tests/test_views_classview.py::TestClassView::test_class_is_available tests/test_views_classview.py::TestClassView::test_classview_equals tests/test_views_classview.py::TestClassView::test_current_handler_is_self tests/test_views_classview.py::TestClassView::test_current_handler_is_wrapper tests/test_views_classview.py::TestClassView::test_current_view tests/test_views_classview.py::TestClassView::test_document_404 tests/test_views_classview.py::TestClassView::test_document_edit tests/test_views_classview.py::TestClassView::test_document_view tests/test_views_classview.py::TestClassView::test_endpoints tests/test_views_classview.py::TestClassView::test_index tests/test_views_classview.py::TestClassView::test_inherited tests/test_views_classview.py::TestClassView::test_is_available tests/test_views_classview.py::TestClassView::test_modelview_instanceloader_requires_permission_edit tests/test_views_classview.py::TestClassView::test_modelview_instanceloader_view tests/test_views_classview.py::TestClassView::test_modelview_url_for tests/test_views_classview.py::TestClassView::test_multi_view tests/test_views_classview.py::TestClassView::test_page tests/test_views_classview.py::TestClassView::test_redirectablemodel_view tests/test_views_classview.py::TestClassView::test_registered_views tests/test_views_classview.py::TestClassView::test_requires_roles_layered tests/test_views_classview.py::TestClassView::test_rerouted tests/test_views_classview.py::TestClassView::test_rerouted_with_new_routes tests/test_views_classview.py::TestClassView::test_scopedmodelview_view tests/test_views_classview.py::TestClassView::test_second_subview_reroute tests/test_views_classview.py::TestClassView::test_unrouted tests/test_views_classview.py::TestClassView::test_view_args_are_received tests/test_views_classview.py::TestClassView::test_view_for tests/test_views_classview.py::TestClassView::test_viewdata tests/test_views_classview.py::TestClassView::test_viewlist tests/test_views_loadmodels.py::TestLoadModels::test_callable_document tests/test_views_loadmodels.py::TestLoadModels::test_container tests/test_views_loadmodels.py::TestLoadModels::test_direct_permissions tests/test_views_loadmodels.py::TestLoadModels::test_dotted_document tests/test_views_loadmodels.py::TestLoadModels::test_id_named_document tests/test_views_loadmodels.py::TestLoadModels::test_inherited_permissions tests/test_views_loadmodels.py::TestLoadModels::test_load_user_to_g tests/test_views_loadmodels.py::TestLoadModels::test_loadmodel_permissions tests/test_views_loadmodels.py::TestLoadModels::test_named_document tests/test_views_loadmodels.py::TestLoadModels::test_redirect_document tests/test_views_loadmodels.py::TestLoadModels::test_scoped_id_document tests/test_views_loadmodels.py::TestLoadModels::test_scoped_id_named_document tests/test_views_loadmodels.py::TestLoadModels::test_scoped_named_document tests/test_views_loadmodels.py::TestLoadModels::test_single_model_in_loadmodels tests/test_views_loadmodels.py::TestLoadModels::test_unmutated_inherited_permissions
coaster/views/classview.py coaster/sqlalchemy/mixins.py
coaster/views/classview.py coaster/sqlalchemy/mixins.py