prepare 0.5.8; dynamic initial values; minor fixes/improvements

This commit is contained in:
Artur Barseghyan 2015-08-17 01:23:46 +02:00
parent d974e27a8e
commit 210f517787
18 changed files with 306 additions and 52 deletions

View file

@ -15,6 +15,15 @@ are used for versioning (schema follows below):
0.3.4 to 0.4).
- All backwards incompatible changes are mentioned in this document.
0.5.8
-----
2015-08-16
- Made it possible to define dynamic initials for form fields. Example initial
dynamic values in the form (like {{ request.path }}).
- Minor fixes/improvements.
0.5.7
-----
2015-08-03

View file

@ -83,6 +83,7 @@ Main features and highlights
- Data export (`db_store
<https://github.com/barseghyanartur/django-fobi/tree/stable/src/fobi/contrib/plugins/form_handlers/db_store>`_
form handler plugin) into XLS/CSV format.
- Dynamic initial values for form elements.
Roadmap
=======
@ -1423,6 +1424,49 @@ should be constructing your URL to the form as follows:
http://127.0.0.1:8001/fobi/view/test-form/?fobi_initial_data&email=test@example.com&age=19
Dynamic initial values
======================
It's possible to provide a dynamic initial value for any of the elements.
In order to do that, you should use the build-in context processor or make
your own one. The only requirement is that you should store all values that
should be exposes in the form as a dict for `fobi_dynamic_values` dictionary
key. Beware, that passing the original request object might be unsafe in
many ways. Currently, a stripped down version of the request object is being
passed as a context variable.
.. code-block:: python
TEMPLATE_CONTEXT_PROCESSORS = (
# ...
"fobi.context_processors.dynamic_values",
# ...
)
.. code-block:: python
def dynamic_values(request):
return {
'fobi_dynamic_values': {
'request': StrippedRequest(request),
'now': datetime.datetime.now(),
'today': datetime.date.today(),
}
}
In your GUI, you should be refering to the initial values in the following way:
.. code-block:: none
{{ request.path }} {{ now }} {{ today }}
Notice, that you should not provide the `fobi_dynamic_values.` as a prefix.
Currently, the following variables are available in the
`fobi.context_processors.dynamic_values` context processor:
- request: Stripped HttpRequest object.
- now: datetime.datetime.now()
- today: datetime.date.today()
Submitted form element plugins values
=====================================
While some values of form element plugins are submitted as is, some others

View file

@ -23,13 +23,11 @@ change of the name of the "simple" theme into "django_admin_style" theme.
- Internally, make a date when form has been created. Also keep track of when
the form has been last edited.
0.5.8
0.5.9
-----
yyyy-mm-dd (upcoming).
- Export/import forms saved as JSON. Validate the imports and mention that
some plugins are not installed if there are plugins that should be installed
first.
- Made it possible to define dynamic fields and use then in the form. Let
developers themselves define what should be in there and the contents of it
(pluggable and replaceable).

View file

@ -276,6 +276,7 @@ Must haves
developers themselves define what should be in there (some sort of
register in global scope, maybe just a context processor).
Make it pluggable and replaceable.
- Check if it's safe to use the initial dynamic values.
Should haves
============

View file

@ -83,6 +83,7 @@ Main features and highlights
- Data export (`db_store
<https://github.com/barseghyanartur/django-fobi/tree/stable/src/fobi/contrib/plugins/form_handlers/db_store>`_
form handler plugin) into XLS/CSV format.
- Dynamic initial values for form elements.
Roadmap
=======
@ -1423,6 +1424,49 @@ should be constructing your URL to the form as follows:
http://127.0.0.1:8001/fobi/view/test-form/?fobi_initial_data&email=test@example.com&age=19
Dynamic initial values
======================
It's possible to provide a dynamic initial value for any of the elements.
In order to do that, you should use the build-in context processor or make
your own one. The only requirement is that you should store all values that
should be exposes in the form as a dict for `fobi_dynamic_values` dictionary
key. Beware, that passing the original request object might be unsafe in
many ways. Currently, a stripped down version of the request object is being
passed as a context variable.
.. code-block:: python
TEMPLATE_CONTEXT_PROCESSORS = (
# ...
"fobi.context_processors.dynamic_values",
# ...
)
.. code-block:: python
def dynamic_values(request):
return {
'fobi_dynamic_values': {
'request': StrippedRequest(request),
'now': datetime.datetime.now(),
'today': datetime.date.today(),
}
}
In your GUI, you should be refering to the initial values in the following way:
.. code-block:: none
{{ request.path }} {{ now }} {{ today }}
Notice, that you should not provide the `fobi_dynamic_values.` as a prefix.
Currently, the following variables are available in the
`fobi.context_processors.dynamic_values` context processor:
- request: Stripped HttpRequest object.
- now: datetime.datetime.now()
- today: datetime.date.today()
Submitted form element plugins values
=====================================
While some values of form element plugins are submitted as is, some others

