Source code for zengine.forms.model_form
# -*- coding: utf-8 -*-
"""
this module holds methods that responsible for form generation
both from models or standalone forms
"""
# Copyright (C) 2015 ZetaOps Inc.
#
# This file is licensed under the GNU General Public License v3
# (GPLv3). See LICENSE.txt for details.
import os
from pyoko.lib.utils import un_camel_id
from .fields import *
import six
from pyoko.model import Model
BYPASS_REQUIRED_FIELDS = os.getenv('BYPASS_REQUIRED_FIELDS')
[docs]class FormMeta(type):
_meta = None
def __new__(mcs, name, bases, attrs):
if name == 'ModelForm':
FormMeta._meta = attrs['Meta']
else:
if 'Meta' not in attrs:
attrs['Meta'] = type('Meta', (object,), dict(FormMeta._meta.__dict__))
else:
for k, v in FormMeta._meta.__dict__.items():
if k not in attrs['Meta'].__dict__:
setattr(attrs['Meta'], k, v)
new_class = super(FormMeta, mcs).__new__(mcs, name, bases, attrs)
return new_class
@six.add_metaclass(FormMeta)
[docs]class ModelForm(object):
"""
Serializes / Deserializes pyoko models.
"""
[docs] class Meta:
"""
ModelForm Meta class holds config data that modifies the behaviour of form objects.
Attributes:
`~ModelForm.Meta.title` (str): Title text to be shown top of the form.
`~ModelForm.Meta.help_text` (str): Help text to be shown under the title.
`~ModelForm.Meta.customize_types` (dict): Override field types.
A dict that maps fields names with desired field types.
>>> customize_types={"user_password": "password"}
`~ModelForm.Meta.include` ([]): List of field names to be included.
If given, all other fields will be excluded.
`~ModelForm.Meta.exclude` ([]): List of field names to be excluded.
If given, all other fields will be included.
`~ModelForm.Meta.constraints` (dict): Form constraints to be enforced by
both client side and backend form processors.
See `Ulakbus-UI API`_ docs for possible constraints.
.. code-block:: python
constraints = [
{
'cons': [{'id': 'field2_id', 'cond': 'exists'}],
'do': 'change_fields', 'fields': [{'field2_id': None}]
},
{
'cons': [{'id': 'field2_id', 'cond': 'exists'}],
'do': 'change_fields', 'fields': [{'field1_id': None}]
}
]
.. _Ulakbus-UI API: http://www.ulakbus.org/wiki/ulakbus-api-ui-iliskisi.html
"""
customize_types = {}
help_text = None
title = None
include = []
exclude = []
grouping = []
constraints = {}
# if the intention is to fill the form from task_data, it must be passed False
always_blank = True
[docs] def __init__(self, model=None, exclude=None, include=None, types=None, title=None, **kwargs):
"""
.. note:: *include* and *exclude* does not support fields that placed in nodes.
Args:
model: A pyoko model instance, may be empty
exclude ([]): list of fields to be excluded from serialization
include ([]): list of fields to be included into serialization
types (dict): override type of fields
"""
self._model = model or self
self._config = {'fields': True, 'nodes': True, 'models': True, 'list_nodes': True}
self._config.update(kwargs)
self.readable = False
self._ordered_fields = []
self.exclude = exclude or self.Meta.exclude
self.include = include or self.Meta.include
self.non_data_fields = ['object_key']
self.customize_types = types or getattr(self.Meta, 'customize_types', {})
self.help_text = self.Meta.help_text or getattr(self._model.Meta, 'help_text', None)
self.title = title or self.Meta.title or self._model.get_verbose_name()
[docs] def get_verbose_name(self):
return getattr(self._model.Meta, 'verbose_name', self._model.__class__.__name__)
def _deserialize(self, data):
"""
Creates a model instance with given form data.
Args:
data (dict): Form data in key / value form.
Returns:
Un-saved model instance.
"""
# FIXME: investigate and integrate necessary security precautions on received data
# ie: received keys should be defined in the form
# compare with output of self._serialize()
self.process_form()
new_instance = self._model
new_instance.key = self._model.key
for key, val in data.items():
if key in self.non_data_fields:
continue
if key.endswith('_id') and val: # linked model
name = key[:-3]
linked_model = self._model.get_link(field=name)['mdl']
linked_model_instance = linked_model(self._model._context).objects.get(val)
setattr(new_instance, name, linked_model_instance)
elif (isinstance(val, (six.string_types, bool, int, float)) and
key in new_instance._fields): # field
setattr(new_instance, key, val)
elif val and isinstance(new_instance.get_field(key), File): # File field
_val = {
'name': val['file_name'],
'content': val['file_content'],
}
setattr(new_instance, key, _val)
elif isinstance(val, dict): # Node
node = getattr(new_instance, key)
for k in val:
setattr(node, k, val[k])
elif isinstance(val, list): # ListNode
# get the listnode instance from model
list_node = getattr(new_instance, key)
# clear out it's existing content
list_node.clear()
# fill with form input
for ln_item_data in val:
kwargs = {}
for k in ln_item_data:
if k.endswith('_id'): # linked model in a ListNode
name = k[:-3]
kwargs[name] = getattr(list_node, name).__class__(
self._model._context).objects.get(ln_item_data[k])
else:
kwargs[k] = ln_item_data[k]
list_node(**kwargs)
return new_instance
def _serialize(self, readable=False):
"""
Converts model/form data into a serialization ready dictionary format.
Args:
readable (bool): creates human readable output.
*e.g.* Instead of raw values, uses get_field_name_display() when available.
Returns:
List of dicts.
Example:
.. code-block:: python
In [1]: from zengine.forms.model_form import ModelForm
In [2]: from zengine.models import User
In [3]: user = User.objects.get(username='test_user')
In [4]: ModelForm(user)._serialize()
Out[4]:
[{'choices': None,
'default': None,
'kwargs': {},
'name': 'username',
'required': True,
'title': 'Username',
'type': 'string',
'value': 'test_user'},
{'choices': None,
'default': None,
'kwargs': {},
'name': 'password',
'required': True,
'title': 'Password',
'type': 'string',
'value': '$pbkdf2-sha512$10000$nTMGwBjDWCslpA$iRDbnITH...'},
{'choices': None,
'default': False,
'kwargs': {},
'name': 'superuser',
'required': False,
'title': 'Super user',
'type': 'boolean',
'value': True}]
"""
self.process_form()
self.readable = readable
result = []
if self._config['fields']:
self._get_fields(result, self._model)
if self._config['models']:
self._get_models(result, self._model)
if self is not self._model: # to allow additional fields
try:
self._get_fields(result, self)
# If self._model is instance of Model and it has linked models
self._get_models(result, self)
except AttributeError:
# TODO: all "forms" of world, unite!
pass
if self._config['nodes'] or self._config['list_nodes']:
self._get_nodes(result)
return result
def _filter_out(self, name):
"""
returns true if given name should be
filtered out from serialization.
:param name: field, node or model name.
:return:
"""
# if self._model is form
# if self._model is Model and model or model class does not have `name` attribute
# then skip include/exclude checking
if not any([isinstance(self._model, Model),
getattr(self._model.__class__, name, False),
getattr(self._model, name, False)]
):
return False
if self.exclude and name in self.exclude:
return True
if self.include and name not in self.include and name not in dict(self._ordered_fields):
return True
def _get_nodes(self, result):
for node_name in self._model._nodes:
if self._filter_out(node_name):
continue
instance_node = getattr(self._model, node_name)
node_type = instance_node.__class__.__base__.__name__
node_data = None
if (instance_node._is_auto_created or
(node_type == 'Node' and not self._config['nodes']) or
(node_type == 'ListNode' and not self._config['list_nodes'])):
continue
if node_type == 'Node':
schema = self._node_schema(instance_node, node_name)
if self._model.is_in_db():
node_data = self._node_data([instance_node], node_name)
else: # ListNode
# to get schema of empty listnode we need to create an instance of it
if len(instance_node) == 0:
instance_node()
else:
node_data = self._node_data(instance_node, node_name)
schema = self._node_schema(instance_node[0], node_name)
result.append({'name': node_name,
'type': node_type,
'title': instance_node.get_verbose_name(),
'schema': schema,
'value': node_data if not node_data or node_type == 'ListNode'
else node_data[0],
'required': None,
'default': None,
})
def _get_models(self, result, model_obj):
for lnk in model_obj.get_links(is_set=False):
if self._filter_out(lnk['field']):
continue
model = lnk['mdl']
model_instance = getattr(model_obj, lnk['field'])
result.append({'name': un_camel_id(lnk['field']),
'model_name': model.__name__,
'type': 'model',
'title': model_instance.get_verbose_name(),
'value': model_instance.key,
'content': (self.__class__(model_instance,
models=False,
list_nodes=False,
nodes=False)._serialize()
if model_obj.is_in_db() else None),
'required': None,
'default': None,
})
def _serialize_value(self, val):
if isinstance(val, datetime.datetime):
return val.strftime(DATE_TIME_FORMAT)
elif isinstance(val, datetime.date):
return val.strftime(DATE_FORMAT)
elif isinstance(val, BaseField):
return None
else:
return val
def _get_fields(self, result, model_obj):
for name, field in model_obj._ordered_fields:
if not isinstance(field, Button) and (
name in ['deleted', 'timestamp', 'deleted_at', 'updated_at'
] or self._filter_out(name)):
continue
if self.readable:
val = model_obj.get_humane_value(name)
else:
val = self._serialize_value(getattr(model_obj, name))
item = {'name': name,
'type': self.customize_types.get(name,
field.solr_type),
'value': val,
'required': (False if BYPASS_REQUIRED_FIELDS or
field.solr_type is 'boolean' else field.required),
'choices': getattr(field, 'choices', None),
'kwargs': field.kwargs,
'title': field.title,
'default': field.default() if callable(
field.default) else field.default,
'help_text': field.help_text,
}
if isinstance(field, (Date, DateTime)):
item['format'] = field.format
result.append(item)
def _node_schema(self, node, parent_name):
result = []
# node_data = {'models': [], 'fields': []}
for lnk in node.get_links():
model_instance = getattr(node, lnk['field'])
result.append({'name': un_camel_id(lnk['field']),
'model_name': model_instance.__class__.__name__,
'type': 'model',
'title': model_instance.Meta.verbose_name,
'required': None,
'help_text': model_instance.help_text})
for name, field in node._ordered_fields:
if field.kwargs.get('hidden'):
continue
choices = getattr(field, 'choices', None)
typ = 'select' if choices else self.customize_types.get(name, field.solr_type)
data = {
'name': name,
'type': typ,
'title': field.title,
'required': field.required,
'default': field.default() if callable(field.default) else field.default,
'help_text': field.help_text,
}
if choices:
data['titleMap'] = self.get_choices(choices)
result.append(data)
return result
@lazy_property
def catalog_data_manager(self):
return get_object_from_path(settings.CATALOG_DATA_MANAGER)
_choices_cache = {}
@classmethod
[docs] def convert_choices(cls, chc):
_id = id(chc)
cls._choices_cache[_id] = [{'name': name, 'value': value} for value, name in chc]
return cls._choices_cache[_id]
[docs] def get_choices(self, choices):
if callable(choices):
return choices()
elif not isinstance(choices, (list, tuple)):
return self.catalog_data_manager.get_all(choices)
else:
return self._choices_cache.get(id(choices), self.convert_choices(choices))
[docs] def set_choices_of(self, field, choices):
"""
Can be used to dynamically set/modify choices of fields.
Args:
field str: Name of field.
choices tuple: (('name', 'value'), ('name2', 'value2'),...)
"""
self._fields[field].choices = choices
[docs] def set_default_of(self, field, default):
"""
Can be used to dynamically set/modify default of fields.
Args:
field str: Name of field.
"""
self._fields[field].default = default
def _node_data(self, nodes, parent_name):
results = []
for real_node in nodes:
result = {}
# node_data = {'models': [], 'fields': []}
for lnk in real_node.get_links():
model_instance = getattr(real_node, lnk['field'])
result[un_camel_id(lnk['field'])] = {'key': model_instance.key,
'verbose_name': model_instance.get_verbose_name(),
'unicode': six.text_type(model_instance)
}
for name, field in real_node._fields.items():
result[name] = self._serialize_value(real_node._field_values.get(name))
results.append(result)
return results