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 #115 from postatum/104069592_field_processors
Browse files Browse the repository at this point in the history
Rework field processors
  • Loading branch information
jstoiko committed Oct 6, 2015
2 parents b26a214 + 5deb908 commit 1278051
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 136 deletions.
33 changes: 1 addition & 32 deletions docs/source/event_handlers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,12 @@ 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.
Helper function that allows to connect subscriber to multiple events at once. Supports ``model`` subscriber predicate param. 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``.
Expand Down Expand Up @@ -131,30 +128,6 @@ In case we want to limit models for which subscriber will be called, we can conn
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
---

Expand All @@ -166,10 +139,6 @@ API
:members:
:private-members:

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

.. autofunction:: nefertari.events.trigger_events

.. autofunction:: nefertari.events.subscribe_to_events
Expand Down
109 changes: 109 additions & 0 deletions docs/source/field_processors.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
Field processors
================

Nefertari allows users to define functions that accept field data and return modified field value, may perform validation or perform other actions related to field.

These functions are called "field processors". They are set up per-field and are called when request comes into application that modifies the field for which processor is set up (when field is present in request JSON).


Usage basics
------------

Nefertari field processors support consists of ``nefertari.events.add_field_processors`` function which is used to connect processors to model fields. The function is accessible through Pyramid Configurator instance.

``nefertari.events.add_field_processors`` expects following parameters:

**processors**
Sequence of processor functions.

**model**
Model class for field if which processors are registered.

**field**
Field name for which processors are registered.


Nefertari passes following parameters to processors:

**new_value**
New value of of field.

**instance**
Instance affected by request. Is None when set of items is updated in bulk and when item is created.

**field**
Instance of nefertari.utils.data.FieldData instance containing data of changed field.

**request**
Current Pyramid Request instance.

**model**
Model class affected by request.

**event**
Underlying event object. Should be used to edit other fields of instance using ``event.set_field_value(field_name, value)``.


Processors are called in order they are passed to ``nefertari.events.add_field_processors``. Each processor must return processed value which is used a input for next processor if present.


Examples
--------

Having subscriber that strips and lowers value:

.. code-block:: python
# processors.py
def lower_strip(**kwargs):
return kwargs['new_value'].lower().strip()
And basic model:

.. code-block:: python
# models.py
from nefertari import engine
class Item(engine.BaseDocument):
__tablename__ = 'stories'
id = engine.IdField(primary_key=True)
name = engine.StringField(required=True)
And we want to make sure ``Item.name`` is always lowercase and stripped, we can connect ``lower_strip`` to ``Item.name`` field using ``nefertari.events.add_field_processors`` function like so:

.. code-block:: python
# __init__.py
from .models import Item
from .processors import lower_strip
# Get access to Pyramid configurator
...
config.add_field_processors([lower_strip], model=Item, field='name')
When set up as above, ``lower_strip`` processor will be called each time application gets a request that changes ``Item.name`` field.

To edit other fields of instance, ``event.set_field_value`` method should be used. E.g. if we have fields ``due_date`` and ``days_left`` and we connect processor defined below to field ``due_date``, we can update ``days_left`` from it:

.. code-block:: python
from .helpers import parse_data
from datetime import datetime
def calculate_days_left(**kwargs):
parsed_date = parse_data(kwargs['new_value'])
days_left = (parsed_date-datetime.now()).days
event = kwargs['event']
event.set_field_value('days_left', days_left)
return kwargs['new_value']
API
---

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

.. autofunction:: nefertari.events.add_field_processors
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Table of Content
views
models
event_handlers
field_processors
auth
making_requests
development_tools
Expand Down
4 changes: 3 additions & 1 deletion nefertari/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ def includeme(config):
JsonRendererFactory, NefertariJsonRendererFactory)
from nefertari.utils import dictset
from nefertari.events import (
ModelClassIs, FieldIsChanged, subscribe_to_events)
ModelClassIs, FieldIsChanged, subscribe_to_events,
add_field_processors)

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_directive('add_field_processors', add_field_processors)
config.add_renderer('json', JsonRendererFactory)
config.add_renderer('nefertari_json', NefertariJsonRendererFactory)