View file

@ -134,6 +134,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
"django.contrib.messages.context_processors.messages",
"django.core.context_processors.request",
"fobi.context_processors.theme", # Important!
"fobi.context_processors.dynamic_values", # Optional
)
TEMPLATE_DIRS = (

View file

@ -67,7 +67,7 @@ for static_dir in static_dirs:
for locale_dir in locale_dirs:
locale_files += [os.path.join(locale_dir, f) for f in os.listdir(locale_dir)]
version = '0.5.7'
version = '0.5.8'
install_requires = [
'Pillow>=2.0.0',

View file

@ -1,6 +1,6 @@
__title__ = 'django-fobi'
__version__ = '0.5.7'
__build__ = 0x00003c
__version__ = '0.5.8'
__build__ = 0x00003d
__author__ = 'Artur Barseghyan <artur.barseghyan@gmail.com>'
__copyright__ = '2014-2015 Artur Barseghyan'
__license__ = 'GPL 2.0/LGPL 2.1'

View file

@ -50,6 +50,9 @@ from django import forms
from django.forms import ModelForm
from django.http import Http404
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import AnonymousUser
from django.test import RequestFactory
from django.template import RequestContext, Template, Context
from nine.versions import DJANGO_GTE_1_8
@ -73,7 +76,8 @@ from fobi.exceptions import (
)
from fobi.helpers import (
uniquify_sequence, map_field_name_to_label, clean_dict,
map_field_name_to_label, get_ignorable_form_values, safe_text
map_field_name_to_label, get_ignorable_form_values, safe_text,
StrippedRequest,
)
from fobi.data_structures import SortableDict
@ -1118,9 +1122,9 @@ class FormElementPlugin(BasePlugin):
has_value = False
is_hidden = False
def _get_form_field_instances(self, form_element_entry=None, origin=None, \
kwargs_update_func=None, return_func=None, \
extra={}):
def _get_form_field_instances(self, form_element_entry=None, origin=None,
kwargs_update_func=None, return_func=None,
extra={}, request=None):
"""
Used internally. Do not override this method. Gets the instances of
form fields, that plugin contains.
@ -1154,6 +1158,29 @@ class FormElementPlugin(BasePlugin):
Widget = None
if isinstance(Field, (list, tuple)):
Field, Widget = Field
# Consider using context for resolving some variables.
# For instance, if user is logged in, ``request.user.username``
# as an initial value should put the current users' username
# as initial value in the form.
if 'initial' in field_kwargs and field_kwargs['initial']:
# For security reasons we're not using the original request
# here.
stripped_request = StrippedRequest(request)
context = RequestContext(stripped_request)
# In order to be sure, that no accidental sensitive data
# is exposed in the forms, we only vales from the
# fobi specific context processor. By automatically
# force-prefixing all dynamic value definitions with
# "fobi_dynamic_values." string. See the docs for
# more ("Dyamic initial values" section).
initial = field_kwargs['initial']
initial = initial.replace("{{ ", "{{") \
.replace(" }}", "}}") \
.replace("{{", "{{fobi_dynamic_values.")
field_kwargs['initial'] = Template(initial).render(context)
# Data to update field instance kwargs with
kwargs_update = self.get_origin_kwargs_update_func_results(
kwargs_update_func,

View file

@ -9,13 +9,14 @@ from django.conf import settings
from nine.user import User
# Sanity checks. Possibly rely on the dynamic username field in future.
user = User()
if not hasattr(user, 'username'):
from dash.exceptions import ImproperlyConfigured
raise ImproperlyConfigured("Your custom user model ({0}.{1}) doesn't "
"have ``username`` property, while "
"``django-fobi`` relies on its' presence."
"".format(user._meta.app_label, user._meta.object_name))
#user = User()
#
#if not hasattr(user, 'username'):
# from fobi.exceptions import ImproperlyConfigured
# raise ImproperlyConfigured("Your custom user model ({0}.{1}) doesn't "
# "have ``username`` property, while "
# "``django-fobi`` relies on its' presence."
# "".format(user._meta.app_label,
# user._meta.object_name))
AUTH_USER_MODEL = settings.AUTH_USER_MODEL

View file

@ -4,7 +4,10 @@ __copyright__ = 'Copyright (c) 2014 Artur Barseghyan'
__license__ = 'GPL 2.0/LGPL 2.1'
__all__ = ('theme',)
import datetime
from fobi.base import get_theme
from fobi.helpers import StrippedRequest
def theme(request):
"""
@ -14,3 +17,15 @@ def theme(request):
:return fobi.base.BaseTheme: Instance of ``fobi.base.BaseTheme``.
"""
return {'fobi_theme': get_theme(request, as_instance=True)}
def dynamic_values(request):
"""
Dynamic values exposed to public forms.
"""
return {
'fobi_dynamic_values': {
'request': StrippedRequest(request),
'now': datetime.datetime.now(),
'today': datetime.date.today(),
}
}

View file

@ -20,8 +20,8 @@ from django.forms.widgets import media_property
# ****************************************************************************
# ****************************************************************************
def assemble_form_class(form_entry, base_class=BaseForm, request=None, \
origin=None, origin_kwargs_update_func=None, \
def assemble_form_class(form_entry, base_class=BaseForm, request=None,
origin=None, origin_kwargs_update_func=None,
origin_return_func=None, form_element_entries=None):
"""
Assembles a form class by given entry.
@ -53,14 +53,15 @@ def assemble_form_class(form_entry, base_class=BaseForm, request=None, \
# We simply make sure the plugin exists. We don't handle
# exceptions relate to the non-existent plugins here. They
# are istead handled in registry.
# are instead handled in registry.
if plugin:
plugin_form_field_instances = plugin._get_form_field_instances(
form_element_entry = form_element_entry,
origin = origin,
kwargs_update_func = origin_kwargs_update_func,
return_func = origin_return_func,
extra = {'counter': creation_counter}
extra = {'counter': creation_counter},
request = request
)
for form_field_name, form_field_instance in plugin_form_field_instances:
base_fields.append((form_field_name, form_field_instance))

View file

@ -16,6 +16,7 @@ __all__ = (
'update_plugin_data', 'get_select_field_choices',
'validate_initial_for_choices', 'validate_initial_for_multiple_choices',
'validate_submit_value_as', 'get_app_label_and_model_name',
'StrippedUser', 'StrippedRequest',
)
import os
@ -34,9 +35,13 @@ from django.db.utils import DatabaseError
from django.utils.encoding import force_text
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import AnonymousUser
from django.test.client import RequestFactory
from autoslug.settings import slugify
from nine.user import User
from fobi.constants import (
SUBMIT_VALUE_AS_VAL, SUBMIT_VALUE_AS_REPR, SUBMIT_VALUE_AS_MIX
)
@ -125,8 +130,8 @@ def two_dicts_to_string(headers, data, html_element='p'):
(value, data.get(key, '')) for key, value in list(headers.items())
]
return "".join(
["<{0}>{1}: {2}</{3}>".format(html_element, safe_text(key), \
safe_text(value), html_element) \
["<{0}>{1}: {2}</{3}>".format(html_element, safe_text(key),
safe_text(value), html_element)
for key, value in formatted_data]
)
@ -295,7 +300,7 @@ def get_app_label_and_model_name(path):
# *****************************************************************************
# *****************************************************************************
def admin_change_url(app_label, module_name, object_id, extra_path='', \
def admin_change_url(app_label, module_name, object_id, extra_path='',
url_title=None):
"""
Gets an admin change URL for the object given.
@ -309,7 +314,7 @@ def admin_change_url(app_label, module_name, object_id, extra_path='', \
:return str:
"""
try:
url = reverse('admin:{0}_{1}_change'.format(app_label, module_name), \
url = reverse('admin:{0}_{1}_change'.format(app_label, module_name),
args=[object_id]) + extra_path
if url_title:
return u'<a href="{0}">{1}</a>'.format(url, url_title)
@ -370,48 +375,48 @@ def get_select_field_choices(raw_choices_data):
return choices
def validate_initial_for_choices(plugin_form, field_name_choices='choices', \
def validate_initial_for_choices(plugin_form, field_name_choices='choices',
field_name_initial='initial'):
"""
Validates the initial value for the choices given.
:param fobi.base.BaseFormFieldPluginForm plugin_form:
"""
availalble_choices = dict(
available_choices = dict(
get_select_field_choices(plugin_form.cleaned_data[field_name_choices])
).keys()
if plugin_form.cleaned_data[field_name_initial] \
and not plugin_form.cleaned_data[field_name_initial] \
in availalble_choices:
in available_choices:
raise forms.ValidationError(
_("Invalid value for initial: {0}. Should be any of the following"
": {1}".format(plugin_form.cleaned_data[field_name_initial], \
','.join(availalble_choices)))
": {1}".format(plugin_form.cleaned_data[field_name_initial],
','.join(available_choices)))
)
return plugin_form.cleaned_data[field_name_initial]
def validate_initial_for_multiple_choices(plugin_form, \
field_name_choices='choices', \
def validate_initial_for_multiple_choices(plugin_form,
field_name_choices='choices',
field_name_initial='initial'):
"""
Validates the initial value for the multiple choices given.
:param fobi.base.BaseFormFieldPluginForm plugin_form:
"""
availalble_choices = dict(
available_choices = dict(
get_select_field_choices(plugin_form.cleaned_data[field_name_choices])
).keys()
if plugin_form.cleaned_data[field_name_initial]:
for choice in plugin_form.cleaned_data[field_name_initial].split(','):
choice = choice.strip()
if not choice in availalble_choices:
if not choice in available_choices:
raise forms.ValidationError(
_("Invalid value for initial: {0}. Should be any "
"of the following: {1}"
"".format(choice, ','.join(availalble_choices)))
"".format(choice, ','.join(available_choices)))
)
return plugin_form.cleaned_data[field_name_initial]
@ -422,10 +427,110 @@ def validate_submit_value_as(value):
:param str value:
"""
if not value in (SUBMIT_VALUE_AS_VAL, SUBMIT_VALUE_AS_REPR, \
if not value in (SUBMIT_VALUE_AS_VAL, SUBMIT_VALUE_AS_REPR,
SUBMIT_VALUE_AS_MIX):
raise ImproperlyConfigured("The `SUBMIT_AS_VALUE` may have one of "
"the following values: {0}, {1} or {2}"
"".format(SUBMIT_VALUE_AS_VAL, \
SUBMIT_VALUE_AS_REPR, \
"".format(SUBMIT_VALUE_AS_VAL,
SUBMIT_VALUE_AS_REPR,
SUBMIT_VALUE_AS_MIX))
class StrippedUser(object):
"""
Stripped user object.
"""
def __init__(self, user):
"""
:param user:
:return:
"""
self._user = user
if not self._user.is_anonymous():
setattr(self._user, User.USERNAME_FIELD, self._user.get_username())
else:
setattr(self._user, User.USERNAME_FIELD, None)
@property
def email(self):
return self._user.email
def get_username(self):
"""
"""
return self._user.get_username()
def get_full_name(self):
"""
"""
return self._user.get_full_name()
def get_short_name(self):
"""
"""
return self._user.get_full_name()
def is_anonymous(self):
return self._user.is_anonymous()
class StrippedRequest(object):
"""
Stripped request object.
"""
def __init__(self, request):
"""
:param django.http.HttpRequest request:
:return:
"""
# Just to make sure nothing breaks if we don't provide the request
# object, we do fall back to a fake request object.
if request:
self._request = request
else:
request_factory = RequestFactory()
self._request = request_factory.get('/')
if hasattr(request, 'user') and request.user:
self.user = StrippedUser(self._request.user)
else:
self.user = StrippedUser(AnonymousUser())
@property
def path(self):
"""
"""
return self._request.path
@property
def get_full_path(self):
"""
"""
return self._request.get_full_path()
@property
def is_secure(self):
"""
"""
return self._request.is_secure()
@property
def is_ajax(self):
"""
"""
return self._request.is_ajax()
@property
def META(self):
"""
"""
META = {
'HTTP_ACCEPT_ENCODING': self._request.META.get('HTTP_ACCEPT_ENCODING'),
'HTTP_ACCEPT_LANGUAGE': self._request.META.get('HTTP_ACCEPT_LANGUAGE'),
'HTTP_HOST': self._request.META.get('HTTP_HOST'),
'HTTP_REFERER': self._request.META.get('HTTP_REFERER'),
'HTTP_USER_AGENT': self._request.META.get('HTTP_USER_AGENT'),
}
return META

View file

@ -97,7 +97,8 @@ class IntegrationProcessor(object):
# dynamically.
FormClass = assemble_form_class(
instance.form_entry,
form_element_entries = form_element_entries
form_element_entries = form_element_entries,
request = request
)
if 'POST' == request.method:

View file

@ -76,9 +76,8 @@ class AbstractPluginModel(models.Model):
#plugin_uid = models.CharField(_("Plugin UID"), max_length=255,
# unique=True, editable=False)
users = models.ManyToManyField(AUTH_USER_MODEL, verbose_name=_("User"),
null=True, blank=True)
groups = models.ManyToManyField(Group, verbose_name=_("Group"), null=True,
blank=True)
blank=True)
groups = models.ManyToManyField(Group, verbose_name=_("Group"), blank=True)
class Meta:
abstract = True
@ -138,7 +137,7 @@ class AbstractPluginModel(models.Model):
:return string:
"""
return ', '.join([u.username for u in self.users.all()])
return ', '.join([u.get_username() for u in self.users.all()])
users_list.allow_tags = True
users_list.short_description = _('Users')

View file

@ -77,12 +77,15 @@ def get_or_create_admin_user():
"""
User = get_user_model()
try:
u = User._default_manager.get(username=FOBI_TEST_USER_USERNAME)
kwargs = {
User.USERNAME_FIELD: FOBI_TEST_USER_USERNAME,
}
u = User._default_manager.get(**kwargs)
return u
except ObjectDoesNotExist as e:
u = User()
u.username = FOBI_TEST_USER_USERNAME
setattr(u, User.USERNAME_FIELD, FOBI_TEST_USER_USERNAME)
u.email = 'admin@dev.django-fobi.example.com'
u.is_superuser = True
u.is_staff = True

View file

@ -332,15 +332,16 @@ def get_user_form_handler_plugin_uids(user):
# ****************************************************************************
# ****************************************************************************
def get_assembled_form(form_entry):
def get_assembled_form(form_entry, request=None):
"""
Gets assembled form.
:param fobi.models.FormEntry form_entry:
:param django.http.HttpRequest request:
:return django.forms.Form:
"""
# TODO
FormClass = assemble_form_class(form_entry)
FormClass = assemble_form_class(form_entry, request=request)
form = FormClass()
return form

View file

@ -319,7 +319,8 @@ def edit_form_entry(request, form_entry_id, theme=None, template_name=None):
FormClass = assemble_form_class(
form_entry,
origin = 'edit_form_entry',
origin_kwargs_update_func = append_edit_and_delete_links_to_field
origin_kwargs_update_func = append_edit_and_delete_links_to_field,
request = request
)
assembled_form = FormClass()
@ -856,8 +857,11 @@ def view_form_entry(request, form_entry_slug, theme=None, template_name=None):
# This is where the most of the magic happens. Our form is being built
# dynamically.
FormClass = assemble_form_class(form_entry,
form_element_entries=form_element_entries)
FormClass = assemble_form_class(
form_entry,
form_element_entries = form_element_entries,
request = request
)
if 'POST' == request.method:
form = FormClass(request.POST, request.FILES)