From 91066bdf842aa6abf95c8464e509d772e51c3096 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 3 Oct 2012 12:55:14 +0400 Subject: [PATCH 01/42] Make MongoSingleObjectMixin more like SingleObjectMixin --- mongotools/views/__init__.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/mongotools/views/__init__.py b/mongotools/views/__init__.py index a446541..5e58b61 100644 --- a/mongotools/views/__init__.py +++ b/mongotools/views/__init__.py @@ -20,7 +20,7 @@ # This file is based in Django Class Views # adapted for use of mongoengine -from django.views.generic.detail import BaseDetailView +from django.views.generic.detail import SingleObjectMixin, BaseDetailView from django.views.generic.edit import FormMixin, ProcessFormView, DeletionMixin from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.views.generic.base import TemplateResponseMixin, View @@ -29,13 +29,11 @@ from django.shortcuts import render from django.contrib import messages -class MongoSingleObjectMixin(object): +class MongoSingleObjectMixin(SingleObjectMixin): """ Provides the ability to retrieve a single object for further manipulation. """ document = None - queryset = None - context_object_name = None def get_object(self, queryset=None): """ @@ -47,13 +45,18 @@ def get_object(self, queryset=None): if queryset is None: queryset = self.get_queryset() - pk = self.kwargs.get('pk', None) + pk = self.kwargs.get(self.pk_url_kwarg, None) + slug = self.kwargs.get(self.slug_url_kwarg, None) if pk is not None: queryset = queryset.filter(pk=pk) + elif slug is not None: + slug_field = self.get_slug_field() + queryset = queryset.filter(**{slug_field: slug}) + else: - raise AttributeError(u"Generic detail view %s must be" - u" called with object pk." + raise AttributeError(u"Generic detail view %s must be called with " + u"either an object pk or a slug." % self.__class__.__name__) try: @@ -79,8 +82,17 @@ def get_queryset(self): }) return self.queryset.clone() - def get_context_data(self, **kwargs): - return kwargs + def get_context_object_name(self, obj): + """ + Get the name to use for the object. + """ + if self.context_object_name: + return self.context_object_name + elif hasattr(obj, '_meta'): + return smart_str(obj.__class__.__name__.lower()) + else: + return None + class MongoMultipleObjectMixin(MultipleObjectMixin): From 96e216c29292a3a0e785f8a9a31a07972787204d Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 3 Oct 2012 12:57:50 +0400 Subject: [PATCH 02/42] Add context_object_name support for MongoFormMixin --- mongotools/views/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mongotools/views/__init__.py b/mongotools/views/__init__.py index 5e58b61..5ff3e69 100644 --- a/mongotools/views/__init__.py +++ b/mongotools/views/__init__.py @@ -215,6 +215,9 @@ def get_context_data(self, **kwargs): context = kwargs if self.object: context['object'] = self.object + context_object_name = self.get_context_object_name(self.object) + if context_object_name: + context[context_object_name] = self.object return context class BaseDetailView(MongoSingleObjectMixin, View): From bbe8a85982db96e58f4776c7f530e250520d52cc Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 3 Oct 2012 12:59:15 +0400 Subject: [PATCH 03/42] Fix StringField regex arg handling --- mongotools/forms/fields.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mongotools/forms/fields.py b/mongotools/forms/fields.py index 267c74f..ca0db39 100644 --- a/mongotools/forms/fields.py +++ b/mongotools/forms/fields.py @@ -218,6 +218,7 @@ def generate_stringfield(self, field, **kwargs): defaults['widget'] = forms.Textarea if field.regex: + form_class = forms.RegexField defaults['regex'] = field.regex elif field.choices: form_class = forms.TypedChoiceField From a506dc8e15970e6df849d6df10cd28665f40d213 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 3 Oct 2012 13:00:37 +0400 Subject: [PATCH 04/42] Fix detail views --- mongotools/views/__init__.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/mongotools/views/__init__.py b/mongotools/views/__init__.py index 5ff3e69..28cde35 100644 --- a/mongotools/views/__init__.py +++ b/mongotools/views/__init__.py @@ -23,6 +23,7 @@ from django.views.generic.detail import SingleObjectMixin, BaseDetailView from django.views.generic.edit import FormMixin, ProcessFormView, DeletionMixin from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist +from django.utils.encoding import smart_str from django.views.generic.base import TemplateResponseMixin, View from django.http import HttpResponseRedirect, Http404 from django.views.generic.list import MultipleObjectMixin @@ -220,18 +221,8 @@ def get_context_data(self, **kwargs): context[context_object_name] = self.object return context -class BaseDetailView(MongoSingleObjectMixin, View): - historic_view_action = None - def get(self, request, **kwargs): - self.object = self.get_object() - context = self.get_context_data(object=self.object) - - if self.historic_view_action: - self.request.user.register_historic( - self.object, - self.historic_view_action) - - return self.render_to_response(context) +class BaseDetailView(MongoSingleObjectMixin, BaseDetailView, View): + pass class BaseCreateView(MongoFormMixin, ProcessFormView): """ @@ -355,9 +346,12 @@ def get_template_names(self): return names class DetailView(MongoSingleObjectTemplateResponseMixin, BaseDetailView): - template_name_suffix = 'detail' - def get_context_data(self, **kwargs): - return kwargs + """ + Render a "detail" view of an object. + + By default this is a model instance looked up from `self.queryset`, but the + view will support display of *any* object by overriding `self.get_object()`. + """ class ListView(MongoMultipleObjectTemplateResponseMixin, BaseListView): """ From 34340d2bcfa9b96bc55959876010fef7c9b86235 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 3 Oct 2012 13:01:11 +0400 Subject: [PATCH 05/42] Remove unnecessary DeletionMixin --- mongotools/views/__init__.py | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/mongotools/views/__init__.py b/mongotools/views/__init__.py index 28cde35..263a33a 100644 --- a/mongotools/views/__init__.py +++ b/mongotools/views/__init__.py @@ -269,40 +269,6 @@ class UpdateView(MongoSingleObjectTemplateResponseMixin, BaseUpdateView): template_name_suffix = 'form' -class DeletionMixin(object): - """ - A mixin providing the ability to delete objects - """ - success_url = None - success_message = None - historic_action = None - - def delete(self, request, *args, **kwargs): - self.object = self.get_object() - msg = None - - if self.success_message: - msg = self.success_message % self.object - - if self.historic_action: - self.request.user.register_historic(self.object, - self.historic_action) - - self.object.delete() - return HttpResponseRedirect(self.get_success_url()) - - # Add support for browsers which only accept GET and POST for now. - def post(self, *args, **kwargs): - return self.delete(*args, **kwargs) - - def get_success_url(self): - if self.success_url: - return self.success_url - else: - raise ImproperlyConfigured( - "No URL to redirect to. Provide a success_url.") - - class BaseDeleteView(DeletionMixin, BaseDetailView): """ Base view for deleting an object. From 2ec70850b2af1b6ca968a67129ba42b52f601899 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 3 Oct 2012 13:07:04 +0400 Subject: [PATCH 06/42] Fix FileField processing --- mongotools/forms/__init__.py | 3 +-- mongotools/forms/fields.py | 1 + mongotools/forms/utils.py | 14 +++++--------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index b45775c..cba44e6 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -119,8 +119,7 @@ def save(self, commit=True): io = self.cleaned_data.get(field_name) if isinstance(io, UploadedFile): - field = save_file(self.instance, field_name, io) - setattr(self.instance, field_name, field) + save_file(self.instance[field_name], io) continue diff --git a/mongotools/forms/fields.py b/mongotools/forms/fields.py index ca0db39..de1eb03 100644 --- a/mongotools/forms/fields.py +++ b/mongotools/forms/fields.py @@ -389,6 +389,7 @@ def generate_filefield(self, field, **kwargs): defaults = { 'required': field.required, 'label': self.get_field_label(field), + 'initial': field.default, 'help_text': self.get_field_help_text(field), } defaults.update(kwargs) diff --git a/mongotools/forms/utils.py b/mongotools/forms/utils.py index 8fae251..f105838 100644 --- a/mongotools/forms/utils.py +++ b/mongotools/forms/utils.py @@ -5,7 +5,6 @@ from django import forms from mongoengine.base import ValidationError from mongoengine.fields import EmbeddedDocumentField, ListField, ReferenceField -from mongoengine.connection import _get_db from fields import MongoFormFieldGenerator @@ -62,8 +61,7 @@ def iter_valid_fields(meta): yield (field_name, field) -def _get_unique_filename(name): - fs = gridfs.GridFS(_get_db()) +def _get_unique_filename(fs, name): file_root, file_ext = os.path.splitext(name) count = itertools.count(1) while fs.exists(filename=name): @@ -71,11 +69,9 @@ def _get_unique_filename(name): name = os.path.join("%s_%s%s" % (file_root, count.next(), file_ext)) return name -def save_file(instance, field_name, file): - field = getattr(instance, field_name) - - filename = _get_unique_filename(file.name) +def save_file(proxy, file): + filename = _get_unique_filename(proxy.fs, file.name) file.file.seek(0) - field.replace(file, content_type=file.content_type, filename=filename) - return field + proxy.replace(file, content_type=file.content_type, filename=filename) + return proxy From 7a36e06131974324b7b92b372d5ceb271f1a2b98 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 3 Oct 2012 13:08:12 +0400 Subject: [PATCH 07/42] Add ClearableFileInput support --- mongotools/forms/__init__.py | 4 +++- mongotools/forms/fields.py | 3 +++ mongotools/forms/widgets.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 mongotools/forms/widgets.py diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index cba44e6..262ca53 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -118,7 +118,9 @@ def save(self, commit=True): if isinstance(self.instance._fields[field_name], FileField): io = self.cleaned_data.get(field_name) - if isinstance(io, UploadedFile): + if io is False: + self.instance[field_name].delete() + elif isinstance(io, UploadedFile): save_file(self.instance[field_name], io) continue diff --git a/mongotools/forms/fields.py b/mongotools/forms/fields.py index de1eb03..354198c 100644 --- a/mongotools/forms/fields.py +++ b/mongotools/forms/fields.py @@ -11,6 +11,8 @@ from mongoengine.fields import ( IntField, SequenceField) +from .widgets import ClearableGridFSFileInput + BLANK_CHOICE_DASH = [("", "---------")] class MongoChoiceIterator(object): @@ -391,6 +393,7 @@ def generate_filefield(self, field, **kwargs): 'label': self.get_field_label(field), 'initial': field.default, 'help_text': self.get_field_help_text(field), + 'widget': ClearableGridFSFileInput, } defaults.update(kwargs) return forms.FileField(**defaults) diff --git a/mongotools/forms/widgets.py b/mongotools/forms/widgets.py new file mode 100644 index 0000000..9761d84 --- /dev/null +++ b/mongotools/forms/widgets.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +from django.forms.widgets import ClearableFileInput, CheckboxInput +from django.utils.html import escape, conditional_escape +from django.utils.encoding import force_unicode +from django.utils.safestring import mark_safe + +from mongoengine.fields import GridFSProxy + + + +class ClearableGridFSFileInput(ClearableFileInput): + + def render(self, name, value, attrs=None): + substitutions = { + 'initial_text': self.initial_text, + 'input_text': self.input_text, + 'clear_template': '', + 'clear_checkbox_label': self.clear_checkbox_label, + } + template = u'%(input)s' + substitutions['input'] = super(ClearableFileInput, self).render(name, value, attrs) + + if value and isinstance(value, GridFSProxy): + file = value.get() + template = self.template_with_initial + substitutions['initial'] = escape(force_unicode(file.name)) + if not self.is_required: + checkbox_name = self.clear_checkbox_name(name) + checkbox_id = self.clear_checkbox_id(checkbox_name) + substitutions['clear_checkbox_name'] = conditional_escape(checkbox_name) + substitutions['clear_checkbox_id'] = conditional_escape(checkbox_id) + substitutions['clear'] = CheckboxInput().render(checkbox_name, False, attrs={'id': checkbox_id}) + substitutions['clear_template'] = self.template_with_clear % substitutions + + return mark_safe(template % substitutions) From 0e60d89bd938003556c2f8211f53f152d8390e5f Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 3 Oct 2012 18:49:52 +0400 Subject: [PATCH 08/42] Fix BaseListView --- mongotools/views/__init__.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/mongotools/views/__init__.py b/mongotools/views/__init__.py index 263a33a..0bf4ff6 100644 --- a/mongotools/views/__init__.py +++ b/mongotools/views/__init__.py @@ -26,7 +26,7 @@ from django.utils.encoding import smart_str from django.views.generic.base import TemplateResponseMixin, View from django.http import HttpResponseRedirect, Http404 -from django.views.generic.list import MultipleObjectMixin +from django.views.generic.list import MultipleObjectMixin, BaseListView from django.shortcuts import render from django.contrib import messages @@ -282,15 +282,8 @@ class DeleteView(MongoSingleObjectTemplateResponseMixin, BaseDeleteView): """ template_name_suffix = 'confirm_delete' -class BaseListView(MongoMultipleObjectMixin, View): - def get(self, request, *args, **kwargs): - self.object_list = self.get_queryset() - allow_empty = self.get_allow_empty() - if not allow_empty and len(self.object_list) == 0: - raise Http404(_(u"Empty list and '%(class_name)s.allow_empty' is False.") - % {'class_name': self.__class__.__name__}) - context = self.get_context_data(object_list=self.object_list) - return self.render_to_response(context) +class BaseListView(MongoMultipleObjectMixin, BaseListView, View): + pass class MongoMultipleObjectTemplateResponseMixin(TemplateResponseMixin): template_name_suffix = 'list' From cee3c37696f776b0ec33a5ad0d00a30fbda9ada7 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 3 Oct 2012 18:42:45 +0400 Subject: [PATCH 09/42] Add ImageField generator --- mongotools/forms/fields.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/mongotools/forms/fields.py b/mongotools/forms/fields.py index 354198c..22642be 100644 --- a/mongotools/forms/fields.py +++ b/mongotools/forms/fields.py @@ -397,3 +397,16 @@ def generate_filefield(self, field, **kwargs): } defaults.update(kwargs) return forms.FileField(**defaults) + + def generate_imagefield(self, field, **kwargs): + defaults = { + 'required':field.required, + 'label':self.get_field_label(field), + 'initial': field.default, + 'help_text': self.get_field_help_text(field), + 'widget': ClearableGridFSFileInput, + } + defaults.update(kwargs) + return forms.ImageField(**defaults) + + From 5493347873a19dcd3e6a3ac83810203aca9111b5 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 3 Oct 2012 19:15:16 +0400 Subject: [PATCH 10/42] Make MongoFormMixin more like ModelFormMixin --- mongotools/views/__init__.py | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/mongotools/views/__init__.py b/mongotools/views/__init__.py index 0bf4ff6..40441bf 100644 --- a/mongotools/views/__init__.py +++ b/mongotools/views/__init__.py @@ -156,11 +156,8 @@ def get_template_names(self): class MongoFormMixin(FormMixin, MongoSingleObjectMixin): """ - A mixin that provides a way to show and handle a mongo in a request. + A mixin that provides a way to show and handle a mongoform in a request. """ - success_message = None - historic_action = None - save_permission = None def get_form_class(self): """ @@ -188,28 +185,11 @@ def get_success_url(self): except AttributeError: raise ImproperlyConfigured( "No URL to redirect to. Either provide a url or define" - " a get_absolute_url method on the Model.") + " a get_absolute_url method on the Document.") return url - def send_messages(self): - if self.success_message: - messages.success(self.request, - self.success_message % self.object) - - def write_historic(self): - if self.historic_action: - self.request.user.register_historic(self.object, - self.historic_action) - def form_valid(self, form): - if self.save_permission: - if not self.request.user.has_perm(self.save_permission): - return render(self.request, 'access_denied.html', locals()) self.object = form.save() - - self.write_historic() - self.send_messages() - return super(MongoFormMixin, self).form_valid(form) def get_context_data(self, **kwargs): From 748a44364d738ddaffd52827131273058f691630 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 3 Oct 2012 19:15:42 +0400 Subject: [PATCH 11/42] Minor fixes --- mongotools/forms/fields.py | 5 ++--- mongotools/views/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/mongotools/forms/fields.py b/mongotools/forms/fields.py index 22642be..8311f4d 100644 --- a/mongotools/forms/fields.py +++ b/mongotools/forms/fields.py @@ -1,5 +1,4 @@ from django import forms -from django.utils.encoding import smart_unicode from pymongo.errors import InvalidId from bson import ObjectId from django.core.validators import EMPTY_VALUES @@ -45,9 +44,9 @@ class ReferenceField(forms.TypedChoiceField): Reference field for mongo forms. Inspired by `django.forms.models.ModelChoiceField`. """ def __init__(self, queryset, empty_label=u"---------", - *aargs, **kwaargs): + *args, **kwargs): - super(ReferenceField, self).__init__(*aargs, **kwaargs) + super(ReferenceField, self).__init__(*args, **kwargs) self.queryset = queryset self.empty_label = empty_label diff --git a/mongotools/views/__init__.py b/mongotools/views/__init__.py index 40441bf..2b04ddc 100644 --- a/mongotools/views/__init__.py +++ b/mongotools/views/__init__.py @@ -117,7 +117,7 @@ def get_queryset(self): class MongoSingleObjectTemplateResponseMixin(TemplateResponseMixin): template_name_field = None - template_name_suffix = '_detail' + template_name_suffix = 'detail' def get_template_names(self): """ @@ -274,7 +274,7 @@ def get_template_names(self): a list. May not be called if get_template is overridden. """ try: - names = TemplateResponseMixin.get_template_names(self) + names = super(MongoMultipleObjectTemplateResponseMixin, self).get_template_names() except ImproperlyConfigured: names = [] From 1553d8ce5cd93af22166e2b518ebf408ff32febc Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 3 Oct 2012 21:52:20 +0400 Subject: [PATCH 12/42] Add .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3267a8b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*~ +*.pyc +/*.egg-info \ No newline at end of file From 92efe1cd687a3f5ee1c8036c3a5ceb9daeaa685c Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 10 Oct 2012 14:46:09 +0400 Subject: [PATCH 13/42] Refactor MongoForm to be more like ModelForm --- mongotools/forms/__init__.py | 308 +++++++++++++++++++++++------------ mongotools/forms/utils.py | 28 ---- 2 files changed, 207 insertions(+), 129 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index 262ca53..29fe9c3 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -1,133 +1,239 @@ import types +from django.core.exceptions import FieldError from django import forms +from django.forms.forms import get_declared_fields +from django.forms.util import ErrorList +from django.forms.widgets import media_property from django.core.files.uploadedfile import UploadedFile from django.utils.datastructures import SortedDict from mongoengine.base import BaseDocument from mongotools.forms.fields import MongoFormFieldGenerator -from mongotools.forms.utils import mongoengine_validate_wrapper, iter_valid_fields, save_file +from mongotools.forms.utils import mongoengine_validate_wrapper, save_file from mongoengine.fields import ReferenceField, FileField, ListField __all__ = ('MongoForm',) + + +def update_instance(form, instance, fields=None, exclude=None): + """ + Updates and returns a document instance from the bound + ``form``'s ``cleaned_data``, but does not save the instance + to the database. + """ + cleaned_data = form.cleaned_data + file_field_list = [] + for field_name, f in instance._fields.items(): + if not field_name in cleaned_data: + continue + if fields is not None and field_name not in fields: + continue + if exclude and field_name in exclude: + continue + if isinstance(f, FileField): + file_field_list.append(f) + else: + instance[field_name] = cleaned_data[field_name] + + # TODO: should anything done with files before saving form? + + return instance + +def save_instance(form, instance, fields=None, exclude=None, commit=True): + """ + Saves bound Form ``form``'s cleaned_data into document instance ``instance``. + + If commit=True, then the changes to ``instance`` will be saved to the + database. Returns ``instance``. + """ + if form.errors: + raise ValueError("The `%s` could not be saved because the data didn't" + " validate." % (instance,)) + + for field_name, f in instance._fields.items(): + if fields is not None and field_name not in fields: + continue + if exclude and field_name in exclude: + continue + if isinstance(f, FileField): + io = form.cleaned_data.get(field_name) + + # FIXME: should it be saved/deleted only if commit is True? + if io is False: + instance[field_name].delete() + elif isinstance(io, UploadedFile): + save_file(instance[field_name], io) + + continue + + if commit: + instance.save() + return instance + +def document_to_dict(instance, fields=None, exclude=None): + """ + Returns a dict containing the data in ``instance`` suitable for passing as + a Form's ``initial`` keyword argument. + + ``fields`` is an optional list of field names. If provided, only the named + fields will be included in the returned dict. + + ``exclude`` is an optional list of field names. If provided, the named + fields will be excluded from the returned dict, even if they are listed in + the ``fields`` argument. + """ + data = {} + for field_name, f in instance._fields.items(): + if fields and not field_name in fields: + continue + if exclude and field_name in exclude: + continue + if isinstance(f, ReferenceField) and instance[field_name]: + data[field_name] = unicode(instance[field_name].id) + else: + data[field_name] = instance[field_name] + return data + +def fields_for_document(document, fields=None, exclude=None, widgets=None, formfield_generator=None): + """ + Returns a ``SortedDict`` containing form fields for the given document. + + ``fields`` is an optional list of field names. If provided, only the named + fields will be included in the returned fields. + + ``exclude`` is an optional list of field names. If provided, the named + fields will be excluded from the returned fields, even if they are listed + in the ``fields`` argument. + """ + # see django.forms.forms.fields_for_model + field_list = [] + ignored = [] + if hasattr(document, '_meta'): + id_field = document._meta.get('id_field') + if id_field not in fields: + if exclude: + exclude += (id_field,) + else: + exclude = [id_field] + doc_fields = document._fields + for field_name, f in sorted(doc_fields.items(), key=lambda t: t[1].creation_counter): + if fields is not None and not field_name in fields: + continue + if exclude and field_name in exclude: + continue + if widgets and field_name in widgets: + kwargs = {'widget': widgets[field_name]} + else: + kwargs = {} + + if not hasattr(formfield_generator, 'generate'): + raise TypeError('formfield_generator must be an object with "generate" method') + else: + formfield = formfield_generator.generate(f, **kwargs) + + if not isinstance(f, FileField): + formfield.clean = mongoengine_validate_wrapper( + f, + formfield.clean, f._validate) + + if formfield: + field_list.append((field_name, formfield)) + else: + ignored.append(field_name) + field_dict = SortedDict(field_list) + if fields: + field_dict = SortedDict( + [(f, field_dict.get(f)) for f in fields + if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored)] + ) + return field_dict + + + +class MongoFormOptions(object): + def __init__(self, options=None): + self.document = getattr(options, 'document', None) + self.fields = getattr(options, 'fields', None) + self.exclude = getattr(options, 'exclude', None) + self.widgets = getattr(options, 'widgets', None) + self.formfield_generator = getattr(options, 'formfield_generator', MongoFormFieldGenerator()) + + class MongoFormMetaClass(type): """Metaclass to create a new MongoForm.""" + # see django.forms.forms.ModelFormMetaclass def __new__(cls, name, bases, attrs): - # get all valid existing Fields and sort them - fields = [(field_name, attrs.pop(field_name)) for field_name, obj in \ - attrs.items() if isinstance(obj, forms.Field)] - fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) - - # get all Fields from base classes - for base in bases[::-1]: - if hasattr(base, 'base_fields'): - fields = base.base_fields.items() + fields - - # add the fields as "our" base fields - attrs['base_fields'] = SortedDict(fields) - - # Meta class available? - if 'Meta' in attrs and hasattr(attrs['Meta'], 'document') and \ - issubclass(attrs['Meta'].document, BaseDocument): - doc_fields = SortedDict() - - formfield_generator = getattr(attrs['Meta'], 'formfield_generator', \ - MongoFormFieldGenerator)() - - widgets = getattr(attrs["Meta"], "widgets", {}) - - # walk through the document fields - for field_name, field in iter_valid_fields(attrs['Meta']): - # add field and override clean method to respect mongoengine-validator - - # use to get a custom widget - if hasattr(field, 'get_custom_widget'): - widget = field.get_custom_widget() - else: - widget = widgets.get(field_name, None) - - if widget: - doc_fields[field_name] = formfield_generator.generate( - field, widget=widget) - else: - doc_fields[field_name] = formfield_generator.generate( - field) - - if not isinstance(field, FileField): - doc_fields[field_name].clean = mongoengine_validate_wrapper( - field, - doc_fields[field_name].clean, field._validate) - - # write the new document fields to base_fields - doc_fields.update(attrs['base_fields']) - attrs['base_fields'] = doc_fields - - # maybe we need the Meta class later - attrs['_meta'] = attrs.get('Meta', object()) - - return super(MongoFormMetaClass, cls).__new__(cls, name, bases, attrs) + try: + parents = [b for b in bases if issubclass(b, MongoForm)] + except NameError: + # We are defining MongoForm itself. + parents = None + new_class = super(MongoFormMetaClass, cls).__new__(cls, name, bases, + attrs) + if not parents: + return new_class + + if 'media' not in attrs: + new_class.media = media_property(new_class) + opts = new_class._meta = MongoFormOptions(getattr(new_class, 'Meta', None)) + declared_fields = get_declared_fields(bases, attrs, False) + if opts.document: + # If a document is defined, extract form fields from it. + fields = fields_for_document(opts.document, opts.fields, + opts.exclude, opts.widgets, opts.formfield_generator) + # make sure fields doesn't specify an invalid field + none_document_fields = [k for k, v in fields.iteritems() if not v] + missing_fields = set(none_document_fields) - \ + set(declared_fields.keys()) + if missing_fields: + message = 'Unknown field(s) (%s) specified for %s' + message = message % (', '.join(missing_fields), + opts.document.__name__) + raise FieldError(message) + # Override default document fields with any custom declared ones + # (plus, include all the other declared fields). + fields.update(declared_fields) + else: + fields = declared_fields + new_class.declared_fields = declared_fields + new_class.base_fields = fields + return new_class + + class MongoForm(forms.BaseForm): """Base MongoForm class. Used to create new MongoForms""" __metaclass__ = MongoFormMetaClass - def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, - error_class=forms.util.ErrorList, label_suffix=':', + def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, + initial=None, error_class=ErrorList, label_suffix=':', empty_permitted=False, instance=None): - """ initialize the form""" - - assert isinstance(instance, (types.NoneType, BaseDocument)), \ - 'instance must be a mongoengine document, not %s' % \ - type(instance).__name__ - - assert hasattr(self, 'Meta'), 'Meta class is needed to use MongoForm' - # new instance or updating an existing one? + opts = self._meta if instance is None: - if self._meta.document is None: - raise ValueError('MongoForm has no document class specified.') - self.instance = self._meta.document() + if opts.document is None: + raise ValueError('MongjForm has no document class specified.') + # if we didn't get an instance, instantiate a new one + self.instance = opts.document() object_data = {} self.instance._adding = True else: self.instance = instance self.instance._adding = False - object_data = {} - - # walk through the document fields - for field_name, field in iter_valid_fields(self._meta): - # add field data if needed - field_data = getattr(instance, field_name) - if isinstance(self._meta.document._fields[field_name], ReferenceField): - # field data could be None for not populated refs - field_data = field_data and str(field_data.id) - object_data[field_name] = field_data - - # additional initial data available? + object_data = document_to_dict(instance, opts.fields, opts.exclude) + # if initial was provided, it should override the values from instance if initial is not None: object_data.update(initial) - self._validate_unique = False - super(MongoForm, self).__init__(data, files, auto_id, prefix, object_data, \ - error_class, label_suffix, empty_permitted) + super(MongoForm, self).__init__(data, files, auto_id, prefix, object_data, + error_class, label_suffix, empty_permitted) + + def _post_clean(self): + opts = self._meta + # Update the document instance with self.cleaned_data. + update_instance(self, self.instance, opts.fields, opts.exclude) def save(self, commit=True): """save the instance or create a new one..""" - # walk through the document fields - for field_name, field in iter_valid_fields(self._meta): - # FileFields need some more work to ensure the filename is unique - if isinstance(self.instance._fields[field_name], FileField): - io = self.cleaned_data.get(field_name) - - if io is False: - self.instance[field_name].delete() - elif isinstance(io, UploadedFile): - save_file(self.instance[field_name], io) - - continue - - setattr(self.instance, field_name, self.cleaned_data.get(field_name)) - - if commit: - self.instance.save() - - return self.instance + opts = self._meta + return save_instance(self, self.instance, opts.fields, opts.exclude, commit) diff --git a/mongotools/forms/utils.py b/mongotools/forms/utils.py index f105838..06c684a 100644 --- a/mongotools/forms/utils.py +++ b/mongotools/forms/utils.py @@ -33,34 +33,6 @@ def inner_validate(value, *args, **kwargs): raise forms.ValidationError(e) return inner_validate -def iter_valid_fields(meta): - """walk through the available valid fields..""" - - # fetch field configuration and always add the id_field as exclude - meta_fields = getattr(meta, 'fields', ()) - meta_exclude = getattr(meta, 'exclude', ()) - - if hasattr(meta.document, '_meta'): - meta_exclude += (meta.document._meta.get('id_field'),) - # walk through the document fields - - for field_name, field in sorted(meta.document._fields.items(), key=lambda t: t[1].creation_counter): - # skip excluded or not explicit included fields - if (meta_fields and field_name not in meta_fields) or field_name in meta_exclude: - continue - - if isinstance(field, EmbeddedDocumentField): #skip EmbeddedDocumentField - continue - - if isinstance(field, ListField): - if hasattr(field.field, 'choices') and not isinstance(field.field, ReferenceField): - if not field.field.choices: - continue - elif not isinstance(field.field, ReferenceField): - continue - - yield (field_name, field) - def _get_unique_filename(fs, name): file_root, file_ext = os.path.splitext(name) count = itertools.count(1) From 67bfb7e052ee1dfde66685bca80eaf2d494708fa Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Tue, 16 Oct 2012 15:33:54 +0400 Subject: [PATCH 14/42] Fix form validation (required and empty values) --- mongotools/forms/utils.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/mongotools/forms/utils.py b/mongotools/forms/utils.py index 06c684a..26752cb 100644 --- a/mongotools/forms/utils.py +++ b/mongotools/forms/utils.py @@ -3,6 +3,7 @@ import gridfs from django import forms +from django.core.validators import EMPTY_VALUES from mongoengine.base import ValidationError from mongoengine.fields import EmbeddedDocumentField, ListField, ReferenceField @@ -12,7 +13,7 @@ def generate_field(field): generator = MongoFormFieldGenerator() return generator.generate(field) -def mongoengine_validate_wrapper(field, old_clean, new_clean): +def mongoengine_validate_wrapper(field, old_clean, new_validate): """ A wrapper function to validate formdata against mongoengine-field validator and raise a proper django.forms ValidationError if there @@ -21,16 +22,18 @@ def mongoengine_validate_wrapper(field, old_clean, new_clean): def inner_validate(value, *args, **kwargs): value = old_clean(value, *args, **kwargs) - if value is None and field.required: - raise ValidationError("This field is required") - - elif value is None: - return value - try: - new_clean(value) - return value - except ValidationError, e: - raise forms.ValidationError(e) + # see: + # django.forms.field.Field.validate + # mongoengine.base.BaseDocument.validate + if value not in EMPTY_VALUES: + try: + new_validate(value) + except ValidationError, e: + raise forms.ValidationError(e) + else: + value = None + + return value return inner_validate def _get_unique_filename(fs, name): From 19ace5d6b210b5b36a2b6d6dc5bcb2e8ceaa6368 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Tue, 16 Oct 2012 16:23:21 +0400 Subject: [PATCH 15/42] Rename MongoForm to DocumentForm, Add intermediate BaseDocumentForm --- mongotools/forms/__init__.py | 32 +++++++++++++++++--------------- mongotools/forms/fields.py | 6 +++--- mongotools/forms/utils.py | 6 ++---- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index 29fe9c3..ae44610 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -7,11 +7,11 @@ from django.core.files.uploadedfile import UploadedFile from django.utils.datastructures import SortedDict from mongoengine.base import BaseDocument -from mongotools.forms.fields import MongoFormFieldGenerator +from mongotools.forms.fields import DocumentFormFieldGenerator from mongotools.forms.utils import mongoengine_validate_wrapper, save_file from mongoengine.fields import ReferenceField, FileField, ListField -__all__ = ('MongoForm',) +__all__ = ('DocumentForm',) @@ -150,33 +150,34 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, formf -class MongoFormOptions(object): +class DocumentFormOptions(object): def __init__(self, options=None): self.document = getattr(options, 'document', None) self.fields = getattr(options, 'fields', None) self.exclude = getattr(options, 'exclude', None) self.widgets = getattr(options, 'widgets', None) - self.formfield_generator = getattr(options, 'formfield_generator', MongoFormFieldGenerator()) + self.embedded_field = getattr(options, 'embedded_field_name', None) + self.formfield_generator = getattr(options, 'formfield_generator', DocumentFormFieldGenerator()) -class MongoFormMetaClass(type): - """Metaclass to create a new MongoForm.""" +class DocumentFormMetaClass(type): + """Metaclass to create a new DocumentForm.""" # see django.forms.forms.ModelFormMetaclass def __new__(cls, name, bases, attrs): try: - parents = [b for b in bases if issubclass(b, MongoForm)] + parents = [b for b in bases if issubclass(b, DocumentForm)] except NameError: - # We are defining MongoForm itself. + # We are defining DocumentForm itself. parents = None - new_class = super(MongoFormMetaClass, cls).__new__(cls, name, bases, + new_class = super(DocumentFormMetaClass, cls).__new__(cls, name, bases, attrs) if not parents: return new_class if 'media' not in attrs: new_class.media = media_property(new_class) - opts = new_class._meta = MongoFormOptions(getattr(new_class, 'Meta', None)) + opts = new_class._meta = DocumentFormOptions(getattr(new_class, 'Meta', None)) declared_fields = get_declared_fields(bases, attrs, False) if opts.document: # If a document is defined, extract form fields from it. @@ -201,10 +202,7 @@ def __new__(cls, name, bases, attrs): return new_class - -class MongoForm(forms.BaseForm): - """Base MongoForm class. Used to create new MongoForms""" - __metaclass__ = MongoFormMetaClass +class BaseDocumentForm(forms.BaseForm): def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList, label_suffix=':', @@ -225,7 +223,7 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, if initial is not None: object_data.update(initial) - super(MongoForm, self).__init__(data, files, auto_id, prefix, object_data, + super(BaseDocumentForm, self).__init__(data, files, auto_id, prefix, object_data, error_class, label_suffix, empty_permitted) def _post_clean(self): @@ -237,3 +235,7 @@ def save(self, commit=True): """save the instance or create a new one..""" opts = self._meta return save_instance(self, self.instance, opts.fields, opts.exclude, commit) + + +class DocumentForm(BaseDocumentForm): + __metaclass__ = DocumentFormMetaClass diff --git a/mongotools/forms/fields.py b/mongotools/forms/fields.py index 8311f4d..2cd818f 100644 --- a/mongotools/forms/fields.py +++ b/mongotools/forms/fields.py @@ -148,13 +148,13 @@ def prepare_value(self, value): return super(DocumentMultipleChoiceField, self).prepare_value(value) -class MongoFormFieldGenerator(object): +class DocumentFormFieldGenerator(object): """This is singleton class generates Django form-fields for mongoengine-fields.""" _instance = None def __new__(cls, *args, **kwargs): if not cls._instance: - cls._instance = super(MongoFormFieldGenerator, cls).__new__( + cls._instance = super(DocumentFormFieldGenerator, cls).__new__( cls, *args, **kwargs) return cls._instance @@ -172,7 +172,7 @@ def generate(self, field, **kwargs): return getattr(self, 'generate_%s' % \ cls.__name__.lower())(field, **kwargs) - raise NotImplementedError('%s is not supported by MongoForm' % \ + raise NotImplementedError('%s is not supported by DocumentForm' % \ field.__class__.__name__) def get_field_choices(self, field, include_blank=True, diff --git a/mongotools/forms/utils.py b/mongotools/forms/utils.py index 26752cb..2aa6d05 100644 --- a/mongotools/forms/utils.py +++ b/mongotools/forms/utils.py @@ -1,16 +1,14 @@ import os import itertools -import gridfs from django import forms from django.core.validators import EMPTY_VALUES from mongoengine.base import ValidationError -from mongoengine.fields import EmbeddedDocumentField, ListField, ReferenceField -from fields import MongoFormFieldGenerator +from fields import DocumentFormFieldGenerator def generate_field(field): - generator = MongoFormFieldGenerator() + generator = DocumentFormFieldGenerator() return generator.generate(field) def mongoengine_validate_wrapper(field, old_clean, new_validate): From 505e6860108b2f25b7e8328107b25264c21f24d5 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Tue, 23 Oct 2012 17:03:50 +0400 Subject: [PATCH 16/42] Add simple EmbeddedDocumentForm --- mongotools/forms/__init__.py | 37 ++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index ae44610..3b6a94a 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -9,7 +9,8 @@ from mongoengine.base import BaseDocument from mongotools.forms.fields import DocumentFormFieldGenerator from mongotools.forms.utils import mongoengine_validate_wrapper, save_file -from mongoengine.fields import ReferenceField, FileField, ListField +from mongoengine.fields import ReferenceField, EmbeddedDocumentField, \ + FileField, ListField __all__ = ('DocumentForm',) @@ -166,7 +167,8 @@ class DocumentFormMetaClass(type): def __new__(cls, name, bases, attrs): try: - parents = [b for b in bases if issubclass(b, DocumentForm)] + parents = [b for b in bases if issubclass(b, DocumentForm) or + issubclass(b, EmbeddedDocumentForm)] except NameError: # We are defining DocumentForm itself. parents = None @@ -239,3 +241,34 @@ def save(self, commit=True): class DocumentForm(BaseDocumentForm): __metaclass__ = DocumentFormMetaClass + + +class EmbeddedDocumentForm(BaseDocumentForm): + __metaclass__ = DocumentFormMetaClass + + def __init__(self, parent_document, *args, **kwargs): + super(EmbeddedDocumentForm, self).__init__(*args, **kwargs) + self.parent_document = parent_document + field_name = self._meta.embedded_field + if field_name is not None and \ + not hasattr(self.parent_document, field_name): + raise FieldError("Parent document must have field %s" % field_name) + # TODO: list fields (append or save at index), dynamic document fields? + self.single_ref = isinstance(self.parent_document._fields[field_name], + EmbeddedDocumentField) + + def save(self, commit=True): + if self.errors: + raise ValueError("The %s could not be saved because the data didn't" + " validate." % self.instance.__class__.__name__) + + val = None + if self.single_ref: + val = self.instance +# l = getattr(self.parent_document, self._meta.embedded_field) +# l.append(self.instance) + setattr(self.parent_document, self._meta.embedded_field, val) + if commit: + self.parent_document.save() + + return self.instance From 058c30f791163b9d32bd7bc0eeb956823c5a33d3 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Tue, 23 Oct 2012 17:04:35 +0400 Subject: [PATCH 17/42] Minor fixes (fields) --- mongotools/forms/fields.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/mongotools/forms/fields.py b/mongotools/forms/fields.py index 2cd818f..5953eda 100644 --- a/mongotools/forms/fields.py +++ b/mongotools/forms/fields.py @@ -151,13 +151,6 @@ def prepare_value(self, value): class DocumentFormFieldGenerator(object): """This is singleton class generates Django form-fields for mongoengine-fields.""" - _instance = None - def __new__(cls, *args, **kwargs): - if not cls._instance: - cls._instance = super(DocumentFormFieldGenerator, cls).__new__( - cls, *args, **kwargs) - return cls._instance - def generate(self, field, **kwargs): """Tries to lookup a matching formfield generator (lowercase field-classname) and raises a NotImplementedError of no generator @@ -177,6 +170,8 @@ def generate(self, field, **kwargs): def get_field_choices(self, field, include_blank=True, blank_choice=BLANK_CHOICE_DASH): + # TODO: mongoengine supports flat list, Django do not + # should it be supported here? first_choice = include_blank and blank_choice or [] return first_choice + list(field.choices) @@ -198,7 +193,8 @@ def boolean_field(self, value): def get_field_label(self, field): if field.verbose_name: return field.verbose_name - return field.name.capitalize() + if field.name: + return field.name.capitalize() def get_field_help_text(self, field): if field.help_text: @@ -385,7 +381,8 @@ def generate_listfield(self, field, **kwargs): defaults.update(kwargs) f = DocumentMultipleChoiceField(field.field.document_type.objects, **defaults) return f - + raise NotImplementedError('Unsupported ListField configuration') + def generate_filefield(self, field, **kwargs): defaults = { 'required': field.required, From 41b5e97bf6cc6f7e0c4e3452c7c074facfabb972 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 7 Nov 2012 13:57:26 +0400 Subject: [PATCH 18/42] Fixes and cleanup --- mongotools/forms/__init__.py | 33 ++++++++++++++++++++++++++------- mongotools/forms/fields.py | 24 ++++++++++++++---------- mongotools/forms/utils.py | 11 +++++++---- mongotools/forms/widgets.py | 4 ++-- 4 files changed, 49 insertions(+), 23 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index 3b6a94a..9cf83ca 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -1,18 +1,18 @@ -import types -from django.core.exceptions import FieldError +from mongoengine.fields import (ReferenceField, EmbeddedDocumentField, + FileField) + +from django.core.exceptions import FieldError, ValidationError, NON_FIELD_ERRORS from django import forms from django.forms.forms import get_declared_fields from django.forms.util import ErrorList from django.forms.widgets import media_property from django.core.files.uploadedfile import UploadedFile from django.utils.datastructures import SortedDict -from mongoengine.base import BaseDocument + from mongotools.forms.fields import DocumentFormFieldGenerator from mongotools.forms.utils import mongoengine_validate_wrapper, save_file -from mongoengine.fields import ReferenceField, EmbeddedDocumentField, \ - FileField, ListField -__all__ = ('DocumentForm',) +__all__ = ('DocumentForm', 'EmbeddedDocumentForm') @@ -212,7 +212,7 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, opts = self._meta if instance is None: if opts.document is None: - raise ValueError('MongjForm has no document class specified.') + raise ValueError('MongoForm has no document class specified.') # if we didn't get an instance, instantiate a new one self.instance = opts.document() object_data = {} @@ -228,11 +228,30 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, super(BaseDocumentForm, self).__init__(data, files, auto_id, prefix, object_data, error_class, label_suffix, empty_permitted) + def _update_errors(self, message_dict): + # see `django.forms.models.BaseModelForm._update_errors` + for k, v in message_dict.items(): + if k != NON_FIELD_ERRORS: + self._errors.setdefault(k, self.error_class()).extend(v) + # Remove the data from the cleaned_data dict since it was invalid + if k in self.cleaned_data: + del self.cleaned_data[k] + if NON_FIELD_ERRORS in message_dict: + messages = message_dict[NON_FIELD_ERRORS] + self._errors.setdefault(NON_FIELD_ERRORS, self.error_class()).extend(messages) + def _post_clean(self): opts = self._meta # Update the document instance with self.cleaned_data. update_instance(self, self.instance, opts.fields, opts.exclude) + # Call the model instance's clean method. + if hasattr(self.instance, 'clean'): + try: + self.instance.clean() + except ValidationError, e: + self._update_errors({NON_FIELD_ERRORS: e.messages}) + def save(self, commit=True): """save the instance or create a new one..""" opts = self._meta diff --git a/mongotools/forms/fields.py b/mongotools/forms/fields.py index 5953eda..32bee53 100644 --- a/mongotools/forms/fields.py +++ b/mongotools/forms/fields.py @@ -1,16 +1,18 @@ -from django import forms -from pymongo.errors import InvalidId from bson import ObjectId +from pymongo.errors import InvalidId + +from mongoengine.fields import (ReferenceField as MongoReferenceField, + IntField, SequenceField) + +from django import forms from django.core.validators import EMPTY_VALUES from django.utils.encoding import smart_unicode, force_unicode +from django.utils.text import capfirst from django.utils.translation import ugettext_lazy as _ -from mongoengine import ReferenceField as MongoReferenceField +from mongotools.forms.widgets import ClearableGridFSFileInput -from mongoengine.fields import ( - IntField, SequenceField) -from .widgets import ClearableGridFSFileInput BLANK_CHOICE_DASH = [("", "---------")] @@ -192,13 +194,13 @@ def boolean_field(self, value): def get_field_label(self, field): if field.verbose_name: - return field.verbose_name + return capfirst(field.verbose_name) if field.name: - return field.name.capitalize() + return capfirst(field.name) def get_field_help_text(self, field): if field.help_text: - return field.help_text.capitalize() + return field.help_text def generate_stringfield(self, field, **kwargs): form_class = MongoCharField @@ -219,7 +221,9 @@ def generate_stringfield(self, field, **kwargs): defaults['regex'] = field.regex elif field.choices: form_class = forms.TypedChoiceField - defaults['choices'] = self.get_field_choices(field) + include_blank = not field.required + defaults['choices'] = self.get_field_choices(field, + include_blank=include_blank) defaults['coerce'] = self.string_field if not field.required: diff --git a/mongotools/forms/utils.py b/mongotools/forms/utils.py index 2aa6d05..2813566 100644 --- a/mongotools/forms/utils.py +++ b/mongotools/forms/utils.py @@ -1,11 +1,14 @@ import os import itertools +from mongoengine import ValidationError + from django import forms from django.core.validators import EMPTY_VALUES -from mongoengine.base import ValidationError -from fields import DocumentFormFieldGenerator +from mongotools.forms.fields import DocumentFormFieldGenerator + + def generate_field(field): generator = DocumentFormFieldGenerator() @@ -21,8 +24,8 @@ def inner_validate(value, *args, **kwargs): value = old_clean(value, *args, **kwargs) # see: - # django.forms.field.Field.validate - # mongoengine.base.BaseDocument.validate + # `django.forms.field.Field.validate` + # `mongoengine.base.BaseDocument.validate` if value not in EMPTY_VALUES: try: new_validate(value) diff --git a/mongotools/forms/widgets.py b/mongotools/forms/widgets.py index 9761d84..c730652 100644 --- a/mongotools/forms/widgets.py +++ b/mongotools/forms/widgets.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- +from mongoengine.fields import GridFSProxy + from django.forms.widgets import ClearableFileInput, CheckboxInput from django.utils.html import escape, conditional_escape from django.utils.encoding import force_unicode from django.utils.safestring import mark_safe -from mongoengine.fields import GridFSProxy - class ClearableGridFSFileInput(ClearableFileInput): From 8f1d651d1e34ee8ef579eb467f50fbedc06bdeb8 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 7 Nov 2012 14:01:31 +0400 Subject: [PATCH 19/42] Add method to get initial value from GridFSProxy in order to override in subclasses --- mongotools/forms/widgets.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mongotools/forms/widgets.py b/mongotools/forms/widgets.py index c730652..7244511 100644 --- a/mongotools/forms/widgets.py +++ b/mongotools/forms/widgets.py @@ -22,9 +22,8 @@ def render(self, name, value, attrs=None): substitutions['input'] = super(ClearableFileInput, self).render(name, value, attrs) if value and isinstance(value, GridFSProxy): - file = value.get() template = self.template_with_initial - substitutions['initial'] = escape(force_unicode(file.name)) + substitutions['initial'] = self.get_proxy_initial(value) if not self.is_required: checkbox_name = self.clear_checkbox_name(name) checkbox_id = self.clear_checkbox_id(checkbox_name) @@ -34,3 +33,7 @@ def render(self, name, value, attrs=None): substitutions['clear_template'] = self.template_with_clear % substitutions return mark_safe(template % substitutions) + + def get_proxy_initial(self, proxy): + file = proxy.get() + return escape(force_unicode(file.name)) From 2389995deaf6bbffd615827821df4bb73939a781 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 7 Nov 2012 14:02:56 +0400 Subject: [PATCH 20/42] Fix MongoChoiceIterator, ReferenceField, DocumentMultipleChoiceField to be more like original equivalents from Django --- mongotools/forms/fields.py | 92 +++++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 36 deletions(-) diff --git a/mongotools/forms/fields.py b/mongotools/forms/fields.py index 32bee53..0125b93 100644 --- a/mongotools/forms/fields.py +++ b/mongotools/forms/fields.py @@ -17,6 +17,8 @@ BLANK_CHOICE_DASH = [("", "---------")] class MongoChoiceIterator(object): + '''See `django.forms.models.ModelChoiceIterator`.''' + def __init__(self, field): self.field = field self.queryset = field.queryset @@ -24,9 +26,16 @@ def __init__(self, field): def __iter__(self): if self.field.empty_label is not None: yield (u"", self.field.empty_label) - - for obj in self.queryset.all(): - yield self.choice(obj) + if self.field.cache_choices: + if self.field.choice_cache is None: + self.field.choice_cache = [ + self.choice(obj) for obj in self.queryset.all() + ] + for choice in self.field.choice_cache: + yield choice + else: + for obj in self.queryset.all(): + yield self.choice(obj) def __len__(self): return len(self.queryset) @@ -41,32 +50,35 @@ def to_python(self, value): return None return smart_unicode(value) -class ReferenceField(forms.TypedChoiceField): +class ReferenceField(forms.ChoiceField): """ - Reference field for mongo forms. Inspired by `django.forms.models.ModelChoiceField`. + Reference field for Mongo forms. Inspired by + `django.forms.models.ModelChoiceField`. """ - def __init__(self, queryset, empty_label=u"---------", - *args, **kwargs): - - super(ReferenceField, self).__init__(*args, **kwargs) + + def __init__(self, queryset, empty_label=u"---------", cache_choices=False, + required=True, initial=None, *args, **kwargs): + if required and (initial is not None): + self.empty_label = None + else: + self.empty_label = empty_label + self.cache_choices = cache_choices + self.coerce = kwargs.pop('coerce', ObjectId) + + # Call Field instead of ChoiceField __init__() because we don't need + # ChoiceField.__init__(). + super(forms.ChoiceField, self).__init__(required, initial=initial, + *args, **kwargs) self.queryset = queryset - self.empty_label = empty_label - - def _get_queryset(self): - return self._queryset - - def prepare_value(self, value): - if hasattr(value, '_meta'): - return value.pk - - return super(ReferenceField, self).prepare_value(value) + self.choice_cache = None def __deepcopy__(self, memo): result = super(forms.ChoiceField, self).__deepcopy__(memo) - result.queryset = result.queryset - result.empty_label = result.empty_label + result.queryset = result.queryset.clone() return result + def _get_queryset(self): + return self._queryset def _set_queryset(self, queryset): self._queryset = queryset @@ -74,11 +86,6 @@ def _set_queryset(self, queryset): queryset = property(_get_queryset, _set_queryset) - def _get_choices(self): - return MongoChoiceIterator(self) - - choices = property(_get_choices, forms.ChoiceField._set_choices) - def label_from_instance(self, obj): """ This method is used to convert objects into strings; it's used to @@ -87,20 +94,33 @@ def label_from_instance(self, obj): """ return smart_unicode(obj) - def clean(self, oid): - if oid in EMPTY_VALUES and not self.required: + def _get_choices(self): + return MongoChoiceIterator(self) + + choices = property(_get_choices, forms.ChoiceField._set_choices) + + def prepare_value(self, value): + if hasattr(value, '_meta'): + return value.pk + return super(ReferenceField, self).prepare_value(value) + + def clean(self, value): + if value in EMPTY_VALUES: + if self.required: + raise forms.ValidationError(self.error_messages['required']) return None try: - if self.coerce != int: - oid = ObjectId(oid) - - oid = super(ReferenceField, self).clean(oid) + value = self.coerce(value) + value = super(ReferenceField, self).clean(value) queryset = self.queryset.clone() - obj = queryset.get(pk=oid) - except (TypeError, InvalidId, self.queryset._document.DoesNotExist): - raise forms.ValidationError(self.error_messages['invalid_choice'] % {'value': oid}) + obj = queryset.get(pk=value) + except (ValueError, TypeError, InvalidId, + self.queryset._document.DoesNotExist): + raise forms.ValidationError(self.error_messages['invalid_choice'] % + {'value': value}) + self.run_validators(value) return obj class DocumentMultipleChoiceField(ReferenceField): @@ -139,7 +159,7 @@ def clean(self, value): for val in value: if force_unicode(val) not in pks: raise forms.ValidationError(self.error_messages['invalid_choice'] % val) - # Since this overrides the inherited ModelChoiceField.clean + # Since this overrides the inherited ReferenceField.clean # we run custom validators here self.run_validators(value) return list(qs) From 0cdf2ea878cf0776c300032944e5e317699ddc2e Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 7 Nov 2012 14:10:49 +0400 Subject: [PATCH 21/42] Update AUTHORS --- AUTHORS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 2ee135f..413a4a6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,2 +1,3 @@ Stephan Jaekel -Wilson Pinto Júnior \ No newline at end of file +Wilson Pinto Júnior +Aleksey A. Porfirov From 4fc8ac82aa67b77da3933d67c99574232d0aa237 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 7 Nov 2012 20:22:18 +0400 Subject: [PATCH 22/42] Use Document clean method See MongoEngine/mongoengine#60 --- mongotools/forms/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index 9cf83ca..e6a1f4d 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -1,7 +1,8 @@ +import mongoengine from mongoengine.fields import (ReferenceField, EmbeddedDocumentField, FileField) -from django.core.exceptions import FieldError, ValidationError, NON_FIELD_ERRORS +from django.core.exceptions import FieldError, NON_FIELD_ERRORS from django import forms from django.forms.forms import get_declared_fields from django.forms.util import ErrorList @@ -246,11 +247,10 @@ def _post_clean(self): update_instance(self, self.instance, opts.fields, opts.exclude) # Call the model instance's clean method. - if hasattr(self.instance, 'clean'): - try: - self.instance.clean() - except ValidationError, e: - self._update_errors({NON_FIELD_ERRORS: e.messages}) + try: + self.instance.clean() + except mongoengine.ValidationError, e: + self._update_errors({NON_FIELD_ERRORS: [e.message]}) def save(self, commit=True): """save the instance or create a new one..""" From f495c6475bc2e61fe0606dd3e2df49b75d466458 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Tue, 13 Nov 2012 12:55:16 +0400 Subject: [PATCH 23/42] Keep "clean" method existence check --- mongotools/forms/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index e6a1f4d..eddd0df 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -246,11 +246,12 @@ def _post_clean(self): # Update the document instance with self.cleaned_data. update_instance(self, self.instance, opts.fields, opts.exclude) - # Call the model instance's clean method. - try: - self.instance.clean() - except mongoengine.ValidationError, e: - self._update_errors({NON_FIELD_ERRORS: [e.message]}) + if hasattr(self.instance, 'clean'): + # Call the model instance's clean method (mongoengine 0.8+) + try: + self.instance.clean() + except mongoengine.ValidationError, e: + self._update_errors({NON_FIELD_ERRORS: [e.message]}) def save(self, commit=True): """save the instance or create a new one..""" From b74d0d5caacaee9839ac5409b83dd780d6750770 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Sat, 17 Nov 2012 19:54:51 +0400 Subject: [PATCH 24/42] Change Form generation, Fixes for DocumentFormFieldGenerator `fields_for_document` now returns `None` for fields not supported by formfield generator. Such fields are filtered or replaced later by declared fields in `DocumentFormMetaClass`. --- mongotools/forms/__init__.py | 22 ++++++++++++++++------ mongotools/forms/fields.py | 19 ++++++++----------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index eddd0df..6f6a972 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -98,7 +98,9 @@ def document_to_dict(instance, fields=None, exclude=None): def fields_for_document(document, fields=None, exclude=None, widgets=None, formfield_generator=None): """ - Returns a ``SortedDict`` containing form fields for the given document. + Returns a `SortedDict` containing form fields for the given document. + Uses `None` for fields not supported by ``formfield_generator``. + Such fields should be filtered or replaced later. ``fields`` is an optional list of field names. If provided, only the named fields will be included in the returned fields. @@ -130,16 +132,19 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, formf if not hasattr(formfield_generator, 'generate'): raise TypeError('formfield_generator must be an object with "generate" method') - else: + + try: formfield = formfield_generator.generate(f, **kwargs) + except NotImplementedError: + formfield = False - if not isinstance(f, FileField): + if formfield and not isinstance(f, FileField): formfield.clean = mongoengine_validate_wrapper( f, formfield.clean, f._validate) - if formfield: - field_list.append((field_name, formfield)) + if formfield is not None: + field_list.append((field_name, formfield or None)) else: ignored.append(field_name) field_dict = SortedDict(field_list) @@ -198,6 +203,11 @@ def __new__(cls, name, bases, attrs): # Override default document fields with any custom declared ones # (plus, include all the other declared fields). fields.update(declared_fields) + # filter fields not supported by ``formfield_generator`` and not + # replaced by ``declared_fields`` + for n, f in fields.items(): + if not f: + del fields[n] else: fields = declared_fields new_class.declared_fields = declared_fields @@ -213,7 +223,7 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, opts = self._meta if instance is None: if opts.document is None: - raise ValueError('MongoForm has no document class specified.') + raise ValueError('DocumentForm has no document class specified.') # if we didn't get an instance, instantiate a new one self.instance = opts.document() object_data = {} diff --git a/mongotools/forms/fields.py b/mongotools/forms/fields.py index 0125b93..c3661e4 100644 --- a/mongotools/forms/fields.py +++ b/mongotools/forms/fields.py @@ -171,24 +171,21 @@ def prepare_value(self, value): class DocumentFormFieldGenerator(object): - """This is singleton class generates Django form-fields for mongoengine-fields.""" + """Class that generates Django form fields for MongoEngine fields.""" def generate(self, field, **kwargs): """Tries to lookup a matching formfield generator (lowercase - field-classname) and raises a NotImplementedError of no generator + field-classname) or raises a NotImplementedError if no generator can be found. + Formfield generator returns either Field instance or None if field + must be ignored (e.g. `AutoField` in Django). """ - if hasattr(self, 'generate_%s' % field.__class__.__name__.lower()): - return getattr(self, 'generate_%s' % \ - field.__class__.__name__.lower())(field, **kwargs) - else: - for cls in field.__class__.__bases__: - if hasattr(self, 'generate_%s' % cls.__name__.lower()): - return getattr(self, 'generate_%s' % \ - cls.__name__.lower())(field, **kwargs) - + if not hasattr(self, 'generate_%s' % field.__class__.__name__.lower()): raise NotImplementedError('%s is not supported by DocumentForm' % \ field.__class__.__name__) + + return getattr(self, 'generate_%s' % \ + field.__class__.__name__.lower())(field, **kwargs) def get_field_choices(self, field, include_blank=True, blank_choice=BLANK_CHOICE_DASH): From 5cfde5d3faeec2849606c4c5d98eb3f1273c1028 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Sat, 17 Nov 2012 20:12:20 +0400 Subject: [PATCH 25/42] Enhance EmbeddedDocumentForm ``parent_document`` argument is not obligatory anymore. It can be automatically set if embedded doc has a parent reference (MongoEngine/mongoengine#139). Add basic support for ListField(EmbeddedDocumentField). --- mongotools/forms/__init__.py | 64 ++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index 6f6a972..af5f721 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -1,6 +1,6 @@ import mongoengine from mongoengine.fields import (ReferenceField, EmbeddedDocumentField, - FileField) + ListField, FileField) from django.core.exceptions import FieldError, NON_FIELD_ERRORS from django import forms @@ -276,29 +276,65 @@ class DocumentForm(BaseDocumentForm): class EmbeddedDocumentForm(BaseDocumentForm): __metaclass__ = DocumentFormMetaClass - def __init__(self, parent_document, *args, **kwargs): - super(EmbeddedDocumentForm, self).__init__(*args, **kwargs) + def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, + initial=None, error_class=ErrorList, label_suffix=':', + empty_permitted=False, instance=None, parent_document=None): + super(EmbeddedDocumentForm, self).__init__(data, files, auto_id, + prefix, initial, error_class, label_suffix, empty_permitted, instance) + if (not parent_document and hasattr(self.instance, '_instance') and + self.instance._instance is not None): + parent_document = self.instance._instance self.parent_document = parent_document + + def _get_parent_document(self): + return self._parent_document + + def _set_parent_document(self, doc): + self._parent_document = doc + if not self._parent_document: + return field_name = self._meta.embedded_field if field_name is not None and \ - not hasattr(self.parent_document, field_name): + not hasattr(self._parent_document, field_name): raise FieldError("Parent document must have field %s" % field_name) - # TODO: list fields (append or save at index), dynamic document fields? - self.single_ref = isinstance(self.parent_document._fields[field_name], - EmbeddedDocumentField) + + parent_document = property(_get_parent_document, _set_parent_document) def save(self, commit=True): + doc_cls = self._meta.document.__name__ if self.errors: raise ValueError("The %s could not be saved because the data didn't" - " validate." % self.instance.__class__.__name__) + " validate." % doc_cls) + if not self.parent_document: + raise ValueError("The %s could not be saved because the parent" + " document is not assigned." + % doc_cls) - val = None - if self.single_ref: + field_name = self._meta.embedded_field + if not field_name: + raise ValueError("The %s could not be saved because the parent" + " document field is not defined." + % doc_cls) + + parent_field = self._parent_document._fields[field_name] + if isinstance(parent_field, EmbeddedDocumentField): val = self.instance -# l = getattr(self.parent_document, self._meta.embedded_field) -# l.append(self.instance) - setattr(self.parent_document, self._meta.embedded_field, val) + setattr(self.parent_document, self._meta.embedded_field, val) + elif isinstance(parent_field, ListField): + l = getattr(self.parent_document, self._meta.embedded_field) + l.append(self.instance) + else: + raise NotImplementedError("The %s could not be saved because the parent" + " document field type %s is not supported." + % (doc_cls, parent_field.__name__)) + if commit: - self.parent_document.save() + doc = self.parent_document + # try to reach parent `Document` instance if nested + # `EmbeddedDocument`s used + while (not hasattr(doc, 'save') and hasattr(doc, '_instance') and + doc._instance is not None): + doc = doc._instance + doc.save() return self.instance From ffb79162fd8bdc96286727c38d6e4de4779ac049 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Sat, 17 Nov 2012 22:00:58 +0400 Subject: [PATCH 26/42] Fix fields_for_document, Create default generator instance --- mongotools/forms/__init__.py | 9 ++++++--- mongotools/forms/fields.py | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index af5f721..ed85b8a 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -10,7 +10,7 @@ from django.core.files.uploadedfile import UploadedFile from django.utils.datastructures import SortedDict -from mongotools.forms.fields import DocumentFormFieldGenerator +from mongotools.forms.fields import default_generator from mongotools.forms.utils import mongoengine_validate_wrapper, save_file __all__ = ('DocumentForm', 'EmbeddedDocumentForm') @@ -114,7 +114,7 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, formf ignored = [] if hasattr(document, '_meta'): id_field = document._meta.get('id_field') - if id_field not in fields: + if fields is None or id_field not in fields: if exclude: exclude += (id_field,) else: @@ -130,6 +130,9 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, formf else: kwargs = {} + if formfield_generator is None: + formfield_generator = default_generator + if not hasattr(formfield_generator, 'generate'): raise TypeError('formfield_generator must be an object with "generate" method') @@ -164,7 +167,7 @@ def __init__(self, options=None): self.exclude = getattr(options, 'exclude', None) self.widgets = getattr(options, 'widgets', None) self.embedded_field = getattr(options, 'embedded_field_name', None) - self.formfield_generator = getattr(options, 'formfield_generator', DocumentFormFieldGenerator()) + self.formfield_generator = getattr(options, 'formfield_generator', None) class DocumentFormMetaClass(type): diff --git a/mongotools/forms/fields.py b/mongotools/forms/fields.py index c3661e4..6483d27 100644 --- a/mongotools/forms/fields.py +++ b/mongotools/forms/fields.py @@ -427,3 +427,5 @@ def generate_imagefield(self, field, **kwargs): return forms.ImageField(**defaults) +# common single instance +default_generator = DocumentFormFieldGenerator() From bead24e6c3d7869a4f6ee28e1c4b42570d97c85f Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Sat, 17 Nov 2012 22:01:07 +0400 Subject: [PATCH 27/42] Add documentform_factory --- mongotools/forms/__init__.py | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index ed85b8a..aeae571 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -341,3 +341,46 @@ def save(self, commit=True): doc.save() return self.instance + + + +def documentform_factory(document, form=DocumentForm, fields=None, exclude=None, + widgets=None, formfield_generator=None, embedded_field=None): + # see: `django.forms.models.modelform_factory` + + # Create the inner Meta class. + + # Build up a list of attributes that the Meta object will have. + attrs = {'document': document} + if fields is not None: + attrs['fields'] = fields + if exclude is not None: + attrs['exclude'] = exclude + if widgets is not None: + attrs['widgets'] = widgets + if formfield_generator is not None: + attrs['formfield_generator'] = formfield_generator + if embedded_field is not None: + attrs['embedded_field'] = embedded_field + + # If parent form class already has an inner Meta, the Meta we're + # creating needs to inherit from the parent's inner meta. + parent = (object,) + if hasattr(form, 'Meta'): + parent = (form.Meta, object) + Meta = type('Meta', parent, attrs) + + # Give this new form class a reasonable name. + class_name = document.__name__ + 'Form' + + # Class attributes for the new form class. + form_class_attrs = { + 'Meta': Meta, + } + + form_metaclass = DocumentFormMetaClass + + if issubclass(form, BaseDocumentForm) and hasattr(form, '__metaclass__'): + form_metaclass = form.__metaclass__ + + return form_metaclass(class_name, (form,), form_class_attrs) From 770fba4dc4c3ee711181466a4c49cfe216c7c216 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Sun, 18 Nov 2012 18:00:01 +0400 Subject: [PATCH 28/42] Fix form field kwargs generation --- mongotools/forms/fields.py | 176 +++++++++++++------------------------ 1 file changed, 63 insertions(+), 113 deletions(-) diff --git a/mongotools/forms/fields.py b/mongotools/forms/fields.py index 6483d27..28f4004 100644 --- a/mongotools/forms/fields.py +++ b/mongotools/forms/fields.py @@ -219,13 +219,18 @@ def get_field_help_text(self, field): if field.help_text: return field.help_text + def get_common_kwargs(self, field): + return { + 'required': field.required, + 'initial': field.default, + 'label': self.get_field_label(field), + 'help_text': self.get_field_help_text(field), + } + def generate_stringfield(self, field, **kwargs): form_class = MongoCharField - defaults = {'label': self.get_field_label(field), - 'initial': field.default, - 'required': field.required, - 'help_text': self.get_field_help_text(field)} + defaults = self.get_common_kwargs(field) if field.max_length and not field.choices: defaults['max_length'] = field.max_length @@ -250,126 +255,84 @@ def generate_stringfield(self, field, **kwargs): return form_class(**defaults) def generate_emailfield(self, field, **kwargs): - defaults = { - 'required': field.required, + defaults = self.get_common_kwargs(field) + defaults.update({ 'min_length': field.min_length, 'max_length': field.max_length, - 'initial': field.default, - 'label': self.get_field_label(field), - 'help_text': self.get_field_help_text(field) - } - - defaults.update(kwargs) + }, **kwargs) + return forms.EmailField(**defaults) def generate_urlfield(self, field, **kwargs): - defaults = { - 'required': field.required, + defaults = self.get_common_kwargs(field) + defaults.update({ 'min_length': field.min_length, 'max_length': field.max_length, - 'initial': field.default, - 'label': self.get_field_label(field), - 'help_text': self.get_field_help_text(field) - } - - defaults.update(kwargs) + }, **kwargs) + return forms.URLField(**defaults) def generate_intfield(self, field, **kwargs): + defaults = self.get_common_kwargs(field) + if field.choices: - defaults = { + defaults.update({ 'coerce': self.integer_field, 'empty_value': None, - 'required': field.required, - 'initial': field.default, - 'label': self.get_field_label(field), 'choices': self.get_field_choices(field), - 'help_text': self.get_field_help_text(field) - } - - defaults.update(kwargs) + }, **kwargs) + return forms.TypedChoiceField(**defaults) else: - defaults = { - 'required': field.required, + defaults.update({ 'min_value': field.min_value, 'max_value': field.max_value, - 'initial': field.default, - 'label': self.get_field_label(field), - 'help_text': self.get_field_help_text(field) - } - - defaults.update(kwargs) + }, **kwargs) + return forms.IntegerField(**defaults) def generate_floatfield(self, field, **kwargs): + defaults = self.get_common_kwargs(field) + defaults.update({ + 'min_value': field.min_value, + 'max_value': field.max_value, + }, **kwargs) - form_class = forms.FloatField - - defaults = {'label': self.get_field_label(field), - 'initial': field.default, - 'required': field.required, - 'min_value': field.min_value, - 'max_value': field.max_value, - 'help_text': self.get_field_help_text(field)} - - defaults.update(kwargs) - return form_class(**defaults) + return forms.FloatField(**defaults) def generate_decimalfield(self, field, **kwargs): - form_class = forms.DecimalField - defaults = {'label': self.get_field_label(field), - 'initial': field.default, - 'required': field.required, - 'min_value': field.min_value, - 'max_value': field.max_value, - 'help_text': self.get_field_help_text(field)} + defaults = self.get_common_kwargs(field) + defaults.update({ + 'min_value': field.min_value, + 'max_value': field.max_value, + }, **kwargs) - defaults.update(kwargs) - return form_class(**defaults) + return forms.DecimalField(**defaults) def generate_booleanfield(self, field, **kwargs): + defaults = self.get_common_kwargs(field) + if field.choices: - defaults = { + defaults.update({ 'coerce': self.boolean_field, 'empty_value': None, - 'required': field.required, - 'initial': field.default, - 'label': self.get_field_label(field), 'choices': self.get_field_choices(field), - 'help_text': self.get_field_help_text(field) - } - - defaults.update(kwargs) + }, **kwargs) + return forms.TypedChoiceField(**defaults) else: - defaults = { - 'required': field.required, - 'initial': field.default, - 'label': self.get_field_label(field), - 'help_text': self.get_field_help_text(field) - } - defaults.update(kwargs) + return forms.BooleanField(**defaults) def generate_datetimefield(self, field, **kwargs): - defaults = { - 'required': field.required, - 'initial': field.default, - 'label': self.get_field_label(field), - } - + defaults = self.get_common_kwargs(field) defaults.update(kwargs) + return forms.DateTimeField(**defaults) def generate_referencefield(self, field, **kwargs): - defaults = { - 'label': self.get_field_label(field), - 'help_text': self.get_field_help_text(field), - 'required': field.required - } - + defaults = self.get_common_kwargs(field) defaults.update(kwargs) id_field_name = field.document_type._meta['id_field'] @@ -381,49 +344,36 @@ def generate_referencefield(self, field, **kwargs): return ReferenceField(field.document_type.objects, **defaults) def generate_listfield(self, field, **kwargs): + defaults = self.get_common_kwargs(field) if field.field.choices: - defaults = { + defaults.update({ 'choices': field.field.choices, - 'required': field.required, - 'label': self.get_field_label(field), - 'help_text': self.get_field_help_text(field), - 'widget': forms.CheckboxSelectMultiple - } - - defaults.update(kwargs) + 'widget': forms.CheckboxSelectMultiple, + }, **kwargs) + return forms.MultipleChoiceField(**defaults) elif isinstance(field.field, MongoReferenceField): - defaults = { - 'label': self.get_field_label(field), - 'help_text': self.get_field_help_text(field), - 'required': field.required - } - defaults.update(kwargs) - f = DocumentMultipleChoiceField(field.field.document_type.objects, **defaults) - return f + + return DocumentMultipleChoiceField( + field.field.document_type.objects, **defaults) + raise NotImplementedError('Unsupported ListField configuration') def generate_filefield(self, field, **kwargs): - defaults = { - 'required': field.required, - 'label': self.get_field_label(field), - 'initial': field.default, - 'help_text': self.get_field_help_text(field), + defaults = self.get_common_kwargs(field) + defaults.update({ 'widget': ClearableGridFSFileInput, - } - defaults.update(kwargs) + }, **kwargs) + return forms.FileField(**defaults) def generate_imagefield(self, field, **kwargs): - defaults = { - 'required':field.required, - 'label':self.get_field_label(field), - 'initial': field.default, - 'help_text': self.get_field_help_text(field), + defaults = self.get_common_kwargs(field) + defaults.update({ 'widget': ClearableGridFSFileInput, - } - defaults.update(kwargs) + }, **kwargs) + return forms.ImageField(**defaults) From 668f11dfca72b00698bbb74acf966dcca8e90c44 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Sun, 18 Nov 2012 22:22:10 +0400 Subject: [PATCH 29/42] Fix Meta option "embedded_field" name --- mongotools/forms/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index aeae571..197c491 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -166,7 +166,7 @@ def __init__(self, options=None): self.fields = getattr(options, 'fields', None) self.exclude = getattr(options, 'exclude', None) self.widgets = getattr(options, 'widgets', None) - self.embedded_field = getattr(options, 'embedded_field_name', None) + self.embedded_field = getattr(options, 'embedded_field', None) self.formfield_generator = getattr(options, 'formfield_generator', None) @@ -304,7 +304,9 @@ def _set_parent_document(self, doc): parent_document = property(_get_parent_document, _set_parent_document) def save(self, commit=True): - doc_cls = self._meta.document.__name__ + opts = self._meta + doc_cls = opts.document.__name__ + if self.errors: raise ValueError("The %s could not be saved because the data didn't" " validate." % doc_cls) @@ -313,7 +315,7 @@ def save(self, commit=True): " document is not assigned." % doc_cls) - field_name = self._meta.embedded_field + field_name = opts.embedded_field if not field_name: raise ValueError("The %s could not be saved because the parent" " document field is not defined." @@ -322,9 +324,9 @@ def save(self, commit=True): parent_field = self._parent_document._fields[field_name] if isinstance(parent_field, EmbeddedDocumentField): val = self.instance - setattr(self.parent_document, self._meta.embedded_field, val) + setattr(self.parent_document, opts.embedded_field, val) elif isinstance(parent_field, ListField): - l = getattr(self.parent_document, self._meta.embedded_field) + l = getattr(self.parent_document, opts.embedded_field) l.append(self.instance) else: raise NotImplementedError("The %s could not be saved because the parent" From d4517bf9246d66326d0dd697f61b2b4143180042 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Sun, 18 Nov 2012 23:19:12 +0400 Subject: [PATCH 30/42] Fix clean wrapper --- mongotools/forms/__init__.py | 7 +++---- mongotools/forms/utils.py | 6 ++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index 197c491..371204f 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -11,7 +11,7 @@ from django.utils.datastructures import SortedDict from mongotools.forms.fields import default_generator -from mongotools.forms.utils import mongoengine_validate_wrapper, save_file +from mongotools.forms.utils import mongoengine_clean_wrapper, save_file __all__ = ('DocumentForm', 'EmbeddedDocumentForm') @@ -142,9 +142,8 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, formf formfield = False if formfield and not isinstance(f, FileField): - formfield.clean = mongoengine_validate_wrapper( - f, - formfield.clean, f._validate) + formfield.clean = mongoengine_clean_wrapper(formfield.clean, f, + f._validate) if formfield is not None: field_list.append((field_name, formfield or None)) diff --git a/mongotools/forms/utils.py b/mongotools/forms/utils.py index 2813566..7fc89b9 100644 --- a/mongotools/forms/utils.py +++ b/mongotools/forms/utils.py @@ -1,5 +1,6 @@ import os import itertools +from functools import wraps from mongoengine import ValidationError @@ -14,14 +15,15 @@ def generate_field(field): generator = DocumentFormFieldGenerator() return generator.generate(field) -def mongoengine_validate_wrapper(field, old_clean, new_validate): +def mongoengine_clean_wrapper(orig_clean, field, new_validate): """ A wrapper function to validate formdata against mongoengine-field validator and raise a proper django.forms ValidationError if there are any problems. """ + @wraps(orig_clean) def inner_validate(value, *args, **kwargs): - value = old_clean(value, *args, **kwargs) + value = orig_clean(value, *args, **kwargs) # see: # `django.forms.field.Field.validate` From 4df75802a54ac3b9d739d45c9c27d710eb767421 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Mon, 19 Nov 2012 00:51:13 +0400 Subject: [PATCH 31/42] Fix file upload/save/delete support --- mongotools/forms/__init__.py | 85 ++++++++++++++++++++++++++---------- mongotools/forms/utils.py | 7 +++ 2 files changed, 70 insertions(+), 22 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index 371204f..b92172c 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -1,3 +1,5 @@ +from functools import wraps + import mongoengine from mongoengine.fields import (ReferenceField, EmbeddedDocumentField, ListField, FileField) @@ -7,11 +9,11 @@ from django.forms.forms import get_declared_fields from django.forms.util import ErrorList from django.forms.widgets import media_property -from django.core.files.uploadedfile import UploadedFile from django.utils.datastructures import SortedDict from mongotools.forms.fields import default_generator -from mongotools.forms.utils import mongoengine_clean_wrapper, save_file +from mongotools.forms.utils import (mongoengine_clean_wrapper, save_file, + save_file_field) __all__ = ('DocumentForm', 'EmbeddedDocumentForm') @@ -35,9 +37,13 @@ def update_instance(form, instance, fields=None, exclude=None): if isinstance(f, FileField): file_field_list.append(f) else: - instance[field_name] = cleaned_data[field_name] + value = cleaned_data[field_name] + instance[field_name] = value - # TODO: should anything done with files before saving form? + if file_field_list: + instance._file_field_data = data = [] + for f in file_field_list: + data.append((f.name, cleaned_data[f.name])) return instance @@ -52,24 +58,53 @@ def save_instance(form, instance, fields=None, exclude=None, commit=True): raise ValueError("The `%s` could not be saved because the data didn't" " validate." % (instance,)) - for field_name, f in instance._fields.items(): - if fields is not None and field_name not in fields: - continue - if exclude and field_name in exclude: - continue - if isinstance(f, FileField): - io = form.cleaned_data.get(field_name) - - # FIXME: should it be saved/deleted only if commit is True? - if io is False: - instance[field_name].delete() - elif isinstance(io, UploadedFile): - save_file(instance[field_name], io) - - continue + def process_file_field_data(doc): + if hasattr(doc, '_file_field_data'): + for name, val in doc._file_field_data: + save_file_field(val, doc, name) + + def save_files(): + for field_name, f in instance._fields.items(): + if fields is not None and field_name not in fields: + continue + if exclude and field_name in exclude: + continue + + # search for file data in `FileField`s + if isinstance(f, FileField): + value = form.cleaned_data.get(field_name) + save_file_field(value, instance, field_name) + + # search for file data in embedded docs + # with ``_file_field_data`` prop created by forms (subforms) + elif isinstance(f, EmbeddedDocumentField): + doc = instance[field_name] + process_file_field_data(doc) + elif (isinstance(f, ListField) and + isinstance(f.field, EmbeddedDocumentField)): + for doc in instance[field_name]: + process_file_field_data(doc) + + if not hasattr(instance, 'save'): + instance.save_files = save_files + return instance if commit: + save_files() instance.save() + else: + orig_save = instance.save + def save_files_once_wrapper(f): + @wraps(f) + def wrapper(*args, **kwds): + save_files() + instance.save = orig_save + return f(*args, **kwds) + return wrapper + + # save files right before next ``instance.save`` call + instance.save = save_files_once_wrapper(orig_save) + return instance def document_to_dict(instance, fields=None, exclude=None): @@ -305,6 +340,7 @@ def _set_parent_document(self, doc): def save(self, commit=True): opts = self._meta doc_cls = opts.document.__name__ + instance = self.instance if self.errors: raise ValueError("The %s could not be saved because the data didn't" @@ -320,13 +356,15 @@ def save(self, commit=True): " document field is not defined." % doc_cls) + save_instance(self, instance, opts.fields, opts.exclude, commit=False) + parent_field = self._parent_document._fields[field_name] if isinstance(parent_field, EmbeddedDocumentField): - val = self.instance + val = instance setattr(self.parent_document, opts.embedded_field, val) elif isinstance(parent_field, ListField): l = getattr(self.parent_document, opts.embedded_field) - l.append(self.instance) + l.append(instance) else: raise NotImplementedError("The %s could not be saved because the parent" " document field type %s is not supported." @@ -339,9 +377,12 @@ def save(self, commit=True): while (not hasattr(doc, 'save') and hasattr(doc, '_instance') and doc._instance is not None): doc = doc._instance + + if hasattr(instance, 'save_files'): + instance.save_files() doc.save() - return self.instance + return instance diff --git a/mongotools/forms/utils.py b/mongotools/forms/utils.py index 7fc89b9..19429d7 100644 --- a/mongotools/forms/utils.py +++ b/mongotools/forms/utils.py @@ -6,6 +6,7 @@ from django import forms from django.core.validators import EMPTY_VALUES +from django.core.files.uploadedfile import UploadedFile from mongotools.forms.fields import DocumentFormFieldGenerator @@ -53,3 +54,9 @@ def save_file(proxy, file): proxy.replace(file, content_type=file.content_type, filename=filename) return proxy + +def save_file_field(value, instance, field_name): + if value is False: + instance[field_name].delete() + elif isinstance(value, UploadedFile): + save_file(instance[field_name], value) From 0e89d14880fdf8faa9289ee0ca050ab0c0244a7c Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Tue, 20 Nov 2012 16:31:45 +0400 Subject: [PATCH 32/42] Fix for weak ref in EmbeddedDocumentForm --- mongotools/forms/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index b92172c..f08ba60 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -327,6 +327,11 @@ def _get_parent_document(self): return self._parent_document def _set_parent_document(self, doc): +# try: +# # test for weak ref +# bool(doc) +# except ReferenceError: +# doc = None self._parent_document = doc if not self._parent_document: return From a9ac87544f43e8f689f4555c1ac056b1366afde0 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 21 Nov 2012 18:28:26 +0400 Subject: [PATCH 33/42] Fix form field generation Get rid of positional arguments. Use only kwargs to simplify generated fields customization in forms. E.g.: class MyForm(DocumentForm): class Meta: document = MyDoc my_field = default_generator.generate(MyDoc.my_field, formfield_kwarg=custom_value) --- mongotools/forms/fields.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/mongotools/forms/fields.py b/mongotools/forms/fields.py index 28f4004..4bcf085 100644 --- a/mongotools/forms/fields.py +++ b/mongotools/forms/fields.py @@ -333,15 +333,17 @@ def generate_datetimefield(self, field, **kwargs): def generate_referencefield(self, field, **kwargs): defaults = self.get_common_kwargs(field) - defaults.update(kwargs) id_field_name = field.document_type._meta['id_field'] id_field = field.document_type._fields[id_field_name] - if isinstance(id_field, (SequenceField, IntField)): defaults['coerce'] = int - return ReferenceField(field.document_type.objects, **defaults) + defaults.update({ + 'queryset': field.document_type.objects, + }, **kwargs) + + return ReferenceField(**defaults) def generate_listfield(self, field, **kwargs): defaults = self.get_common_kwargs(field) @@ -353,10 +355,11 @@ def generate_listfield(self, field, **kwargs): return forms.MultipleChoiceField(**defaults) elif isinstance(field.field, MongoReferenceField): - defaults.update(kwargs) + defaults.update({ + 'queryset': field.field.document_type.objects, + }, **kwargs) - return DocumentMultipleChoiceField( - field.field.document_type.objects, **defaults) + return DocumentMultipleChoiceField(**defaults) raise NotImplementedError('Unsupported ListField configuration') From 79cdbcbe826423fb16b59bbc7a0d22e08740c4c2 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Mon, 26 Nov 2012 00:14:41 +0400 Subject: [PATCH 34/42] Fix clean wrapper: bound/unbound methods, deepcopy issues Now it's ok to dynamically overwrite field attrs (like `required`) at form initialization. --- mongotools/forms/__init__.py | 5 ++--- mongotools/forms/utils.py | 27 ++++++++++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index f08ba60..83a4649 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -12,7 +12,7 @@ from django.utils.datastructures import SortedDict from mongotools.forms.fields import default_generator -from mongotools.forms.utils import (mongoengine_clean_wrapper, save_file, +from mongotools.forms.utils import (wrap_formfield_clean, save_file, save_file_field) __all__ = ('DocumentForm', 'EmbeddedDocumentForm') @@ -177,8 +177,7 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, formf formfield = False if formfield and not isinstance(f, FileField): - formfield.clean = mongoengine_clean_wrapper(formfield.clean, f, - f._validate) + wrap_formfield_clean(formfield, f) if formfield is not None: field_list.append((field_name, formfield or None)) diff --git a/mongotools/forms/utils.py b/mongotools/forms/utils.py index 19429d7..44efda8 100644 --- a/mongotools/forms/utils.py +++ b/mongotools/forms/utils.py @@ -16,29 +16,38 @@ def generate_field(field): generator = DocumentFormFieldGenerator() return generator.generate(field) -def mongoengine_clean_wrapper(orig_clean, field, new_validate): +def wrap_formfield_clean(formfield, field): """ - A wrapper function to validate formdata against mongoengine-field - validator and raise a proper django.forms ValidationError if there - are any problems. + Wraps ``formfield.clean`` method to validate form data against + MongoEngine field validator and reraise `django.forms.ValidationError`. """ + + orig_clean = formfield.__class__.clean + @wraps(orig_clean) - def inner_validate(value, *args, **kwargs): - value = orig_clean(value, *args, **kwargs) + def do_clean(self, value, *args, **kwargs): + value = orig_clean(self, value, *args, **kwargs) # see: # `django.forms.field.Field.validate` # `mongoengine.base.BaseDocument.validate` if value not in EMPTY_VALUES: try: - new_validate(value) + field._validate(value) except ValidationError, e: raise forms.ValidationError(e) else: value = None - return value - return inner_validate + + formfield.clean = do_clean.__get__(formfield, formfield.__class__) + + orig_deepcopy = formfield.__deepcopy__ + def new_deep_copy(memo): + result = orig_deepcopy(memo) + result.clean = do_clean.__get__(result, result.__class__) + return result + formfield.__deepcopy__ = new_deep_copy def _get_unique_filename(fs, name): file_root, file_ext = os.path.splitext(name) From 55c5d962f1e999c74a5e9df51d65a6a01be34227 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 5 Dec 2012 15:00:05 +0400 Subject: [PATCH 35/42] Uncomment accidentally commented code --- mongotools/forms/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index 83a4649..ee6b899 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -326,11 +326,11 @@ def _get_parent_document(self): return self._parent_document def _set_parent_document(self, doc): -# try: -# # test for weak ref -# bool(doc) -# except ReferenceError: -# doc = None + try: + # test for weak ref + bool(doc) + except ReferenceError: + doc = None self._parent_document = doc if not self._parent_document: return From c4b3dc256f7c4bb2774387e46f164790de7d9ea6 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 5 Dec 2012 17:09:53 +0400 Subject: [PATCH 36/42] Revert back to original `construct_instance` name and usage --- mongotools/forms/__init__.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index ee6b899..ea6b38c 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -19,9 +19,9 @@ -def update_instance(form, instance, fields=None, exclude=None): +def construct_instance(form, instance, fields=None, exclude=None): """ - Updates and returns a document instance from the bound + Constructs and returns a document instance from the bound ``form``'s ``cleaned_data``, but does not save the instance to the database. """ @@ -47,13 +47,20 @@ def update_instance(form, instance, fields=None, exclude=None): return instance -def save_instance(form, instance, fields=None, exclude=None, commit=True): +def save_instance(form, instance, fields=None, exclude=None, commit=True, + construct=True): """ Saves bound Form ``form``'s cleaned_data into document instance ``instance``. If commit=True, then the changes to ``instance`` will be saved to the database. Returns ``instance``. + + If construct=False, assume ``instance`` has already been constructed and + just needs to be saved. """ + if construct: + instance = construct_instance(form, instance, fields, exclude) + if form.errors: raise ValueError("The `%s` could not be saved because the data didn't" " validate." % (instance,)) @@ -360,7 +367,8 @@ def save(self, commit=True): " document field is not defined." % doc_cls) - save_instance(self, instance, opts.fields, opts.exclude, commit=False) + save_instance(self, instance, opts.fields, opts.exclude, commit=False, + construct=False) parent_field = self._parent_document._fields[field_name] if isinstance(parent_field, EmbeddedDocumentField): From 7951661a711405de4fdce6be07ffb97d61fa0420 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 5 Dec 2012 17:28:31 +0400 Subject: [PATCH 37/42] Validate doc in `_post_clean` without form field wrappers See Django `ModelForm._post_clean`. --- mongotools/forms/__init__.py | 81 ++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index ea6b38c..fcf93dd 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -5,6 +5,7 @@ ListField, FileField) from django.core.exceptions import FieldError, NON_FIELD_ERRORS +from django.core.validators import EMPTY_VALUES from django import forms from django.forms.forms import get_declared_fields from django.forms.util import ErrorList @@ -12,8 +13,7 @@ from django.utils.datastructures import SortedDict from mongotools.forms.fields import default_generator -from mongotools.forms.utils import (wrap_formfield_clean, save_file, - save_file_field) +from mongotools.forms.utils import save_file, save_file_field __all__ = ('DocumentForm', 'EmbeddedDocumentForm') @@ -98,7 +98,9 @@ def save_files(): if commit: save_files() - instance.save() + # do not validate as it's already done in + # `BaseDocumentForm._post_clean` + instance.save(validate=False) else: orig_save = instance.save def save_files_once_wrapper(f): @@ -183,8 +185,11 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, formf except NotImplementedError: formfield = False - if formfield and not isinstance(f, FileField): - wrap_formfield_clean(formfield, f) + # old way - use formfield wrapper + # new way - validate doc in `BaseDocumentForm._post_clean` and + # update error dict +# if formfield and not isinstance(f, FileField): +# wrap_formfield_clean(formfield, f) if formfield is not None: field_list.append((field_name, formfield or None)) @@ -294,13 +299,73 @@ def _update_errors(self, message_dict): messages = message_dict[NON_FIELD_ERRORS] self._errors.setdefault(NON_FIELD_ERRORS, self.error_class()).extend(messages) + def _get_validation_exclusions(self): + """ + For backwards-compatibility, several types of fields need to be + excluded from model validation. See the following tickets for + details: #12507, #12521, #12553 + """ + exclude = [] + # Build up a list of fields that should be excluded from model field + # validation and unique checks. + for field_name, f in self.instance._fields.items(): + # Exclude fields that aren't on the form. The developer may be + # adding these values to the model after form validation. + if field_name not in self.fields: + exclude.append(field_name) + + # Don't perform model validation on fields that were defined + # manually on the form and excluded via the ModelForm's Meta + # class. See #12901. + elif self._meta.fields and field_name not in self._meta.fields: + exclude.append(field_name) + elif self._meta.exclude and field_name in self._meta.exclude: + exclude.append(field_name) + + # Exclude fields that failed form validation. There's no need for + # the model fields to validate them as well. + elif field_name in self._errors.keys(): + exclude.append(field_name) + + # Exclude empty fields that are not required by the form, if the + # underlying model field is required. This keeps the model field + # from raising a required error. Note: don't exclude the field from + # validation if the model field allows blanks. If it does, the blank + # value may be included in a unique check, so cannot be excluded + # from validation. + else: + form_field = self.fields[field_name] + field_value = self.cleaned_data.get(field_name, None) + if not form_field.required and field_value in EMPTY_VALUES: + exclude.append(field_name) + return exclude + def _post_clean(self): opts = self._meta # Update the document instance with self.cleaned_data. - update_instance(self, self.instance, opts.fields, opts.exclude) + self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude) - if hasattr(self.instance, 'clean'): - # Call the model instance's clean method (mongoengine 0.8+) + # mongoengine 0.8+ + is_clean_supported = hasattr(mongoengine.Document, 'clean') + + # Validate the document instance's fields. + exclude = self._get_validation_exclusions() + validate_kwargs = {} + if is_clean_supported: + validate_kwargs['clean'] = False + try: + self.instance.validate(**validate_kwargs) + except mongoengine.ValidationError, e: + errors = e.errors + used_errors = {} + for field, err in errors.items(): + if field in exclude: + continue + used_errors[field] = [str(err)] + self._update_errors(used_errors) + + # Call the document instance's clean method. + if is_clean_supported: try: self.instance.clean() except mongoengine.ValidationError, e: From a8e62191638a938d4d200a8c66c65796e569dd09 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 5 Dec 2012 17:41:35 +0400 Subject: [PATCH 38/42] Use new document instance for form processing Otherwise we can get unexpected results sometimes. Example: There is a document with "alias" field. Alias is used in all urls incl. update view url. We use UpdateView with DocumentForm, set some field to invalid value and change alias. Our document is not saved because of one invalid field value. Update view is rendered again. But `object` var from context_data is an updated document with another alias. So html form `action` arg is a wrong url pointing to non-existent doc. And we can get 404 error on next form submission. --- mongotools/views/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongotools/views/__init__.py b/mongotools/views/__init__.py index 2b04ddc..4892691 100644 --- a/mongotools/views/__init__.py +++ b/mongotools/views/__init__.py @@ -173,7 +173,7 @@ def get_form_kwargs(self): Returns the keyword arguments for instanciating the form. """ kwargs = super(MongoFormMixin, self).get_form_kwargs() - kwargs.update({'instance': self.object}) + kwargs.update({'instance': self.get_object()}) return kwargs def get_success_url(self): From abb91a589cd2df718f900c5891d4f158757f4524 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 5 Dec 2012 17:44:45 +0400 Subject: [PATCH 39/42] Handle `NotUniqueError` somehow Better than nothing. TODO: implement something like `BaseModelForm.validate_unique`. --- mongotools/forms/__init__.py | 9 ++++++++- mongotools/views/__init__.py | 6 +++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index fcf93dd..43d0656 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -374,7 +374,14 @@ def _post_clean(self): def save(self, commit=True): """save the instance or create a new one..""" opts = self._meta - return save_instance(self, self.instance, opts.fields, opts.exclude, commit) + if not commit: + return save_instance(self, self.instance, opts.fields, opts.exclude, commit) + try: + doc = save_instance(self, self.instance, opts.fields, opts.exclude, commit) + except mongoengine.NotUniqueError, e: + self._update_errors({NON_FIELD_ERRORS: [e.message]}) + return None + return doc class DocumentForm(BaseDocumentForm): diff --git a/mongotools/views/__init__.py b/mongotools/views/__init__.py index 4892691..3041d96 100644 --- a/mongotools/views/__init__.py +++ b/mongotools/views/__init__.py @@ -189,7 +189,11 @@ def get_success_url(self): return url def form_valid(self, form): - self.object = form.save() + instance = form.save() + if instance is None: + # see `BaseDocumentForm.save` + return super(MongoFormMixin, self).form_invalid(form) + self.object = instance return super(MongoFormMixin, self).form_valid(form) def get_context_data(self, **kwargs): From 89d2a1a19f0124fde3322d9766d3b43bd7c43b59 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Wed, 5 Dec 2012 17:50:06 +0400 Subject: [PATCH 40/42] Update TODO --- TODO | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TODO b/TODO index 387e506..eba0820 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,3 @@ -* Generate view with only document class -* write a full documentation -* support slug fields +* add documentation +* add tests +* implement `BaseDocumentForm.validate_unique` (like `BaseModelForm.validate_unique`) From 9b702b34180285efe1216dbbb05e873cca9fcf7b Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Thu, 6 Dec 2012 20:10:48 +0400 Subject: [PATCH 41/42] Fix getting new document instance --- mongotools/views/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mongotools/views/__init__.py b/mongotools/views/__init__.py index 3041d96..103ebfe 100644 --- a/mongotools/views/__init__.py +++ b/mongotools/views/__init__.py @@ -173,7 +173,10 @@ def get_form_kwargs(self): Returns the keyword arguments for instanciating the form. """ kwargs = super(MongoFormMixin, self).get_form_kwargs() - kwargs.update({'instance': self.get_object()}) + obj = self.object + if obj is not None: + obj = self.get_object() # get copy for form processing + kwargs.update({'instance': obj}) return kwargs def get_success_url(self): From 9e9f44da3d4c3264af89a46181f47eee8600d643 Mon Sep 17 00:00:00 2001 From: Aleksey Porfirov Date: Thu, 6 Dec 2012 20:11:47 +0400 Subject: [PATCH 42/42] Fix form field empty values --- mongotools/forms/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mongotools/forms/__init__.py b/mongotools/forms/__init__.py index 43d0656..7c171e2 100644 --- a/mongotools/forms/__init__.py +++ b/mongotools/forms/__init__.py @@ -38,6 +38,9 @@ def construct_instance(form, instance, fields=None, exclude=None): file_field_list.append(f) else: value = cleaned_data[field_name] + # force `None` value for empty values (mongoengine specific) + if value in EMPTY_VALUES: + value = None instance[field_name] = value if file_field_list: