Skip to content

Commit

Permalink
Merge pull request #167 from hasgeek/classview
Browse files Browse the repository at this point in the history
Class-based views
  • Loading branch information
jace authored Feb 22, 2018
2 parents ab2a911 + a93fccf commit 049d982
Show file tree
Hide file tree
Showing 17 changed files with 1,127 additions and 36 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
* New: ``coaster.auth`` module with a ``current_auth`` proxy that provides
a standardised API for login managers to use
* New: ``is_collection`` util for testing if an item is a collection data type
* New: ``coaster.views.requires_permission`` decorator
* New: ``coaster.views.classview`` provides a new take on organising views
into a class


0.6.0
Expand Down
3 changes: 3 additions & 0 deletions coaster/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from flask.json import tojson_filter as _tojson_filter
from . import logger
from .auth import current_auth
from .views import current_view

__all__ = ['SandboxedFlask', 'init_app']

Expand Down Expand Up @@ -103,6 +104,8 @@ def init_app(app, env=None):
"""
# Make current_auth available to app templates
app.jinja_env.globals['current_auth'] = current_auth
# Make the current view available to app templates
app.jinja_env.globals['current_view'] = current_view
# Disable Flask-SQLAlchemy events.
# Apps that want it can turn it back on in their config
app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', False)
Expand Down
13 changes: 13 additions & 0 deletions coaster/sqlalchemy/comparators.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from __future__ import absolute_import
import uuid as uuid_
from sqlalchemy.ext.hybrid import Comparator
from flask import abort
from flask_sqlalchemy import BaseQuery
import six
from ..utils import buid2uuid, suuid2uuid
Expand Down Expand Up @@ -38,6 +39,18 @@ def isempty(self):
"""
return not self.session.query(self.exists()).scalar()

def one_or_404(self):
"""
Extends :meth:`~sqlalchemy.orm.query.Query.one_or_none` to raise a 404
if no result is found. This method offers a safety net over
:meth:`~flask_sqlalchemy.BaseQuery.first_or_404` as it helps identify
poorly specified queries that could have returned more than one result.
"""
result = self.one_or_none()
if not result:
abort(404)
return result


class SplitIndexComparator(Comparator):
"""
Expand Down
35 changes: 29 additions & 6 deletions coaster/sqlalchemy/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ class MyModel(BaseMixin, db.Model):
from sqlalchemy_utils.types import UUIDType
from flask import url_for
import six
from ..utils import make_name, uuid2suuid, uuid2buid, buid2uuid, suuid2uuid
from ..utils import make_name, uuid2suuid, uuid2buid, buid2uuid, suuid2uuid, InspectableSet
from ..auth import current_auth
from .immutable_annotation import immutable
from .roles import RoleMixin, with_roles
from .comparators import Query, SqlSplitIdComparator, SqlHexUuidComparator, SqlBuidComparator, SqlSuuidComparator
Expand Down Expand Up @@ -225,6 +226,15 @@ def permissions(self, user, inherited=None):
else:
return set()

@property
def current_permissions(self):
"""
:class:`~coaster.utils.classes.InspectableSet` containing currently
available permissions from this object, using
:obj:`~coaster.auth.current_auth`.
"""
return InspectableSet(self.permissions(current_auth.actor))


class UrlForMixin(object):
"""
Expand Down Expand Up @@ -262,6 +272,9 @@ def url_for(self, action='view', **kwargs):

@classmethod
def is_url_for(cls, _action, _endpoint=None, _external=None, **paramattrs):
"""
View decorator that registers the view as a :meth:`url_for` target.
"""
def decorator(f):
if 'url_for_endpoints' not in cls.__dict__:
cls.url_for_endpoints = {} # Stick it into the class with the first endpoint
Expand Down Expand Up @@ -536,7 +549,7 @@ def __init__(self, *args, **kw):
self.make_name()

def __repr__(self):
return '<%s %s "%s">' % (self.__class__.__name__, self.url_name, self.title)
return '<%s %s "%s">' % (self.__class__.__name__, self.url_id_name, self.title)

