Skip to content
This repository has been archived by the owner on Sep 28, 2022. It is now read-only.

Commit

Permalink
Merge pull request #100 from postatum/101820116_crud_hooks
Browse files Browse the repository at this point in the history
Implement CRUD events
  • Loading branch information
jstoiko committed Sep 5, 2015
2 parents 10a6c02 + 6d03854 commit 376cfba
Show file tree
Hide file tree
Showing 15 changed files with 760 additions and 36 deletions.
159 changes: 159 additions & 0 deletions docs/source/crud_events.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
CRUD Events
===========

In order to allow users perform custom actions on CRUD request events, nefertari provides CRUD events and hepler functions.

Nefertari CRUD events module includes a set of events, maps of events, subscriber predicates and helper function to connect it all together. Read further for mode detailed description.

All the objects are contained in ``nefertari.events`` module. Nefertari CRUD events use Pyramid event system.


Events
------

``nefertari.events`` defines a set of event classes inherited from ``nefertari.events.RequestEvent``.

There are two types of nefertari CRUD events:
* "Before" events, which are run after view class is instantiated, but before view method is run, thus before request is processed.
* "After" events, which are run after view method was called.

All events are named after camel-cased name of view method they are called around and prefixed with "Before" or "After" depending on the place event is triggered from (as described above). E.g. event classed for view method ``update_many`` are called ``BeforeUpdateMany`` and ``AfterUpdateMany``.

Check API section for a full list of attributes/params events have.

It's recommended to use ``before`` events to:
* Transform input
* Perform validation
* Apply changes to object that is being affected by request using ``event.set_field_value`` method.

And ``after`` events to:
* Change DB objects which are not affected by request.
* Perform notifications/logging.

Complete list of events:
* BeforeIndex
* BeforeShow
* BeforeCreate
* BeforeUpdate
* BeforeReplace
* BeforeDelete
* BeforeUpdateMany
* BeforeDeleteMany
* BeforeItemOptions
* BeforeCollectionOptions
* AfterIndex
* AfterShow
* AfterCreate
* AfterUpdate
* AfterReplace
* AfterDelete
* AfterUpdateMany
* AfterDeleteMany
* AfterItemOptions
* AfterCollectionOptions


Predicates
----------

Nefertari defines and sets up following subscriber predicates:

**nefertari.events.ModelClassIs**
Available under ``model`` param when connecting subscribers, it allows to connect subscribers on per-model basis. When subscriber is connected using this predicate, it will only be called when ``view.Model`` is the same class or subclass of this param value.

**nefertari.events.FieldIsChanged**
Available under ``field`` param when connecting subscribers, it allows to connect subscribers on per-field basis. When subscriber is connected using this predicate, it will only be called when value of this param is present in request JSON body.


Utilities
----------

**nefertari.events.subscribe_to_events**
Helper function that allows to connect subscriber to multiple events at once. Supports ``model`` and ``field`` subscriber predicate params. Available at ``config.subscribe_to_events``. Subscribers are run in order connected.

**nefertari.events.BEFORE_EVENTS**
Map of ``{view_method_name: EventClass}`` of "Before" events. E.g. one of its elements is ``'index': BeforeIndex``.

**nefertari.events.AFTER_EVENTS**
Map of ``{view_method_name: EventClass}`` of "AFter" events. E.g. one of its elements is ``'index': AfterIndex``.



Examples
--------

Having subscriber that logs request body:

.. code-block:: python
import logging
log = logging.getLogger(__name__)
def log_request(event):
log.debug(event.request.body)
**Having access to configurator**, we can connect it to any of nefertari CRUD events. E.g. lets log all collection POST requests (view ``create`` method):

.. code-block:: python
from nefertari import events
config.subscribe_to_events(log_request, [events.AfterCreate])
Connected this way ``log_request`` subscriber will be called after every collection POST request.

In case we want to limit models for which subscriber will be called, we can connect subscriber with a ``model`` predicate:

.. code-block:: python
from nefertari import events
from .models import User
config.subscribe_to_events(
log_request, [events.AfterCreate],
model=User)
Connected this way ``log_request`` subscriber will only be called when collection POST request comes at endpoint which handles our ``User`` model.

We can also use ``field`` predicate to make subscriber run only when particular field is present in request JSON body. E.g. if we only want to log collection POST requests for model ``User`` which contain ``first_name`` field, we connect subscriber like so:

