From 210f51778700c962a70153dcb2d27ec6606df596 Mon Sep 17 00:00:00 2001 From: Artur Barseghyan Date: Mon, 17 Aug 2015 01:23:46 +0200 Subject: [PATCH] prepare 0.5.8; dynamic initial values; minor fixes/improvements --- CHANGELOG.rst | 9 ++ README.rst | 44 +++++++++ ROADMAP.rst | 6 +- TODOS.rst | 1 + docs/index.rst | 44 +++++++++ examples/simple/settings.py | 1 + setup.py | 2 +- src/fobi/__init__.py | 4 +- src/fobi/base.py | 35 +++++++- src/fobi/compat.py | 17 ++-- src/fobi/context_processors.py | 15 ++++ src/fobi/dynamic.py | 9 +- src/fobi/helpers.py | 139 +++++++++++++++++++++++++---- src/fobi/integration/processors.py | 3 +- src/fobi/models.py | 7 +- src/fobi/tests/helpers.py | 7 +- src/fobi/utils.py | 5 +- src/fobi/views.py | 10 ++- 18 files changed, 306 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ae756b34..9b17b9d7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 diff --git a/README.rst b/README.rst index 2809d830..042ca66e 100644 --- a/README.rst +++ b/README.rst @@ -83,6 +83,7 @@ Main features and highlights - Data export (`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 diff --git a/ROADMAP.rst b/ROADMAP.rst index 7a923d01..d2a4927e 100644 --- a/ROADMAP.rst +++ b/ROADMAP.rst @@ -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). + diff --git a/TODOS.rst b/TODOS.rst index 49d26a62..9bad8497 100644 --- a/TODOS.rst +++ b/TODOS.rst @@ -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 ============ diff --git a/docs/index.rst b/docs/index.rst index 33aeac5e..c31e4aaa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -83,6 +83,7 @@ Main features and highlights - Data export (`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 diff --git a/examples/simple/settings.py b/examples/simple/settings.py index 2bb8075e..4e0c6f4a 100644 --- a/examples/simple/settings.py +++ b/examples/simple/settings.py @@ -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 = ( diff --git a/setup.py b/setup.py index 851471fd..d4cf1e17 100644 --- a/setup.py +++ b/setup.py @@ -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', diff --git a/src/fobi/__init__.py b/src/fobi/__init__.py index e16d8042..49d45586 100644 --- a/src/fobi/__init__.py +++ b/src/fobi/__init__.py @@ -1,6 +1,6 @@ __title__ = 'django-fobi' -__version__ = '0.5.7' -__build__ = 0x00003c +__version__ = '0.5.8' +__build__ = 0x00003d __author__ = 'Artur Barseghyan ' __copyright__ = '2014-2015 Artur Barseghyan' __license__ = 'GPL 2.0/LGPL 2.1' diff --git a/src/fobi/base.py b/src/fobi/base.py index afaa009e..90dab22e 100644 --- a/src/fobi/base.py +++ b/src/fobi/base.py @@ -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, diff --git a/src/fobi/compat.py b/src/fobi/compat.py index 1a836dea..e1d8193a 100644 --- a/src/fobi/compat.py +++ b/src/fobi/compat.py @@ -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 diff --git a/src/fobi/context_processors.py b/src/fobi/context_processors.py index 7d1fbc04..217d181f 100644 --- a/src/fobi/context_processors.py +++ b/src/fobi/context_processors.py @@ -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(), + } + } diff --git a/src/fobi/dynamic.py b/src/fobi/dynamic.py index b1321109..f673af0c 100644 --- a/src/fobi/dynamic.py +++ b/src/fobi/dynamic.py @@ -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)) diff --git a/src/fobi/helpers.py b/src/fobi/helpers.py index 8bfa0324..650da5fa 100644 --- a/src/fobi/helpers.py +++ b/src/fobi/helpers.py @@ -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}".format(html_element, safe_text(key), \ - safe_text(value), html_element) \ + ["<{0}>{1}: {2}".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'{1}'.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 diff --git a/src/fobi/integration/processors.py b/src/fobi/integration/processors.py index 64d628ab..3ca086dc 100644 --- a/src/fobi/integration/processors.py +++ b/src/fobi/integration/processors.py @@ -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: diff --git a/src/fobi/models.py b/src/fobi/models.py index 62b9cd49..22c0abfb 100644 --- a/src/fobi/models.py +++ b/src/fobi/models.py @@ -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') diff --git a/src/fobi/tests/helpers.py b/src/fobi/tests/helpers.py index 9d5385d7..e54d1e5a 100644 --- a/src/fobi/tests/helpers.py +++ b/src/fobi/tests/helpers.py @@ -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 diff --git a/src/fobi/utils.py b/src/fobi/utils.py index 65874600..c2db2be7 100644 --- a/src/fobi/utils.py +++ b/src/fobi/utils.py @@ -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 diff --git a/src/fobi/views.py b/src/fobi/views.py index cc9f84e8..a1d43140 100644 --- a/src/fobi/views.py +++ b/src/fobi/views.py @@ -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)