Expand Down
24 changes: 6 additions & 18 deletions nefertari/authentication/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,9 @@ def includeme(config):
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')
add_proc = config.add_field_processors
add_proc(
[random_uuid, lower_strip],
model=AuthUserMixin, field='username')
add_proc([lower_strip], model=AuthUserMixin, field='email')
add_proc([encrypt_password], model=AuthUserMixin, field='password')
21 changes: 9 additions & 12 deletions nefertari/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,30 +148,27 @@ def get_authuser_by_name(cls, request):
return cls.get_item(username=username)


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


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


def encrypt_password(event):
def encrypt_password(**kwargs):
""" Crypt :new_value: if it's not crypted yet. """
field = event.field
new_value = field.new_value
new_value = kwargs['new_value']
field = kwargs['field']
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):
encrypted = str(crypt.encode(new_value))
field.new_value = encrypted
event.set_field_value(encrypted)
new_value = str(crypt.encode(new_value))
return new_value


class AuthUserMixin(AuthModelMethodsMixin):
Expand Down
67 changes: 56 additions & 11 deletions nefertari/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,18 @@ def __init__(self, model, view,
self.field = field
self.instance = instance

def set_field_value(self, value, field_name=None):
def set_field_value(self, field_name, value):
""" Set value of field named `field_name`.
Use this method to apply changes to object which is affected
by request. Values are set on `view._json_params` dict.
:param value: Value to be set.
:param field_name: Name of field value of which should be set.
Optional if `self.field` is set; in this case `self.field.name`
is used. If `self.field` is None and `field_name` is not
provided, KeyError is raised.
:param value: Value to be set.
"""
if field_name is None:
if self.field is None:
raise KeyError('Field name is not specified')
field_name = self.field.name
self.view._json_params[field_name] = value


Expand Down Expand Up @@ -201,7 +197,7 @@ def __call__(self, event):
class FieldIsChanged(object):
""" Subscriber predicate to check particular field is changed.
Example: config.add_subscriber(func, event, field=field_name)
Used to implement field processors.
"""

def __init__(self, field, config):
Expand Down Expand Up @@ -255,25 +251,74 @@ def trigger_events(view_obj):
request.registry.notify(after_event(**event_kwargs))


def subscribe_to_events(config, subscriber, events, model=None, field=None):
def subscribe_to_events(config, subscriber, events, model=None):
""" Helper function to subscribe to group of events.
:param config: Pyramid contig instance.
:param subscriber: Event subscriber function.
:param events: Sequence of events to subscribe to.
:param model: Model predicate value.
:param field: Field predicate value.
"""
kwargs = {}
if model is not None:
kwargs['model'] = model
if field is not None:
kwargs['field'] = field

for evt in events:
config.add_subscriber(subscriber, evt, **kwargs)


def add_field_processors(config, processors, model, field):
""" Add processors for model field.
Under the hood, regular nefertari event subscribed is created which
calls field processors in order passed to this function.
Processors are passed following params:
* **new_value**: New value of of field.
* **instance**: Instance affected by request. Is None when set of
items is updated in bulk and when item is created.
* **field**: Instance of nefertari.utils.data.FieldData instance
containing data of changed field.
* **request**: Current Pyramid Request instance.
* **model**: Model class affected by request.
* **event**: Underlying event object.
Each processor must return processed value which is passed to next
processor.
:param config: Pyramid Congurator instance.
:param processors: Sequence of processor functions.
:param model: Model class for field if which processors are
registered.
:param field: Field name for which processors are registered.
"""
before_change_events = (
BeforeCreate,
BeforeUpdate,
BeforeReplace,
BeforeUpdateMany,
)

def wrapper(event, _processors=processors, _field=field):
proc_kw = {
'new_value': event.field.new_value,
'instance': event.instance,
'field': event.field,
'request': event.view.request,
'model': event.model,
'event': event,
}
for proc_func in _processors:
proc_kw['new_value'] = proc_func(**proc_kw)

event.field.new_value = proc_kw['new_value']
event.set_field_value(_field, proc_kw['new_value'])

for evt in before_change_events:
config.add_subscriber(wrapper, evt, model=model, field=field)


def silent(obj):
""" Mark view method or class as "silent" so events won't be fired.
Expand Down

0 comments on commit 1278051

Please sign in to comment.