def make_name(self):
"""Autogenerates a :attr:`name` from the :attr:`title`"""
Expand Down Expand Up @@ -571,11 +584,19 @@ def url_name_suuid(self):
Returns a URL name combining :attr:`name` and :attr:`suuid` in name-suuid syntax.
To use this, the class must derive from :class:`UuidMixin`.
"""
return '%s-%s' % (self.name, self.suuid)
if isinstance(self, UuidMixin):
return '%s-%s' % (self.name, self.suuid)
else:
return '%s-%s' % (self.name, self.url_id)

@url_name_suuid.comparator
def url_name_suuid(cls):
return SqlSuuidComparator(cls.uuid, splitindex=-1)
if issubclass(cls, UuidMixin):
return SqlSuuidComparator(cls.uuid, splitindex=-1)
elif cls.__uuid_primary_key__:
return SqlHexUuidComparator(cls.id, splitindex=-1)
else:
return SqlSplitIdComparator(cls.id, splitindex=-1)


class BaseScopedIdMixin(BaseMixin):
Expand Down Expand Up @@ -624,8 +645,10 @@ def permissions(self, user, inherited=None):
"""
if inherited is not None:
return inherited | super(BaseScopedIdMixin, self).permissions(user)
else:
elif self.parent is not None and isinstance(self.parent, PermissionMixin):
return self.parent.permissions(user) | super(BaseScopedIdMixin, self).permissions(user)
else:
return super(BaseScopedIdMixin, self).permissions(user)


class BaseScopedIdNameMixin(BaseScopedIdMixin):
Expand Down Expand Up @@ -682,7 +705,7 @@ def __init__(self, *args, **kw):
self.make_name()

def __repr__(self):
return '<%s %s "%s" of %s>' % (self.__class__.__name__, self.url_name, self.title,
return '<%s %s "%s" of %s>' % (self.__class__.__name__, self.url_id_name, self.title,
repr(self.parent)[1:-1] if self.parent else None)

@classmethod
Expand Down
16 changes: 11 additions & 5 deletions coaster/sqlalchemy/statemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,9 @@ def add_transition(self, statemanager, from_, to, if_=None, data=None):
'if': if_, # Additional conditions that must ALL pass
}

def __set_name__(self, owner, name): # pragma: no cover
self.name = name

# Make the transition a non-data descriptor
def __get__(self, obj, cls=None):
if obj is None:
Expand Down Expand Up @@ -501,6 +504,9 @@ def is_available(self):
"""
return not self._state_invalid()

def __getattr__(self, name):
return getattr(self.statetransition, name)

def __call__(self, *args, **kwargs):
"""Call the transition"""
# Validate that each of the state managers is in the correct state
Expand Down Expand Up @@ -827,17 +833,17 @@ def group(self, items, keep_empty=False):
del groups[key]
return groups

def __getattr__(self, attr):
def __getattr__(self, name):
"""
Given the name of a state, returns:
1. If called on an instance, a boolean indicating if the state is active
1. If called on an instance, a ManagedStateWrapper, which implements __bool__
2. If called on a class, a query filter
Returns the default value or raises :exc:`AttributeError` on anything else.
"""
if hasattr(self.statemanager, attr):
mstate = getattr(self.statemanager, attr)
if hasattr(self.statemanager, name):
mstate = getattr(self.statemanager, name)
if isinstance(mstate, (ManagedState, ManagedStateGroup)):
return mstate(self.obj, self.cls)
raise AttributeError("Not a state: %s" % attr)
raise AttributeError("Not a state: %s" % name)
21 changes: 18 additions & 3 deletions coaster/utils/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,10 @@ def value_for(cls, name):

class InspectableSet(Set):
"""
Given a set, mimics a dictionary where the items are keys and have a value
of ``True``, and any other key has a value of ``False``. Also supports
attribute access. Useful in templates to simplify membership inspection::
Given a set, mimics a read-only dictionary where the items are keys and
have a value of ``True``, and any other key has a value of ``False``. Also
supports attribute access. Useful in templates to simplify membership
inspection::
>>> myset = InspectableSet({'member', 'other'})
>>> 'member' in myset
Expand All @@ -231,6 +232,20 @@ class InspectableSet(Set):
True
>>> myset['random']
False
>>> joinset = myset | {'added'}
>>> isinstance(joinset, InspectableSet)
True
>>> joinset = joinset | InspectableSet({'inspectable'})
>>> isinstance(joinset, InspectableSet)
True
>>> 'member' in joinset
True
>>> 'other' in joinset
True
>>> 'added' in joinset
True
>>> 'inspectable' in joinset
True
"""
def __init__(self, members):
if not isinstance(members, set):
Expand Down
1 change: 1 addition & 0 deletions coaster/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
from __future__ import absolute_import
from .misc import * # NOQA
from .decorators import * # NOQA
from .classview import * # NOQA
Loading

0 comments on commit 049d982

Please sign in to comment.