.. code-block:: python
from nefertari import events
from .models import User
config.subscribe_to_events(
log_request, [events.AfterCreate],
model=User, field='first_name')
Predicate ``fields`` can also be used without ``model`` predicate. E.g. if we want to log all POST request bodies of when they have field ``favourite`` we should connect subscriber like so:

.. code-block:: python
from nefertari import events
from .models import User
config.subscribe_to_events(
log_request, [events.AfterCreate],
field='favourite')
API
---

.. autoclass:: nefertari.events.RequestEvent
:members:
:private-members:

.. autoclass:: nefertari.events.ModelClassIs
:members:
:private-members:

.. autoclass:: nefertari.events.FieldIsChanged
:members:
:private-members:

.. autofunction:: nefertari.events.trigger_events

.. autofunction:: nefertari.events.subscribe_to_events
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Table of Content
getting_started
views
models
crud_events
auth
making_requests
development_tools
Expand Down
6 changes: 6 additions & 0 deletions nefertari/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ def includeme(config):
from nefertari.renderers import (
JsonRendererFactory, NefertariJsonRendererFactory)
from nefertari.utils import dictset
from nefertari.events import (
ModelClassIs, FieldIsChanged, subscribe_to_events)

log.info("%s %s" % (APP_NAME, __version__))
config.add_directive('get_root_resource', get_root_resource)
config.add_directive('subscribe_to_events', subscribe_to_events)
config.add_renderer('json', JsonRendererFactory)
config.add_renderer('nefertari_json', NefertariJsonRendererFactory)

Expand All @@ -44,6 +47,9 @@ def includeme(config):

config.add_tween('nefertari.tweens.cache_control')

config.add_subscriber_predicate('model', ModelClassIs)
config.add_subscriber_predicate('field', FieldIsChanged)

Settings = dictset(config.registry.settings)
root = config.get_root_resource()
root.auth = Settings.asbool('auth')
26 changes: 26 additions & 0 deletions nefertari/authentication/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
def includeme(config):
""" Set up event subscribers. """
from .models import (
AuthUserMixin,
random_uuid,
lower_strip,
encrypt_password,
)
from nefertari import events

subscribe_to = (
events.BeforeCreate,
events.BeforeUpdate,
events.BeforeReplace,
events.BeforeUpdateMany,
)

add_sub = config.subscribe_to_events
add_sub(random_uuid, subscribe_to, model=AuthUserMixin,
field='username')
add_sub(lower_strip, subscribe_to, model=AuthUserMixin,
field='username')
add_sub(lower_strip, subscribe_to, model=AuthUserMixin,
field='email')
add_sub(encrypt_password, subscribe_to, model=AuthUserMixin,
field='password')
32 changes: 15 additions & 17 deletions nefertari/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,27 +148,30 @@ def get_authuser_by_name(cls, request):
return cls.get_resource(username=username)


def lower_strip(**kwargs):
return (kwargs['new_value'] or '').lower().strip()
def lower_strip(event):
value = (event.field.new_value or '').lower().strip()
event.set_field_value(value)


def random_uuid(**kwargs):
return kwargs['new_value'] or uuid.uuid4().hex
def random_uuid(event):
if not event.field.new_value:
event.set_field_value(uuid.uuid4().hex)


def encrypt_password(**kwargs):
def encrypt_password(event):
""" Crypt :new_value: if it's not crypted yet. """
new_value = kwargs['new_value']
field = kwargs['field']
field = event.field
new_value = field.new_value
min_length = field.params['min_length']
if len(new_value) < min_length:
raise ValueError(
'`{}`: Value length must be more than {}'.format(
field.name, field.params['min_length']))

if new_value and not crypt.match(new_value):
new_value = str(crypt.encode(new_value))
return new_value
encrypted = str(crypt.encode(new_value))
field.new_value = encrypted
event.set_field_value(encrypted)


class AuthUserMixin(AuthModelMethodsMixin):
Expand All @@ -179,14 +182,9 @@ class AuthUserMixin(AuthModelMethodsMixin):
"""
username = engine.StringField(
primary_key=True, unique=True,
min_length=1, max_length=50,
before_validation=[random_uuid, lower_strip])
email = engine.StringField(
unique=True, required=True,
before_validation=[lower_strip])
password = engine.StringField(
min_length=3, required=True,
before_validation=[encrypt_password])
min_length=1, max_length=50)
email = engine.StringField(unique=True, required=True)
password = engine.StringField(min_length=3, required=True)
groups = engine.ListField(
item_type=engine.StringField,
choices=['admin', 'user'], default=['user'])
Expand Down

0 comments on commit 376cfba

Please sign in to comment.