From 747e0e92f96258def4ab5716cb628a91a9a11ccd Mon Sep 17 00:00:00 2001 From: Andy Kilner Date: Tue, 27 Nov 2012 14:40:35 +0000 Subject: [PATCH 001/136] Implement MutableMapping in DocumentMetaWrapper Meta is optional (for EmbeddedDocuments) in MongoEngine 0.7.x and requires more of dict's raw functionality. --- mongodbforms/documentoptions.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 63dbaee9..5c929aab 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -1,4 +1,5 @@ import sys +from collections import MutableMapping from django.db.models.fields import FieldDoesNotExist from django.db.models.options import get_verbose_name @@ -20,7 +21,7 @@ def __setattr__(self, attr, value): setattr(self.obj, attr, value) super(PkWrapper, self).__setattr__(attr, value) -class DocumentMetaWrapper(object): +class DocumentMetaWrapper(MutableMapping): """ Used to store mongoengine's _meta dict to make the document admin as compatible as possible to django's meta class on models. @@ -41,7 +42,7 @@ class DocumentMetaWrapper(object): def __init__(self, document): self.document = document - self._meta = document._meta + self._meta = getattr(document, '_meta', {}) try: self.object_name = self.document.__name__ @@ -191,6 +192,21 @@ def __setattr__(self, name, value): def __getitem__(self, key): return self._meta[key] + def __setitem__(self, key, value): + self._meta[key] = value + + def __delitem__(self, key): + return self._meta.__delitem__(key) + + def __contains__(self, key): + return key in self._meta + + def __iter__(self): + return self._meta.__iter__() + + def __len__(self): + return self._meta.__len__() + def get(self, key, default=None): try: return self.__getitem__(key) From 11bc76bf9592edf521eadfd4e605ad0309ead26e Mon Sep 17 00:00:00 2001 From: Jack Saunders Date: Thu, 6 Dec 2012 15:37:58 +0000 Subject: [PATCH 002/136] fix issue with meta on embedded documents in mongoengine >= 0.7 causing mongoadmin to refuse to start --- mongodbforms/documents.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 971e15d8..336ee4d9 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -195,8 +195,9 @@ class ModelFormOptions(object): def __init__(self, options=None): self.document = getattr(options, 'document', None) self.model = self.document + meta = getattr(self.document, '_meta', {}) # set up the document meta wrapper if document meta is a dict - if self.document is not None and isinstance(self.document._meta, dict): + if self.document is not None and isinstance(meta, dict): self.document._meta = DocumentMetaWrapper(self.document) self.document._admin_opts = self.document._meta self.fields = getattr(options, 'fields', None) From 1752f3c822ff9cdd8dfd8667359bbe9410f6b49c Mon Sep 17 00:00:00 2001 From: Felipe Ryan Date: Sun, 3 Feb 2013 00:03:21 +1100 Subject: [PATCH 003/136] added __contains__ which in turn returns key in self.meta --- mongodbforms/documentoptions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 63dbaee9..6973dae6 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -187,7 +187,10 @@ def __setattr__(self, name, value): self._meta[name] = value else: super(DocumentMetaWrapper, self).__setattr__(name, value) - + + def __contains__(self,key): + return key in self._meta + def __getitem__(self, key): return self._meta[key] From 0ded2297d61edbceff903eab4a7e034d60b8307b Mon Sep 17 00:00:00 2001 From: Alex Kelly Date: Tue, 30 Apr 2013 15:00:44 +0100 Subject: [PATCH 004/136] Exceptions have been moved in mongoengine 0.8. --- mongodbforms/documents.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 336ee4d9..ab1c8eb5 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -14,7 +14,10 @@ from django.utils.text import capfirst from mongoengine.fields import ObjectIdField, ListField, ReferenceField, FileField, ImageField -from mongoengine.base import ValidationError +try: + from mongoengine.base import ValidationError +except ImportError: + from mongoengine.errors import ValidationError from mongoengine.connection import _get_db from fieldgenerator import MongoDefaultFormFieldGenerator @@ -683,4 +686,4 @@ def embeddedformset_factory(document, parent_document, form=EmbeddedDocumentForm } FormSet = inlineformset_factory(document, **kwargs) FormSet.parent_document = parent_document - return FormSet \ No newline at end of file + return FormSet From 51429720f9e3fbdb6ab4f5beebc7c44b0a62a808 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 10 Jun 2013 20:18:32 +0200 Subject: [PATCH 005/136] Two fixes and cleanup. Removed duplicate __contains__() in DocumentMetaWrapper. Fixes for issues #13 and #15 --- mongodbforms/documentoptions.py | 3 --- mongodbforms/documents.py | 2 +- mongodbforms/fields.py | 10 ++++++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index d23b65f6..120a1241 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -201,9 +201,6 @@ def __setitem__(self, key, value): def __delitem__(self, key): return self._meta.__delitem__(key) - def __contains__(self, key): - return key in self._meta - def __iter__(self): return self._meta.__iter__() diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index ab1c8eb5..d86a934e 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -161,7 +161,7 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, \ for f in sorted_fields: if isinstance(f, ObjectIdField): continue - if isinstance(f, ListField) and not (f.field.choices or isinstance(f.field, ReferenceField)): + if isinstance(f, ListField) and not (hasattr(f.field,'choices') or isinstance(f.field, ReferenceField)): continue if fields is not None and not f.name in fields: continue diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index bf705471..e02eba27 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -81,8 +81,14 @@ def label_from_instance(self, obj): return smart_unicode(obj) def clean(self, value): - if value in EMPTY_VALUES and not self.required: - return None + # Check for empty values. + if value in EMPTY_VALUES: + # Raise exception if it's empty and required. + if self.required: + raise forms.ValidationError(self.error_messages['required']) + # If it's not required just ignore it. + else: + return None try: oid = ObjectId(value) From 6fdccd55aa1ff1b1d18bc1b7f6a4988e4e5ba388 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 10 Jun 2013 21:05:37 +0200 Subject: [PATCH 006/136] Python 3 support via 2to3 --- mongodbforms/__init__.py | 6 ++-- mongodbforms/documentoptions.py | 11 ++++---- mongodbforms/documents.py | 49 ++++++++++++++++----------------- mongodbforms/fieldgenerator.py | 2 +- mongodbforms/fields.py | 12 ++++---- mongodbforms/util.py | 9 +++--- 6 files changed, 45 insertions(+), 44 deletions(-) diff --git a/mongodbforms/__init__.py b/mongodbforms/__init__.py index a97bad77..ad943d5e 100644 --- a/mongodbforms/__init__.py +++ b/mongodbforms/__init__.py @@ -1,4 +1,4 @@ from django.forms.fields import * -from documents import * -from fieldgenerator import * -from util import * +from .documents import * +from .fieldgenerator import * +from .util import * diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 120a1241..c671749e 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -114,10 +114,11 @@ def _init_pk(self): self.document._pk_val = getattr(self.document, self.pk_name) # avoid circular import - from mongodbforms.util import patch_document - def _get_pk_val(self): + #from mongodbforms.util import patch_document + def get_pk_val(): return self._pk_val - patch_document(_get_pk_val, self.document) + #patch_document(_get_pk_val, self.document) + self.document._get_pk_val = get_pk_val except AttributeError: return @@ -160,7 +161,7 @@ def _init_field_cache(self): if self._field_cache is None: self._field_cache = {} - for f in self.document._fields.itervalues(): + for f in self.document._fields.values(): if isinstance(f, ReferenceField): document = f.document_type document._meta = DocumentMetaWrapper(document) @@ -220,4 +221,4 @@ def get_all_related_objects(self, *args, **kwargs): return [] def iteritems(self): - return self._meta.iteritems() + return iter(self._meta.items()) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index d86a934e..556975e5 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -14,14 +14,15 @@ from django.utils.text import capfirst from mongoengine.fields import ObjectIdField, ListField, ReferenceField, FileField, ImageField +import collections try: from mongoengine.base import ValidationError except ImportError: from mongoengine.errors import ValidationError from mongoengine.connection import _get_db -from fieldgenerator import MongoDefaultFormFieldGenerator -from documentoptions import DocumentMetaWrapper +from .fieldgenerator import MongoDefaultFormFieldGenerator +from .documentoptions import DocumentMetaWrapper def _get_unique_filename(name): @@ -30,7 +31,7 @@ def _get_unique_filename(name): count = itertools.count(1) while fs.exists(filename=name): # file_ext includes the dot. - name = os.path.join("%s_%s%s" % (file_root, count.next(), file_ext)) + name = os.path.join("%s_%s%s" % (file_root, next(count), file_ext)) return name def construct_instance(form, instance, fields=None, exclude=None, ignore=None): @@ -46,7 +47,7 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): if isinstance(instance, type): instance = instance() - for f in instance._fields.itervalues(): + for f in instance._fields.values(): if isinstance(f, ObjectIdField): continue if not f.name in cleaned_data: @@ -101,7 +102,7 @@ def save_instance(form, instance, fields=None, fail_message='saved', # see BaseDocumentForm._post_clean for an explanation if hasattr(form, '_delete_before_save'): fields = instance._fields - new_fields = dict([(n, f) for n, f in fields.iteritems() if not n in form._delete_before_save]) + new_fields = dict([(n, f) for n, f in fields.items() if not n in form._delete_before_save]) if hasattr(instance, '_changed_fields'): for field in form._delete_before_save: instance._changed_fields.remove(field) @@ -126,7 +127,7 @@ def document_to_dict(instance, fields=None, exclude=None): the ``fields`` argument. """ data = {} - for f in instance._fields.itervalues(): + for f in instance._fields.values(): if fields and not f.name in fields: continue if exclude and f.name in exclude: @@ -156,7 +157,7 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, \ # they were defined on he document (at least with cPython) and I can't see # any other way for now. Oh, yeah, it works because we sort on the memory address # and hope that the earlier fields have a lower address. - sorted_fields = sorted(document._fields.values(), key=lambda field: field.__hash__()) + sorted_fields = sorted(list(document._fields.values()), key=lambda field: field.__hash__()) for f in sorted_fields: if isinstance(f, ObjectIdField): @@ -174,7 +175,7 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, \ if formfield_callback is None: formfield = field_generator.generate(f, **kwargs) - elif not callable(formfield_callback): + elif not isinstance(formfield_callback, collections.Callable): raise TypeError('formfield_callback must be a function or callable') else: formfield = formfield_callback(f, **kwargs) @@ -234,7 +235,7 @@ def __new__(cls, name, bases, attrs): fields = fields_for_document(opts.document, opts.fields, opts.exclude, opts.widgets, formfield_callback, formfield_generator) # make sure opts.fields doesn't specify an invalid field - none_document_fields = [k for k, v in fields.iteritems() if not v] + none_document_fields = [k for k, v in fields.items() if not v] missing_fields = set(none_document_fields) - \ set(declared_fields.keys()) if missing_fields: @@ -282,7 +283,7 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, error_class, label_suffix, empty_permitted) def _update_errors(self, message_dict): - for k, v in message_dict.items(): + for k, v in list(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 @@ -301,7 +302,7 @@ def _get_validation_exclusions(self): exclude = [] # Build up a list of fields that should be excluded from model field # validation and unique checks. - for f in self.instance._fields.itervalues(): + for f in self.instance._fields.values(): field = f.name # Exclude fields that aren't on the form. The developer may be # adding these values to the model after form validation. @@ -318,7 +319,7 @@ def _get_validation_exclusions(self): # Exclude fields that failed form validation. There's no need for # the model fields to validate them as well. - elif field in self._errors.keys(): + elif field in list(self._errors.keys()): exclude.append(f.name) # Exclude empty fields that are not required by the form, if the @@ -347,7 +348,7 @@ def _post_clean(self): # Clean the model instance's fields. to_delete = [] try: - for f in self.instance._fields.itervalues(): + for f in self.instance._fields.values(): value = getattr(self.instance, f.name) if f.name not in exclude: f.validate(value) @@ -356,7 +357,7 @@ def _post_clean(self): # that are not required. Clean them up here, though # this is maybe not the right place :-) to_delete.append(f.name) - except ValidationError, e: + except ValidationError as e: err = {f.name: [e.message]} self._update_errors(err) @@ -372,7 +373,7 @@ def _post_clean(self): if hasattr(self.instance, 'clean'): try: self.instance.clean() - except ValidationError, e: + except ValidationError as e: self._update_errors({NON_FIELD_ERRORS: e.messages}) # Validate uniqueness if needed. @@ -386,7 +387,7 @@ def validate_unique(self): """ errors = [] exclude = self._get_validation_exclusions() - for f in self.instance._fields.itervalues(): + for f in self.instance._fields.values(): if f.unique and f.name not in exclude: filter_kwargs = { f.name: getattr(self.instance, f.name) @@ -397,9 +398,9 @@ def validate_unique(self): if self.instance.pk is not None: qs = qs.filter(pk__ne=self.instance.pk) if len(qs) > 0: - message = _(u"%(model_name)s with this %(field_label)s already exists.") % { - 'model_name': unicode(capfirst(self.instance._meta.verbose_name)), - 'field_label': unicode(pretty_name(f.name)) + message = _("%(model_name)s with this %(field_label)s already exists.") % { + 'model_name': str(capfirst(self.instance._meta.verbose_name)), + 'field_label': str(pretty_name(f.name)) } err_dict = {f.name: [message]} self._update_errors(err_dict) @@ -430,8 +431,8 @@ def save(self, commit=True): return obj save.alters_data = True -class DocumentForm(BaseDocumentForm): - __metaclass__ = DocumentFormMetaclass +class DocumentForm(BaseDocumentForm, metaclass=DocumentFormMetaclass): + pass def documentform_factory(document, form=DocumentForm, fields=None, exclude=None, formfield_callback=None): @@ -465,9 +466,7 @@ def documentform_factory(document, form=DocumentForm, fields=None, exclude=None, return DocumentFormMetaclass(class_name, (form,), form_class_attrs) -class EmbeddedDocumentForm(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 @@ -565,7 +564,7 @@ def get_date_error_message(self, date_check): "which must be unique for the %(lookup)s in %(date_field)s.") % { 'field_name': date_check[2], 'date_field': date_check[3], - 'lookup': unicode(date_check[1]), + 'lookup': str(date_check[1]), } def get_form_error(self): diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 73a586cb..e9d4675e 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -13,7 +13,7 @@ from mongoengine import ReferenceField as MongoReferenceField -from fields import MongoCharField, ReferenceField, DocumentMultipleChoiceField +from .fields import MongoCharField, ReferenceField, DocumentMultipleChoiceField BLANK_CHOICE_DASH = [("", "---------")] diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index e02eba27..7a9229ed 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -24,7 +24,7 @@ def __init__(self, field): def __iter__(self): if self.field.empty_label is not None: - yield (u"", self.field.empty_label) + yield ("", self.field.empty_label) for obj in self.queryset.all(): yield self.choice(obj) @@ -45,7 +45,7 @@ class ReferenceField(forms.ChoiceField): """ Reference field for mongo forms. Inspired by `django.forms.models.ModelChoiceField`. """ - def __init__(self, queryset, empty_label=u"---------", + def __init__(self, queryset, empty_label="---------", *aargs, **kwaargs): forms.Field.__init__(self, *aargs, **kwaargs) @@ -115,10 +115,10 @@ class DocumentMultipleChoiceField(ReferenceField): widget = forms.SelectMultiple hidden_widget = forms.MultipleHiddenInput default_error_messages = { - 'list': _(u'Enter a list of values.'), - 'invalid_choice': _(u'Select a valid choice. %s is not one of the' - u' available choices.'), - 'invalid_pk_value': _(u'"%s" is not a valid value for a primary key.') + 'list': _('Enter a list of values.'), + 'invalid_choice': _('Select a valid choice. %s is not one of the' + ' available choices.'), + 'invalid_pk_value': _('"%s" is not a valid value for a primary key.') } def __init__(self, queryset, *args, **kwargs): diff --git a/mongodbforms/util.py b/mongodbforms/util.py index 6dd407a1..8d1c0a9d 100644 --- a/mongodbforms/util.py +++ b/mongodbforms/util.py @@ -1,9 +1,10 @@ -import new +#import new -from documentoptions import DocumentMetaWrapper +from .documentoptions import DocumentMetaWrapper -def patch_document(function, instance): - setattr(instance, function.__name__, new.instancemethod(function, instance, instance.__class__)) +# Appearently no longer needed for python > 2.6 and deprecated for python 3 +#def patch_document(function, instance): +# setattr(instance, function.__name__, new.instancemethod(function, instance, instance.__class__)) def init_document_options(document): if not hasattr(document, '_meta') or not isinstance(document._meta, DocumentMetaWrapper): From a2bb112f37550e4d63f3bf495623147a1f7db35d Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 10 Jun 2013 22:19:07 +0200 Subject: [PATCH 007/136] Fixes #24 Fixes issue #24. Additionally EmbeddedDocumentForm now honors the fiels default value upon save. --- mongodbforms/documents.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 556975e5..ad1bb3f6 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -471,17 +471,25 @@ def __init__(self, parent_document, *args, **kwargs): super(EmbeddedDocumentForm, self).__init__(*args, **kwargs) self.parent_document = parent_document if self._meta.embedded_field is not None and \ - not hasattr(self.parent_document, self._meta.embedded_field): + self._meta.embedded_field in self.parent_document._fields: raise FieldError("Parent document must have field %s" % self._meta.embedded_field) def save(self, commit=True): + """If commit is True the embedded document is added to the parent + document. Otherwise the parent_document is left untouched and the + embedded is returned as usual. + """ if self.errors: raise ValueError("The %s could not be saved because the data didn't" " validate." % self.instance.__class__.__name__) - if commit: - l = getattr(self.parent_document, self._meta.embedded_field) + field = self.parent_document._fields.get(self._meta.embedded_field) + if isinstance(field, ListField) and field.default is None: + default = [] + else: + default = field.default + l = getattr(self.parent_document, self._meta.embedded_field, default) l.append(self.instance) setattr(self.parent_document, self._meta.embedded_field, l) self.parent_document.save() From 95f0331d28d9faacc6ebc56137165381d2d579bb Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 11 Jun 2013 00:29:58 +0200 Subject: [PATCH 008/136] Metaclass declaration fix to work in 2 and 3. Added six.py for that --- .gitignore | 1 + mongodbforms/documents.py | 6 +- mongodbforms/six.py | 423 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 mongodbforms/six.py diff --git a/.gitignore b/.gitignore index 20877c10..669100e2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ dist/* MANIFEST build/* *egg* +*.bak diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index ad1bb3f6..f4e3a87b 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -24,6 +24,8 @@ from .fieldgenerator import MongoDefaultFormFieldGenerator from .documentoptions import DocumentMetaWrapper +from .six import with_metaclass + def _get_unique_filename(name): fs = gridfs.GridFS(_get_db()) @@ -431,7 +433,7 @@ def save(self, commit=True): return obj save.alters_data = True -class DocumentForm(BaseDocumentForm, metaclass=DocumentFormMetaclass): +class DocumentForm(with_metaclass(DocumentFormMetaclass, BaseDocumentForm)): pass def documentform_factory(document, form=DocumentForm, fields=None, exclude=None, @@ -466,7 +468,7 @@ def documentform_factory(document, form=DocumentForm, fields=None, exclude=None, return DocumentFormMetaclass(class_name, (form,), form_class_attrs) -class EmbeddedDocumentForm(BaseDocumentForm, metaclass=DocumentFormMetaclass): +class EmbeddedDocumentForm(with_metaclass(DocumentFormMetaclass, BaseDocumentForm)): def __init__(self, parent_document, *args, **kwargs): super(EmbeddedDocumentForm, self).__init__(*args, **kwargs) self.parent_document = parent_document diff --git a/mongodbforms/six.py b/mongodbforms/six.py new file mode 100644 index 00000000..e635fc99 --- /dev/null +++ b/mongodbforms/six.py @@ -0,0 +1,423 @@ +"""Utilities for writing code that runs on Python 2 and 3""" + +# Copyright (c) 2010-2013 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.3.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) + # This is a bit ugly, but it avoids running this again. + delattr(tp, self.name) + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + + +class _MovedItems(types.ModuleType): + """Lazy loading of moved objects""" + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("winreg", "_winreg"), +] +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) +del attr + +moves = sys.modules[__name__ + ".moves"] = _MovedItems("moves") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" + + _iterkeys = "keys" + _itervalues = "values" + _iteritems = "items" + _iterlists = "lists" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + _iterkeys = "iterkeys" + _itervalues = "itervalues" + _iteritems = "iteritems" + _iterlists = "iterlists" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +def iterkeys(d, **kw): + """Return an iterator over the keys of a dictionary.""" + return iter(getattr(d, _iterkeys)(**kw)) + +def itervalues(d, **kw): + """Return an iterator over the values of a dictionary.""" + return iter(getattr(d, _itervalues)(**kw)) + +def iteritems(d, **kw): + """Return an iterator over the (key, value) pairs of a dictionary.""" + return iter(getattr(d, _iteritems)(**kw)) + +def iterlists(d, **kw): + """Return an iterator over the (key, [values]) pairs of a dictionary.""" + return iter(getattr(d, _iterlists)(**kw)) + + +if PY3: + def b(s): + return s.encode("latin-1") + def u(s): + return s + unichr = chr + if sys.version_info[1] <= 1: + def int2byte(i): + return bytes((i,)) + else: + # This is about 2x faster than the implementation above on 3.2+ + int2byte = operator.methodcaller("to_bytes", 1, "big") + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO +else: + def b(s): + return s + def u(s): + return unicode(s, "unicode_escape") + unichr = unichr + int2byte = chr + def byte2int(bs): + return ord(bs[0]) + def indexbytes(buf, i): + return ord(buf[i]) + def iterbytes(buf): + return (ord(byte) for byte in buf) + import StringIO + StringIO = BytesIO = StringIO.StringIO +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +if PY3: + import builtins + exec_ = getattr(builtins, "exec") + + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + + print_ = getattr(builtins, "print") + del builtins + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + + def print_(*args, **kwargs): + """The new-style print function.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + def write(data): + if not isinstance(data, basestring): + data = str(data) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) + +_add_doc(reraise, """Reraise an exception.""") + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + return meta("NewBase", bases, {}) \ No newline at end of file From f56ee92d5e9f091e408810f4314c6cc00bb3a32f Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 11 Jun 2013 01:28:19 +0200 Subject: [PATCH 009/136] Logic is hard and a 'not' was missing. patch_document is still required but should hopefully work in python 2 and 3 now --- mongodbforms/documents.py | 2 +- mongodbforms/util.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index f4e3a87b..9c60fa4e 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -472,7 +472,7 @@ class EmbeddedDocumentForm(with_metaclass(DocumentFormMetaclass, BaseDocumentFor def __init__(self, parent_document, *args, **kwargs): super(EmbeddedDocumentForm, self).__init__(*args, **kwargs) self.parent_document = parent_document - if self._meta.embedded_field is not None and \ + if self._meta.embedded_field is not None and not \ self._meta.embedded_field in self.parent_document._fields: raise FieldError("Parent document must have field %s" % self._meta.embedded_field) diff --git a/mongodbforms/util.py b/mongodbforms/util.py index 8d1c0a9d..83829da8 100644 --- a/mongodbforms/util.py +++ b/mongodbforms/util.py @@ -1,10 +1,9 @@ -#import new +from types import MethodType from .documentoptions import DocumentMetaWrapper -# Appearently no longer needed for python > 2.6 and deprecated for python 3 -#def patch_document(function, instance): -# setattr(instance, function.__name__, new.instancemethod(function, instance, instance.__class__)) +def patch_document(function, instance): + setattr(instance, function.__name__, MethodType(function, instance)) def init_document_options(document): if not hasattr(document, '_meta') or not isinstance(document._meta, DocumentMetaWrapper): From cf3e8f9df8d0f3459730e8ac93950e59fdaa098c Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 11 Jun 2013 02:39:19 +0200 Subject: [PATCH 010/136] Moved with_metaclasses to util and removed the unneeded six stuff --- mongodbforms/documents.py | 2 +- mongodbforms/six.py | 423 -------------------------------------- mongodbforms/util.py | 28 +++ 3 files changed, 29 insertions(+), 424 deletions(-) delete mode 100644 mongodbforms/six.py diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 9c60fa4e..44438ca6 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -24,7 +24,7 @@ from .fieldgenerator import MongoDefaultFormFieldGenerator from .documentoptions import DocumentMetaWrapper -from .six import with_metaclass +from .util import with_metaclass def _get_unique_filename(name): diff --git a/mongodbforms/six.py b/mongodbforms/six.py deleted file mode 100644 index e635fc99..00000000 --- a/mongodbforms/six.py +++ /dev/null @@ -1,423 +0,0 @@ -"""Utilities for writing code that runs on Python 2 and 3""" - -# Copyright (c) 2010-2013 Benjamin Peterson -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import operator -import sys -import types - -__author__ = "Benjamin Peterson " -__version__ = "1.3.0" - - -# Useful for very coarse version differentiation. -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 - -if PY3: - string_types = str, - integer_types = int, - class_types = type, - text_type = str - binary_type = bytes - - MAXSIZE = sys.maxsize -else: - string_types = basestring, - integer_types = (int, long) - class_types = (type, types.ClassType) - text_type = unicode - binary_type = str - - if sys.platform.startswith("java"): - # Jython always uses 32 bits. - MAXSIZE = int((1 << 31) - 1) - else: - # It's possible to have sizeof(long) != sizeof(Py_ssize_t). - class X(object): - def __len__(self): - return 1 << 31 - try: - len(X()) - except OverflowError: - # 32-bit - MAXSIZE = int((1 << 31) - 1) - else: - # 64-bit - MAXSIZE = int((1 << 63) - 1) - del X - - -def _add_doc(func, doc): - """Add documentation to a function.""" - func.__doc__ = doc - - -def _import_module(name): - """Import module, returning the module after the last dot.""" - __import__(name) - return sys.modules[name] - - -class _LazyDescr(object): - - def __init__(self, name): - self.name = name - - def __get__(self, obj, tp): - result = self._resolve() - setattr(obj, self.name, result) - # This is a bit ugly, but it avoids running this again. - delattr(tp, self.name) - return result - - -class MovedModule(_LazyDescr): - - def __init__(self, name, old, new=None): - super(MovedModule, self).__init__(name) - if PY3: - if new is None: - new = name - self.mod = new - else: - self.mod = old - - def _resolve(self): - return _import_module(self.mod) - - -class MovedAttribute(_LazyDescr): - - def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): - super(MovedAttribute, self).__init__(name) - if PY3: - if new_mod is None: - new_mod = name - self.mod = new_mod - if new_attr is None: - if old_attr is None: - new_attr = name - else: - new_attr = old_attr - self.attr = new_attr - else: - self.mod = old_mod - if old_attr is None: - old_attr = name - self.attr = old_attr - - def _resolve(self): - module = _import_module(self.mod) - return getattr(module, self.attr) - - - -class _MovedItems(types.ModuleType): - """Lazy loading of moved objects""" - - -_moved_attributes = [ - MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), - MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), - MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), - MovedAttribute("map", "itertools", "builtins", "imap", "map"), - MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("reload_module", "__builtin__", "imp", "reload"), - MovedAttribute("reduce", "__builtin__", "functools"), - MovedAttribute("StringIO", "StringIO", "io"), - MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), - - MovedModule("builtins", "__builtin__"), - MovedModule("configparser", "ConfigParser"), - MovedModule("copyreg", "copy_reg"), - MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), - MovedModule("http_cookies", "Cookie", "http.cookies"), - MovedModule("html_entities", "htmlentitydefs", "html.entities"), - MovedModule("html_parser", "HTMLParser", "html.parser"), - MovedModule("http_client", "httplib", "http.client"), - MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), - MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), - MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), - MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), - MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), - MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), - MovedModule("cPickle", "cPickle", "pickle"), - MovedModule("queue", "Queue"), - MovedModule("reprlib", "repr"), - MovedModule("socketserver", "SocketServer"), - MovedModule("tkinter", "Tkinter"), - MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), - MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), - MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), - MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), - MovedModule("tkinter_tix", "Tix", "tkinter.tix"), - MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), - MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), - MovedModule("tkinter_colorchooser", "tkColorChooser", - "tkinter.colorchooser"), - MovedModule("tkinter_commondialog", "tkCommonDialog", - "tkinter.commondialog"), - MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), - MovedModule("tkinter_font", "tkFont", "tkinter.font"), - MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), - MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", - "tkinter.simpledialog"), - MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), - MovedModule("winreg", "_winreg"), -] -for attr in _moved_attributes: - setattr(_MovedItems, attr.name, attr) -del attr - -moves = sys.modules[__name__ + ".moves"] = _MovedItems("moves") - - -def add_move(move): - """Add an item to six.moves.""" - setattr(_MovedItems, move.name, move) - - -def remove_move(name): - """Remove item from six.moves.""" - try: - delattr(_MovedItems, name) - except AttributeError: - try: - del moves.__dict__[name] - except KeyError: - raise AttributeError("no such move, %r" % (name,)) - - -if PY3: - _meth_func = "__func__" - _meth_self = "__self__" - - _func_closure = "__closure__" - _func_code = "__code__" - _func_defaults = "__defaults__" - _func_globals = "__globals__" - - _iterkeys = "keys" - _itervalues = "values" - _iteritems = "items" - _iterlists = "lists" -else: - _meth_func = "im_func" - _meth_self = "im_self" - - _func_closure = "func_closure" - _func_code = "func_code" - _func_defaults = "func_defaults" - _func_globals = "func_globals" - - _iterkeys = "iterkeys" - _itervalues = "itervalues" - _iteritems = "iteritems" - _iterlists = "iterlists" - - -try: - advance_iterator = next -except NameError: - def advance_iterator(it): - return it.next() -next = advance_iterator - - -try: - callable = callable -except NameError: - def callable(obj): - return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) - - -if PY3: - def get_unbound_function(unbound): - return unbound - - create_bound_method = types.MethodType - - Iterator = object -else: - def get_unbound_function(unbound): - return unbound.im_func - - def create_bound_method(func, obj): - return types.MethodType(func, obj, obj.__class__) - - class Iterator(object): - - def next(self): - return type(self).__next__(self) - - callable = callable -_add_doc(get_unbound_function, - """Get the function out of a possibly unbound function""") - - -get_method_function = operator.attrgetter(_meth_func) -get_method_self = operator.attrgetter(_meth_self) -get_function_closure = operator.attrgetter(_func_closure) -get_function_code = operator.attrgetter(_func_code) -get_function_defaults = operator.attrgetter(_func_defaults) -get_function_globals = operator.attrgetter(_func_globals) - - -def iterkeys(d, **kw): - """Return an iterator over the keys of a dictionary.""" - return iter(getattr(d, _iterkeys)(**kw)) - -def itervalues(d, **kw): - """Return an iterator over the values of a dictionary.""" - return iter(getattr(d, _itervalues)(**kw)) - -def iteritems(d, **kw): - """Return an iterator over the (key, value) pairs of a dictionary.""" - return iter(getattr(d, _iteritems)(**kw)) - -def iterlists(d, **kw): - """Return an iterator over the (key, [values]) pairs of a dictionary.""" - return iter(getattr(d, _iterlists)(**kw)) - - -if PY3: - def b(s): - return s.encode("latin-1") - def u(s): - return s - unichr = chr - if sys.version_info[1] <= 1: - def int2byte(i): - return bytes((i,)) - else: - # This is about 2x faster than the implementation above on 3.2+ - int2byte = operator.methodcaller("to_bytes", 1, "big") - byte2int = operator.itemgetter(0) - indexbytes = operator.getitem - iterbytes = iter - import io - StringIO = io.StringIO - BytesIO = io.BytesIO -else: - def b(s): - return s - def u(s): - return unicode(s, "unicode_escape") - unichr = unichr - int2byte = chr - def byte2int(bs): - return ord(bs[0]) - def indexbytes(buf, i): - return ord(buf[i]) - def iterbytes(buf): - return (ord(byte) for byte in buf) - import StringIO - StringIO = BytesIO = StringIO.StringIO -_add_doc(b, """Byte literal""") -_add_doc(u, """Text literal""") - - -if PY3: - import builtins - exec_ = getattr(builtins, "exec") - - - def reraise(tp, value, tb=None): - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - - - print_ = getattr(builtins, "print") - del builtins - -else: - def exec_(_code_, _globs_=None, _locs_=None): - """Execute code in a namespace.""" - if _globs_ is None: - frame = sys._getframe(1) - _globs_ = frame.f_globals - if _locs_ is None: - _locs_ = frame.f_locals - del frame - elif _locs_ is None: - _locs_ = _globs_ - exec("""exec _code_ in _globs_, _locs_""") - - - exec_("""def reraise(tp, value, tb=None): - raise tp, value, tb -""") - - - def print_(*args, **kwargs): - """The new-style print function.""" - fp = kwargs.pop("file", sys.stdout) - if fp is None: - return - def write(data): - if not isinstance(data, basestring): - data = str(data) - fp.write(data) - want_unicode = False - sep = kwargs.pop("sep", None) - if sep is not None: - if isinstance(sep, unicode): - want_unicode = True - elif not isinstance(sep, str): - raise TypeError("sep must be None or a string") - end = kwargs.pop("end", None) - if end is not None: - if isinstance(end, unicode): - want_unicode = True - elif not isinstance(end, str): - raise TypeError("end must be None or a string") - if kwargs: - raise TypeError("invalid keyword arguments to print()") - if not want_unicode: - for arg in args: - if isinstance(arg, unicode): - want_unicode = True - break - if want_unicode: - newline = unicode("\n") - space = unicode(" ") - else: - newline = "\n" - space = " " - if sep is None: - sep = space - if end is None: - end = newline - for i, arg in enumerate(args): - if i: - write(sep) - write(arg) - write(end) - -_add_doc(reraise, """Reraise an exception.""") - - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass.""" - return meta("NewBase", bases, {}) \ No newline at end of file diff --git a/mongodbforms/util.py b/mongodbforms/util.py index 83829da8..4ca4e7b8 100644 --- a/mongodbforms/util.py +++ b/mongodbforms/util.py @@ -14,3 +14,31 @@ def init_document_options(document): def get_document_options(document): return DocumentMetaWrapper(document) + + +# Taken from six (https://pypi.python.org/pypi/six) +# by "Benjamin Peterson " +# +# Copyright (c) 2010-2013 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + return meta("NewBase", bases, {}) From 0230cb15449adecaf1cbb7847945a8035bf4412c Mon Sep 17 00:00:00 2001 From: Matthew Gerring Date: Tue, 11 Jun 2013 01:05:46 -0700 Subject: [PATCH 011/136] Setting can_delete to False shouldn't raise a KeyError --- mongodbforms/documents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 44438ca6..4e0022b1 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -546,7 +546,7 @@ def save(self, commit=True): if not form.has_changed() and not form in self.initial_forms: continue obj = self.save_object(form) - if form.cleaned_data["DELETE"]: + if form.cleaned_data.get("DELETE", False): try: obj.delete() except AttributeError: From f0969bb8b5515ed773efc094df34f03497173cb5 Mon Sep 17 00:00:00 2001 From: Matthew Gerring Date: Tue, 11 Jun 2013 01:13:01 -0700 Subject: [PATCH 012/136] Emedded formsets should take arguments in the same order listed in the Django ModelForm docs. Right now, trying to bind POST data to an embedded document form set throws an error, because the first keyword argument to EmbeddedDocumentFormSet is parent_document rather than data. This commit moves parent_document to the end of the keyword arguments list so that it's more likely to behave consistently with the Django docs. --- mongodbforms/documents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 4e0022b1..322424d0 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -660,8 +660,8 @@ def inlineformset_factory(document, form=DocumentForm, return FormSet class EmbeddedDocumentFormSet(BaseInlineDocumentFormSet): - def __init__(self, parent_document=None, data=None, files=None, instance=None, - save_as_new=False, prefix=None, queryset=[], **kwargs): + def __init__(self, data=None, files=None, instance=None, + save_as_new=False, prefix=None, queryset=[], parent_document=None, **kwargs): self.parent_document = parent_document super(EmbeddedDocumentFormSet, self).__init__(data, files, instance, save_as_new, prefix, queryset, **kwargs) From 8d6e6f2e25c45f9b266d21bf2ce9384338b5eabf Mon Sep 17 00:00:00 2001 From: Matthew Gerring Date: Tue, 11 Jun 2013 01:33:26 -0700 Subject: [PATCH 013/136] s/cook/cool/ --- mongodbforms/documents.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 322424d0..6efcdde5 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -552,7 +552,7 @@ def save(self, commit=True): except AttributeError: # if it has no delete method it is an # embedded object. We just don't add to the list - # and it's gone. Cook huh? + # and it's gone. Cool huh? continue saved.append(obj) return saved @@ -671,6 +671,7 @@ def _construct_form(self, i, **kwargs): form = super(BaseDocumentFormSet, self)._construct_form(i, **defaults) return form + def embeddedformset_factory(document, parent_document, form=EmbeddedDocumentForm, formset=EmbeddedDocumentFormSet, fields=None, exclude=None, From c892bab4bdfc48825641e7b5b23d872573472482 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 11 Jun 2013 14:51:01 +0200 Subject: [PATCH 014/136] Using patch document again in documentoptions.py --- mongodbforms/documentoptions.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index c671749e..17e175a2 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -114,11 +114,10 @@ def _init_pk(self): self.document._pk_val = getattr(self.document, self.pk_name) # avoid circular import - #from mongodbforms.util import patch_document - def get_pk_val(): + from mongodbforms.util import patch_document + def _get_pk_val(): return self._pk_val - #patch_document(_get_pk_val, self.document) - self.document._get_pk_val = get_pk_val + patch_document(_get_pk_val, self.document) except AttributeError: return From 521d743e78e949854ca37664098f0199f26df94c Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 11 Jun 2013 15:17:40 +0200 Subject: [PATCH 015/136] Accept model and document keywords on form meta. Closes #25 --- mongodbforms/documents.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 44438ca6..6d4db6ac 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -200,6 +200,9 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, \ class ModelFormOptions(object): def __init__(self, options=None): self.document = getattr(options, 'document', None) + if self.document is None: + self.document = getattr(options, 'model', None) + self.model = self.document meta = getattr(self.document, '_meta', {}) # set up the document meta wrapper if document meta is a dict From d35cb85ad58ff2db59849f795c9f56585c699564 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 11 Jun 2013 15:27:15 +0200 Subject: [PATCH 016/136] Handle embeddef ield types correctly --- mongodbforms/documents.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 6d4db6ac..2bce8e22 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -494,9 +494,13 @@ def save(self, commit=True): default = [] else: default = field.default - l = getattr(self.parent_document, self._meta.embedded_field, default) - l.append(self.instance) - setattr(self.parent_document, self._meta.embedded_field, l) + attr = getattr(self.parent_document, self._meta.embedded_field, default) + try: + attr.append(self.instance) + except AttributeError: + # not a listfield on parent, treat as an embedded field + attr = self.instance + setattr(self.parent_document, self._meta.embedded_field, attr) self.parent_document.save() return self.instance From eccb8bdd0feac29b329a6a2249ecdcc0f477dc5a Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 11 Jun 2013 15:52:58 +0200 Subject: [PATCH 017/136] Correctly import smart_unicode. A bit of cleanup --- mongodbforms/documents.py | 10 +++++----- mongodbforms/fields.py | 9 ++++++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 2bce8e22..dde854c0 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -1,9 +1,8 @@ import os import itertools -import gridfs +from collections import Callable from django.utils.datastructures import SortedDict - from django.forms.forms import BaseForm, get_declared_fields, NON_FIELD_ERRORS, pretty_name from django.forms.widgets import media_property from django.core.exceptions import FieldError @@ -14,12 +13,12 @@ from django.utils.text import capfirst from mongoengine.fields import ObjectIdField, ListField, ReferenceField, FileField, ImageField -import collections try: from mongoengine.base import ValidationError except ImportError: from mongoengine.errors import ValidationError from mongoengine.connection import _get_db +from gridfs import GridFS from .fieldgenerator import MongoDefaultFormFieldGenerator from .documentoptions import DocumentMetaWrapper @@ -28,7 +27,7 @@ def _get_unique_filename(name): - fs = gridfs.GridFS(_get_db()) + fs = GridFS(_get_db()) file_root, file_ext = os.path.splitext(name) count = itertools.count(1) while fs.exists(filename=name): @@ -177,7 +176,7 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, \ if formfield_callback is None: formfield = field_generator.generate(f, **kwargs) - elif not isinstance(formfield_callback, collections.Callable): + elif not isinstance(formfield_callback, Callable): raise TypeError('formfield_callback must be a function or callable') else: formfield = formfield_callback(f, **kwargs) @@ -199,6 +198,7 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, \ class ModelFormOptions(object): def __init__(self, options=None): + # document class can be declared with 'document =' or 'model =' self.document = getattr(options, 'document', None) if self.document is None: self.document = getattr(options, 'model', None) diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index 7a9229ed..01e2f46b 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -7,7 +7,14 @@ from django import forms from django.core.validators import EMPTY_VALUES -from django.utils.encoding import smart_unicode, force_unicode +from django.utils.encoding import force_unicode +try: + from django.utils.encoding import smart_text as smart_unicode +except ImportError: + try: + from django.utils.encoding import smart_unicode + except ImportError: + from django.forms.util import smart_unicode from django.utils.translation import ugettext_lazy as _ try: # objectid was moved into bson in pymongo 1.9 From 55cbee53bdc68c3a9ce9b714428c4b07540c15bf Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 11 Jun 2013 15:54:56 +0200 Subject: [PATCH 018/136] Argh, forgot one import on last commit --- mongodbforms/fieldgenerator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index e9d4675e..bb014cb9 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -7,7 +7,13 @@ from django import forms from django.core.validators import EMPTY_VALUES -from django.utils.encoding import smart_unicode +try: + from django.utils.encoding import smart_text as smart_unicode +except ImportError: + try: + from django.utils.encoding import smart_unicode + except ImportError: + from django.forms.util import smart_unicode from django.db.models.options import get_verbose_name from django.utils.text import capfirst From 686663b6ea56ad4b98513d65028d4911ab21a43d Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 11 Jun 2013 20:01:37 +0200 Subject: [PATCH 019/136] Added a proper save method to EmbeddedDocumentFormSet. Hopefully closes #23 --- mongodbforms/documents.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index dde854c0..ef713cf0 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -553,13 +553,13 @@ def save(self, commit=True): if not form.has_changed() and not form in self.initial_forms: continue obj = self.save_object(form) - if form.cleaned_data["DELETE"]: + if form.cleaned_data.get("DELETE", False): try: obj.delete() except AttributeError: # if it has no delete method it is an # embedded object. We just don't add to the list - # and it's gone. Cook huh? + # and it's gone. Cool huh? continue saved.append(obj) return saved @@ -576,6 +576,7 @@ def validate_unique(self): if errors: raise ValidationError(errors) + def get_date_error_message(self, date_check): return ugettext("Please correct the duplicate data for %(field_name)s " "which must be unique for the %(lookup)s in %(date_field)s.") % { @@ -675,8 +676,18 @@ def __init__(self, parent_document=None, data=None, files=None, instance=None, def _construct_form(self, i, **kwargs): defaults = {'parent_document': self.parent_document} defaults.update(kwargs) - form = super(BaseDocumentFormSet, self)._construct_form(i, **defaults) + form = super(EmbeddedDocumentFormSet, self)._construct_form(i, **defaults) return form + + def save(self, commit=True): + objs = super(EmbeddedDocumentFormSet, self).save(commit) + + if commit: + form = self.empty_form + setattr(self.parent_document, form._meta.embedded_field, objs) + self.parent_document.save() + + return objs def embeddedformset_factory(document, parent_document, form=EmbeddedDocumentForm, formset=EmbeddedDocumentFormSet, From ff396f2eefa5438afa69941f6b0a9131759d8fd3 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 11 Jun 2013 20:04:48 +0200 Subject: [PATCH 020/136] And a fix for the last fix. Dough --- mongodbforms/documents.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index ef713cf0..119a99a3 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -684,7 +684,8 @@ def save(self, commit=True): if commit: form = self.empty_form - setattr(self.parent_document, form._meta.embedded_field, objs) + list = getattr(self.parent_document, form._meta.embedded_field) + setattr(self.parent_document, form._meta.embedded_field, list + objs) self.parent_document.save() return objs From 9c3f622077bafcc9d6e31b7956875b4f3c473ba7 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 11 Jun 2013 20:12:03 +0200 Subject: [PATCH 021/136] Rename to not use reserved words for vars. --- mongodbforms/documents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 119a99a3..f3777975 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -684,8 +684,8 @@ def save(self, commit=True): if commit: form = self.empty_form - list = getattr(self.parent_document, form._meta.embedded_field) - setattr(self.parent_document, form._meta.embedded_field, list + objs) + attr_data = getattr(self.parent_document, form._meta.embedded_field) + setattr(self.parent_document, form._meta.embedded_field, attr_data + objs) self.parent_document.save() return objs From bc175bf9c3b46bd9a68a08e6a287670269fe7bf8 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 11 Jun 2013 20:30:06 +0200 Subject: [PATCH 022/136] getattr needs a default if you expect a list. Oh dear. --- mongodbforms/documents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 76932ee7..7bd819bb 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -684,7 +684,7 @@ def save(self, commit=True): if commit: form = self.empty_form - attr_data = getattr(self.parent_document, form._meta.embedded_field) + attr_data = getattr(self.parent_document, form._meta.embedded_field, []) setattr(self.parent_document, form._meta.embedded_field, attr_data + objs) self.parent_document.save() From 4d32454859b330c3dd79fc9cc6e5df2e45b7874e Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 11 Jun 2013 22:43:42 +0200 Subject: [PATCH 023/136] More FormSet stuff and a fix that closes #27 --- mongodbforms/documents.py | 189 ++++++++++++++++++++++++-------------- mongodbforms/fields.py | 8 +- 2 files changed, 127 insertions(+), 70 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 7bd819bb..1c548f37 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -561,7 +561,9 @@ def save(self, commit=True): # embedded object. We just don't add to the list # and it's gone. Cool huh? continue - saved.append(obj) + if commit: + obj.save() + saved.append(obj) return saved def clean(self): @@ -603,71 +605,118 @@ def documentformset_factory(document, form=DocumentForm, formfield_callback=None FormSet.document = document return FormSet -class BaseInlineDocumentFormSet(BaseDocumentFormSet): - """ - A formset for child objects related to a parent. - - self.instance -> the document containing the inline objects - """ - def __init__(self, data=None, files=None, instance=None, - save_as_new=False, prefix=None, queryset=[], **kwargs): - self.instance = instance - self.save_as_new = save_as_new - - super(BaseInlineDocumentFormSet, self).__init__(data, files, prefix=prefix, queryset=queryset, **kwargs) - - def initial_form_count(self): - if self.save_as_new: - return 0 - return super(BaseInlineDocumentFormSet, self).initial_form_count() - - #@classmethod - def get_default_prefix(cls): - return cls.model.__name__.lower() - get_default_prefix = classmethod(get_default_prefix) - - - def add_fields(self, form, index): - super(BaseInlineDocumentFormSet, self).add_fields(form, index) - - # Add the generated field to form._meta.fields if it's defined to make - # sure validation isn't skipped on that field. - if form._meta.fields: - if isinstance(form._meta.fields, tuple): - form._meta.fields = list(form._meta.fields) - #form._meta.fields.append(self.fk.name) - - def get_unique_error_message(self, unique_check): - unique_check = [field for field in unique_check if field != self.fk.name] - return super(BaseInlineDocumentFormSet, self).get_unique_error_message(unique_check) - - -def inlineformset_factory(document, form=DocumentForm, - formset=BaseInlineDocumentFormSet, - fields=None, exclude=None, - extra=1, can_order=False, can_delete=True, max_num=None, - formfield_callback=None): - """ - Returns an ``InlineFormSet`` for the given kwargs. - - You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` - to ``parent_model``. - """ - kwargs = { - 'form': form, - 'formfield_callback': formfield_callback, - 'formset': formset, - 'extra': extra, - 'can_delete': can_delete, - 'can_order': can_order, - 'fields': fields, - 'exclude': exclude, - 'max_num': max_num, - } - FormSet = documentformset_factory(document, **kwargs) - return FormSet - -class EmbeddedDocumentFormSet(BaseInlineDocumentFormSet): +#class BaseInlineDocumentFormSet(BaseDocumentFormSet): +# """A formset for child objects related to a parent.""" +# def __init__(self, data=None, files=None, instance=None, +# save_as_new=False, prefix=None, queryset=None, **kwargs): +# if instance is None: +# self.instance = self.fk.rel.to() +# else: +# self.instance = instance +# self.save_as_new = save_as_new +# if queryset is None: +# queryset = self.model._default_manager +# if self.instance.pk: +# qs = queryset.filter(**{self.fk.name: self.instance}) +# else: +# qs = queryset.none() +# super(BaseInlineDocumentFormSet, self).__init__(data, files, prefix=prefix, +# queryset=qs, **kwargs) +# +# def initial_form_count(self): +# if self.save_as_new: +# return 0 +# return super(BaseInlineDocumentFormSet, self).initial_form_count() +# +# +# def _construct_form(self, i, **kwargs): +# form = super(BaseInlineDocumentFormSet, self)._construct_form(i, **kwargs) +# if self.save_as_new: +# # Remove the primary key from the form's data, we are only +# # creating new instances +# form.data[form.add_prefix(self._pk_field.name)] = None +# +# # Remove the foreign key from the form's data +# form.data[form.add_prefix(self.fk.name)] = None +# +# # Set the fk value here so that the form can do its validation. +# setattr(form.instance, self.fk.get_attname(), self.instance.pk) +# return form +# +# @classmethod +# def get_default_prefix(cls): +# from django.db.models.fields.related import RelatedObject +# return RelatedObject(cls.fk.rel.to, cls.model, cls.fk).get_accessor_name().replace('+','') +# +# def save_new(self, form, commit=True): +# # Use commit=False so we can assign the parent key afterwards, then +# # save the object. +# obj = form.save(commit=False) +# pk_value = getattr(self.instance, self.fk.rel.field_name) +# setattr(obj, self.fk.get_attname(), getattr(pk_value, 'pk', pk_value)) +# if commit: +# obj.save() +# # form.save_m2m() can be called via the formset later on if commit=False +# if commit and hasattr(form, 'save_m2m'): +# form.save_m2m() +# return obj +# +# def add_fields(self, form, index): +# super(BaseInlineDocumentFormSet, self).add_fields(form, index) +# if self._pk_field == self.fk: +# name = self._pk_field.name +# kwargs = {'pk_field': True} +# else: +# # The foreign key field might not be on the form, so we poke at the +# # Model field to get the label, since we need that for error messages. +# name = self.fk.name +# kwargs = { +# 'label': getattr(form.fields.get(name), 'label', capfirst(self.fk.verbose_name)) +# } +# if self.fk.rel.field_name != self.fk.rel.to._meta.pk.name: +# kwargs['to_field'] = self.fk.rel.field_name +# +# form.fields[name] = InlineForeignKeyField(self.instance, **kwargs) +# +# # Add the generated field to form._meta.fields if it's defined to make +# # sure validation isn't skipped on that field. +# if form._meta.fields: +# if isinstance(form._meta.fields, tuple): +# form._meta.fields = list(form._meta.fields) +# form._meta.fields.append(self.fk.name) +# +# def get_unique_error_message(self, unique_check): +# unique_check = [field for field in unique_check if field != self.fk.name] +# return super(BaseInlineDocumentFormSet, self).get_unique_error_message(unique_check) +# +# +# +#def inlineformset_factory(document, form=DocumentForm, +# formset=BaseInlineDocumentFormSet, +# fields=None, exclude=None, +# extra=1, can_order=False, can_delete=True, max_num=None, +# formfield_callback=None): +# """ +# Returns an ``InlineFormSet`` for the given kwargs. +# +# You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` +# to ``parent_model``. +# """ +# kwargs = { +# 'form': form, +# 'formfield_callback': formfield_callback, +# 'formset': formset, +# 'extra': extra, +# 'can_delete': can_delete, +# 'can_order': can_order, +# 'fields': fields, +# 'exclude': exclude, +# 'max_num': max_num, +# } +# FormSet = documentformset_factory(document, **kwargs) +# return FormSet + +class EmbeddedDocumentFormSet(BaseDocumentFormSet): def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=[], parent_document=None, **kwargs): self.parent_document = parent_document @@ -680,9 +729,11 @@ def _construct_form(self, i, **kwargs): return form def save(self, commit=True): - objs = super(EmbeddedDocumentFormSet, self).save(commit) + # Don't try to save the new documents. Embedded objects don't have + # a save method anyway. + objs = super(EmbeddedDocumentFormSet, self).save(commit=False) - if commit: + if commit and self.parent_document is not None: form = self.empty_form attr_data = getattr(self.parent_document, form._meta.embedded_field, []) setattr(self.parent_document, form._meta.embedded_field, attr_data + objs) @@ -713,6 +764,6 @@ def embeddedformset_factory(document, parent_document, form=EmbeddedDocumentForm 'exclude': exclude, 'max_num': max_num, } - FormSet = inlineformset_factory(document, **kwargs) + FormSet = documentformset_factory(document, **kwargs) FormSet.parent_document = parent_document return FormSet diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index 01e2f46b..3e0167c6 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -7,7 +7,12 @@ from django import forms from django.core.validators import EMPTY_VALUES -from django.utils.encoding import force_unicode + +try: + from django.utils.encoding import force_text as force_unicode +except ImportError: + from django.utils.encoding import force_unicode + try: from django.utils.encoding import smart_text as smart_unicode except ImportError: @@ -15,6 +20,7 @@ from django.utils.encoding import smart_unicode except ImportError: from django.forms.util import smart_unicode + from django.utils.translation import ugettext_lazy as _ try: # objectid was moved into bson in pymongo 1.9 From ce7a38a90474adab1cd5c770171b72d286bcd5d2 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 12 Jun 2013 00:21:53 +0200 Subject: [PATCH 024/136] Data cleanup on nonrequired empty fields before save. Closes #28 --- mongodbforms/documents.py | 120 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 4 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 1c548f37..9d47ac03 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -102,14 +102,18 @@ def save_instance(form, instance, fields=None, fail_message='saved', if commit and hasattr(instance, 'save'): # see BaseDocumentForm._post_clean for an explanation if hasattr(form, '_delete_before_save'): - fields = instance._fields - new_fields = dict([(n, f) for n, f in fields.items() if not n in form._delete_before_save]) + #fields = instance._fields + #new_fields = dict([(n, f) for n, f in fields.items() if not n in form._delete_before_save]) + data = instance._data + new_data = dict([(n, f) for n, f in data.items() if not n in form._delete_before_save]) if hasattr(instance, '_changed_fields'): for field in form._delete_before_save: instance._changed_fields.remove(field) - instance._fields = new_fields + #instance._fields = new_fields + instance._data = new_data instance.save() - instance._fields = fields + #instance._fields = fields + instance._data = data else: instance.save() @@ -605,6 +609,72 @@ def documentformset_factory(document, form=DocumentForm, formfield_callback=None FormSet.document = document return FormSet + +class BaseInlineDocumentFormSet(BaseDocumentFormSet): + """ + A formset for child objects related to a parent. + + self.instance -> the document containing the inline objects + """ + def __init__(self, data=None, files=None, instance=None, + save_as_new=False, prefix=None, queryset=[], **kwargs): + self.instance = instance + self.save_as_new = save_as_new + + super(BaseInlineDocumentFormSet, self).__init__(data, files, prefix=prefix, queryset=queryset, **kwargs) + + def initial_form_count(self): + if self.save_as_new: + return 0 + return super(BaseInlineDocumentFormSet, self).initial_form_count() + + #@classmethod + def get_default_prefix(cls): + return cls.model.__name__.lower() + get_default_prefix = classmethod(get_default_prefix) + + + def add_fields(self, form, index): + super(BaseInlineDocumentFormSet, self).add_fields(form, index) + + # Add the generated field to form._meta.fields if it's defined to make + # sure validation isn't skipped on that field. + if form._meta.fields: + if isinstance(form._meta.fields, tuple): + form._meta.fields = list(form._meta.fields) + #form._meta.fields.append(self.fk.name) + + def get_unique_error_message(self, unique_check): + unique_check = [field for field in unique_check if field != self.fk.name] + return super(BaseInlineDocumentFormSet, self).get_unique_error_message(unique_check) + + +def inlineformset_factory(document, form=DocumentForm, + formset=BaseInlineDocumentFormSet, + fields=None, exclude=None, + extra=1, can_order=False, can_delete=True, max_num=None, + formfield_callback=None): + """ + Returns an ``InlineFormSet`` for the given kwargs. + + You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` + to ``parent_model``. + """ + kwargs = { + 'form': form, + 'formfield_callback': formfield_callback, + 'formset': formset, + 'extra': extra, + 'can_delete': can_delete, + 'can_order': can_order, + 'fields': fields, + 'exclude': exclude, + 'max_num': max_num, + } + FormSet = documentformset_factory(document, **kwargs) + return FormSet + + #class BaseInlineDocumentFormSet(BaseDocumentFormSet): # """A formset for child objects related to a parent.""" # def __init__(self, data=None, files=None, instance=None, @@ -689,6 +759,48 @@ def documentformset_factory(document, form=DocumentForm, formfield_callback=None # unique_check = [field for field in unique_check if field != self.fk.name] # return super(BaseInlineDocumentFormSet, self).get_unique_error_message(unique_check) # +# +#def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False): +# """ +# Finds and returns the ForeignKey from model to parent if there is one +# (returns None if can_fail is True and no such field exists). If fk_name is +# provided, assume it is the name of the ForeignKey field. Unles can_fail is +# True, an exception is raised if there is no ForeignKey from model to +# parent_model. +# """ +# #opts = model._meta +# fields = model._fields +# if fk_name: +# if fk_name not in fields: +# raise Exception("%s has no field named '%s'" % (model, fk_name)) +# +# rel_model = getattr(model, fk_name, None) +# if not isinstance(fields.get(fk_name), ReferenceField) or \ +# rel_model != parent_model: +# raise Exception("rel_name '%s' is not a reference to %s" % (fk_name, parent_model)) +# else: +# # Try to discover what the ForeignKey from model to parent_model is +# rel_to_parent = [ +# f for f in fields +# if +# ] +# fks_to_parent = [ +# f for f in opts.fields +# if isinstance(f, ForeignKey) +# and (f.rel.to == parent_model +# or f.rel.to in parent_model._meta.get_parent_list()) +# ] +# if len(fks_to_parent) == 1: +# fk = fks_to_parent[0] +# elif len(fks_to_parent) == 0: +# if can_fail: +# return +# raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) +# else: +# raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) +# return fk + + # # #def inlineformset_factory(document, form=DocumentForm, From 09d92c9109802e2907ca98981b76cd0e89fb30a1 Mon Sep 17 00:00:00 2001 From: Matthew Gerring Date: Tue, 11 Jun 2013 21:22:33 -0700 Subject: [PATCH 025/136] EmbeddedDocumentFormSet should inherit from BaseInlineDocumentFormset --- mongodbforms/documents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 9d47ac03..52540ff5 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -828,7 +828,7 @@ def inlineformset_factory(document, form=DocumentForm, # FormSet = documentformset_factory(document, **kwargs) # return FormSet -class EmbeddedDocumentFormSet(BaseDocumentFormSet): +class EmbeddedDocumentFormSet(BaseInlineDocumentFormSet): def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=[], parent_document=None, **kwargs): self.parent_document = parent_document From e6a6fbbe0bd3d10dd790828a88a0a5765343cd58 Mon Sep 17 00:00:00 2001 From: Matthew Gerring Date: Tue, 11 Jun 2013 21:55:44 -0700 Subject: [PATCH 026/136] making parent_document into a keyword argument on EmbeddedDocumentForm to avoid a problem with formsets --- mongodbforms/documents.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 52540ff5..b42ef2ce 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -476,7 +476,7 @@ def documentform_factory(document, form=DocumentForm, fields=None, exclude=None, class EmbeddedDocumentForm(with_metaclass(DocumentFormMetaclass, BaseDocumentForm)): - def __init__(self, parent_document, *args, **kwargs): + def __init__(self, parent_document = parent_document, *args, **kwargs): super(EmbeddedDocumentForm, self).__init__(*args, **kwargs) self.parent_document = parent_document if self._meta.embedded_field is not None and not \ @@ -839,6 +839,7 @@ def _construct_form(self, i, **kwargs): defaults.update(kwargs) form = super(EmbeddedDocumentFormSet, self)._construct_form(i, **defaults) return form + def save(self, commit=True): # Don't try to save the new documents. Embedded objects don't have From 3aa75a1444f756f5a3c79cb94d2ef4b561eb9655 Mon Sep 17 00:00:00 2001 From: Matthew Gerring Date: Tue, 11 Jun 2013 22:04:21 -0700 Subject: [PATCH 027/136] trying a different approach with patchin EmbeddedDocumentFormSet --- mongodbforms/documents.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index b42ef2ce..32a9977b 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -476,7 +476,7 @@ def documentform_factory(document, form=DocumentForm, fields=None, exclude=None, class EmbeddedDocumentForm(with_metaclass(DocumentFormMetaclass, BaseDocumentForm)): - def __init__(self, parent_document = parent_document, *args, **kwargs): + def __init__(self, parent_document, *args, **kwargs): super(EmbeddedDocumentForm, self).__init__(*args, **kwargs) self.parent_document = parent_document if self._meta.embedded_field is not None and not \ @@ -840,6 +840,16 @@ def _construct_form(self, i, **kwargs): form = super(EmbeddedDocumentFormSet, self)._construct_form(i, **defaults) return form + @property + def empty_form(self): + form = self.form( + parent_document, + auto_id=self.auto_id, + prefix=self.add_prefix('__prefix__'), + empty_permitted=True, + ) + self.add_fields(form, None) + return form def save(self, commit=True): # Don't try to save the new documents. Embedded objects don't have From c96030fa409f8131a64400a849bcf1312b159ae9 Mon Sep 17 00:00:00 2001 From: Matthew Gerring Date: Tue, 11 Jun 2013 22:06:37 -0700 Subject: [PATCH 028/136] hurble duh and fries --- mongodbforms/documents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 32a9977b..6f2e10a7 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -843,7 +843,7 @@ def _construct_form(self, i, **kwargs): @property def empty_form(self): form = self.form( - parent_document, + self.parent_document, auto_id=self.auto_id, prefix=self.add_prefix('__prefix__'), empty_permitted=True, From f20c40a20042cc6329548c2db77c6780c8cae5f5 Mon Sep 17 00:00:00 2001 From: Matthew Gerring Date: Tue, 11 Jun 2013 22:23:46 -0700 Subject: [PATCH 029/136] The save method on EmbeddedDocumentFormSet should simply replace the embedded field with the new values - details are in a comment --- mongodbforms/documents.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 6f2e10a7..6a9b9be1 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -858,8 +858,22 @@ def save(self, commit=True): if commit and self.parent_document is not None: form = self.empty_form - attr_data = getattr(self.parent_document, form._meta.embedded_field, []) - setattr(self.parent_document, form._meta.embedded_field, attr_data + objs) + # The thing about formsets is that the base use case is to edit *all* + # of the associated objects on a model. As written, using these FormSets this + # way will cause the existing embedded documents to get saved along with a + # copy of themselves plus any new ones you added. + # + # The only way you could do "updates" of existing embedded document fields is + # if those embedded documents had ObjectIDs of their own, which they don't + # by default in Mongoengine. + # + # In this case it makes the most sense to simply replace the embedded field + # with the new values gathered form the formset, rather than adding the new + # values to the existing values, because the new values will almost always + # contain the old values (with the default use case.) + # + # attr_data = getattr(self.parent_document, form._meta.embedded_field, []) + setattr(self.parent_document, form._meta.embedded_field, objs or []) self.parent_document.save() return objs From bc56010ca3bc16559e933eb81b32366bd54e2c35 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 12 Jun 2013 22:47:19 +0200 Subject: [PATCH 030/136] First try at proper ListField handling. More in #21 --- mongodbforms/documents.py | 79 +++++++++++++------------ mongodbforms/fieldgenerator.py | 20 ++++++- mongodbforms/fields.py | 104 +++++++++++++++++++++++++++++++++ mongodbforms/widgets.py | 100 +++++++++++++++++++++++++++++++ 4 files changed, 260 insertions(+), 43 deletions(-) create mode 100644 mongodbforms/widgets.py diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 9d47ac03..cd730a74 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -62,7 +62,7 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): if isinstance(f, FileField) or isinstance(f, ImageField): file_field_list.append(f) else: - setattr(instance, f.name, cleaned_data[f.name]) + setattr(instance, f.name, cleaned_data.get(f.name)) for f in file_field_list: upload = cleaned_data[f.name] @@ -102,17 +102,13 @@ def save_instance(form, instance, fields=None, fail_message='saved', if commit and hasattr(instance, 'save'): # see BaseDocumentForm._post_clean for an explanation if hasattr(form, '_delete_before_save'): - #fields = instance._fields - #new_fields = dict([(n, f) for n, f in fields.items() if not n in form._delete_before_save]) data = instance._data new_data = dict([(n, f) for n, f in data.items() if not n in form._delete_before_save]) if hasattr(instance, '_changed_fields'): for field in form._delete_before_save: instance._changed_fields.remove(field) - #instance._fields = new_fields instance._data = new_data instance.save() - #instance._fields = fields instance._data = data else: instance.save() @@ -353,7 +349,6 @@ def _post_clean(self): self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude) exclude = self._get_validation_exclusions() - # Clean the model instance's fields. to_delete = [] try: @@ -680,7 +675,7 @@ def inlineformset_factory(document, form=DocumentForm, # def __init__(self, data=None, files=None, instance=None, # save_as_new=False, prefix=None, queryset=None, **kwargs): # if instance is None: -# self.instance = self.fk.rel.to() +# self.instance = self.rel_field.name # else: # self.instance = instance # self.save_as_new = save_as_new @@ -760,60 +755,59 @@ def inlineformset_factory(document, form=DocumentForm, # return super(BaseInlineDocumentFormSet, self).get_unique_error_message(unique_check) # # -#def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False): +#def _get_rel_field(parent_document, model, rel_name=None, can_fail=False): # """ # Finds and returns the ForeignKey from model to parent if there is one # (returns None if can_fail is True and no such field exists). If fk_name is # provided, assume it is the name of the ForeignKey field. Unles can_fail is # True, an exception is raised if there is no ForeignKey from model to -# parent_model. +# parent_document. # """ # #opts = model._meta # fields = model._fields -# if fk_name: -# if fk_name not in fields: -# raise Exception("%s has no field named '%s'" % (model, fk_name)) +# if rel_name: +# if rel_name not in fields: +# raise Exception("%s has no field named '%s'" % (model, rel_name)) # -# rel_model = getattr(model, fk_name, None) -# if not isinstance(fields.get(fk_name), ReferenceField) or \ -# rel_model != parent_model: -# raise Exception("rel_name '%s' is not a reference to %s" % (fk_name, parent_model)) +# rel_model = getattr(model, rel_name, None) +# ref_field = fields.get(rel_name) +# if not isinstance(ref_field, ReferenceField) or \ +# rel_model != parent_document: +# raise Exception("rel_name '%s' is not a reference to %s" % (rel_name, parent_document)) # else: -# # Try to discover what the ForeignKey from model to parent_model is +# # Try to discover what the ForeignKey from model to parent_document is # rel_to_parent = [ # f for f in fields -# if -# ] -# fks_to_parent = [ -# f for f in opts.fields -# if isinstance(f, ForeignKey) -# and (f.rel.to == parent_model -# or f.rel.to in parent_model._meta.get_parent_list()) +# if isinstance(f, ReferenceField) +# and getattr(model, f.name) == parent_document # ] -# if len(fks_to_parent) == 1: -# fk = fks_to_parent[0] -# elif len(fks_to_parent) == 0: +# if len(rel_to_parent) == 1: +# ref_field = rel_to_parent[0] +# elif len(rel_to_parent) == 0: # if can_fail: # return -# raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) +# raise Exception("%s has no relation to %s" % (model, parent_document)) # else: -# raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) -# return fk - - +# raise Exception("%s has more than 1 relation to %s" % (model, parent_document)) +# return ref_field +# # -# -#def inlineformset_factory(document, form=DocumentForm, -# formset=BaseInlineDocumentFormSet, -# fields=None, exclude=None, -# extra=1, can_order=False, can_delete=True, max_num=None, -# formfield_callback=None): +#def inlineformset_factory(parent_document, model, form=ModelForm, +# formset=BaseInlineFormSet, fk_name=None, +# fields=None, exclude=None, extra=3, can_order=False, +# can_delete=True, max_num=None, formfield_callback=None, +# widgets=None, validate_max=False, localized_fields=None): # """ # Returns an ``InlineFormSet`` for the given kwargs. # # You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` -# to ``parent_model``. +# to ``parent_document``. # """ +# rel_field = _get_rel_field(parent_document, model, fk_name=fk_name) +# # You can't have more then reference in a ReferenceField +# # so max_num is always one for now (maybe +# # ListFields(ReferenceFields) will be supported one day). +# max_num = 1 # kwargs = { # 'form': form, # 'formfield_callback': formfield_callback, @@ -824,10 +818,15 @@ def inlineformset_factory(document, form=DocumentForm, # 'fields': fields, # 'exclude': exclude, # 'max_num': max_num, +# 'widgets': widgets, +# 'validate_max': validate_max, +# 'localized_fields': localized_fields, # } -# FormSet = documentformset_factory(document, **kwargs) +# FormSet = documentformset_factory(model, **kwargs) +# FormSet.rel_field = rel_field # return FormSet + class EmbeddedDocumentFormSet(BaseDocumentFormSet): def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=[], parent_document=None, **kwargs): diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index bb014cb9..f157e622 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -17,9 +17,9 @@ from django.db.models.options import get_verbose_name from django.utils.text import capfirst -from mongoengine import ReferenceField as MongoReferenceField +from mongoengine import ReferenceField as MongoReferenceField, EmbeddedDocumentField as MongoEmbeddedDocumentField -from .fields import MongoCharField, ReferenceField, DocumentMultipleChoiceField +from .fields import MongoCharField, ReferenceField, DocumentMultipleChoiceField, ListField BLANK_CHOICE_DASH = [("", "---------")] @@ -66,7 +66,9 @@ def boolean_field(self, value): def get_field_label(self, field): if field.verbose_name: return field.verbose_name - return capfirst(get_verbose_name(field.name)) + if field.name is not None: + return capfirst(get_verbose_name(field.name)) + return '' def get_field_help_text(self, field): if field.help_text: @@ -245,6 +247,18 @@ def generate_listfield(self, field, **kwargs): defaults.update(kwargs) f = DocumentMultipleChoiceField(field.field.document_type.objects, **defaults) return f + elif not isinstance(field.field, MongoEmbeddedDocumentField): + defaults = { + 'label': self.get_field_label(field), + 'help_text': self.get_field_help_text(field), + 'required': field.required, + 'initial': getattr(field._owner_document, field.name, []) + } + defaults.update(kwargs) + # figure out which type of field is stored in the list + form_field = self.generate(field.field) + f = ListField(form_field.__class__, **defaults) + return f def generate_filefield(self, field, **kwargs): defaults = { diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index 3e0167c6..93430bd5 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -22,6 +22,8 @@ from django.forms.util import smart_unicode from django.utils.translation import ugettext_lazy as _ +from django.forms.util import ErrorList +from django.core.exceptions import ValidationError try: # objectid was moved into bson in pymongo 1.9 from bson.objectid import ObjectId @@ -29,6 +31,8 @@ except ImportError: from pymongo.objectid import ObjectId from pymongo.errors import InvalidId + +from .widgets import MultiWidget class MongoChoiceIterator(object): def __init__(self, field): @@ -168,3 +172,103 @@ def prepare_value(self, value): if hasattr(value, '__iter__') and not hasattr(value, '_meta'): return [super(DocumentMultipleChoiceField, self).prepare_value(v) for v in value] return super(DocumentMultipleChoiceField, self).prepare_value(value) + + +class ListField(forms.Field): + """ + A Field that aggregates the logic of multiple Fields. + + Its clean() method takes a "decompressed" list of values, which are then + cleaned into a single value according to self.fields. Each value in + this list is cleaned by the corresponding field -- the first value is + cleaned by the first field, the second value is cleaned by the second + field, etc. Once all fields are cleaned, the list of clean values is + "compressed" into a single value. + + Subclasses should not have to implement clean(). Instead, they must + implement compress(), which takes a list of valid values and returns a + "compressed" version of those values -- a single value. + + You'll probably want to use this with MultiWidget. + """ + default_error_messages = { + 'invalid': _('Enter a list of values.'), + } + widget = MultiWidget + + def __init__(self, field_type, *args, **kwargs): + self.field_type = field_type + self.fields = [] + widget = self.field_type().widget + if isinstance(widget, type): + w_type = widget + else: + w_type = widget.__class__ + self.widget = self.widget(w_type) + + super(ListField, self).__init__(*args, **kwargs) + + if not hasattr(self, 'empty_values'): + self.empty_values = list(EMPTY_VALUES) + + def _init_fields(self, initial): + empty_val = ['',] + if initial is None: + initial = empty_val + else: + initial = initial + empty_val + print initial + + fields = [self.field_type(initial=d) for d in initial] + + return fields + + def validate(self, value): + pass + + def clean(self, value): + """ + Validates every value in the given list. A value is validated against + the corresponding Field in self.fields. + + For example, if this MultiValueField was instantiated with + fields=(DateField(), TimeField()), clean() would call + DateField.clean(value[0]) and TimeField.clean(value[1]). + """ + clean_data = [] + errors = ErrorList() + if not value or isinstance(value, (list, tuple)): + if not value or not [v for v in value if v not in self.empty_values]: + if self.required: + raise ValidationError(self.error_messages['required']) + else: + return [] + else: + raise ValidationError(self.error_messages['invalid']) + + field = self.field_type() + for field_value in value: + if self.required and field_value in self.empty_values: + raise ValidationError(self.error_messages['required']) + try: + clean_data.append(field.clean(field_value)) + except ValidationError as e: + # Collect all validation errors in a single list, which we'll + # raise at the end of clean(), rather than raising a single + # exception for the first error we encounter. + errors.extend(e.messages) + if errors: + raise ValidationError(errors) + + self.validate(clean_data) + self.run_validators(clean_data) + return clean_data + + def _has_changed(self, initial, data): + if initial is None: + initial = ['' for x in range(0, len(data))] + for field, initial, data in zip(self.fields, initial, data): + if field._has_changed(initial, data): + return True + return False + diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py new file mode 100644 index 00000000..b5b7cd14 --- /dev/null +++ b/mongodbforms/widgets.py @@ -0,0 +1,100 @@ +import copy + +from django.forms.widgets import Widget, Media +from django.utils.safestring import mark_safe + + +class MultiWidget(Widget): + """ + A widget that is composed of multiple widgets. + + Its render() method is different than other widgets', because it has to + figure out how to split a single value for display in multiple widgets. + The ``value`` argument can be one of two things: + + * A list. + * A normal value (e.g., a string) that has been "compressed" from + a list of values. + + In the second case -- i.e., if the value is NOT a list -- render() will + first "decompress" the value into a list before rendering it. It does so by + calling the decompress() method, which MultiWidget subclasses must + implement. This method takes a single "compressed" value and returns a + list. + + When render() does its HTML rendering, each value in the list is rendered + with the corresponding widget -- the first value is rendered in the first + widget, the second value is rendered in the second widget, etc. + + Subclasses may implement format_output(), which takes the list of rendered + widgets and returns a string of HTML that formats them any way you'd like. + + You'll probably want to use this class with MultiValueField. + """ + def __init__(self, widget_type, attrs=None): + self.widget_type = widget_type + self.widgets = [] + super(MultiWidget, self).__init__(attrs) + + def render(self, name, value, attrs=None): + if not isinstance(value, list): + # Yes! This is a stub. Some exception should be raised here I suppose + # I'm totally not sure which one though. + pass + self.widgets = [self.widget_type() for v in value] + # always add an empty field to be able to add data + self.widgets.append(self.widget_type()) + if self.is_localized: + for widget in self.widgets: + widget.is_localized = self.is_localized + output = [] + final_attrs = self.build_attrs(attrs) + id_ = final_attrs.get('id', None) + for i, widget in enumerate(self.widgets): + try: + widget_value = value[i] + except IndexError: + widget_value = None + if id_: + final_attrs = dict(final_attrs, id='%s_%s' % (id_, i)) + output.append(widget.render(name + '_%s' % i, widget_value, final_attrs)) + return mark_safe(self.format_output(output)) + + def id_for_label(self, id_): + # See the comment for RadioSelect.id_for_label() + if id_: + id_ += '_0' + return id_ + + def value_from_datadict(self, data, files, name): + widget = self.widget_type() + i = 0 + ret = [] + while data.has_key(name + '_%s' % i): + ret.append(widget.value_from_datadict(data, files, name + '_%s' % i)) + i = i + 1 + return ret + + def format_output(self, rendered_widgets): + """ + Given a list of rendered widgets (as strings), returns a Unicode string + representing the HTML for the whole lot. + + This hook allows you to format the HTML design of the widgets, if + needed. + """ + return ''.join(rendered_widgets) + + def _get_media(self): + "Media for a multiwidget is the combination of all media of the subwidgets" + media = Media() + for w in self.widgets: + media = media + w.media + return media + media = property(_get_media) + + def __deepcopy__(self, memo): + obj = super(MultiWidget, self).__deepcopy__(memo) + obj.widgets = copy.deepcopy(self.widgets) + obj.widget_type = copy.deepcopy(self.widget_type) + return obj \ No newline at end of file From e558856627cf6f57ae83a88c7060c78d5be1eb48 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 12 Jun 2013 23:42:21 +0200 Subject: [PATCH 031/136] Handle empty fields correctly --- mongodbforms/fields.py | 5 +++-- mongodbforms/widgets.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index 93430bd5..f0dcd463 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -217,7 +217,6 @@ def _init_fields(self, initial): initial = empty_val else: initial = initial + empty_val - print initial fields = [self.field_type(initial=d) for d in initial] @@ -246,7 +245,7 @@ def clean(self, value): else: raise ValidationError(self.error_messages['invalid']) - field = self.field_type() + field = self.field_type(required=self.required) for field_value in value: if self.required and field_value in self.empty_values: raise ValidationError(self.error_messages['required']) @@ -257,6 +256,8 @@ def clean(self, value): # raise at the end of clean(), rather than raising a single # exception for the first error we encounter. errors.extend(e.messages) + if field.required: + field.required = False if errors: raise ValidationError(errors) diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index b5b7cd14..f3aae354 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -2,6 +2,7 @@ from django.forms.widgets import Widget, Media from django.utils.safestring import mark_safe +from django.core.validators import EMPTY_VALUES class MultiWidget(Widget): @@ -42,8 +43,11 @@ def render(self, name, value, attrs=None): # I'm totally not sure which one though. pass self.widgets = [self.widget_type() for v in value] - # always add an empty field to be able to add data - self.widgets.append(self.widget_type()) + if len(value[-1:]) == 0 or value[-1:][0] != '': + # add an empty field to be able to add data + empty_widget = self.widget_type() + empty_widget.is_required = False + self.widgets.append(empty_widget) if self.is_localized: for widget in self.widgets: widget.is_localized = self.is_localized @@ -71,7 +75,9 @@ def value_from_datadict(self, data, files, name): i = 0 ret = [] while data.has_key(name + '_%s' % i): - ret.append(widget.value_from_datadict(data, files, name + '_%s' % i)) + value = widget.value_from_datadict(data, files, name + '_%s' % i) + if value not in EMPTY_VALUES: + ret.append(value) i = i + 1 return ret From b9aeb2faf89eafec8796ebd5944c52be376f5560 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Thu, 13 Jun 2013 01:02:02 +0200 Subject: [PATCH 032/136] Hopefully will make as_p work. --- mongodbforms/fieldgenerator.py | 2 +- mongodbforms/widgets.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index f157e622..7011fdd9 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -252,7 +252,7 @@ def generate_listfield(self, field, **kwargs): 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field), 'required': field.required, - 'initial': getattr(field._owner_document, field.name, []) + #'initial': getattr(field._owner_document, field.name, []) } defaults.update(kwargs) # figure out which type of field is stored in the list diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index f3aae354..2e26dd61 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -42,8 +42,9 @@ def render(self, name, value, attrs=None): # Yes! This is a stub. Some exception should be raised here I suppose # I'm totally not sure which one though. pass - self.widgets = [self.widget_type() for v in value] - if len(value[-1:]) == 0 or value[-1:][0] != '': + if value is not None: + self.widgets = [self.widget_type() for v in value] + if value is None or (len(value[-1:]) == 0 or value[-1:][0] != ''): # add an empty field to be able to add data empty_widget = self.widget_type() empty_widget.is_required = False @@ -57,7 +58,7 @@ def render(self, name, value, attrs=None): for i, widget in enumerate(self.widgets): try: widget_value = value[i] - except IndexError: + except (IndexError, TypeError): widget_value = None if id_: final_attrs = dict(final_attrs, id='%s_%s' % (id_, i)) From e6a4fb5e9eacab30f10aa2a1d981e80547c0cf5f Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Thu, 13 Jun 2013 01:43:42 +0200 Subject: [PATCH 033/136] QueryDicts have no has_key. So we use 'in' --- mongodbforms/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index 2e26dd61..0a2ea745 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -75,7 +75,7 @@ def value_from_datadict(self, data, files, name): widget = self.widget_type() i = 0 ret = [] - while data.has_key(name + '_%s' % i): + while (name + '_%s' % i) in data: value = widget.value_from_datadict(data, files, name + '_%s' % i) if value not in EMPTY_VALUES: ret.append(value) From fc23b1a4fefeb1f24ef42f4f8421abe327fff15d Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Thu, 13 Jun 2013 02:47:39 +0200 Subject: [PATCH 034/136] Cleanups and some renaming --- mongodbforms/fields.py | 39 +++++++++++++++------------------------ mongodbforms/widgets.py | 23 ++++++++++++++--------- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index f0dcd463..078b6e90 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -32,7 +32,7 @@ from pymongo.objectid import ObjectId from pymongo.errors import InvalidId -from .widgets import MultiWidget +from .widgets import ListWidget class MongoChoiceIterator(object): def __init__(self, field): @@ -148,17 +148,18 @@ def clean(self, value): return [] if not isinstance(value, (list, tuple)): raise forms.ValidationError(self.error_messages['list']) + key = 'pk' - - filter_ids = [] - for pk in value: - try: - oid = ObjectId(pk) - filter_ids.append(oid) - except InvalidId: - raise forms.ValidationError(self.error_messages['invalid_pk_value'] % pk) +# filter_ids = [] +# for pk in value: +# filter_ids.append(pk) +# try: +# oid = ObjectId(pk) +# filter_ids.append(oid) +# except InvalidId: +# raise forms.ValidationError(self.error_messages['invalid_pk_value'] % pk) qs = self.queryset.clone() - qs = qs.filter(**{'%s__in' % key: filter_ids}) + qs = qs.filter(**{'%s__in' % key: value}) pks = set([force_unicode(getattr(o, key)) for o in qs]) for val in value: if force_unicode(val) not in pks: @@ -194,11 +195,10 @@ class ListField(forms.Field): default_error_messages = { 'invalid': _('Enter a list of values.'), } - widget = MultiWidget + widget = ListWidget def __init__(self, field_type, *args, **kwargs): self.field_type = field_type - self.fields = [] widget = self.field_type().widget if isinstance(widget, type): w_type = widget @@ -210,17 +210,6 @@ def __init__(self, field_type, *args, **kwargs): if not hasattr(self, 'empty_values'): self.empty_values = list(EMPTY_VALUES) - - def _init_fields(self, initial): - empty_val = ['',] - if initial is None: - initial = empty_val - else: - initial = initial + empty_val - - fields = [self.field_type(initial=d) for d in initial] - - return fields def validate(self, value): pass @@ -268,7 +257,9 @@ def clean(self, value): def _has_changed(self, initial, data): if initial is None: initial = ['' for x in range(0, len(data))] - for field, initial, data in zip(self.fields, initial, data): + + field = self.field_type(required=self.required) + for initial, data in zip(initial, data): if field._has_changed(initial, data): return True return False diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index 0a2ea745..4dd43ead 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -5,7 +5,7 @@ from django.core.validators import EMPTY_VALUES -class MultiWidget(Widget): +class ListWidget(Widget): """ A widget that is composed of multiple widgets. @@ -35,23 +35,25 @@ class MultiWidget(Widget): def __init__(self, widget_type, attrs=None): self.widget_type = widget_type self.widgets = [] - super(MultiWidget, self).__init__(attrs) + super(ListWidget, self).__init__(attrs) def render(self, name, value, attrs=None): - if not isinstance(value, list): - # Yes! This is a stub. Some exception should be raised here I suppose - # I'm totally not sure which one though. - pass + if value is not None and not isinstance(value, (list, tuple)): + raise TypeError("Value supplied for %s must be a list or tuple." % name) + if value is not None: self.widgets = [self.widget_type() for v in value] + if value is None or (len(value[-1:]) == 0 or value[-1:][0] != ''): - # add an empty field to be able to add data + # there should be exactly one empty widget at the end of the list empty_widget = self.widget_type() empty_widget.is_required = False self.widgets.append(empty_widget) + if self.is_localized: for widget in self.widgets: widget.is_localized = self.is_localized + output = [] final_attrs = self.build_attrs(attrs) id_ = final_attrs.get('id', None) @@ -101,7 +103,10 @@ def _get_media(self): media = property(_get_media) def __deepcopy__(self, memo): - obj = super(MultiWidget, self).__deepcopy__(memo) + obj = super(ListWidget, self).__deepcopy__(memo) obj.widgets = copy.deepcopy(self.widgets) obj.widget_type = copy.deepcopy(self.widget_type) - return obj \ No newline at end of file + return obj + + + \ No newline at end of file From db34ed2d2566b355ee4f37265e5f7e04d4ceb746 Mon Sep 17 00:00:00 2001 From: Rafael Novello Date: Wed, 12 Jun 2013 23:41:46 -0300 Subject: [PATCH 035/136] Changed to support unique_with --- mongodbforms/documents.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index d26f2651..227b5c12 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -387,7 +387,7 @@ def _post_clean(self): def validate_unique(self): """ Validates unique constrains on the document. - unique_with is not checked at the moment. + unique_with is supported now. """ errors = [] exclude = self._get_validation_exclusions() @@ -396,6 +396,9 @@ def validate_unique(self): filter_kwargs = { f.name: getattr(self.instance, f.name) } + if f.unique_with: + for u_with in f.unique_with: + filter_kwargs[u_with] = getattr(self.instance, u_with) qs = self.instance.__class__.objects().filter(**filter_kwargs) # Exclude the current object from the query if we are editing an # instance (as opposed to creating a new one) From 22982bbda4b153168491cb3618063eb021095d63 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 14 Jun 2013 00:57:55 +0200 Subject: [PATCH 036/136] More glue for admin in documentoptions and some dynamic widget stuff (not useable yet) --- mongodbforms/documentoptions.py | 30 +- .../static/mongodbforms/dynamiclistwidget.js | 376 ++++++++++++++++++ mongodbforms/util.py | 1 - mongodbforms/widgets.py | 25 ++ 4 files changed, 428 insertions(+), 4 deletions(-) create mode 100644 mongodbforms/static/mongodbforms/dynamiclistwidget.js diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 17e175a2..127bd64b 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -2,11 +2,19 @@ from collections import MutableMapping from django.db.models.fields import FieldDoesNotExist -from django.db.models.options import get_verbose_name from django.utils.text import capfirst from mongoengine.fields import ReferenceField +from django.db.models.options import get_verbose_name + +def create_verbose_name(name): + name = get_verbose_name(name) + name.replace('_', ' ') + print "create_verbose_name" + print name + return name + class PkWrapper(object): def __init__(self, wrapped): self.obj = wrapped @@ -75,9 +83,9 @@ def verbose_name(self): """ if self._verbose_name is None: try: - self._verbose_name = capfirst(get_verbose_name(self._meta['verbose_name'])) + self._verbose_name = capfirst(create_verbose_name(self._meta['verbose_name'])) except KeyError: - self._verbose_name = capfirst(get_verbose_name(self.object_name)) + self._verbose_name = capfirst(create_verbose_name(self.object_name)) return self._verbose_name @@ -161,6 +169,22 @@ def _init_field_cache(self): self._field_cache = {} for f in self.document._fields.values(): + # Yay, more glue. Django expects fields to have a rel attribute + # at least in the admin, probably in more places. So we add them here + # and hope that this is the common path to access the fields. + if not hasattr(f, 'rel'): + f.rel = None + if getattr(f, 'verbose_name', None) is None: + f.verbose_name = capfirst(create_verbose_name(f.name)) + if not hasattr(f, 'flatchoices'): + flat = [] + if f.choices is not None: + for choice, value in f.choices: + if isinstance(value, (list, tuple)): + flat.extend(value) + else: + flat.append((choice,value)) + f.flatchoices = flat if isinstance(f, ReferenceField): document = f.document_type document._meta = DocumentMetaWrapper(document) diff --git a/mongodbforms/static/mongodbforms/dynamiclistwidget.js b/mongodbforms/static/mongodbforms/dynamiclistwidget.js new file mode 100644 index 00000000..35709f56 --- /dev/null +++ b/mongodbforms/static/mongodbforms/dynamiclistwidget.js @@ -0,0 +1,376 @@ +(function(context) { + window.mdbf = {} + /* by @k33g_org */ + var root = this; + + (function(speculoos) { + + var Class = (function() { + function Class(definition) { + /* from CoffeeScript */ + var __hasProp = Object.prototype.hasOwnProperty, m, F; + + this["extends"] = function(child, parent) { + for (m in parent) { + if (__hasProp.call(parent, m)) + child[m] = parent[m]; + } + function ctor() { + this.constructor = child; + } + ctor.prototype = parent.prototype; + child.prototype = new ctor; + child.__super__ = child["super"] = parent.prototype; + return child; + } + + if (definition.constructor.name === "Object") { + F = function() { + }; + } else { + F = definition.constructor; + } + + /* inheritance */ + if (definition["extends"]) { + this["extends"](F, definition["extends"]) + } + for (m in definition) { + if (m != 'constructor' && m != 'extends') { + if (m[0] != '$') { + F.prototype[m] = definition[m]; + } else { /* static members */ + F[m.split('$')[1]] = definition[m]; + } + } + } + + return F; + } + return Class; + })(); + + speculoos.Class = Class; + + })(mdbf); + + /* + * ! onDomReady.js 1.2 (c) 2012 Tubal Martin - MIT license + */ + !function(definition) { + if (typeof define === "function" && define.amd) { + // Register as an AMD module. + define(definition); + } else { + // Browser globals + window.mdbf.onDomReady = definition(); + } + } + (function() { + + 'use strict'; + + var win = window, doc = win.document, docElem = doc.documentElement, + + FALSE = false, COMPLETE = "complete", READYSTATE = "readyState", ATTACHEVENT = "attachEvent", ADDEVENTLISTENER = "addEventListener", DOMCONTENTLOADED = "DOMContentLoaded", ONREADYSTATECHANGE = "onreadystatechange", + + // W3C Event model + w3c = ADDEVENTLISTENER in doc, top = FALSE, + + // isReady: Is the DOM ready to be used? Set to true once it + // occurs. + isReady = FALSE, + + // Callbacks pending execution until DOM is ready + callbacks = []; + + // Handle when the DOM is ready + function ready(fn) { + if (!isReady) { + + // Make sure body exists, at least, in case IE gets a + // little overzealous (ticket #5443). + if (!doc.body) { + return defer(ready); + } + + // Remember that the DOM is ready + isReady = true; + + // Execute all callbacks + while (fn = callbacks.shift()) { + defer(fn); + } + } + } + + // The document ready event handler + function DOMContentLoadedHandler() { + if (w3c) { + doc.removeEventListener(DOMCONTENTLOADED, + DOMContentLoadedHandler, FALSE); + ready(); + } else if (doc[READYSTATE] === COMPLETE) { + // we're here because readyState === "complete" in oldIE + // which is good enough for us to call the dom ready! + doc.detachEvent(ONREADYSTATECHANGE, + DOMContentLoadedHandler); + ready(); + } + } + + // Defers a function, scheduling it to run after the current + // call stack has cleared. + function defer(fn, wait) { + // Allow 0 to be passed + setTimeout(fn, +wait >= 0 ? wait : 1); + } + + // Attach the listeners: + + // Catch cases where onDomReady is called after the browser + // event has already occurred. + // we once tried to use readyState "interactive" here, but it + // caused issues like the one + // discovered by ChrisS here: + // http://bugs.jquery.com/ticket/12282#comment:15 + if (doc[READYSTATE] === COMPLETE) { + // Handle it asynchronously to allow scripts the opportunity + // to delay ready + defer(ready); + + // Standards-based browsers support DOMContentLoaded + } else if (w3c) { + // Use the handy event callback + doc[ADDEVENTLISTENER](DOMCONTENTLOADED, + DOMContentLoadedHandler, FALSE); + + // A fallback to window.onload, that will always work + win[ADDEVENTLISTENER]("load", ready, FALSE); + + // If IE event model is used + } else { + // ensure firing before onload, + // maybe late but safe also for iframes + doc[ATTACHEVENT](ONREADYSTATECHANGE, + DOMContentLoadedHandler); + + // A fallback to window.onload, that will always work + win[ATTACHEVENT]("onload", ready); + + // If IE and not a frame + // continually check to see if the document is ready + try { + top = win.frameElement == null && docElem; + } catch (e) { + } + + if (top && top.doScroll) { + (function doScrollCheck() { + if (!isReady) { + try { + // Use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + top.doScroll("left"); + } catch (e) { + return defer(doScrollCheck, 50); + } + + // and execute any waiting functions + ready(); + } + })(); + } + } + + function onDomReady(fn) { + // If DOM is ready, execute the function (async), otherwise + // wait + isReady ? defer(fn) : callbacks.push(fn); + } + + // Add version + onDomReady.version = "1.2"; + + return onDomReady; + }); + /* + * nut, the concise CSS selector engine + * + * Version : 0.2.0 Author : Aurélien Delogu (dev@dreamysource.fr) Homepage : + * https://github.com/pyrsmk/nut License : MIT + */ + + window.mdbf.nut = function() { + + var doc = document, firstChild = 'firstChild', nextSibling = 'nextSibling', getElementsByClassName = 'getElementsByClassName', length = 'length', + + /* + * Get id node + * + * Parameters String selector : one selector Object context : one + * context + * + * Return Array : nodes + */ + getNodesFromIdSelector = function(selector, context) { + var node = doc.getElementById(selector); + if (!node) { + return []; + } else { + return [ node ]; + } + }, + + /* + * Get nodes corresponding to one class name (for IE<9) + * + * Parameters String selector : one selector Object context : one + * context + * + * Return Array : nodes + */ + getNodesByClassName = function(name, context) { + // Init vars + var node = context[firstChild], nodes = [], elements; + // Browse children + if (node) { + do { + if (node.nodeType == 1) { + // Match the class + if (node.className + && node.className.match('\\b' + name + '\\b')) { + nodes.push(node); + } + // Get nodes from node's children + if ((elements = getNodesByClassName(name, node))[length]) { + nodes = nodes.concat(elements); + } + } + } while (node = node[nextSibling]); + } + return nodes; + }, + + /* + * Get nodes from a class selector + * + * Parameters String selector : one selector Object context : one + * context + * + * Return Array : nodes + */ + getNodesFromClassSelector = function(selector, context) { + if (context[getElementsByClassName]) { + return context[getElementsByClassName](selector); + } else { + return getNodesByClassName(selector, context); + } + }, + + /* + * Get nodes from a tag selector + * + * Parameters String selector : one selector Object context : one + * context + * + * Return Array : nodes + */ + getNodesFromTagSelector = function(selector, context) { + return context.getElementsByTagName(selector); + }; + + /* + * Select DOM nodes + * + * Parameters String selectors : CSS selectors Array, Object context : + * contextual node + * + * Return Array : found nodes + */ + return function(selectors, context) { + // Format + if (!context) { + context = doc; + } + if (typeof context == 'object' && context.pop) { + context = context[0]; + } + // Init vars + var local_contexts, future_local_contexts, selector, elements, nodes = [], j, k, l, m, n, o, getNodesFromSelector; + // Prepare selectors + selectors = selectors.split(','); + n = -1; + while (selector = selectors[++n]) { + selectors[n] = selector.split(/\s+/); + } + // Evaluate selectors for each global context + j = selectors[length]; + while (j) { + // Init local context + local_contexts = [ context ]; + // Evaluate selectors + k = -1; + l = selectors[--j][length]; + while (++k < l) { + // Drop empty selectors + if (selector = selectors[j][k]) { + // Id + if (selector.charAt(0) == '#') { + selector = selector.substr(1); + getNodesFromSelector = getNodesFromIdSelector; + } + // Class + else if (selector.charAt(0) == '.') { + selector = selector.substr(1); + getNodesFromSelector = getNodesFromClassSelector; + } + // Tag + else { + getNodesFromSelector = getNodesFromTagSelector; + } + // Evaluate current selector for each local context + future_local_contexts = []; + m = -1; + while (local_contexts[++m]) { + elements = getNodesFromSelector(selector, + local_contexts[m]); + n = -1; + o = elements[length]; + while (++n < o) { + future_local_contexts.push(elements[n]); + } + } + // Set new local contexts + local_contexts = future_local_contexts; + } + } + // Append new nodes + nodes = nodes.concat(local_contexts); + } + return nodes; + }; + + }(); + + window.mdbf.FieldClass = mdbf.Class({ + constructor: function FieldClass(class_name) { + // name of the field. used to generate additional fields + this.name = class_name.split('-')[1]; + this.context = mdbf.nut('.' + class_name); + this.inputs = mdbf.nut('input', context); + // contains the max number in use for the inputs id and name + this.input_counter = this.inputs.length - 1; + // base to add more fields on click + this.empty_input = this.inputs[0].cloneNode(true); + this.empty_input.value = ''; + this.empty_input.id = ''; + this.empty_input.name = ''; + console.log(this.inputs); + }, + }); + + window.mdbf.init = function(class_name) { + var field = mdbf.FieldClass(class_name); + } +}()); diff --git a/mongodbforms/util.py b/mongodbforms/util.py index 4ca4e7b8..3c0febb1 100644 --- a/mongodbforms/util.py +++ b/mongodbforms/util.py @@ -15,7 +15,6 @@ def init_document_options(document): def get_document_options(document): return DocumentMetaWrapper(document) - # Taken from six (https://pypi.python.org/pypi/six) # by "Benjamin Peterson " # diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index 4dd43ead..0961f2ec 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -41,6 +41,9 @@ def render(self, name, value, attrs=None): if value is not None and not isinstance(value, (list, tuple)): raise TypeError("Value supplied for %s must be a list or tuple." % name) + # save the name should we need it later + self._name = name + if value is not None: self.widgets = [self.widget_type() for v in value] @@ -108,5 +111,27 @@ def __deepcopy__(self, memo): obj.widget_type = copy.deepcopy(self.widget_type) return obj +class DynamicListWidget(ListWidget): + def format_output(self, rendered_widgets): + """ + Given a list of rendered widgets (as strings), returns a Unicode string + representing the HTML for the whole lot. + + This hook allows you to format the HTML design of the widgets, if + needed. + """ + output = [] + for widget in rendered_widgets: + output.append("

%s

" % widget) + output.append('' % self._name) + return ''.join(output) + def _get_media(self): + "Media for a multiwidget is the combination of all media of the subwidgets" + media = Media(js=('mongodbforms/dynamiclistwidget.js',)) + for w in self.widgets: + media = media + w.media + return media + media = property(_get_media) + \ No newline at end of file From e72a2121a946b988ddab0fe0def3463819ec9c3d Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 14 Jun 2013 14:12:34 +0200 Subject: [PATCH 037/136] Add more attributes to make delete in Django's 1.5 admin work --- mongodbforms/documentoptions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 127bd64b..08f8cc04 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -47,9 +47,13 @@ class DocumentMetaWrapper(MutableMapping): _field_cache = None document = None _meta = None + concrete_model = None def __init__(self, document): self.document = document + # used by Django to distinguish between abstract and concrete models + # here for now always the document + self.concrete_model = document self._meta = getattr(document, '_meta', {}) try: @@ -123,7 +127,7 @@ def _init_pk(self): self.document._pk_val = getattr(self.document, self.pk_name) # avoid circular import from mongodbforms.util import patch_document - def _get_pk_val(): + def _get_pk_val(self): return self._pk_val patch_document(_get_pk_val, self.document) except AttributeError: From 80606d540c2ecc7de6fcc91e52045a84c34913bc Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 14 Jun 2013 14:29:50 +0200 Subject: [PATCH 038/136] Removed forgotton print statements --- mongodbforms/documentoptions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 08f8cc04..34ec8966 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -11,8 +11,6 @@ def create_verbose_name(name): name = get_verbose_name(name) name.replace('_', ' ') - print "create_verbose_name" - print name return name class PkWrapper(object): From b71981f01da6476a4f1db0123526b319d8ca12cc Mon Sep 17 00:00:00 2001 From: Laurent Payot Date: Thu, 20 Jun 2013 08:05:28 +0200 Subject: [PATCH 039/136] bugfix: instance empty ListFields were not removed before saving, so they could be lost. --- mongodbforms/documents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 227b5c12..f1579f0d 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -356,7 +356,7 @@ def _post_clean(self): value = getattr(self.instance, f.name) if f.name not in exclude: f.validate(value) - elif value == '': + elif value == '' or value == []: # mongoengine chokes on empty strings for fields # that are not required. Clean them up here, though # this is maybe not the right place :-) From e40982627283ce14fd317843137eb707a28a0c10 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Thu, 20 Jun 2013 14:40:04 +0200 Subject: [PATCH 040/136] Minor changes --- mongodbforms/documentoptions.py | 3 +-- mongodbforms/static/mongodbforms/dynamiclistwidget.js | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 34ec8966..0429af11 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -3,11 +3,10 @@ from django.db.models.fields import FieldDoesNotExist from django.utils.text import capfirst +from django.db.models.options import get_verbose_name from mongoengine.fields import ReferenceField -from django.db.models.options import get_verbose_name - def create_verbose_name(name): name = get_verbose_name(name) name.replace('_', ' ') diff --git a/mongodbforms/static/mongodbforms/dynamiclistwidget.js b/mongodbforms/static/mongodbforms/dynamiclistwidget.js index 35709f56..55210435 100644 --- a/mongodbforms/static/mongodbforms/dynamiclistwidget.js +++ b/mongodbforms/static/mongodbforms/dynamiclistwidget.js @@ -353,6 +353,12 @@ }(); + window.mdbf.EmptyInput = mdbf.Class({ + constructor: function EmptyInput(elem) { + + }, + }); + window.mdbf.FieldClass = mdbf.Class({ constructor: function FieldClass(class_name) { // name of the field. used to generate additional fields @@ -368,6 +374,7 @@ this.empty_input.name = ''; console.log(this.inputs); }, + }); window.mdbf.init = function(class_name) { From 75ac5b8d5ec7cf1986c52b711e3c21ca1808f289 Mon Sep 17 00:00:00 2001 From: Laurent Payot Date: Fri, 21 Jun 2013 03:00:22 +0200 Subject: [PATCH 041/136] EmbeddedDocumentForm optimization with position argument and atomic updates --- mongodbforms/documents.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index f1579f0d..7e22a0a5 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -17,6 +17,7 @@ from mongoengine.base import ValidationError except ImportError: from mongoengine.errors import ValidationError +from mongoengine.queryset import OperationError from mongoengine.connection import _get_db from gridfs import GridFS @@ -474,9 +475,11 @@ def documentform_factory(document, form=DocumentForm, fields=None, exclude=None, class EmbeddedDocumentForm(with_metaclass(DocumentFormMetaclass, BaseDocumentForm)): - def __init__(self, parent_document, *args, **kwargs): + + def __init__(self, parent_document, position=None, *args, **kwargs): super(EmbeddedDocumentForm, self).__init__(*args, **kwargs) self.parent_document = parent_document + self.position = position if self._meta.embedded_field is not None and not \ self._meta.embedded_field in self.parent_document._fields: raise FieldError("Parent document must have field %s" % self._meta.embedded_field) @@ -491,20 +494,26 @@ def save(self, commit=True): " validate." % self.instance.__class__.__name__) if commit: - field = self.parent_document._fields.get(self._meta.embedded_field) - if isinstance(field, ListField) and field.default is None: - default = [] + field = self.parent_document._fields.get(self._meta.embedded_field) + if isinstance(field, ListField): + if self.position is None: + # no position given, simply appending to ListField + try: + self.parent_document.update(**{"push__" + self._meta.embedded_field: self.instance}) + except: + raise OperationError("The %s could not be appended." % self.instance.__class__.__name__) + else: + # updating ListField at given position + try: + self.parent_document.update(**{"__".join(("set", self._meta.embedded_field, + str(self.position))): self.instance}) + except: + raise OperationError("The %s could not be updated at position " + "%d." % (self.instance.__class__.__name__, self.position)) else: - default = field.default - attr = getattr(self.parent_document, self._meta.embedded_field, default) - try: - attr.append(self.instance) - except AttributeError: # not a listfield on parent, treat as an embedded field - attr = self.instance - setattr(self.parent_document, self._meta.embedded_field, attr) - self.parent_document.save() - + setattr(self.parent_document, self._meta.embedded_field, self.instance) + self.parent_document.save() return self.instance From 26d1d696f83b19b618cc19c885a39b692ecb11de Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 21 Jun 2013 15:02:16 +0200 Subject: [PATCH 042/136] Load instance in EmbeddedDocumentForm.__init__ if position argument is provided --- mongodbforms/documents.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 7e22a0a5..1d6cf105 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -476,8 +476,13 @@ def documentform_factory(document, form=DocumentForm, fields=None, exclude=None, class EmbeddedDocumentForm(with_metaclass(DocumentFormMetaclass, BaseDocumentForm)): - def __init__(self, parent_document, position=None, *args, **kwargs): - super(EmbeddedDocumentForm, self).__init__(*args, **kwargs) + def __init__(self, parent_document, instance=None, position=None, *args, **kwargs): + # if we received a list position of the instance and no instance + # load the instance from the parent document and preceed as normal + if instance is None and position is not None: + instance = getattr(parent_document, self._meta.embedded_field)[position] + + super(EmbeddedDocumentForm, self).__init__(instance=instance, *args, **kwargs) self.parent_document = parent_document self.position = position if self._meta.embedded_field is not None and not \ From e66c492fd60a75cf1e19e84bab3cc988a75b7aa8 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 21 Jun 2013 15:06:11 +0200 Subject: [PATCH 043/136] Test for parent_embedded_field before using it --- mongodbforms/documents.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 1d6cf105..654438d0 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -477,6 +477,10 @@ def documentform_factory(document, form=DocumentForm, fields=None, exclude=None, class EmbeddedDocumentForm(with_metaclass(DocumentFormMetaclass, BaseDocumentForm)): def __init__(self, parent_document, instance=None, position=None, *args, **kwargs): + if self._meta.embedded_field is not None and not \ + self._meta.embedded_field in parent_document._fields: + raise FieldError("Parent document must have field %s" % self._meta.embedded_field) + # if we received a list position of the instance and no instance # load the instance from the parent document and preceed as normal if instance is None and position is not None: @@ -485,9 +489,6 @@ def __init__(self, parent_document, instance=None, position=None, *args, **kwarg super(EmbeddedDocumentForm, self).__init__(instance=instance, *args, **kwargs) self.parent_document = parent_document self.position = position - if self._meta.embedded_field is not None and not \ - self._meta.embedded_field in self.parent_document._fields: - raise FieldError("Parent document must have field %s" % self._meta.embedded_field) def save(self, commit=True): """If commit is True the embedded document is added to the parent From d020626e7e75fa3b385eb2bb1b0d5988a0ac3e07 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 21 Jun 2013 15:25:58 +0200 Subject: [PATCH 044/136] Updated documentation for embedded fields --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 318dda40..96e436f4 100644 --- a/readme.md +++ b/readme.md @@ -22,9 +22,9 @@ To use mongodbforms with normal documents replace djangos forms with mongodbform ### Embedded documents -For embedded documents use `EmbeddedDocumentForm`. The Meta-object of the form has to be provided with an embedded field name. The embedded object is appended to this. The form constructor takes an additional argument: The document the embedded document gets added to. +For embedded documents use `EmbeddedDocumentForm`. The Meta-object of the form has to be provided with an embedded field name. The embedded object is appended to this. The form constructor takes a couple of additional arguments: The document the embedded document gets added to and an optional position argument. -If the form is saved the new embedded object is automatically added to the provided parent document. If the embedded field is a list field the embedded document is appended to the list, if it is a plain embedded field the current object is overwritten. Note that the parent document is not saved. +If no position is provided the form adds a new embedded document to the list if the form is saved. To edit an embedded document stored in a list field the position argument is required. If you provide a position and no instance to the form the instance is automatically loaded using the position argument. If the embedded field is a plain embedded field the current object is overwritten. # forms.py from mongodbforms import EmbeddedDocumentForm From 6ad8c057bfa2e477a15e49230a557646b2bcdddf Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 21 Jun 2013 15:29:22 +0200 Subject: [PATCH 045/136] More docs --- readme.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/readme.md b/readme.md index 96e436f4..735822a4 100644 --- a/readme.md +++ b/readme.md @@ -26,18 +26,23 @@ For embedded documents use `EmbeddedDocumentForm`. The Meta-object of the form h If no position is provided the form adds a new embedded document to the list if the form is saved. To edit an embedded document stored in a list field the position argument is required. If you provide a position and no instance to the form the instance is automatically loaded using the position argument. If the embedded field is a plain embedded field the current object is overwritten. - # forms.py - from mongodbforms import EmbeddedDocumentForm +````python +# forms.py +from mongodbforms import EmbeddedDocumentForm - class MessageForm(EmbeddedDocumentForm): - class Meta: - document = Message - embedded_field_name = 'messages' +class MessageForm(EmbeddedDocumentForm): + class Meta: + document = Message + embedded_field_name = 'messages' - fields = ['subject', 'sender', 'message',] - - # views.py - form = MessageForm(parent_document=some_document, ...) + fields = ['subject', 'sender', 'message',] + +# views.py +# create a new embedded object +form = MessageForm(parent_document=some_document, ...) +# edit the 4th embedded object +form = MessageForm(parent_document=some_document, position=3, ...) +``` ## Documentation From 5cbe856f08c1fe85be75ab37d81476ce16111539 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 21 Jun 2013 15:33:25 +0200 Subject: [PATCH 046/136] And again doc formatting --- readme.md | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/readme.md b/readme.md index 735822a4..c9b2a40d 100644 --- a/readme.md +++ b/readme.md @@ -15,10 +15,12 @@ mongodbforms supports forms for normal documents and embedded documents. To use mongodbforms with normal documents replace djangos forms with mongodbform forms. - from mongodbforms import DocumentForm +```python +from mongodbforms import DocumentForm - class BlogForm(DocumentForm) - ... +class BlogForm(DocumentForm) + ... +``` ### Embedded documents @@ -38,6 +40,7 @@ class MessageForm(EmbeddedDocumentForm): fields = ['subject', 'sender', 'message',] # views.py + # create a new embedded object form = MessageForm(parent_document=some_document, ...) # edit the 4th embedded object @@ -50,24 +53,26 @@ In theory the documentation [Django's modelform](https://docs.djangoproject.com/ ### Form field generation -Because the fields on mongoengine documents have no notion of form fields every mongodbform uses a generator class to generate the form field for a db field, which is not explicitly set. +Because the fields on mongoengine documents have no notion of form fields mongodbform uses a generator class to generate the form field for a db field, which is not explicitly set. If you want to use your own generator class you can use the ``formfield_generator`` option on the form's Meta class. - # generator.py - from mongodbforms.fieldgenerator import MongoFormFieldGenerator +````python +# generator.py +from mongodbforms.fieldgenerator import MongoFormFieldGenerator - class MyFieldGenerator(MongoFormFieldGenerator): - ... +class MyFieldGenerator(MongoFormFieldGenerator): + ... - # forms.py - from mongodbforms import DocumentForm +# forms.py +from mongodbforms import DocumentForm - from generator import MyFieldGenerator +from generator import MyFieldGenerator - class MessageForm(DocumentForm): - class Meta: - formfield_generator = MyFieldGenerator +class MessageForm(DocumentForm): + class Meta: + formfield_generator = MyFieldGenerator +``` From 6931bb30cae1b59b5d12de69580229c56d5ac8f7 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 21 Jun 2013 15:50:02 +0200 Subject: [PATCH 047/136] Figure out position if instance is provided for embedded forms --- mongodbforms/documents.py | 16 ++++++++++++---- readme.md | 4 +++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 654438d0..89ddacba 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -481,10 +481,18 @@ def __init__(self, parent_document, instance=None, position=None, *args, **kwarg self._meta.embedded_field in parent_document._fields: raise FieldError("Parent document must have field %s" % self._meta.embedded_field) - # if we received a list position of the instance and no instance - # load the instance from the parent document and preceed as normal - if instance is None and position is not None: - instance = getattr(parent_document, self._meta.embedded_field)[position] + if isinstance(self.parent_document._fields.get(self._meta.embedded_field), ListField): + # if we received a list position of the instance and no instance + # load the instance from the parent document and proceed as normal + if instance is None and position is not None: + instance = getattr(parent_document, self._meta.embedded_field)[position] + + # same as above only the other way around. Note: Mongoengine defines equality + # as having the same data, so if you have 2 objects with the same data the first + # one will be edited. That nay or may not be the right one. + if instance is not None and position is None: + emb_list = getattr(parent_document, self._meta.embedded_field) + [i for i, obj in enumerate(emb_list) if obj == instance] super(EmbeddedDocumentForm, self).__init__(instance=instance, *args, **kwargs) self.parent_document = parent_document diff --git a/readme.md b/readme.md index c9b2a40d..58cbab01 100644 --- a/readme.md +++ b/readme.md @@ -26,7 +26,9 @@ class BlogForm(DocumentForm) For embedded documents use `EmbeddedDocumentForm`. The Meta-object of the form has to be provided with an embedded field name. The embedded object is appended to this. The form constructor takes a couple of additional arguments: The document the embedded document gets added to and an optional position argument. -If no position is provided the form adds a new embedded document to the list if the form is saved. To edit an embedded document stored in a list field the position argument is required. If you provide a position and no instance to the form the instance is automatically loaded using the position argument. If the embedded field is a plain embedded field the current object is overwritten. +If no position is provided the form adds a new embedded document to the list if the form is saved. To edit an embedded document stored in a list field the position argument is required. If you provide a position and no instance to the form the instance is automatically loaded using the position argument. + +If the embedded field is a plain embedded field the current object is simply overwritten. ````python # forms.py From 655aeeea5d18ffbc95dee3d39e8f85fc175e5922 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 21 Jun 2013 15:55:04 +0200 Subject: [PATCH 048/136] actually assign position and not just figure it out. Dough! --- mongodbforms/documents.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 89ddacba..b217e546 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -481,7 +481,7 @@ def __init__(self, parent_document, instance=None, position=None, *args, **kwarg self._meta.embedded_field in parent_document._fields: raise FieldError("Parent document must have field %s" % self._meta.embedded_field) - if isinstance(self.parent_document._fields.get(self._meta.embedded_field), ListField): + if isinstance(parent_document._fields.get(self._meta.embedded_field), ListField): # if we received a list position of the instance and no instance # load the instance from the parent document and proceed as normal if instance is None and position is not None: @@ -492,7 +492,11 @@ def __init__(self, parent_document, instance=None, position=None, *args, **kwarg # one will be edited. That nay or may not be the right one. if instance is not None and position is None: emb_list = getattr(parent_document, self._meta.embedded_field) - [i for i, obj in enumerate(emb_list) if obj == instance] + position = [i for i, obj in enumerate(emb_list) if obj == instance] + if len(position) > 0: + position = position[0] + else: + position = None super(EmbeddedDocumentForm, self).__init__(instance=instance, *args, **kwargs) self.parent_document = parent_document From 036d519c4f2af11310eb3b2d916bd99ddca22171 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 21 Jun 2013 17:00:32 +0200 Subject: [PATCH 049/136] Use position argument in embedded formsets --- mongodbforms/documents.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index b217e546..b0d99156 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -864,7 +864,14 @@ def __init__(self, data=None, files=None, instance=None, def _construct_form(self, i, **kwargs): defaults = {'parent_document': self.parent_document} + + # add position argument to the form. Otherwise we will spend + # a huge amount of time iterating over the list field on form __init__ + emb_list = getattr(self.parent_document, self.form._meta.embedded_field) + if len(emb_list) <= i: + defaults['position'] = i defaults.update(kwargs) + form = super(EmbeddedDocumentFormSet, self)._construct_form(i, **defaults) return form From a3745c894051a8b25d04c59713e070deafe9e3a9 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Sat, 22 Jun 2013 23:16:03 +0200 Subject: [PATCH 050/136] A couple small fixes and some code formatting --- mongodbforms/documents.py | 55 ++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index b0d99156..cc2d4e3f 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -107,7 +107,10 @@ def save_instance(form, instance, fields=None, fail_message='saved', new_data = dict([(n, f) for n, f in data.items() if not n in form._delete_before_save]) if hasattr(instance, '_changed_fields'): for field in form._delete_before_save: - instance._changed_fields.remove(field) + try: + instance._changed_fields.remove(field) + except ValueError: + pass instance._data = new_data instance.save() instance._data = data @@ -489,14 +492,10 @@ def __init__(self, parent_document, instance=None, position=None, *args, **kwarg # same as above only the other way around. Note: Mongoengine defines equality # as having the same data, so if you have 2 objects with the same data the first - # one will be edited. That nay or may not be the right one. + # one will be edited. That may or may not be the right one. if instance is not None and position is None: emb_list = getattr(parent_document, self._meta.embedded_field) - position = [i for i, obj in enumerate(emb_list) if obj == instance] - if len(position) > 0: - position = position[0] - else: - position = None + position = next((i for i, obj in enumerate(emb_list) if obj == instance), None) super(EmbeddedDocumentForm, self).__init__(instance=instance, *args, **kwargs) self.parent_document = parent_document @@ -513,21 +512,20 @@ def save(self, commit=True): if commit: field = self.parent_document._fields.get(self._meta.embedded_field) - if isinstance(field, ListField): - if self.position is None: - # no position given, simply appending to ListField - try: - self.parent_document.update(**{"push__" + self._meta.embedded_field: self.instance}) - except: - raise OperationError("The %s could not be appended." % self.instance.__class__.__name__) - else: - # updating ListField at given position - try: - self.parent_document.update(**{"__".join(("set", self._meta.embedded_field, - str(self.position))): self.instance}) - except: - raise OperationError("The %s could not be updated at position " - "%d." % (self.instance.__class__.__name__, self.position)) + if isinstance(field, ListField) and self.position is None: + # no position given, simply appending to ListField + try: + self.parent_document.update(**{"push__" + self._meta.embedded_field: self.instance}) + except: + raise OperationError("The %s could not be appended." % self.instance.__class__.__name__) + elif isinstance(field, ListField) and self.position is not None: + # updating ListField at given position + try: + self.parent_document.update(**{"__".join(("set", self._meta.embedded_field, + str(self.position))): self.instance}) + except: + raise OperationError("The %s could not be updated at position " + "%d." % (self.instance.__class__.__name__, self.position)) else: # not a listfield on parent, treat as an embedded field setattr(self.parent_document, self._meta.embedded_field, self.instance) @@ -655,7 +653,7 @@ def initial_form_count(self): #@classmethod def get_default_prefix(cls): - return cls.model.__name__.lower() + return cls.document.__name__.lower() get_default_prefix = classmethod(get_default_prefix) @@ -856,11 +854,11 @@ def inlineformset_factory(document, form=DocumentForm, # FormSet.rel_field = rel_field # return FormSet -class EmbeddedDocumentFormSet(BaseInlineDocumentFormSet): - def __init__(self, data=None, files=None, instance=None, - save_as_new=False, prefix=None, queryset=[], parent_document=None, **kwargs): +class EmbeddedDocumentFormSet(BaseDocumentFormSet): + def __init__(self, data=None, files=None, save_as_new=False, + prefix=None, queryset=[], parent_document=None, **kwargs): self.parent_document = parent_document - super(EmbeddedDocumentFormSet, self).__init__(data, files, instance, save_as_new, prefix, queryset, **kwargs) + super(EmbeddedDocumentFormSet, self).__init__(data, files, save_as_new, prefix, queryset, **kwargs) def _construct_form(self, i, **kwargs): defaults = {'parent_document': self.parent_document} @@ -868,7 +866,7 @@ def _construct_form(self, i, **kwargs): # add position argument to the form. Otherwise we will spend # a huge amount of time iterating over the list field on form __init__ emb_list = getattr(self.parent_document, self.form._meta.embedded_field) - if len(emb_list) <= i: + if len(emb_list) < i: defaults['position'] = i defaults.update(kwargs) @@ -937,5 +935,4 @@ def embeddedformset_factory(document, parent_document, form=EmbeddedDocumentForm 'max_num': max_num, } FormSet = documentformset_factory(document, **kwargs) - FormSet.parent_document = parent_document return FormSet From 29f4f74b8e17633b4fe90d57fdfb4d226f857c42 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Sun, 23 Jun 2013 04:15:59 +0200 Subject: [PATCH 051/136] Initial support for MapFields. Probably full of bugs and not too sure about the HTML --- mongodbforms/fieldgenerator.py | 15 +++- mongodbforms/fields.py | 139 +++++++++++++++++++++++++++++++- mongodbforms/widgets.py | 142 ++++++++++++++++++++++++++------- 3 files changed, 262 insertions(+), 34 deletions(-) diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 7011fdd9..d655e8cb 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -19,7 +19,7 @@ from mongoengine import ReferenceField as MongoReferenceField, EmbeddedDocumentField as MongoEmbeddedDocumentField -from .fields import MongoCharField, ReferenceField, DocumentMultipleChoiceField, ListField +from .fields import MongoCharField, ReferenceField, DocumentMultipleChoiceField, ListField, MapField BLANK_CHOICE_DASH = [("", "---------")] @@ -257,8 +257,17 @@ def generate_listfield(self, field, **kwargs): defaults.update(kwargs) # figure out which type of field is stored in the list form_field = self.generate(field.field) - f = ListField(form_field.__class__, **defaults) - return f + return ListField(form_field.__class__, **defaults) + + def generate_mapfield(self, field, **kwargs): + defaults = { + 'label': self.get_field_label(field), + 'help_text': self.get_field_help_text(field), + 'required': field.required + } + defaults.update(kwargs) + form_field = self.generate(field.field) + return MapField(form_field.__class__, **defaults) def generate_filefield(self, field, **kwargs): defaults = { diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index 078b6e90..0945b13d 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -6,7 +6,7 @@ """ from django import forms -from django.core.validators import EMPTY_VALUES +from django.core.validators import EMPTY_VALUES, MinLengthValidator, MaxLengthValidator try: from django.utils.encoding import force_text as force_unicode @@ -32,7 +32,7 @@ from pymongo.objectid import ObjectId from pymongo.errors import InvalidId -from .widgets import ListWidget +from .widgets import ListWidget, MapWidget class MongoChoiceIterator(object): def __init__(self, field): @@ -236,8 +236,6 @@ def clean(self, value): field = self.field_type(required=self.required) for field_value in value: - if self.required and field_value in self.empty_values: - raise ValidationError(self.error_messages['required']) try: clean_data.append(field.clean(field_value)) except ValidationError as e: @@ -264,3 +262,136 @@ def _has_changed(self, initial, data): return True return False +class MapField(forms.Field): + default_error_messages = { + 'invalid': _('Enter a list of values.'), + 'key_required': _('A key is required.'), + } + widget = MapWidget + + def __init__(self, field_type, max_key_length=None, min_key_length=None, + key_validators=[], field_kwargs={}, *args, **kwargs): + + widget = field_type().widget + if isinstance(widget, type): + w_type = widget + else: + w_type = widget.__class__ + self.widget = self.widget(w_type) + + super(MapField, self).__init__(*args, **kwargs) + + self.key_validators = key_validators + if min_key_length is not None: + self.key_validators.append(MinLengthValidator(int(min_key_length))) + if max_key_length is not None: + self.key_validators.append(MaxLengthValidator(int(max_key_length))) + + # type of field used to store the dicts value + self.field_type = field_type + field_kwargs['required'] = self.required + self.field_kwargs = field_kwargs + + if not hasattr(self, 'empty_values'): + self.empty_values = list(EMPTY_VALUES) + + def _validate_key(self, key): + if key in self.empty_values and self.required: + raise ValidationError(self.error_messages['key_required'], code='key_required') + errors = [] + for v in self.key_validators: + try: + v(key) + except ValidationError as e: + if hasattr(e, 'code'): + code = 'key_%s' % e.code + if code in self.error_messages: + e.message = self.error_messages[e.code] + errors.extend(e.error_list) + if errors: + raise ValidationError(errors) + + def validate(self, value): + pass + + def clean(self, value): + """ + Validates every value in the given list. A value is validated against + the corresponding Field in self.fields. + + For example, if this MultiValueField was instantiated with + fields=(DateField(), TimeField()), clean() would call + DateField.clean(value[0]) and TimeField.clean(value[1]). + """ + clean_data = {} + errors = ErrorList() + if not value or isinstance(value, dict): + if not value or not [v for v in value.values() if v not in self.empty_values]: + if self.required: + raise ValidationError(self.error_messages['required']) + else: + return {} + else: + raise ValidationError(self.error_messages['invalid']) + + # sort out required => at least one element must be in there + + data_field = self.field_type(**self.field_kwargs) + for key, val in value.items(): + # ignore empties. Can they even come up here? + if key in self.empty_values and val in self.empty_values: + continue + + try: + val = data_field.clean(val) + except ValidationError as e: + # Collect all validation errors in a single list, which we'll + # raise at the end of clean(), rather than raising a single + # exception for the first error we encounter. + errors.extend(e.messages) + + try: + self._validate_key(key) + except ValidationError as e: + # Collect all validation errors in a single list, which we'll + # raise at the end of clean(), rather than raising a single + # exception for the first error we encounter. + errors.extend(e.messages) + + clean_data[key] = val + + if data_field.required: + data_field.required = False + + if errors: + raise ValidationError(errors) + + self.validate(clean_data) + self.run_validators(clean_data) + return clean_data + + def _has_changed(self, initial, data): + field = self.field_type(**self.field_kwargs) + for k, v in data.items(): + if initial is None: + init_val = '' + else: + try: + init_val = initial[k] + except KeyError: + return True + if field._has_changed(init_val, v): + return True + return False + + + + + + + + + + + + diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index 0961f2ec..846183f0 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -1,9 +1,10 @@ import copy -from django.forms.widgets import Widget, Media +from django.forms.widgets import Widget, Media, TextInput from django.utils.safestring import mark_safe from django.core.validators import EMPTY_VALUES - +from django.forms.util import flatatt +from django.utils.html import format_html class ListWidget(Widget): """ @@ -34,40 +35,23 @@ class ListWidget(Widget): """ def __init__(self, widget_type, attrs=None): self.widget_type = widget_type - self.widgets = [] + self.widget = widget_type() + if self.is_localized: + self.widget.is_localized = self.is_localized super(ListWidget, self).__init__(attrs) def render(self, name, value, attrs=None): if value is not None and not isinstance(value, (list, tuple)): - raise TypeError("Value supplied for %s must be a list or tuple." % name) - - # save the name should we need it later - self._name = name - - if value is not None: - self.widgets = [self.widget_type() for v in value] - - if value is None or (len(value[-1:]) == 0 or value[-1:][0] != ''): - # there should be exactly one empty widget at the end of the list - empty_widget = self.widget_type() - empty_widget.is_required = False - self.widgets.append(empty_widget) - - if self.is_localized: - for widget in self.widgets: - widget.is_localized = self.is_localized + raise TypeError("Value supplied for %s must be a list or tuple." % name) output = [] final_attrs = self.build_attrs(attrs) id_ = final_attrs.get('id', None) - for i, widget in enumerate(self.widgets): - try: - widget_value = value[i] - except (IndexError, TypeError): - widget_value = None + value.append('') + for i, widget_value in enumerate(value): if id_: final_attrs = dict(final_attrs, id='%s_%s' % (id_, i)) - output.append(widget.render(name + '_%s' % i, widget_value, final_attrs)) + output.append(self.widget.render(name + '_%s' % i, widget_value, final_attrs)) return mark_safe(self.format_output(output)) def id_for_label(self, id_): @@ -107,7 +91,7 @@ def _get_media(self): def __deepcopy__(self, memo): obj = super(ListWidget, self).__deepcopy__(memo) - obj.widgets = copy.deepcopy(self.widgets) + obj.widget = copy.deepcopy(self.widget) obj.widget_type = copy.deepcopy(self.widget_type) return obj @@ -133,5 +117,109 @@ def _get_media(self): media = media + w.media return media media = property(_get_media) + + +class MapWidget(Widget): + """ + A widget that is composed of multiple widgets. + + Its render() method is different than other widgets', because it has to + figure out how to split a single value for display in multiple widgets. + The ``value`` argument can be one of two things: + + * A list. + * A normal value (e.g., a string) that has been "compressed" from + a list of values. + + In the second case -- i.e., if the value is NOT a list -- render() will + first "decompress" the value into a list before rendering it. It does so by + calling the decompress() method, which MultiWidget subclasses must + implement. This method takes a single "compressed" value and returns a + list. + + When render() does its HTML rendering, each value in the list is rendered + with the corresponding widget -- the first value is rendered in the first + widget, the second value is rendered in the second widget, etc. + + Subclasses may implement format_output(), which takes the list of rendered + widgets and returns a string of HTML that formats them any way you'd like. + + You'll probably want to use this class with MultiValueField. + """ + def __init__(self, widget_type, attrs=None): + self.widget_type = widget_type + self.key_widget = TextInput() + self.key_widget.is_localized = self.is_localized + self.data_widget = self.widget_type() + self.data_widget.is_localized = self.is_localized + super(MapWidget, self).__init__(attrs) + + def render(self, name, value, attrs=None): + if value is not None and not isinstance(value, dict): + raise TypeError("Value supplied for %s must be a dict." % name) + + output = [] + final_attrs = self.build_attrs(attrs) + id_ = final_attrs.get('id', None) + fieldset_attr = {} + value = value.items() + value.append(('', '')) + for i, (key, widget_value) in enumerate(value): + if id_: + final_attrs = dict(final_attrs, id='%s_%s' % (id_, i)) + fieldset_attr = dict(final_attrs, id='fieldset_%s_%s' % (id_, i)) + + group = [] + group.append(format_html('', flatatt(fieldset_attr))) + group.append(self.key_widget.render(name + '_key_%s' % i, key, final_attrs)) + group.append(self.data_widget.render(name + '_value_%s' % i, widget_value, final_attrs)) + group.append('') + + output.append(mark_safe(''.join(group))) + return mark_safe(self.format_output(output)) + + def id_for_label(self, id_): + # See the comment for RadioSelect.id_for_label() + if id_: + id_ += '_0' + return id_ + + def value_from_datadict(self, data, files, name): + i = 0 + ret = {} + while (name + '_key_%s' % i) in data: + key = self.key_widget.value_from_datadict(data, files, name + '_key_%s' % i) + value = self.data_widget.value_from_datadict(data, files, name + '_value_%s' % i) + if key not in EMPTY_VALUES: + ret.update(((key, value), )) + i = i + 1 + return ret + + def format_output(self, rendered_widgets): + """ + Given a list of rendered widgets (as strings), returns a Unicode string + representing the HTML for the whole lot. + + This hook allows you to format the HTML design of the widgets, if + needed. + """ + return ''.join(rendered_widgets) + + def _get_media(self): + "Media for a multiwidget is the combination of all media of the subwidgets" + media = Media() + for w in self.widgets: + media = media + w.media + return media + media = property(_get_media) + + def __deepcopy__(self, memo): + obj = super(MapWidget, self).__deepcopy__(memo) + obj.widget_type = copy.deepcopy(self.widget_type) + obj.key_widget = copy.deepcopy(self.key_widget) + obj.data_widget = copy.deepcopy(self.data_widget) + return obj + + \ No newline at end of file From 9e024870131a828eb58c5a6e54f3a2d3c9b71cc0 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Sun, 23 Jun 2013 04:31:40 +0200 Subject: [PATCH 052/136] Added a field map to the generator to handle fields like SortedListField --- mongodbforms/fieldgenerator.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index d655e8cb..60e77783 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -25,6 +25,12 @@ class MongoFormFieldGenerator(object): """This class generates Django form-fields for mongoengine-fields.""" + + # used for fields that fit in one of the generate functions + # but don't actually have the name. + field_map = { + 'sortedlistfield': 'generate_listfield', + } def generate(self, field, **kwargs): """Tries to lookup a matching formfield generator (lowercase @@ -37,11 +43,14 @@ def generate(self, field, **kwargs): for cls in field.__class__.__bases__: cls_name = cls.__name__.lower() - if hasattr(self, 'generate_%s' % cls_name): + try: return getattr(self, 'generate_%s' % cls_name)(field, **kwargs) - - raise NotImplementedError('%s is not supported by MongoForm' % \ - field.__class__.__name__) + except AttributeError: + if cls_name in self.field_map: + return getattr(self, self.field_map.get(cls_name))(field, **kwargs) + else: + raise NotImplementedError('%s is not supported by MongoForm' % \ + field.__class__.__name__) def get_field_choices(self, field, include_blank=True, blank_choice=BLANK_CHOICE_DASH): From 6edf16c48df9f29f1ddc57a15626fa69af65d43e Mon Sep 17 00:00:00 2001 From: Laurent Payot Date: Sun, 23 Jun 2013 09:24:14 +0200 Subject: [PATCH 053/136] small MapField bugfixes --- mongodbforms/documents.py | 2 +- mongodbforms/widgets.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index cc2d4e3f..e49930f2 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -360,7 +360,7 @@ def _post_clean(self): value = getattr(self.instance, f.name) if f.name not in exclude: f.validate(value) - elif value == '' or value == []: + elif value is not False and not value: # in case of '' or [] or {} # mongoengine chokes on empty strings for fields # that are not required. Clean them up here, though # this is maybe not the right place :-) diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index 846183f0..5867f5fd 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -163,7 +163,7 @@ def render(self, name, value, attrs=None): id_ = final_attrs.get('id', None) fieldset_attr = {} - value = value.items() + value = list(value.items()) # in Python 3.X dict.items() returns dynamic *view objects* value.append(('', '')) for i, (key, widget_value) in enumerate(value): if id_: @@ -222,4 +222,4 @@ def __deepcopy__(self, memo): return obj - \ No newline at end of file + From da4b1344e113ffd8b4ec869b538f07b00f581449 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Sun, 23 Jun 2013 18:24:17 +0200 Subject: [PATCH 054/136] MapFields with FileFields save now. Not exactly sure what though. But to gte more peopple testing this, here is the code. Assume it's broken please and don't use this in production. --- mongodbforms/documents.py | 85 +++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 17 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index e49930f2..c989a4a7 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -12,7 +12,7 @@ from django.utils.translation import ugettext_lazy as _, ugettext from django.utils.text import capfirst -from mongoengine.fields import ObjectIdField, ListField, ReferenceField, FileField, ImageField +from mongoengine.fields import ObjectIdField, ListField, ReferenceField, FileField, MapField try: from mongoengine.base import ValidationError except ImportError: @@ -36,6 +36,20 @@ def _get_unique_filename(name): name = os.path.join("%s_%s%s" % (file_root, next(count), file_ext)) return name +# The awesome Mongoengine ImageGridFsProxy wants to pull a field +# from a document to get necessary data. Trouble is that this doesn't work +# if the ImageField is stored on List or MapField. So we pass a nice fake +# document to +class FakeDocument(object): + def __init__(self, key, field): + super(FakeDocument, self).__init__() + if not hasattr(self, '_fields'): + self._fields = {} + self._fields.update({key: field}) + + def _mark_as_changed(self, key): + pass + def construct_instance(form, instance, fields=None, exclude=None, ignore=None): """ Constructs and returns a document instance from the bound ``form``'s @@ -60,26 +74,63 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): continue # Defer saving file-type fields until after the other fields, so a # callable upload_to can use the values from other fields. - if isinstance(f, FileField) or isinstance(f, ImageField): + if isinstance(f, FileField) or (isinstance(f, (MapField, ListField)) and isinstance(f.field, FileField)): file_field_list.append(f) else: setattr(instance, f.name, cleaned_data.get(f.name)) for f in file_field_list: - upload = cleaned_data[f.name] - if upload is None: - continue - field = getattr(instance, f.name) - try: - upload.file.seek(0) - filename = _get_unique_filename(upload.name) - field.replace(upload, content_type=upload.content_type, filename=filename) - setattr(instance, f.name, field) - except AttributeError: - # file was already uploaded and not changed during edit. - # upload is already the gridfsproxy object we need. - upload.get() - setattr(instance, f.name, upload) + if isinstance(f, MapField): + map_field = getattr(instance, f.name) + uploads = cleaned_data[f.name] + for key, uploaded_file in uploads.items(): + if uploaded_file is None: + continue + + file_data = map_field.get(key, None) + uploaded_file.seek(0) + filename = _get_unique_filename(uploaded_file.name) + # add a file + _fake_document = FakeDocument(f.name, f.field) + overwrote_instance = False + overwrote_key = False + if file_data is None: + proxy = f.field.proxy_class(instance=_fake_document, key=f.name) + proxy.put(uploaded_file, content_type=uploaded_file.content_type, filename=filename) + proxy.instance = None + proxy.key = None + map_field[key] = proxy + else: + if file_data.instance is None: + overwrote_instance = True + file_data.instance = _fake_document + if file_data.key is None: + file_data.key = f.name + overwrote_key = True + file_data.delete() + file_data.put(uploaded_file, content_type=uploaded_file.content_type, filename=filename) + if overwrote_instance: + file_data.instance = None + if overwrote_key: + file_data.key = None + map_field[key] = file_data + setattr(instance, f.name, map_field) + else: + field = getattr(instance, f.name) + upload = cleaned_data[f.name] + if upload is None: + continue + + try: + upload.file.seek(0) + filename = _get_unique_filename(upload.name) + field.replace(upload, content_type=upload.content_type, filename=filename) + setattr(instance, f.name, field) + except AttributeError: + # file was already uploaded and not changed during edit. + # upload is already the gridfsproxy object we need. + upload.get() + setattr(instance, f.name, upload) return instance @@ -360,7 +411,7 @@ def _post_clean(self): value = getattr(self.instance, f.name) if f.name not in exclude: f.validate(value) - elif value is not False and not value: # in case of '' or [] or {} + elif value in EMPTY_VALUES: # mongoengine chokes on empty strings for fields # that are not required. Clean them up here, though # this is maybe not the right place :-) From d37e8921a516f697e23ef85f602db00fe590c59c Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Sun, 23 Jun 2013 20:58:09 +0200 Subject: [PATCH 055/136] Correctly init proxy_class for new images --- mongodbforms/documents.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index c989a4a7..aa1d41a5 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -39,14 +39,18 @@ def _get_unique_filename(name): # The awesome Mongoengine ImageGridFsProxy wants to pull a field # from a document to get necessary data. Trouble is that this doesn't work # if the ImageField is stored on List or MapField. So we pass a nice fake -# document to +# document to the proxy to get saving the file done. Yeah it really is that ugly. class FakeDocument(object): + _fields = {} + def __init__(self, key, field): super(FakeDocument, self).__init__() - if not hasattr(self, '_fields'): - self._fields = {} - self._fields.update({key: field}) - + + self._fields[key] = field + + # don't care if anything gets marked on this + # we do update a real field later though. That should + # trigger the same thing on the real document though. def _mark_as_changed(self, key): pass @@ -90,30 +94,36 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): file_data = map_field.get(key, None) uploaded_file.seek(0) filename = _get_unique_filename(uploaded_file.name) - # add a file - _fake_document = FakeDocument(f.name, f.field) + fake_document = FakeDocument(f.name, f.field) overwrote_instance = False overwrote_key = False + # save a new file if file_data is None: - proxy = f.field.proxy_class(instance=_fake_document, key=f.name) + proxy = f.field.proxy_class(instance=fake_document, key=f.name, + db_alias=f.field.db_alias, + collection_name=f.field.collection_name) proxy.put(uploaded_file, content_type=uploaded_file.content_type, filename=filename) + proxy.close() proxy.instance = None proxy.key = None map_field[key] = proxy + # overwrite an existing file else: if file_data.instance is None: + file_data.instance = fake_document overwrote_instance = True - file_data.instance = _fake_document if file_data.key is None: file_data.key = f.name overwrote_key = True file_data.delete() file_data.put(uploaded_file, content_type=uploaded_file.content_type, filename=filename) + file_data.close() if overwrote_instance: file_data.instance = None if overwrote_key: file_data.key = None map_field[key] = file_data + print map_field setattr(instance, f.name, map_field) else: field = getattr(instance, f.name) From f2d6ef9a0508af3b62e892de96ca2db827a9d37a Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Sun, 23 Jun 2013 21:04:13 +0200 Subject: [PATCH 056/136] Forgotten print statement --- mongodbforms/documents.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index aa1d41a5..3d6dadb4 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -123,7 +123,6 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): if overwrote_key: file_data.key = None map_field[key] = file_data - print map_field setattr(instance, f.name, map_field) else: field = getattr(instance, f.name) From e0717606f64e23e2cd00b08bfb5f8e1b7bad5839 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Sun, 23 Jun 2013 21:25:23 +0200 Subject: [PATCH 057/136] Handle file fields in list fields too and a bit of clean up --- mongodbforms/documents.py | 81 +++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 3d6dadb4..078fc16e 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -53,6 +53,42 @@ def __init__(self, key, field): # trigger the same thing on the real document though. def _mark_as_changed(self, key): pass + +def _save_iterator_file(field, uploaded_file, file_data=None): + """ + Takes care of saving a file for a list field. Returns a Mongoengine + fileproxy object or the file field. + """ + uploaded_file.seek(0) + filename = _get_unique_filename(uploaded_file.name) + fake_document = FakeDocument(field.name, field.field) + overwrote_instance = False + overwrote_key = False + # for a new file we need a new proxy object + if file_data is None: + file_data = field.field.proxy_class(db_alias=field.field.db_alias, + collection_name=field.field.collection_name) + + # overwrite an existing file + if file_data.instance is None: + file_data.instance = fake_document + overwrote_instance = True + if file_data.key is None: + file_data.key = field.name + overwrote_key = True + + if file_data.grid_id: + file_data.delete() + + file_data.put(uploaded_file, content_type=uploaded_file.content_type, filename=filename) + file_data.close() + + if overwrote_instance: + file_data.instance = None + if overwrote_key: + file_data.key = None + + return file_data def construct_instance(form, instance, fields=None, exclude=None, ignore=None): """ @@ -90,40 +126,21 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): for key, uploaded_file in uploads.items(): if uploaded_file is None: continue - file_data = map_field.get(key, None) - uploaded_file.seek(0) - filename = _get_unique_filename(uploaded_file.name) - fake_document = FakeDocument(f.name, f.field) - overwrote_instance = False - overwrote_key = False - # save a new file - if file_data is None: - proxy = f.field.proxy_class(instance=fake_document, key=f.name, - db_alias=f.field.db_alias, - collection_name=f.field.collection_name) - proxy.put(uploaded_file, content_type=uploaded_file.content_type, filename=filename) - proxy.close() - proxy.instance = None - proxy.key = None - map_field[key] = proxy - # overwrite an existing file - else: - if file_data.instance is None: - file_data.instance = fake_document - overwrote_instance = True - if file_data.key is None: - file_data.key = f.name - overwrote_key = True - file_data.delete() - file_data.put(uploaded_file, content_type=uploaded_file.content_type, filename=filename) - file_data.close() - if overwrote_instance: - file_data.instance = None - if overwrote_key: - file_data.key = None - map_field[key] = file_data + map_field[key] = _save_iterator_file(f, uploaded_file, file_data) setattr(instance, f.name, map_field) + elif isinstance(f, ListField): + list_field = getattr(instance, f.name) + uploads = cleaned_data[f.name] + for i, uploaded_file in enumerate(uploads): + if uploaded_file is None: + continue + try: + file_data = list_field[i] + except IndexError: + file_data = None + map_field[i] = _save_iterator_file(f, uploaded_file, file_data) + setattr(instance, f.name, list_field) else: field = getattr(instance, f.name) upload = cleaned_data[f.name] From fd3fc2acca4704774b83bc4c3e5cc2bc5147f20d Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 24 Jun 2013 01:03:39 +0200 Subject: [PATCH 058/136] get the filename for uploaded files after an old file has been deleted --- mongodbforms/documents.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 078fc16e..ecbf0d6c 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -23,7 +23,6 @@ from .fieldgenerator import MongoDefaultFormFieldGenerator from .documentoptions import DocumentMetaWrapper - from .util import with_metaclass @@ -48,9 +47,9 @@ def __init__(self, key, field): self._fields[key] = field - # don't care if anything gets marked on this + # We don't care if anything gets marked on this # we do update a real field later though. That should - # trigger the same thing on the real document though. + # trigger the same thing on the real document. def _mark_as_changed(self, key): pass @@ -59,8 +58,6 @@ def _save_iterator_file(field, uploaded_file, file_data=None): Takes care of saving a file for a list field. Returns a Mongoengine fileproxy object or the file field. """ - uploaded_file.seek(0) - filename = _get_unique_filename(uploaded_file.name) fake_document = FakeDocument(field.name, field.field) overwrote_instance = False overwrote_key = False @@ -79,7 +76,9 @@ def _save_iterator_file(field, uploaded_file, file_data=None): if file_data.grid_id: file_data.delete() - + + uploaded_file.seek(0) + filename = _get_unique_filename(uploaded_file.name) file_data.put(uploaded_file, content_type=uploaded_file.content_type, filename=filename) file_data.close() @@ -123,7 +122,7 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): if isinstance(f, MapField): map_field = getattr(instance, f.name) uploads = cleaned_data[f.name] - for key, uploaded_file in uploads.items(): + for key, uploaded_file in uploads.items(): if uploaded_file is None: continue file_data = map_field.get(key, None) From e9be0174dbd21ac3cac5af052a3e87dabe87c2d8 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 24 Jun 2013 16:17:56 +0200 Subject: [PATCH 059/136] Better support for custom fieldgenerators. --- mongodbforms/documents.py | 11 +- mongodbforms/fieldgenerator.py | 255 +++++++++++++++++++++------------ mongodbforms/fields.py | 46 +----- mongodbforms/util.py | 42 ++++++ readme.md | 9 +- 5 files changed, 225 insertions(+), 138 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index ecbf0d6c..8862f556 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -21,10 +21,11 @@ from mongoengine.connection import _get_db from gridfs import GridFS -from .fieldgenerator import MongoDefaultFormFieldGenerator +#from .fieldgenerator import MongoDefaultFormFieldGenerator from .documentoptions import DocumentMetaWrapper -from .util import with_metaclass +from .util import with_metaclass, load_field_generator +_fieldgenerator = load_field_generator() def _get_unique_filename(name): fs = GridFS(_get_db()) @@ -218,7 +219,7 @@ def document_to_dict(instance, fields=None, exclude=None): return data def fields_for_document(document, fields=None, exclude=None, widgets=None, \ - formfield_callback=None, field_generator=MongoDefaultFormFieldGenerator): + formfield_callback=None, field_generator=_fieldgenerator): """ Returns a ``SortedDict`` containing form fields for the given model. @@ -293,7 +294,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', MongoDefaultFormFieldGenerator) + self.formfield_generator = getattr(options, 'formfield_generator', _fieldgenerator) class DocumentFormMetaclass(type): @@ -314,7 +315,7 @@ def __new__(cls, name, bases, attrs): opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None)) if opts.document: - formfield_generator = getattr(opts, 'formfield_generator', MongoDefaultFormFieldGenerator) + formfield_generator = getattr(opts, 'formfield_generator', _fieldgenerator) # If a model is defined, extract form fields from it. fields = fields_for_document(opts.document, opts.fields, diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 60e77783..19103ece 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -28,28 +28,67 @@ class MongoFormFieldGenerator(object): # used for fields that fit in one of the generate functions # but don't actually have the name. - field_map = { + generator_map = { 'sortedlistfield': 'generate_listfield', } + + form_field_map = { + 'stringfield': MongoCharField, + 'stringfield_choices': forms.TypedChoiceField, + 'stringfield_long': MongoCharField, + 'emailfield': forms.EmailField, + 'urlfield': forms.URLField, + 'intfield': forms.IntegerField, + 'intfield_choices': forms.TypedChoiceField, + 'floatfield': forms.FloatField, + 'decimalfield': forms.DecimalField, + 'booleanfield': forms.BooleanField, + 'booleanfield_choices': forms.TypedChoiceField, + 'datetimefield': forms.DateTimeField, + 'referencefield': ReferenceField, + 'listfield': ListField, + 'listfield_choices': forms.MultipleChoiceField, + 'listfield_references': DocumentMultipleChoiceField, + 'mapfield': MapField, + 'filefield': forms.FileField, + 'imagefield': forms.ImageField, + } + + # uses the same keys as form_field_map + widget_override_map = { + 'stringfield_long': forms.Textarea, + } + + def __init__(self, field_overrides={}, widget_overrides={}): + self.form_field_map.update(field_overrides) + self.widget_override_map.update(widget_overrides) def generate(self, field, **kwargs): """Tries to lookup a matching formfield generator (lowercase field-classname) and raises a NotImplementedError of no generator can be found. """ + # do not handle embedded documents here. They're more or less special + # and require some form of inline formset or something more complex + # to handle then a simple field + if isinstance(field, MongoEmbeddedDocumentField): + return + field_name = field.__class__.__name__.lower() - if hasattr(self, 'generate_%s' % field_name): + try: return getattr(self, 'generate_%s' % field_name)(field, **kwargs) + except AttributeError: + pass for cls in field.__class__.__bases__: cls_name = cls.__name__.lower() try: return getattr(self, 'generate_%s' % cls_name)(field, **kwargs) except AttributeError: - if cls_name in self.field_map: - return getattr(self, self.field_map.get(cls_name))(field, **kwargs) - else: - raise NotImplementedError('%s is not supported by MongoForm' % \ + if cls_name in self.form_field_map: + return getattr(self, self.generator_map.get(cls_name))(field, **kwargs) + + raise NotImplementedError('%s is not supported by MongoForm' % \ field.__class__.__name__) def get_field_choices(self, field, include_blank=True, @@ -82,35 +121,51 @@ def get_field_label(self, field): def get_field_help_text(self, field): if field.help_text: return field.help_text.capitalize() + + def _check_widget(self, map_key): + if map_key in self.widget_override_map: + return {'widget': self.widget_override_map.get(map_key)} + else: + return {} 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)} - - if field.max_length and not field.choices: - defaults['max_length'] = field.max_length - - if field.max_length is None and not field.choices: - defaults['widget'] = forms.Textarea - - if field.regex: - defaults['regex'] = field.regex - elif field.choices: - form_class = forms.TypedChoiceField - defaults['choices'] = self.get_field_choices(field) - defaults['coerce'] = self.string_field - - if not field.required: - defaults['empty_value'] = None - + if field.choices: + map_key = 'stringfield_choices' + defaults = { + 'label': self.get_field_label(field), + 'initial': field.default, + 'required': field.required, + 'help_text': self.get_field_help_text(field), + 'choices': self.get_field_choices(field), + 'coerce': self.string_field, + } + elif field.max_length is None: + map_key = 'stringfield_long' + defaults = { + 'label': self.get_field_label(field), + 'initial': field.default, + 'required': field.required, + 'help_text': self.get_field_help_text(field) + } + else: + map_key = 'stringfield' + defaults = { + 'label': self.get_field_label(field), + 'initial': field.default, + 'required': field.required, + 'help_text': self.get_field_help_text(field), + 'max_length': field.max_length, + } + if field.regex: + defaults['regex'] = field.regex + + form_class = self.form_field_map.get(map_key) + defaults.update(self._check_widget(map_key)) defaults.update(kwargs) return form_class(**defaults) def generate_emailfield(self, field, **kwargs): + map_key = 'emailfield' defaults = { 'required': field.required, 'min_length': field.min_length, @@ -119,11 +174,13 @@ def generate_emailfield(self, field, **kwargs): 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field) } - + defaults.update(self._check_widget(map_key)) + form_class = self.form_field_map.get(map_key) defaults.update(kwargs) - return forms.EmailField(**defaults) + return form_class(**defaults) def generate_urlfield(self, field, **kwargs): + map_key = 'urlfield' defaults = { 'required': field.required, 'min_length': field.min_length, @@ -132,12 +189,14 @@ def generate_urlfield(self, field, **kwargs): 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field) } - + form_class = self.form_field_map.get(map_key) + defaults.update(self._check_widget(map_key)) defaults.update(kwargs) - return forms.URLField(**defaults) + return form_class(**defaults) def generate_intfield(self, field, **kwargs): if field.choices: + map_key = 'intfield_choices' defaults = { 'coerce': self.integer_field, 'empty_value': None, @@ -147,10 +206,8 @@ def generate_intfield(self, field, **kwargs): 'choices': self.get_field_choices(field), 'help_text': self.get_field_help_text(field) } - - defaults.update(kwargs) - return forms.TypedChoiceField(**defaults) else: + map_key = 'intfield' defaults = { 'required': field.required, 'min_value': field.min_value, @@ -159,38 +216,44 @@ def generate_intfield(self, field, **kwargs): 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field) } - - defaults.update(kwargs) - return forms.IntegerField(**defaults) + form_class = self.form_field_map.get(map_key) + defaults.update(self._check_widget(map_key)) + defaults.update(kwargs) + return form_class(**defaults) def generate_floatfield(self, field, **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)} - + map_key = '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) + } + form_class = self.form_field_map.get(map_key) + defaults.update(self._check_widget(map_key)) defaults.update(kwargs) return form_class(**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)} - + map_key = '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) + } + form_class = self.form_field_map.get(map_key) + defaults.update(self._check_widget(map_key)) defaults.update(kwargs) return form_class(**defaults) def generate_booleanfield(self, field, **kwargs): if field.choices: + map_key = 'booleanfield_choices' defaults = { 'coerce': self.boolean_field, 'empty_value': None, @@ -200,42 +263,51 @@ def generate_booleanfield(self, field, **kwargs): 'choices': self.get_field_choices(field), 'help_text': self.get_field_help_text(field) } - - defaults.update(kwargs) - return forms.TypedChoiceField(**defaults) else: + map_key = 'booleanfield' 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) + } + form_class = self.form_field_map.get(map_key) + defaults.update(self._check_widget(map_key)) + defaults.update(kwargs) + return form_class(**defaults) def generate_datetimefield(self, field, **kwargs): + map_key = 'datetimefield' defaults = { 'required': field.required, 'initial': field.default, 'label': self.get_field_label(field), } - + form_class = self.form_field_map.get(map_key) + defaults.update(self._check_widget(map_key)) defaults.update(kwargs) - return forms.DateTimeField(**defaults) + return form_class(**defaults) def generate_referencefield(self, field, **kwargs): + map_key = 'referencefield' defaults = { 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field), - 'required': field.required + 'required': field.required, + 'queryset': field.document_type.objects, } - + form_class = self.form_field_map.get(map_key) + defaults.update(self._check_widget(map_key)) defaults.update(kwargs) - return ReferenceField(field.document_type.objects, **defaults) + return form_class(**defaults) def generate_listfield(self, field, **kwargs): + # we can't really handle embedded documents here. So we just ignore them + if isinstance(field.field, MongoEmbeddedDocumentField): + return + if field.field.choices: + map_key = 'listfield_choices' defaults = { 'choices': field.field.choices, 'required': field.required, @@ -243,60 +315,67 @@ def generate_listfield(self, field, **kwargs): 'help_text': self.get_field_help_text(field), 'widget': forms.CheckboxSelectMultiple } - - defaults.update(kwargs) - return forms.MultipleChoiceField(**defaults) elif isinstance(field.field, MongoReferenceField): + map_key = 'listfield_references' defaults = { 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field), - 'required': field.required + 'required': field.required, + 'queryset': field.document_type.objects, } - - defaults.update(kwargs) - f = DocumentMultipleChoiceField(field.field.document_type.objects, **defaults) - return f - elif not isinstance(field.field, MongoEmbeddedDocumentField): + else: + map_key = 'listfield' + form_field = self.generate(field.field) defaults = { 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field), 'required': field.required, - #'initial': getattr(field._owner_document, field.name, []) + 'field_type': form_field.__class__, } - defaults.update(kwargs) - # figure out which type of field is stored in the list - form_field = self.generate(field.field) - return ListField(form_field.__class__, **defaults) + form_class = self.form_field_map.get(map_key) + defaults.update(self._check_widget(map_key)) + defaults.update(kwargs) + return form_class(**defaults) def generate_mapfield(self, field, **kwargs): + map_key = 'mapfield' + form_field = self.generate(field.field) defaults = { 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field), - 'required': field.required + 'required': field.required, + 'field_type': form_field.__class__, } + form_class = self.form_field_map.get(map_key) + defaults.update(self._check_widget(map_key)) defaults.update(kwargs) - form_field = self.generate(field.field) - return MapField(form_field.__class__, **defaults) + return form_class(**defaults) def generate_filefield(self, field, **kwargs): + map_key = 'filefield' defaults = { 'required':field.required, 'label':self.get_field_label(field), 'initial': field.default, 'help_text': self.get_field_help_text(field) } + form_class = self.form_field_map.get(map_key) + defaults.update(self._check_widget(map_key)) defaults.update(kwargs) - return forms.FileField(**defaults) + return form_class(**defaults) def generate_imagefield(self, field, **kwargs): + map_key = 'imagefield' defaults = { 'required':field.required, 'label':self.get_field_label(field), 'initial': field.default, 'help_text': self.get_field_help_text(field) } + form_class = self.form_field_map.get(map_key) + defaults.update(self._check_widget(map_key)) defaults.update(kwargs) - return forms.ImageField(**defaults) + return form_class(**defaults) class MongoDefaultFormFieldGenerator(MongoFormFieldGenerator): diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index 0945b13d..f0b51540 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -62,10 +62,8 @@ class ReferenceField(forms.ChoiceField): """ Reference field for mongo forms. Inspired by `django.forms.models.ModelChoiceField`. """ - def __init__(self, queryset, empty_label="---------", - *aargs, **kwaargs): - - forms.Field.__init__(self, *aargs, **kwaargs) + def __init__(self, queryset, empty_label="---------", *args, **kwargs): + forms.Field.__init__(self, *args, **kwargs) self.queryset = queryset self.empty_label = empty_label @@ -150,14 +148,6 @@ def clean(self, value): raise forms.ValidationError(self.error_messages['list']) key = 'pk' -# filter_ids = [] -# for pk in value: -# filter_ids.append(pk) -# try: -# oid = ObjectId(pk) -# filter_ids.append(oid) -# except InvalidId: -# raise forms.ValidationError(self.error_messages['invalid_pk_value'] % pk) qs = self.queryset.clone() qs = qs.filter(**{'%s__in' % key: value}) pks = set([force_unicode(getattr(o, key)) for o in qs]) @@ -176,22 +166,6 @@ def prepare_value(self, value): class ListField(forms.Field): - """ - A Field that aggregates the logic of multiple Fields. - - Its clean() method takes a "decompressed" list of values, which are then - cleaned into a single value according to self.fields. Each value in - this list is cleaned by the corresponding field -- the first value is - cleaned by the first field, the second value is cleaned by the second - field, etc. Once all fields are cleaned, the list of clean values is - "compressed" into a single value. - - Subclasses should not have to implement clean(). Instead, they must - implement compress(), which takes a list of valid values and returns a - "compressed" version of those values -- a single value. - - You'll probably want to use this with MultiWidget. - """ default_error_messages = { 'invalid': _('Enter a list of values.'), } @@ -215,14 +189,6 @@ def validate(self, value): pass def clean(self, value): - """ - Validates every value in the given list. A value is validated against - the corresponding Field in self.fields. - - For example, if this MultiValueField was instantiated with - fields=(DateField(), TimeField()), clean() would call - DateField.clean(value[0]) and TimeField.clean(value[1]). - """ clean_data = [] errors = ErrorList() if not value or isinstance(value, (list, tuple)): @@ -315,14 +281,6 @@ def validate(self, value): pass def clean(self, value): - """ - Validates every value in the given list. A value is validated against - the corresponding Field in self.fields. - - For example, if this MultiValueField was instantiated with - fields=(DateField(), TimeField()), clean() would call - DateField.clean(value[0]) and TimeField.clean(value[1]). - """ clean_data = {} errors = ErrorList() if not value or isinstance(value, dict): diff --git a/mongodbforms/util.py b/mongodbforms/util.py index 3c0febb1..e5084df8 100644 --- a/mongodbforms/util.py +++ b/mongodbforms/util.py @@ -1,6 +1,48 @@ from types import MethodType +from django.conf import settings + from .documentoptions import DocumentMetaWrapper +from .fieldgenerator import MongoDefaultFormFieldGenerator + +try: + from django.utils.module_loading import import_by_path +except ImportError: + # this is only in Django's devel version for now + # and the following code comes from there. Yet it's too nice to + # pass on this. So we do define it here for now. + import sys + from django.core.exceptions import ImproperlyConfigured + from django.utils.importlib import import_module + from django.utils import six + def import_by_path(dotted_path, error_prefix=''): + """ + Import a dotted module path and return the attribute/class designated by the + last name in the path. Raise ImproperlyConfigured if something goes wrong. + """ + try: + module_path, class_name = dotted_path.rsplit('.', 1) + except ValueError: + raise ImproperlyConfigured("%s%s doesn't look like a module path" % ( + error_prefix, dotted_path)) + try: + module = import_module(module_path) + except ImportError as e: + msg = '%sError importing module %s: "%s"' % ( + error_prefix, module_path, e) + six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg), + sys.exc_info()[2]) + try: + attr = getattr(module, class_name) + except AttributeError: + raise ImproperlyConfigured('%sModule "%s" does not define a "%s" attribute/class' % ( + error_prefix, module_path, class_name)) + return attr + +def load_field_generator(): + if hasattr(settings, 'MONGODBFORMS_FIELDGENERATOR'): + return import_by_path(settings.MONGODBFORMS_FIELDGENERATOR) + return MongoDefaultFormFieldGenerator def patch_document(function, instance): setattr(instance, function.__name__, MethodType(function, instance)) diff --git a/readme.md b/readme.md index 58cbab01..f262ebe5 100644 --- a/readme.md +++ b/readme.md @@ -57,9 +57,16 @@ In theory the documentation [Django's modelform](https://docs.djangoproject.com/ Because the fields on mongoengine documents have no notion of form fields mongodbform uses a generator class to generate the form field for a db field, which is not explicitly set. -If you want to use your own generator class you can use the ``formfield_generator`` option on the form's Meta class. +To use your own field generator you can either set a generator for your whole project using ```MONGODBFORMS_FIELDGENERATOR``` in settings.py or you can use the ``formfield_generator`` option on the form's Meta class. + +The default generator is defined in ```mongodbforms/fieldgenerator.py``` and should make it easy to override form fields and widgets. If you set a generator on the document form you can also pass two dicts ```field_overrides``` and ```widget_overrides``` to ```__init__```. For a list of valid keys have a look at ```MongoFormFieldGenerator```. ````python +# settings.py + +# set the fieldgeneretor for the whole application +MONGODBFORMS_FIELDGENERATOR = 'myproject.fieldgenerator.GeneratorClass' + # generator.py from mongodbforms.fieldgenerator import MongoFormFieldGenerator From 0cf56e8b1e67ac4b85fb5d5a0ddab22b6227b379 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 26 Jun 2013 16:14:05 +0200 Subject: [PATCH 060/136] Correctly handle ListField(FileField()) --- mongodbforms/documentoptions.py | 2 +- mongodbforms/documents.py | 7 +++++-- mongodbforms/widgets.py | 34 ++++++--------------------------- 3 files changed, 12 insertions(+), 31 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 0429af11..89f87c8e 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -236,7 +236,7 @@ def get(self, key, default=None): try: return self.__getitem__(key) except KeyError: - return default + return default def get_parent_list(self): return [] diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 8862f556..ce936b71 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -21,7 +21,6 @@ from mongoengine.connection import _get_db from gridfs import GridFS -#from .fieldgenerator import MongoDefaultFormFieldGenerator from .documentoptions import DocumentMetaWrapper from .util import with_metaclass, load_field_generator @@ -139,7 +138,11 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): file_data = list_field[i] except IndexError: file_data = None - map_field[i] = _save_iterator_file(f, uploaded_file, file_data) + file_obj = _save_iterator_file(f, uploaded_file, file_data) + try: + list_field[i] = file_obj + except IndexError: + list_field.append(file_obj) setattr(instance, f.name, list_field) else: field = getattr(instance, f.name) diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index 5867f5fd..6fa5c736 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -7,32 +7,6 @@ from django.utils.html import format_html class ListWidget(Widget): - """ - A widget that is composed of multiple widgets. - - Its render() method is different than other widgets', because it has to - figure out how to split a single value for display in multiple widgets. - The ``value`` argument can be one of two things: - - * A list. - * A normal value (e.g., a string) that has been "compressed" from - a list of values. - - In the second case -- i.e., if the value is NOT a list -- render() will - first "decompress" the value into a list before rendering it. It does so by - calling the decompress() method, which MultiWidget subclasses must - implement. This method takes a single "compressed" value and returns a - list. - - When render() does its HTML rendering, each value in the list is rendered - with the corresponding widget -- the first value is rendered in the first - widget, the second value is rendered in the second widget, etc. - - Subclasses may implement format_output(), which takes the list of rendered - widgets and returns a string of HTML that formats them any way you'd like. - - You'll probably want to use this class with MultiValueField. - """ def __init__(self, widget_type, attrs=None): self.widget_type = widget_type self.widget = widget_type() @@ -64,9 +38,13 @@ def value_from_datadict(self, data, files, name): widget = self.widget_type() i = 0 ret = [] - while (name + '_%s' % i) in data: + while (name + '_%s' % i) in data or (name + '_%s' % i) in files: value = widget.value_from_datadict(data, files, name + '_%s' % i) - if value not in EMPTY_VALUES: + # we need a different list if we handle files. Basicly Django sends + # back the initial values if we're not dealing with files. If we store + # files on the list, we need to add empty values to the clean data, + # so the list positions are kept. + if value not in EMPTY_VALUES or (value is None and len(files) > 0): ret.append(value) i = i + 1 return ret From 48e2ab95b6b78a24d8361d2eb35ed1f8d382c505 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 26 Jun 2013 16:28:41 +0200 Subject: [PATCH 061/136] Version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a41ae33d..ebf27099 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def convert_readme(): return open('README.txt').read() setup(name='mongodbforms', - version='0.1.5', + version='0.2', description="An implementation of django forms using mongoengine.", author='Jan Schrewe', author_email='jan@schafproductions.com', From 821fde8c21368bb2f5d8905dd21351b079b77246 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 26 Jun 2013 16:32:26 +0200 Subject: [PATCH 062/136] Git ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 669100e2..e066dffd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ MANIFEST build/* *egg* *.bak +.settings/* From 8086f019a590ca9f5541b427b9f164f5b9ee7fea Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 26 Jun 2013 16:33:17 +0200 Subject: [PATCH 063/136] Romved eclipse stuff --- .settings/org.eclipse.core.resources.prefs | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .settings/org.eclipse.core.resources.prefs diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 4cb6794b..00000000 --- a/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,4 +0,0 @@ -#Mon Jan 23 16:54:50 CET 2012 -eclipse.preferences.version=1 -encoding//mongodbforms/fieldgenerator.py=utf-8 -encoding//mongodbforms/fields.py=utf-8 From 75029e5f6e79c08eead42d9d891f6a11c2a4617f Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Thu, 27 Jun 2013 00:34:18 +0200 Subject: [PATCH 064/136] DocumentWrapper clean and removed all _admin_opt references --- .gitignore | 2 + README.txt | 93 ++++++++++++--------- mongodbforms/documentoptions.py | 138 ++++++++++++++------------------ mongodbforms/documents.py | 3 +- mongodbforms/util.py | 11 +-- 5 files changed, 121 insertions(+), 126 deletions(-) diff --git a/.gitignore b/.gitignore index e066dffd..2a9e5a46 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,7 @@ build/* *egg* *.bak .settings/* +*.tmproj + diff --git a/README.txt b/README.txt index 803d2419..e9369262 100644 --- a/README.txt +++ b/README.txt @@ -7,7 +7,8 @@ documents. Requirements ------------ -- `mongoengine `_ +- Django >= 1.3 +- `mongoengine `_ >= 0.6 Usage ----- @@ -20,41 +21,46 @@ Normal documents To use mongodbforms with normal documents replace djangos forms with mongodbform forms. -:: - - from mongodbforms import DocumentForm +\`\`\`python from mongodbforms import DocumentForm - class BlogForm(DocumentForm) - ... +class BlogForm(DocumentForm) ... \`\`\` Embedded documents ~~~~~~~~~~~~~~~~~~ For embedded documents use ``EmbeddedDocumentForm``. The Meta-object of the form has to be provided with an embedded field name. The embedded -object is appended to this. The form constructor takes an additional -argument: The document the embedded document gets added to. +object is appended to this. The form constructor takes a couple of +additional arguments: The document the embedded document gets added to +and an optional position argument. + +If no position is provided the form adds a new embedded document to the +list if the form is saved. To edit an embedded document stored in a list +field the position argument is required. If you provide a position and +no instance to the form the instance is automatically loaded using the +position argument. + +If the embedded field is a plain embedded field the current object is +simply overwritten. + +\`\`\`\`python # forms.py from mongodbforms import EmbeddedDocumentForm -If the form is saved the new embedded object is automatically added to -the provided parent document. If the embedded field is a list field the -embedded document is appended to the list, if it is a plain embedded -field the current object is overwritten. Note that the parent document -is not saved. +class MessageForm(EmbeddedDocumentForm): class Meta: document = Message +embedded\_field\_name = 'messages' :: - # forms.py - from mongodbforms import EmbeddedDocumentForm + fields = ['subject', 'sender', 'message',] - class MessageForm(EmbeddedDocumentForm): - class Meta: - document = Message - embedded_field_name = 'messages' +views.py +======== - fields = ['subject', 'sender', 'message',] +create a new embedded object +============================ - # views.py - form = MessageForm(parent_document=some_document, ...) +form = MessageForm(parent\_document=some\_document, ...) # edit the 4th +embedded object form = MessageForm(parent\_document=some\_document, +position=3, ...) \`\`\` Documentation ------------- @@ -71,26 +77,39 @@ Form field generation ~~~~~~~~~~~~~~~~~~~~~ Because the fields on mongoengine documents have no notion of form -fields every mongodbform uses a generator class to generate the form -field for a db field, which is not explicitly set. +fields mongodbform uses a generator class to generate the form field for +a db field, which is not explicitly set. -If you want to use your own generator class you can use the -``formfield_generator`` option on the form's Meta class. +To use your own field generator you can either set a generator for your +whole project using ``MONGODBFORMS_FIELDGENERATOR`` in settings.py or +you can use the ``formfield_generator`` option on the form's Meta class. -:: +The default generator is defined in ``mongodbforms/fieldgenerator.py`` +and should make it easy to override form fields and widgets. If you set +a generator on the document form you can also pass two dicts +``field_overrides`` and ``widget_overrides`` to ``__init__``. For a list +of valid keys have a look at ``MongoFormFieldGenerator``. + +\`\`\`\`python # settings.py + +set the fieldgeneretor for the whole application +================================================ + +MONGODBFORMS\_FIELDGENERATOR = 'myproject.fieldgenerator.GeneratorClass' + +generator.py +============ - # generator.py - from mongodbforms.fieldgenerator import MongoFormFieldGenerator +from mongodbforms.fieldgenerator import MongoFormFieldGenerator - class MyFieldGenerator(MongoFormFieldGenerator): - ... +class MyFieldGenerator(MongoFormFieldGenerator): ... - # forms.py - from mongodbforms import DocumentForm +forms.py +======== - from generator import MyFieldGenerator +from mongodbforms import DocumentForm - class MessageForm(DocumentForm): - class Meta: - formfield_generator = MyFieldGenerator +from generator import MyFieldGenerator +class MessageForm(DocumentForm): class Meta: formfield\_generator = +MyFieldGenerator \`\`\` diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 89f87c8e..f6897a9c 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -1,5 +1,6 @@ import sys from collections import MutableMapping +from types import MethodType from django.db.models.fields import FieldDoesNotExist from django.utils.text import capfirst @@ -7,6 +8,9 @@ from mongoengine.fields import ReferenceField +def patch_document(function, instance): + setattr(instance, function.__name__, MethodType(function, instance)) + def create_verbose_name(name): name = get_verbose_name(name) name.replace('_', ' ') @@ -31,7 +35,7 @@ class DocumentMetaWrapper(MutableMapping): Used to store mongoengine's _meta dict to make the document admin as compatible as possible to django's meta class on models. """ - _pk = None + pk = None pk_name = None _app_label = None module_name = None @@ -47,6 +51,8 @@ class DocumentMetaWrapper(MutableMapping): concrete_model = None def __init__(self, document): + super(DocumentMetaWrapper, self).__init__() + self.document = document # used by Django to distinguish between abstract and concrete models # here for now always the document @@ -60,12 +66,54 @@ def __init__(self, document): self.module_name = self.object_name.lower() - # EmbeddedDocuments don't have an id field. - try: + # add the gluey stuff to the document and it's fields to make + # everything play nice with Django + self._setup_document_fields() + # Setup self.pk if the document has an id_field in it's meta + # if it doesn't have one it's an embedded document + if 'id_field' in self._meta: self.pk_name = self._meta['id_field'] self._init_pk() - except KeyError: - pass + + def _setup_document_fields(self): + for f in self.document._fields.values(): + # Yay, more glue. Django expects fields to have a couple attributes + # at least in the admin, probably in more places. + if not hasattr(f, 'rel'): + # need a bit more for actual reference fields here + f.rel = None + if not hasattr(f, 'verbose_name'): + f.verbose_name = capfirst(create_verbose_name(f.name)) + if not hasattr(f, 'flatchoices'): + flat = [] + if f.choices is not None: + for choice, value in f.choices: + if isinstance(value, (list, tuple)): + flat.extend(value) + else: + flat.append((choice,value)) + f.flatchoices = flat + if isinstance(f, ReferenceField): + document = f.document_type + if not isinstance(document._meta, DocumentMetaWrapper): + document._meta = DocumentMetaWrapper(document) + + def _init_pk(self): + """ + Adds a wrapper around the documents pk field. The wrapper object gets the attributes + django expects on the pk field, like name and attname. + + The function also adds a _get_pk_val method to the document. + """ + pk_field = getattr(self.document, self.pk_name) + self.pk = PkWrapper(pk_field) + self.pk.name = self.pk_name + self.pk.attname = self.pk_name + + self.document._pk_val = pk_field + def _get_pk_val(self): + return self._pk_val + patch_document(_get_pk_val, self.document) @property def app_label(self): @@ -97,38 +145,6 @@ def verbose_name_raw(self): @property def verbose_name_plural(self): return "%ss" % self.verbose_name - - @property - def pk(self): - if not hasattr(self._pk, 'attname'): - self._init_pk() - return self._pk - - def _init_pk(self): - """ - Adds a wrapper around the documents pk field. The wrapper object gets the attributes - django expects on the pk field, like name and attname. - - The function also adds a _get_pk_val method to the document. - """ - if self.id_field is None: - return - - try: - pk_field = getattr(self.document, self.id_field) - self._pk = PkWrapper(pk_field) - self._pk.name = self.id_field - self._pk.attname = self.id_field - self._pk_name = self.id_field - - self.document._pk_val = getattr(self.document, self.pk_name) - # avoid circular import - from mongodbforms.util import patch_document - def _get_pk_val(self): - return self._pk_val - patch_document(_get_pk_val, self.document) - except AttributeError: - return def get_add_permission(self): return 'add_%s' % self.object_name.lower() @@ -151,50 +167,16 @@ def get_field_by_name(self, name): 'direct' is False, 'field_object' is the corresponding RelatedObject for this field (since the field doesn't have an instance associated with it). - - Uses a cache internally, so after the first access, this is very fast. """ - try: - try: - return self._field_cache[name] - except TypeError: - self._init_field_cache() - return self._field_cache[name] - except KeyError: + if name in self.document._fields: + field = self.document._fields[name] + if isinstance(field, ReferenceField): + return (field, field.document_type, False, False) + else: + return (field, None, True, False) + else: raise FieldDoesNotExist('%s has no field named %r' % (self.object_name, name)) - - - def _init_field_cache(self): - if self._field_cache is None: - self._field_cache = {} - - for f in self.document._fields.values(): - # Yay, more glue. Django expects fields to have a rel attribute - # at least in the admin, probably in more places. So we add them here - # and hope that this is the common path to access the fields. - if not hasattr(f, 'rel'): - f.rel = None - if getattr(f, 'verbose_name', None) is None: - f.verbose_name = capfirst(create_verbose_name(f.name)) - if not hasattr(f, 'flatchoices'): - flat = [] - if f.choices is not None: - for choice, value in f.choices: - if isinstance(value, (list, tuple)): - flat.extend(value) - else: - flat.append((choice,value)) - f.flatchoices = flat - if isinstance(f, ReferenceField): - document = f.document_type - document._meta = DocumentMetaWrapper(document) - document._admin_opts = document._meta - self._field_cache[document._meta.module_name] = (f, document, False, False) - else: - self._field_cache[f.name] = (f, None, True, False) - - return self._field_cache def get_field(self, name, many_to_many=True): """ diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index ce936b71..a92fb9c6 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -290,9 +290,8 @@ def __init__(self, options=None): self.model = self.document meta = getattr(self.document, '_meta', {}) # set up the document meta wrapper if document meta is a dict - if self.document is not None and isinstance(meta, dict): + if self.document is not None and not isinstance(meta, DocumentMetaWrapper): self.document._meta = DocumentMetaWrapper(self.document) - self.document._admin_opts = self.document._meta self.fields = getattr(options, 'fields', None) self.exclude = getattr(options, 'exclude', None) self.widgets = getattr(options, 'widgets', None) diff --git a/mongodbforms/util.py b/mongodbforms/util.py index e5084df8..70c2aaa2 100644 --- a/mongodbforms/util.py +++ b/mongodbforms/util.py @@ -1,5 +1,3 @@ -from types import MethodType - from django.conf import settings from .documentoptions import DocumentMetaWrapper @@ -44,14 +42,9 @@ def load_field_generator(): return import_by_path(settings.MONGODBFORMS_FIELDGENERATOR) return MongoDefaultFormFieldGenerator -def patch_document(function, instance): - setattr(instance, function.__name__, MethodType(function, instance)) - def init_document_options(document): - if not hasattr(document, '_meta') or not isinstance(document._meta, DocumentMetaWrapper): - document._admin_opts = DocumentMetaWrapper(document) - if not isinstance(document._admin_opts, DocumentMetaWrapper): - document._admin_opts = document._meta + if not isinstance(document._meta, DocumentMetaWrapper): + document._meta = DocumentMetaWrapper(document) return document def get_document_options(document): From 1b883c98aa22f184a923f330b21e2b0834543370 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Thu, 27 Jun 2013 22:05:16 +0200 Subject: [PATCH 065/136] Define format_html if we can't import it from Django. Closes #41 --- mongodbforms/documentoptions.py | 60 ++++++++++++++++++++++++++++----- mongodbforms/widgets.py | 25 +++++++++++++- 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index f6897a9c..553fe6f4 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -29,6 +29,55 @@ def __setattr__(self, attr, value): if attr != 'obj' and hasattr(self.obj, attr): setattr(self.obj, attr, value) super(PkWrapper, self).__setattr__(attr, value) + +class Relation(object): + def __init__(self, field, to, field_name, related_name=None, limit_choices_to=None, + parent_link=False, on_delete=None): + + + self.field_name = field_name + self.field = field + self.to = to + self.related_name = related_name + self.limit_choices_to = {} if limit_choices_to is None else limit_choices_to + self.multiple = True + self.parent_link = parent_link + self.on_delete = on_delete + + def is_hidden(self): + "Should the related object be hidden?" + return self.related_name and self.related_name[-1] == '+' + + def get_joining_columns(self): + return self.field.get_reverse_joining_columns() + + def get_extra_restriction(self, where_class, alias, related_alias): + return self.field.get_extra_restriction(where_class, related_alias, alias) + + def set_field_name(self): + """ + Sets the related field's name, this is not available until later stages + of app loading, so set_field_name is called from + set_attributes_from_rel() + """ + # By default foreign object doesn't relate to any remote field (for + # example custom multicolumn joins currently have no remote field). + self.field_name = None + + def get_related_field(self): + """ + Returns the Field in the 'to' object to which this relationship is + tied. + """ + data = self.to._meta.get_field_by_name(self.field_name) + if not data[2]: + raise FieldDoesNotExist("No related field named '%s'" % + self.field_name) + return data[0] + + def set_field_name(self): + self.field_name = self.field_name or self.to._meta.pk.name + class DocumentMetaWrapper(MutableMapping): """ @@ -93,10 +142,8 @@ def _setup_document_fields(self): else: flat.append((choice,value)) f.flatchoices = flat - if isinstance(f, ReferenceField): - document = f.document_type - if not isinstance(document._meta, DocumentMetaWrapper): - document._meta = DocumentMetaWrapper(document) + if isinstance(f, ReferenceField) and not isinstance(f.document_type._meta, DocumentMetaWrapper): + f.document_type._meta = DocumentMetaWrapper(f.document_type) def _init_pk(self): """ @@ -131,10 +178,7 @@ def verbose_name(self): then generates a verbose name from from the object name. """ if self._verbose_name is None: - try: - self._verbose_name = capfirst(create_verbose_name(self._meta['verbose_name'])) - except KeyError: - self._verbose_name = capfirst(create_verbose_name(self.object_name)) + self._verbose_name = capfirst(create_verbose_name(self._meta.get('verbose_name', self.object_name))) return self._verbose_name diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index 6fa5c736..1da29bf8 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -4,7 +4,30 @@ from django.utils.safestring import mark_safe from django.core.validators import EMPTY_VALUES from django.forms.util import flatatt -from django.utils.html import format_html +try: + from django.utils.html import format_html +except ImportError: + # support django < 1.5. Taken from django.utils.html + from django.utils import six + def conditional_escape(text): + """ + Similar to escape(), except that it doesn't operate on pre-escaped strings. + """ + if isinstance(text, SafeData): + return text + else: + return escape(text) + + def format_html(format_string, *args, **kwargs): + """ + Similar to str.format, but passes all arguments through conditional_escape, + and calls 'mark_safe' on the result. This function should be used instead + of str.format or % interpolation to build up small HTML fragments. + """ + args_safe = map(conditional_escape, args) + kwargs_safe = dict([(k, conditional_escape(v)) for (k, v) in + six.iteritems(kwargs)]) + return mark_safe(format_string.format(*args_safe, **kwargs_safe)) class ListWidget(Widget): def __init__(self, widget_type, attrs=None): From 3d173b840283bfececa2b0f6e1cc9365743707ed Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Thu, 27 Jun 2013 23:17:38 +0200 Subject: [PATCH 066/136] Avoid format_html altogether now. A real fix for #41, not just copy&paste --- mongodbforms/widgets.py | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index 1da29bf8..a980d612 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -4,30 +4,6 @@ from django.utils.safestring import mark_safe from django.core.validators import EMPTY_VALUES from django.forms.util import flatatt -try: - from django.utils.html import format_html -except ImportError: - # support django < 1.5. Taken from django.utils.html - from django.utils import six - def conditional_escape(text): - """ - Similar to escape(), except that it doesn't operate on pre-escaped strings. - """ - if isinstance(text, SafeData): - return text - else: - return escape(text) - - def format_html(format_string, *args, **kwargs): - """ - Similar to str.format, but passes all arguments through conditional_escape, - and calls 'mark_safe' on the result. This function should be used instead - of str.format or % interpolation to build up small HTML fragments. - """ - args_safe = map(conditional_escape, args) - kwargs_safe = dict([(k, conditional_escape(v)) for (k, v) in - six.iteritems(kwargs)]) - return mark_safe(format_string.format(*args_safe, **kwargs_safe)) class ListWidget(Widget): def __init__(self, widget_type, attrs=None): @@ -172,10 +148,10 @@ def render(self, name, value, attrs=None): fieldset_attr = dict(final_attrs, id='fieldset_%s_%s' % (id_, i)) group = [] - group.append(format_html('', flatatt(fieldset_attr))) + group.append(mark_safe('
' % flatatt(fieldset_attr))) group.append(self.key_widget.render(name + '_key_%s' % i, key, final_attrs)) group.append(self.data_widget.render(name + '_value_%s' % i, widget_value, final_attrs)) - group.append('
') + group.append(mark_safe('')) output.append(mark_safe(''.join(group))) return mark_safe(self.format_output(output)) From b6000a071b7e275e32d4a77246283d113c3e8c9a Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 28 Jun 2013 02:35:33 +0200 Subject: [PATCH 067/136] Removed the relationship experiment --- mongodbforms/documentoptions.py | 51 +-------------------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 553fe6f4..8d97eb91 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -28,57 +28,8 @@ def __getattr__(self, attr): def __setattr__(self, attr, value): if attr != 'obj' and hasattr(self.obj, attr): setattr(self.obj, attr, value) - super(PkWrapper, self).__setattr__(attr, value) + super(PkWrapper, self).__setattr__(attr, value) -class Relation(object): - def __init__(self, field, to, field_name, related_name=None, limit_choices_to=None, - parent_link=False, on_delete=None): - - - self.field_name = field_name - self.field = field - self.to = to - self.related_name = related_name - self.limit_choices_to = {} if limit_choices_to is None else limit_choices_to - self.multiple = True - self.parent_link = parent_link - self.on_delete = on_delete - - def is_hidden(self): - "Should the related object be hidden?" - return self.related_name and self.related_name[-1] == '+' - - def get_joining_columns(self): - return self.field.get_reverse_joining_columns() - - def get_extra_restriction(self, where_class, alias, related_alias): - return self.field.get_extra_restriction(where_class, related_alias, alias) - - def set_field_name(self): - """ - Sets the related field's name, this is not available until later stages - of app loading, so set_field_name is called from - set_attributes_from_rel() - """ - # By default foreign object doesn't relate to any remote field (for - # example custom multicolumn joins currently have no remote field). - self.field_name = None - - def get_related_field(self): - """ - Returns the Field in the 'to' object to which this relationship is - tied. - """ - data = self.to._meta.get_field_by_name(self.field_name) - if not data[2]: - raise FieldDoesNotExist("No related field named '%s'" % - self.field_name) - return data[0] - - def set_field_name(self): - self.field_name = self.field_name or self.to._meta.pk.name - - class DocumentMetaWrapper(MutableMapping): """ Used to store mongoengine's _meta dict to make the document admin From 660ff2ac963e8e4da1f3052c67ff83fb3e07640c Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 1 Jul 2013 22:37:43 +0200 Subject: [PATCH 068/136] Deal with None in EmbeddedDocumentFormSet._construct_form --- mongodbforms/documents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index a92fb9c6..ccb74bb0 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -945,7 +945,7 @@ def _construct_form(self, i, **kwargs): # add position argument to the form. Otherwise we will spend # a huge amount of time iterating over the list field on form __init__ emb_list = getattr(self.parent_document, self.form._meta.embedded_field) - if len(emb_list) < i: + if emb_list is not None and len(emb_list) < i: defaults['position'] = i defaults.update(kwargs) From c7ae5bb2147b5d62bc8368986ab5e16fb80396c4 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 8 Jul 2013 20:27:41 +0200 Subject: [PATCH 069/136] Correctly handle widget overrides in Map and ListFields --- mongodbforms/fieldgenerator.py | 8 ++++++++ mongodbforms/fields.py | 23 +++++++++++++++-------- mongodbforms/widgets.py | 13 ++++++++++++- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 19103ece..0d2f4e50 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -20,6 +20,7 @@ from mongoengine import ReferenceField as MongoReferenceField, EmbeddedDocumentField as MongoEmbeddedDocumentField from .fields import MongoCharField, ReferenceField, DocumentMultipleChoiceField, ListField, MapField +from .widgets import DynamicListWidget BLANK_CHOICE_DASH = [("", "---------")] @@ -405,3 +406,10 @@ def generate(self, field, **kwargs): defaults.update(kwargs) return forms.CharField(**defaults) + +class DynamicFormFieldGenerator(MongoDefaultFormFieldGenerator): + widget_override_map = { + 'stringfield_long': forms.Textarea, + 'listfield': DynamicListWidget, + } + diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index f0b51540..ad2ffd33 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -172,12 +172,16 @@ class ListField(forms.Field): widget = ListWidget def __init__(self, field_type, *args, **kwargs): + if 'widget' in kwargs: + self.widget = kwargs.pop('widget') + self.field_type = field_type - widget = self.field_type().widget - if isinstance(widget, type): - w_type = widget + + contained_widget = self.field_type().widget + if isinstance(contained_widget, type): + w_type = contained_widget else: - w_type = widget.__class__ + w_type = contained_widget.__class__ self.widget = self.widget(w_type) super(ListField, self).__init__(*args, **kwargs) @@ -237,12 +241,15 @@ class MapField(forms.Field): def __init__(self, field_type, max_key_length=None, min_key_length=None, key_validators=[], field_kwargs={}, *args, **kwargs): + + if 'widget' in kwargs: + self.widget = kwargs.pop('widget') - widget = field_type().widget - if isinstance(widget, type): - w_type = widget + contained_widget = field_type().widget + if isinstance(contained_widget, type): + w_type = contained_widget else: - w_type = widget.__class__ + w_type = contained_widget.__class__ self.widget = self.widget(w_type) super(MapField, self).__init__(*args, **kwargs) diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index a980d612..60ef2277 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -73,6 +73,16 @@ def __deepcopy__(self, memo): return obj class DynamicListWidget(ListWidget): + def __init__(self, widget_type, attrs=None): + class_attr = {'class': 'dynamiclistwidget'} + if attrs is None: + attrs = class_attr + elif 'class' in attrs: + attrs['class'] = '%s %s' % (attrs['class'], class_attr['class']) + else: + attrs.update(class_attr) + super(DynamicListWidget, self).__init__(widget_type, attrs) + def format_output(self, rendered_widgets): """ Given a list of rendered widgets (as strings), returns a Unicode string @@ -81,10 +91,11 @@ def format_output(self, rendered_widgets): This hook allows you to format the HTML design of the widgets, if needed. """ + #print(rendered_widgets) output = [] for widget in rendered_widgets: output.append("

%s

" % widget) - output.append('' % self._name) + #output.append('' % self._name) return ''.join(output) def _get_media(self): From 8d96398740cb5c88ec128ee0a952dedf67738382 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 8 Jul 2013 20:29:07 +0200 Subject: [PATCH 070/136] Version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ebf27099..c9446339 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def convert_readme(): return open('README.txt').read() setup(name='mongodbforms', - version='0.2', + version='0.2.1', description="An implementation of django forms using mongoengine.", author='Jan Schrewe', author_email='jan@schafproductions.com', From ce0287b7bf3f5454ada7e21b939f19a12b9b8339 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 9 Jul 2013 00:07:46 +0200 Subject: [PATCH 071/136] Handling of unique_with and ListField(ReferenceField()). Closes #43 --- mongodbforms/documents.py | 24 +++++++++++++++++++----- mongodbforms/fieldgenerator.py | 3 ++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index ccb74bb0..d547b871 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -17,7 +17,7 @@ from mongoengine.base import ValidationError except ImportError: from mongoengine.errors import ValidationError -from mongoengine.queryset import OperationError +from mongoengine.queryset import OperationError, Q from mongoengine.connection import _get_db from gridfs import GridFS @@ -477,17 +477,31 @@ def validate_unique(self): for f in self.instance._fields.values(): if f.unique and f.name not in exclude: filter_kwargs = { - f.name: getattr(self.instance, f.name) + f.name: getattr(self.instance, f.name), + 'q_obj': None, } if f.unique_with: for u_with in f.unique_with: - filter_kwargs[u_with] = getattr(self.instance, u_with) - qs = self.instance.__class__.objects().filter(**filter_kwargs) + u_with_field = self.instance._fields[u_with] + u_with_attr = getattr(self.instance, u_with) + # handling ListField(ReferenceField()) sucks big time + # What we need to do is construct a Q object that + # queries for the pk of every list entry and only accepts + # lists with the same length as the list we have + if isinstance(u_with_field, ListField) and \ + isinstance(u_with_field.field, ReferenceField): + q = reduce(lambda x, y: x & y, [Q(**{u_with: k.pk}) for k in u_with_attr]) + size_key = '%s__size' % u_with + q = q & Q(**{size_key: len(u_with_attr)}) + filter_kwargs['q_obj'] = q & filter_kwargs['q_obj'] + else: + filter_kwargs[u_with] = u_with_attr + qs = self.instance.__class__.objects.no_dereference().filter(**filter_kwargs) # Exclude the current object from the query if we are editing an # instance (as opposed to creating a new one) if self.instance.pk is not None: qs = qs.filter(pk__ne=self.instance.pk) - if len(qs) > 0: + if qs.count() > 0: message = _("%(model_name)s with this %(field_label)s already exists.") % { 'model_name': str(capfirst(self.instance._meta.verbose_name)), 'field_label': str(pretty_name(f.name)) diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 0d2f4e50..6d5fefed 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -76,6 +76,7 @@ def generate(self, field, **kwargs): return field_name = field.__class__.__name__.lower() + # this masks errors in formfield generation. Bad way to do it! try: return getattr(self, 'generate_%s' % field_name)(field, **kwargs) except AttributeError: @@ -322,7 +323,7 @@ def generate_listfield(self, field, **kwargs): 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field), 'required': field.required, - 'queryset': field.document_type.objects, + 'queryset': field.field.document_type.objects.clone(), } else: map_key = 'listfield' From 07c69e64915567d19707b80e342df9202896fe61 Mon Sep 17 00:00:00 2001 From: Laurent Payot Date: Tue, 9 Jul 2013 01:34:29 +0200 Subject: [PATCH 072/136] reduce is in functools module in Python 3 --- mongodbforms/documents.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index d547b871..eead3660 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -1,6 +1,7 @@ import os import itertools from collections import Callable +from functools import reduce from django.utils.datastructures import SortedDict from django.forms.forms import BaseForm, get_declared_fields, NON_FIELD_ERRORS, pretty_name From 113c6a982dfee42e05c2988dd0ec4a59e7be80d0 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 9 Jul 2013 01:55:45 +0200 Subject: [PATCH 073/136] Cleanup of fields_for_document. Finally keep the fields in creation order too. Closes #1 (Yay) --- mongodbforms/documents.py | 35 ++++++++++------------------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index eead3660..a9428fba 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -235,22 +235,17 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, \ in the ``fields`` argument. """ field_list = [] - ignored = [] if isinstance(field_generator, type): field_generator = field_generator() + + if formfield_callback and not isinstance(formfield_callback, Callable): + raise TypeError('formfield_callback must be a function or callable') - # This is actually a bad way to sort the fields, but the fields keep the order - # they were defined on he document (at least with cPython) and I can't see - # any other way for now. Oh, yeah, it works because we sort on the memory address - # and hope that the earlier fields have a lower address. - sorted_fields = sorted(list(document._fields.values()), key=lambda field: field.__hash__()) - - for f in sorted_fields: + for name in document._fields_ordered: + f = document._fields.get(name) if isinstance(f, ObjectIdField): continue - if isinstance(f, ListField) and not (hasattr(f.field,'choices') or isinstance(f.field, ReferenceField)): - continue - if fields is not None and not f.name in fields: + if fields and not f.name in fields: continue if exclude and f.name in exclude: continue @@ -259,25 +254,15 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, \ else: kwargs = {} - if formfield_callback is None: - formfield = field_generator.generate(f, **kwargs) - elif not isinstance(formfield_callback, Callable): - raise TypeError('formfield_callback must be a function or callable') - else: + if formfield_callback: formfield = formfield_callback(f, **kwargs) + else: + formfield = field_generator.generate(f, **kwargs) if formfield: field_list.append((f.name, formfield)) - else: - ignored.append(f.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 + return SortedDict(field_list) From bac9406963e67f65c14099afa300143340ab3efa Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 9 Jul 2013 02:37:04 +0200 Subject: [PATCH 074/136] Use f.name more consistently and not some var that points to f.name --- mongodbforms/documents.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index a9428fba..63775caa 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -218,8 +218,7 @@ def document_to_dict(instance, fields=None, exclude=None): continue if exclude and f.name in exclude: continue - else: - data[f.name] = getattr(instance, f.name) + data[f.name] = getattr(instance, f.name, '') return data def fields_for_document(document, fields=None, exclude=None, widgets=None, \ @@ -377,23 +376,22 @@ def _get_validation_exclusions(self): # Build up a list of fields that should be excluded from model field # validation and unique checks. for f in self.instance._fields.values(): - field = f.name # Exclude fields that aren't on the form. The developer may be # adding these values to the model after form validation. - if field not in self.fields: + if f.name not in self.fields: exclude.append(f.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 not in self._meta.fields: + elif self._meta.fields and f.name not in self._meta.fields: exclude.append(f.name) - elif self._meta.exclude and field in self._meta.exclude: + elif self._meta.exclude and f.name in self._meta.exclude: exclude.append(f.name) # Exclude fields that failed form validation. There's no need for # the model fields to validate them as well. - elif field in list(self._errors.keys()): + elif f.name in list(self._errors.keys()): exclude.append(f.name) # Exclude empty fields that are not required by the form, if the @@ -403,7 +401,7 @@ def _get_validation_exclusions(self): # value may be included in a unique check, so cannot be excluded # from validation. else: - field_value = self.cleaned_data.get(field, None) + field_value = self.cleaned_data.get(f.name, None) if not f.required and field_value in EMPTY_VALUES: exclude.append(f.name) return exclude From 9072c36689c64c9a9742d409d5ba3cf34ade1bda Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 9 Jul 2013 03:03:25 +0200 Subject: [PATCH 075/136] Use db_alias and collection_name in _get_unique_filename. And delete old file before trying to get a new name --- mongodbforms/documents.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 63775caa..8eef02e2 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -19,7 +19,7 @@ except ImportError: from mongoengine.errors import ValidationError from mongoengine.queryset import OperationError, Q -from mongoengine.connection import _get_db +from mongoengine.connection import get_db, DEFAULT_CONNECTION_NAME from gridfs import GridFS from .documentoptions import DocumentMetaWrapper @@ -27,8 +27,8 @@ _fieldgenerator = load_field_generator() -def _get_unique_filename(name): - fs = GridFS(_get_db()) +def _get_unique_filename(name, db_alias=DEFAULT_CONNECTION_NAME, collection_name='fs'): + fs = GridFS(get_db(db_alias), collection_name) file_root, file_ext = os.path.splitext(name) count = itertools.count(1) while fs.exists(filename=name): @@ -79,7 +79,7 @@ def _save_iterator_file(field, uploaded_file, file_data=None): file_data.delete() uploaded_file.seek(0) - filename = _get_unique_filename(uploaded_file.name) + filename = _get_unique_filename(uploaded_file.name, field.field.db_alias, field.field.collection_name) file_data.put(uploaded_file, content_type=uploaded_file.content_type, filename=filename) file_data.close() @@ -153,8 +153,11 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): try: upload.file.seek(0) - filename = _get_unique_filename(upload.name) - field.replace(upload, content_type=upload.content_type, filename=filename) + # delete first to not get the names right + if field.grid_id: + field.delete() + filename = _get_unique_filename(upload.name, f.db_alias, f.collection_name) + field.put(upload, content_type=upload.content_type, filename=filename) setattr(instance, f.name, field) except AttributeError: # file was already uploaded and not changed during edit. From 1ac29d24654205fba1dd55bd88d6cf8c68b20e67 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 9 Jul 2013 03:39:36 +0200 Subject: [PATCH 076/136] Field defaults in Fieldgenerator can be (are supposed to be?) callables --- mongodbforms/documents.py | 2 +- mongodbforms/fieldgenerator.py | 44 ++++++++++++++++++++++------------ 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 8eef02e2..00438fa2 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -153,7 +153,7 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): try: upload.file.seek(0) - # delete first to not get the names right + # delete first to get the names right if field.grid_id: field.delete() filename = _get_unique_filename(upload.name, f.db_alias, f.collection_name) diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 6d5fefed..75f02917 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -4,6 +4,7 @@ Based on django mongotools (https://github.com/wpjunior/django-mongotools) by Wilson Júnior (wilsonpjunior@gmail.com). """ +import collections from django import forms from django.core.validators import EMPTY_VALUES @@ -17,7 +18,8 @@ from django.db.models.options import get_verbose_name from django.utils.text import capfirst -from mongoengine import ReferenceField as MongoReferenceField, EmbeddedDocumentField as MongoEmbeddedDocumentField +from mongoengine import ReferenceField as MongoReferenceField, EmbeddedDocumentField as MongoEmbeddedDocumentField, \ + ListField as MongoListField, MapField as MongoMapField from .fields import MongoCharField, ReferenceField, DocumentMultipleChoiceField, ListField, MapField from .widgets import DynamicListWidget @@ -123,6 +125,18 @@ def get_field_label(self, field): def get_field_help_text(self, field): if field.help_text: return field.help_text.capitalize() + else: + return '' + + def get_field_default(self, field): + if isinstance(field, (MongoListField, MongoMapField)): + f = field.field + else: + f = field + if isinstance(f.default, collections.Callable): + return f.default() + else: + return f.default def _check_widget(self, map_key): if map_key in self.widget_override_map: @@ -135,7 +149,7 @@ def generate_stringfield(self, field, **kwargs): map_key = 'stringfield_choices' defaults = { 'label': self.get_field_label(field), - 'initial': field.default, + 'initial': self.get_field_default(field), 'required': field.required, 'help_text': self.get_field_help_text(field), 'choices': self.get_field_choices(field), @@ -145,7 +159,7 @@ def generate_stringfield(self, field, **kwargs): map_key = 'stringfield_long' defaults = { 'label': self.get_field_label(field), - 'initial': field.default, + 'initial': self.get_field_default(field), 'required': field.required, 'help_text': self.get_field_help_text(field) } @@ -153,7 +167,7 @@ def generate_stringfield(self, field, **kwargs): map_key = 'stringfield' defaults = { 'label': self.get_field_label(field), - 'initial': field.default, + 'initial': self.get_field_default(field), 'required': field.required, 'help_text': self.get_field_help_text(field), 'max_length': field.max_length, @@ -172,7 +186,7 @@ def generate_emailfield(self, field, **kwargs): 'required': field.required, 'min_length': field.min_length, 'max_length': field.max_length, - 'initial': field.default, + 'initial': self.get_field_default(field), 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field) } @@ -187,7 +201,7 @@ def generate_urlfield(self, field, **kwargs): 'required': field.required, 'min_length': field.min_length, 'max_length': field.max_length, - 'initial': field.default, + 'initial': self.get_field_default(field), 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field) } @@ -203,7 +217,7 @@ def generate_intfield(self, field, **kwargs): 'coerce': self.integer_field, 'empty_value': None, 'required': field.required, - 'initial': field.default, + 'initial': self.get_field_default(field), 'label': self.get_field_label(field), 'choices': self.get_field_choices(field), 'help_text': self.get_field_help_text(field) @@ -214,7 +228,7 @@ def generate_intfield(self, field, **kwargs): 'required': field.required, 'min_value': field.min_value, 'max_value': field.max_value, - 'initial': field.default, + 'initial': self.get_field_default(field), 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field) } @@ -227,7 +241,7 @@ def generate_floatfield(self, field, **kwargs): map_key = 'floatfield' defaults = { 'label': self.get_field_label(field), - 'initial': field.default, + 'initial': self.get_field_default(field), 'required': field.required, 'min_value': field.min_value, 'max_value': field.max_value, @@ -242,7 +256,7 @@ def generate_decimalfield(self, field, **kwargs): map_key = 'decimalfield' defaults = { 'label': self.get_field_label(field), - 'initial': field.default, + 'initial': self.get_field_default(field), 'required': field.required, 'min_value': field.min_value, 'max_value': field.max_value, @@ -260,7 +274,7 @@ def generate_booleanfield(self, field, **kwargs): 'coerce': self.boolean_field, 'empty_value': None, 'required': field.required, - 'initial': field.default, + 'initial': self.get_field_default(field), 'label': self.get_field_label(field), 'choices': self.get_field_choices(field), 'help_text': self.get_field_help_text(field) @@ -269,7 +283,7 @@ def generate_booleanfield(self, field, **kwargs): map_key = 'booleanfield' defaults = { 'required': field.required, - 'initial': field.default, + 'initial': self.get_field_default(field), 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field) } @@ -282,7 +296,7 @@ def generate_datetimefield(self, field, **kwargs): map_key = 'datetimefield' defaults = { 'required': field.required, - 'initial': field.default, + 'initial': self.get_field_default(field), 'label': self.get_field_label(field), } form_class = self.form_field_map.get(map_key) @@ -358,7 +372,7 @@ def generate_filefield(self, field, **kwargs): defaults = { 'required':field.required, 'label':self.get_field_label(field), - 'initial': field.default, + 'initial': self.get_field_default(field), 'help_text': self.get_field_help_text(field) } form_class = self.form_field_map.get(map_key) @@ -371,7 +385,7 @@ def generate_imagefield(self, field, **kwargs): defaults = { 'required':field.required, 'label':self.get_field_label(field), - 'initial': field.default, + 'initial': self.get_field_default(field), 'help_text': self.get_field_help_text(field) } form_class = self.form_field_map.get(map_key) From dcb11fce9fae1f1d3109b7ed96865cf93ecb4eb5 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 9 Jul 2013 04:49:26 +0200 Subject: [PATCH 077/136] Don't construct the instance several times during clean and save --- mongodbforms/documents.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 00438fa2..132f3774 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -67,7 +67,6 @@ def _save_iterator_file(field, uploaded_file, file_data=None): file_data = field.field.proxy_class(db_alias=field.field.db_alias, collection_name=field.field.collection_name) - # overwrite an existing file if file_data.instance is None: file_data.instance = fake_document overwrote_instance = True @@ -179,7 +178,9 @@ def save_instance(form, instance, fields=None, fail_message='saved', If construct=False, assume ``instance`` has already been constructed and just needs to be saved. """ - instance = construct_instance(form, instance, fields, exclude) + if construct: + instance = construct_instance(form, instance, fields, exclude) + if form.errors: raise ValueError("The %s could not be %s because the data didn't" " validate." % (instance.__class__.__name__, fail_message)) From 4570a4896ca3b2fa675350179b3321bcb6cd8a6f Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 9 Jul 2013 23:04:05 +0200 Subject: [PATCH 078/136] Cleanup of reference form fields. ReferenceField now uses pk and no longer id for the primary key --- mongodbforms/fields.py | 42 +++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index ad2ffd33..bb19ae57 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -4,6 +4,7 @@ Based on django mongotools (https://github.com/wpjunior/django-mongotools) by Wilson Júnior (wilsonpjunior@gmail.com). """ +import copy from django import forms from django.core.validators import EMPTY_VALUES, MinLengthValidator, MaxLengthValidator @@ -68,12 +69,11 @@ def __init__(self, queryset, empty_label="---------", *args, **kwargs): self.empty_label = empty_label def _get_queryset(self): - return self._queryset + return self._queryset.clone() def _set_queryset(self, queryset): self._queryset = queryset self.widget.choices = self.choices - queryset = property(_get_queryset, _set_queryset) def prepare_value(self, value): @@ -84,7 +84,6 @@ def prepare_value(self, value): def _get_choices(self): return MongoChoiceIterator(self) - choices = property(_get_choices, forms.ChoiceField._set_choices) def label_from_instance(self, obj): @@ -97,32 +96,23 @@ def label_from_instance(self, obj): def clean(self, value): # Check for empty values. - if value in EMPTY_VALUES: - # Raise exception if it's empty and required. - if self.required: - raise forms.ValidationError(self.error_messages['required']) - # If it's not required just ignore it. - else: - return None + if value in EMPTY_VALUES and self-required: + raise forms.ValidationError(self.error_messages['required']) + elif value in EMPTY_VALUES and not self-required: + return None + oid = super(ReferenceField, self).clean(value) + try: - oid = ObjectId(value) - oid = super(ReferenceField, self).clean(oid) - - queryset = self.queryset.clone() - obj = queryset.get(id=oid) + obj = self.queryset.get(pk=oid) except (TypeError, InvalidId, self.queryset._document.DoesNotExist): raise forms.ValidationError(self.error_messages['invalid_choice'] % {'value':value}) return obj - # Fix for Django 1.4 - # TODO: Test with older django versions - # from django-mongotools by wpjunior - # https://github.com/wpjunior/django-mongotools/ def __deepcopy__(self, memo): result = super(forms.ChoiceField, self).__deepcopy__(memo) - result.queryset = result.queryset - result.empty_label = result.empty_label + result.queryset = self.queryset # self.queryset calls clone() + result.empty_label = copy.deepcopy(self.empty_label) return result class DocumentMultipleChoiceField(ReferenceField): @@ -147,10 +137,12 @@ def clean(self, value): if not isinstance(value, (list, tuple)): raise forms.ValidationError(self.error_messages['list']) - key = 'pk' - qs = self.queryset.clone() - qs = qs.filter(**{'%s__in' % key: value}) - pks = set([force_unicode(getattr(o, key)) for o in qs]) + qs = self.queryset + try: + qs = qs.filter(pk__in=value) + except ValidationError: + raise forms.ValidationError(self.error_messages['invalid_pk_value'] % str(value)) + pks = set([force_unicode(getattr(o, 'pk')) for o in qs]) for val in value: if force_unicode(val) not in pks: raise forms.ValidationError(self.error_messages['invalid_choice'] % val) From 57f093953e1ba4261498b65464608e6812f252fc Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 10 Jul 2013 00:27:48 +0200 Subject: [PATCH 079/136] Use hasattr instead of except in fieldgenerator so errors during field generation don't get masked --- mongodbforms/fieldgenerator.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 75f02917..483cccfd 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -71,29 +71,28 @@ def generate(self, field, **kwargs): field-classname) and raises a NotImplementedError of no generator can be found. """ - # do not handle embedded documents here. They're more or less special + # do not handle embedded documents here. They are more or less special # and require some form of inline formset or something more complex # to handle then a simple field if isinstance(field, MongoEmbeddedDocumentField): return - field_name = field.__class__.__name__.lower() - # this masks errors in formfield generation. Bad way to do it! - try: - return getattr(self, 'generate_%s' % field_name)(field, **kwargs) - except AttributeError: - pass + attr_name = 'generate_%s' % field.__class__.__name__.lower() + if hasattr(self, attr_name): + return getattr(self, attr_name)(field, **kwargs) for cls in field.__class__.__bases__: cls_name = cls.__name__.lower() - try: - return getattr(self, 'generate_%s' % cls_name)(field, **kwargs) - except AttributeError: - if cls_name in self.form_field_map: - return getattr(self, self.generator_map.get(cls_name))(field, **kwargs) + + attr_name = 'generate_%s' % cls_name + if hasattr(self, attr_name): + return getattr(self, attr_name)(field, **kwargs) + + if cls_name in self.form_field_map: + return getattr(self, self.generator_map.get(cls_name))(field, **kwargs) - raise NotImplementedError('%s is not supported by MongoForm' % \ - field.__class__.__name__) + raise NotImplementedError('%s is not supported by MongoForm' % \ + field.__class__.__name__) def get_field_choices(self, field, include_blank=True, blank_choice=BLANK_CHOICE_DASH): From 420ef26ee811fafd41d9f440c25e4e9c1ee59287 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 10 Jul 2013 02:54:16 +0200 Subject: [PATCH 080/136] SOme documentation and correct id's in map field widget --- mongodbforms/fields.py | 1 + mongodbforms/widgets.py | 8 ++++++-- readme.md | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index bb19ae57..74b7abe5 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -162,6 +162,7 @@ class ListField(forms.Field): 'invalid': _('Enter a list of values.'), } widget = ListWidget + hidden_widget = forms.MultipleHiddenInput def __init__(self, field_type, *args, **kwargs): if 'widget' in kwargs: diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index 60ef2277..ad39fb2f 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -155,12 +155,16 @@ def render(self, name, value, attrs=None): value.append(('', '')) for i, (key, widget_value) in enumerate(value): if id_: - final_attrs = dict(final_attrs, id='%s_%s' % (id_, i)) fieldset_attr = dict(final_attrs, id='fieldset_%s_%s' % (id_, i)) - group = [] group.append(mark_safe('
' % flatatt(fieldset_attr))) + + if id_: + final_attrs = dict(final_attrs, id='%s_key_%s' % (id_, i)) group.append(self.key_widget.render(name + '_key_%s' % i, key, final_attrs)) + + if id_: + final_attrs = dict(final_attrs, id='%s_value_%s' % (id_, i)) group.append(self.data_widget.render(name + '_value_%s' % i, widget_value, final_attrs)) group.append(mark_safe('
')) diff --git a/readme.md b/readme.md index f262ebe5..c5e7bb78 100644 --- a/readme.md +++ b/readme.md @@ -7,6 +7,20 @@ This is an implementation of django's model forms for mongoengine documents. * Django >= 1.3 * [mongoengine](http://mongoengine.org/) >= 0.6 +## Supported field types + +Mongodbforms supports all the fields that have a simple representation in Django's formfields (IntField, TextField, etc). In addition it also supports `ListField`s and `MapField`s. + +### File fields + +Mongodbforms handles file uploads just like the normal Django forms. Uploaded files are stored in GridFS using the mongoengine fields. Because GridFS has no directories and stores files in a flat space each uploaded file gets a unique filename with the form <filename>\_<unique\_number>.<extension> + +### Container fields + +For container fields like `ListField`s and `MapField`s a very simple widget is used. The widget renders the container content in the appropriate field plus one empty field. This is mainly done to not introduce any Javascript dependencies, the backend code will happily handle any kind of dynamic form, as long as the field ids are continuously numbered in the POST data. + +You can use any of the other supported fields inside list or map fields. Including `FileFields` which aren't really supported by mongoengine inside container fields. + ## Usage mongodbforms supports forms for normal documents and embedded documents. From 6f9d8c95f950912fc9372f499b5a3a9dacd4cc91 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 10 Jul 2013 02:56:42 +0200 Subject: [PATCH 081/136] Formatting fun --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index c5e7bb78..44dcd0e0 100644 --- a/readme.md +++ b/readme.md @@ -13,11 +13,11 @@ Mongodbforms supports all the fields that have a simple representation in Django ### File fields -Mongodbforms handles file uploads just like the normal Django forms. Uploaded files are stored in GridFS using the mongoengine fields. Because GridFS has no directories and stores files in a flat space each uploaded file gets a unique filename with the form <filename>\_<unique\_number>.<extension> +Mongodbforms handles file uploads just like the normal Django forms. Uploaded files are stored in GridFS using the mongoengine fields. Because GridFS has no directories and stores files in a flat space each uploaded file gets a unique filename with the form `_.`. ### Container fields -For container fields like `ListField`s and `MapField`s a very simple widget is used. The widget renders the container content in the appropriate field plus one empty field. This is mainly done to not introduce any Javascript dependencies, the backend code will happily handle any kind of dynamic form, as long as the field ids are continuously numbered in the POST data. +For container fields like `ListFields` and `MapFields` a very simple widget is used. The widget renders the container content in the appropriate field plus one empty field. This is mainly done to not introduce any Javascript dependencies, the backend code will happily handle any kind of dynamic form, as long as the field ids are continuously numbered in the POST data. You can use any of the other supported fields inside list or map fields. Including `FileFields` which aren't really supported by mongoengine inside container fields. From d6d2b3a634a80dcbd32b7f76f86e16137ed50cd4 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 10 Jul 2013 02:57:34 +0200 Subject: [PATCH 082/136] Formatting again --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 44dcd0e0..02071b42 100644 --- a/readme.md +++ b/readme.md @@ -9,7 +9,7 @@ This is an implementation of django's model forms for mongoengine documents. ## Supported field types -Mongodbforms supports all the fields that have a simple representation in Django's formfields (IntField, TextField, etc). In addition it also supports `ListField`s and `MapField`s. +Mongodbforms supports all the fields that have a simple representation in Django's formfields (IntField, TextField, etc). In addition it also supports `ListFields` and `MapFields`. ### File fields From 7759f08b6c95234b6fc3bd7a1c6aa3992237b458 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 10 Jul 2013 05:40:03 +0200 Subject: [PATCH 083/136] More documentation stuff --- README.txt | 108 ++++++++++++++++++++++++++++++++++------------------- readme.md | 10 ++--- setup.py | 4 +- 3 files changed, 77 insertions(+), 45 deletions(-) diff --git a/README.txt b/README.txt index e9369262..6f76ce4f 100644 --- a/README.txt +++ b/README.txt @@ -8,7 +8,37 @@ Requirements ------------ - Django >= 1.3 -- `mongoengine `_ >= 0.6 +- `mongoengine `__ >= 0.6 + +Supported field types +--------------------- + +Mongodbforms supports all the fields that have a simple representation +in Django's formfields (IntField, TextField, etc). In addition it also +supports ``ListFields`` and ``MapFields``. + +File fields +~~~~~~~~~~~ + +Mongodbforms handles file uploads just like the normal Django forms. +Uploaded files are stored in GridFS using the mongoengine fields. +Because GridFS has no directories and stores files in a flat space an +uploaded file whose name already exists gets a unique filename with the +form ``_.``. + +Container fields +~~~~~~~~~~~~~~~~ + +For container fields like ``ListFields`` and ``MapFields`` a very simple +widget is used. The widget renders the container content in the +appropriate field plus one empty field. This is mainly done to not +introduce any Javascript dependencies, the backend code will happily +handle any kind of dynamic form, as long as the field ids are +continuously numbered in the POST data. + +You can use any of the other supported fields inside list or map fields. +Including ``FileFields`` which aren't really supported by mongoengine +inside container fields. Usage ----- @@ -21,9 +51,12 @@ Normal documents To use mongodbforms with normal documents replace djangos forms with mongodbform forms. -\`\`\`python from mongodbforms import DocumentForm +.. code:: python -class BlogForm(DocumentForm) ... \`\`\` + from mongodbforms import DocumentForm + + class BlogForm(DocumentForm) + ... Embedded documents ~~~~~~~~~~~~~~~~~~ @@ -43,35 +76,35 @@ position argument. If the embedded field is a plain embedded field the current object is simply overwritten. -\`\`\`\`python # forms.py from mongodbforms import EmbeddedDocumentForm - -class MessageForm(EmbeddedDocumentForm): class Meta: document = Message -embedded\_field\_name = 'messages' - -:: - - fields = ['subject', 'sender', 'message',] +.. code:: python -views.py -======== + # forms.py + from mongodbforms import EmbeddedDocumentForm + + class MessageForm(EmbeddedDocumentForm): + class Meta: + document = Message + embedded_field_name = 'messages' + + fields = ['subject', 'sender', 'message',] -create a new embedded object -============================ + # views.py -form = MessageForm(parent\_document=some\_document, ...) # edit the 4th -embedded object form = MessageForm(parent\_document=some\_document, -position=3, ...) \`\`\` + # create a new embedded object + form = MessageForm(parent_document=some_document, ...) + # edit the 4th embedded object + form = MessageForm(parent_document=some_document, position=3, ...) Documentation ------------- In theory the documentation `Django's -modelform `_ +modelform `__ documentation should be all you need (except for one exception; read on). If you find a discrepancy between something that mongodbforms does and what Django's documentation says, you have most likely found a bug. Please `report -it `_. +it `__. Form field generation ~~~~~~~~~~~~~~~~~~~~~ @@ -90,26 +123,25 @@ a generator on the document form you can also pass two dicts ``field_overrides`` and ``widget_overrides`` to ``__init__``. For a list of valid keys have a look at ``MongoFormFieldGenerator``. -\`\`\`\`python # settings.py - -set the fieldgeneretor for the whole application -================================================ - -MONGODBFORMS\_FIELDGENERATOR = 'myproject.fieldgenerator.GeneratorClass' - -generator.py -============ - -from mongodbforms.fieldgenerator import MongoFormFieldGenerator +.. code:: python -class MyFieldGenerator(MongoFormFieldGenerator): ... + # settings.py -forms.py -======== + # set the fieldgeneretor for the whole application + MONGODBFORMS_FIELDGENERATOR = 'myproject.fieldgenerator.GeneratorClass' -from mongodbforms import DocumentForm + # generator.py + from mongodbforms.fieldgenerator import MongoFormFieldGenerator + + class MyFieldGenerator(MongoFormFieldGenerator): + ... -from generator import MyFieldGenerator + # forms.py + from mongodbforms import DocumentForm + + from generator import MyFieldGenerator + + class MessageForm(DocumentForm): + class Meta: + formfield_generator = MyFieldGenerator -class MessageForm(DocumentForm): class Meta: formfield\_generator = -MyFieldGenerator \`\`\` diff --git a/readme.md b/readme.md index 02071b42..66807933 100644 --- a/readme.md +++ b/readme.md @@ -13,7 +13,7 @@ Mongodbforms supports all the fields that have a simple representation in Django ### File fields -Mongodbforms handles file uploads just like the normal Django forms. Uploaded files are stored in GridFS using the mongoengine fields. Because GridFS has no directories and stores files in a flat space each uploaded file gets a unique filename with the form `_.`. +Mongodbforms handles file uploads just like the normal Django forms. Uploaded files are stored in GridFS using the mongoengine fields. Because GridFS has no directories and stores files in a flat space an uploaded file whose name already exists gets a unique filename with the form `_.`. ### Container fields @@ -44,7 +44,7 @@ If no position is provided the form adds a new embedded document to the list if If the embedded field is a plain embedded field the current object is simply overwritten. -````python +```python # forms.py from mongodbforms import EmbeddedDocumentForm @@ -71,11 +71,11 @@ In theory the documentation [Django's modelform](https://docs.djangoproject.com/ Because the fields on mongoengine documents have no notion of form fields mongodbform uses a generator class to generate the form field for a db field, which is not explicitly set. -To use your own field generator you can either set a generator for your whole project using ```MONGODBFORMS_FIELDGENERATOR``` in settings.py or you can use the ``formfield_generator`` option on the form's Meta class. +To use your own field generator you can either set a generator for your whole project using `MONGODBFORMS_FIELDGENERATOR` in settings.py or you can use the `formfield_generator` option on the form's Meta class. -The default generator is defined in ```mongodbforms/fieldgenerator.py``` and should make it easy to override form fields and widgets. If you set a generator on the document form you can also pass two dicts ```field_overrides``` and ```widget_overrides``` to ```__init__```. For a list of valid keys have a look at ```MongoFormFieldGenerator```. +The default generator is defined in `mongodbforms/fieldgenerator.py` and should make it easy to override form fields and widgets. If you set a generator on the document form you can also pass two dicts `field_overrides` and `widget_overrides` to `__init__`. For a list of valid keys have a look at `MongoFormFieldGenerator`. -````python +```python # settings.py # set the fieldgeneretor for the whole application diff --git a/setup.py b/setup.py index c9446339..eeb0d1cb 100644 --- a/setup.py +++ b/setup.py @@ -5,13 +5,13 @@ def convert_readme(): try: - call(["pandoc", "-t", "rst", "-o", "README.txt", "readme.md"]) + call(["pandoc", "-f", "markdown_github", "-t", "rst", "-o", "README.txt", "readme.md"]) except OSError: pass return open('README.txt').read() setup(name='mongodbforms', - version='0.2.1', + version='0.2.2', description="An implementation of django forms using mongoengine.", author='Jan Schrewe', author_email='jan@schafproductions.com', From 8d28552fa0a03947f039f169873890d2e775a785 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 10 Jul 2013 16:22:54 +0200 Subject: [PATCH 084/136] Parameters for field generation --- mongodbforms/fieldgenerator.py | 103 ++++++++++++++------------------- 1 file changed, 44 insertions(+), 59 deletions(-) diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 483cccfd..8a5a7157 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -33,6 +33,7 @@ class MongoFormFieldGenerator(object): # but don't actually have the name. generator_map = { 'sortedlistfield': 'generate_listfield', + 'longfield': 'generate_intfield', } form_field_map = { @@ -47,7 +48,7 @@ class MongoFormFieldGenerator(object): 'decimalfield': forms.DecimalField, 'booleanfield': forms.BooleanField, 'booleanfield_choices': forms.TypedChoiceField, - 'datetimefield': forms.DateTimeField, + 'datetimefield': forms.SplitDateTimeField, 'referencefield': ReferenceField, 'listfield': ListField, 'listfield_choices': forms.MultipleChoiceField, @@ -144,33 +145,26 @@ def _check_widget(self, map_key): return {} def generate_stringfield(self, field, **kwargs): + defaults = { + 'label': self.get_field_label(field), + 'initial': self.get_field_default(field), + 'required': field.required, + 'help_text': self.get_field_help_text(field), + 'min_length': field.min_length, + } if field.choices: map_key = 'stringfield_choices' - defaults = { - 'label': self.get_field_label(field), - 'initial': self.get_field_default(field), - 'required': field.required, - 'help_text': self.get_field_help_text(field), + defaults.update({ 'choices': self.get_field_choices(field), 'coerce': self.string_field, - } + }) elif field.max_length is None: map_key = 'stringfield_long' - defaults = { - 'label': self.get_field_label(field), - 'initial': self.get_field_default(field), - 'required': field.required, - 'help_text': self.get_field_help_text(field) - } else: map_key = 'stringfield' - defaults = { - 'label': self.get_field_label(field), - 'initial': self.get_field_default(field), - 'required': field.required, - 'help_text': self.get_field_help_text(field), + defaults.update({ 'max_length': field.max_length, - } + }) if field.regex: defaults['regex'] = field.regex @@ -210,27 +204,25 @@ def generate_urlfield(self, field, **kwargs): return form_class(**defaults) def generate_intfield(self, field, **kwargs): + defaults = { + 'required': field.required, + 'initial': self.get_field_default(field), + 'label': self.get_field_label(field), + 'help_text': self.get_field_help_text(field) + } if field.choices: map_key = 'intfield_choices' - defaults = { + defaults.update({ 'coerce': self.integer_field, 'empty_value': None, - 'required': field.required, - 'initial': self.get_field_default(field), - 'label': self.get_field_label(field), 'choices': self.get_field_choices(field), - 'help_text': self.get_field_help_text(field) - } + }) else: map_key = 'intfield' - defaults = { - 'required': field.required, + defaults.update({ 'min_value': field.min_value, 'max_value': field.max_value, - 'initial': self.get_field_default(field), - 'label': self.get_field_label(field), - 'help_text': self.get_field_help_text(field) - } + }) form_class = self.form_field_map.get(map_key) defaults.update(self._check_widget(map_key)) defaults.update(kwargs) @@ -259,6 +251,7 @@ def generate_decimalfield(self, field, **kwargs): 'required': field.required, 'min_value': field.min_value, 'max_value': field.max_value, + 'decimal_places': field.precision, 'help_text': self.get_field_help_text(field) } form_class = self.form_field_map.get(map_key) @@ -267,25 +260,21 @@ def generate_decimalfield(self, field, **kwargs): return form_class(**defaults) def generate_booleanfield(self, field, **kwargs): + defaults = { + 'required': field.required, + 'initial': self.get_field_default(field), + 'label': self.get_field_label(field), + 'help_text': self.get_field_help_text(field) + } if field.choices: map_key = 'booleanfield_choices' - defaults = { + defaults.update({ 'coerce': self.boolean_field, 'empty_value': None, - 'required': field.required, - 'initial': self.get_field_default(field), - 'label': self.get_field_label(field), 'choices': self.get_field_choices(field), - 'help_text': self.get_field_help_text(field) - } + }) else: map_key = 'booleanfield' - defaults = { - 'required': field.required, - 'initial': self.get_field_default(field), - 'label': self.get_field_label(field), - 'help_text': self.get_field_help_text(field) - } form_class = self.form_field_map.get(map_key) defaults.update(self._check_widget(map_key)) defaults.update(kwargs) @@ -309,7 +298,7 @@ def generate_referencefield(self, field, **kwargs): 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field), 'required': field.required, - 'queryset': field.document_type.objects, + 'queryset': field.document_type.objects.clone(), } form_class = self.form_field_map.get(map_key) defaults.update(self._check_widget(map_key)) @@ -321,32 +310,28 @@ def generate_listfield(self, field, **kwargs): if isinstance(field.field, MongoEmbeddedDocumentField): return + defaults = { + 'label': self.get_field_label(field), + 'help_text': self.get_field_help_text(field), + 'required': field.required, + } if field.field.choices: map_key = 'listfield_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 - } + }) elif isinstance(field.field, MongoReferenceField): map_key = 'listfield_references' - defaults = { - 'label': self.get_field_label(field), - 'help_text': self.get_field_help_text(field), - 'required': field.required, + defaults.update({ 'queryset': field.field.document_type.objects.clone(), - } + }) else: map_key = 'listfield' form_field = self.generate(field.field) - defaults = { - 'label': self.get_field_label(field), - 'help_text': self.get_field_help_text(field), - 'required': field.required, + defaults.update({ 'field_type': form_field.__class__, - } + }) form_class = self.form_field_map.get(map_key) defaults.update(self._check_widget(map_key)) defaults.update(kwargs) From d27b3f557173fe84014a35c880ad408968738837 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 12 Jul 2013 14:11:00 +0200 Subject: [PATCH 085/136] Don't pass min_length to string fields with choices --- mongodbforms/fieldgenerator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 8a5a7157..7020e68b 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -150,7 +150,6 @@ def generate_stringfield(self, field, **kwargs): 'initial': self.get_field_default(field), 'required': field.required, 'help_text': self.get_field_help_text(field), - 'min_length': field.min_length, } if field.choices: map_key = 'stringfield_choices' @@ -160,10 +159,14 @@ def generate_stringfield(self, field, **kwargs): }) elif field.max_length is None: map_key = 'stringfield_long' + defaults.update({ + 'min_length': field.min_length, + }) else: map_key = 'stringfield' defaults.update({ 'max_length': field.max_length, + 'min_length': field.min_length, }) if field.regex: defaults['regex'] = field.regex From c57c6204c084bb0e874cd6c14273e8daa9665ff8 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 12 Jul 2013 15:18:29 +0200 Subject: [PATCH 086/136] Get rid of the fake document hack with mongoengine 0.8.3 --- mongodbforms/documents.py | 39 +++++---------------------------------- readme.md | 4 ++-- setup.py | 2 +- 3 files changed, 8 insertions(+), 37 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 132f3774..cc474577 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -35,44 +35,20 @@ def _get_unique_filename(name, db_alias=DEFAULT_CONNECTION_NAME, collection_name # file_ext includes the dot. name = os.path.join("%s_%s%s" % (file_root, next(count), file_ext)) return name - -# The awesome Mongoengine ImageGridFsProxy wants to pull a field -# from a document to get necessary data. Trouble is that this doesn't work -# if the ImageField is stored on List or MapField. So we pass a nice fake -# document to the proxy to get saving the file done. Yeah it really is that ugly. -class FakeDocument(object): - _fields = {} - - def __init__(self, key, field): - super(FakeDocument, self).__init__() - - self._fields[key] = field - # We don't care if anything gets marked on this - # we do update a real field later though. That should - # trigger the same thing on the real document. - def _mark_as_changed(self, key): - pass - -def _save_iterator_file(field, uploaded_file, file_data=None): +def _save_iterator_file(field, instance, uploaded_file, file_data=None): """ Takes care of saving a file for a list field. Returns a Mongoengine fileproxy object or the file field. """ - fake_document = FakeDocument(field.name, field.field) - overwrote_instance = False - overwrote_key = False # for a new file we need a new proxy object if file_data is None: - file_data = field.field.proxy_class(db_alias=field.field.db_alias, - collection_name=field.field.collection_name) + file_data = field.field.get_proxy_obj(key=field.name, instance=instance) if file_data.instance is None: - file_data.instance = fake_document - overwrote_instance = True + file_data.instance = instance if file_data.key is None: file_data.key = field.name - overwrote_key = True if file_data.grid_id: file_data.delete() @@ -81,11 +57,6 @@ def _save_iterator_file(field, uploaded_file, file_data=None): filename = _get_unique_filename(uploaded_file.name, field.field.db_alias, field.field.collection_name) file_data.put(uploaded_file, content_type=uploaded_file.content_type, filename=filename) file_data.close() - - if overwrote_instance: - file_data.instance = None - if overwrote_key: - file_data.key = None return file_data @@ -126,7 +97,7 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): if uploaded_file is None: continue file_data = map_field.get(key, None) - map_field[key] = _save_iterator_file(f, uploaded_file, file_data) + map_field[key] = _save_iterator_file(f, instance, uploaded_file, file_data) setattr(instance, f.name, map_field) elif isinstance(f, ListField): list_field = getattr(instance, f.name) @@ -138,7 +109,7 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): file_data = list_field[i] except IndexError: file_data = None - file_obj = _save_iterator_file(f, uploaded_file, file_data) + file_obj = _save_iterator_file(f, instance, uploaded_file, file_data) try: list_field[i] = file_obj except IndexError: diff --git a/readme.md b/readme.md index 66807933..17e8c781 100644 --- a/readme.md +++ b/readme.md @@ -4,8 +4,8 @@ This is an implementation of django's model forms for mongoengine documents. ## Requirements - * Django >= 1.3 - * [mongoengine](http://mongoengine.org/) >= 0.6 + * Django >= 1.4 + * [mongoengine](http://mongoengine.org/) >= 0.8.3 ## Supported field types diff --git a/setup.py b/setup.py index eeb0d1cb..f61e49f5 100644 --- a/setup.py +++ b/setup.py @@ -30,5 +30,5 @@ def convert_readme(): long_description=convert_readme(), include_package_data=True, zip_safe=False, - install_requires=['setuptools', 'django>=1.3', 'mongoengine>=0.6',], + install_requires=['setuptools', 'django>=1.4', 'mongoengine>=0.8.3',], ) From 3886b87764127085701c71795ed7791d8a91ded5 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 15 Jul 2013 19:03:17 +0200 Subject: [PATCH 087/136] Add a swapped property to DocumentOptions. Might be usefull for more then just admin auth (though probably isn't) --- mongodbforms/documentoptions.py | 36 ++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 8d97eb91..6cebaf6b 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -129,8 +129,8 @@ def verbose_name(self): then generates a verbose name from from the object name. """ if self._verbose_name is None: - self._verbose_name = capfirst(create_verbose_name(self._meta.get('verbose_name', self.object_name))) - + verbose_name = self._meta.get('verbose_name', self.object_name) + self._verbose_name = capfirst(create_verbose_name(verbose_name)) return self._verbose_name @property @@ -179,12 +179,42 @@ def get_field(self, name, many_to_many=True): """ return self.get_field_by_name(name)[0] + @property + def swapped(self): + """ + Has this model been swapped out for another? If so, return the model + name of the replacement; otherwise, return None. + + For historical reasons, model name lookups using get_model() are + case insensitive, so we make sure we are case insensitive here. + + NOTE: Not sure this is actually usefull for documents. So at the moment + it's really only here because the admin wants it. It might prove usefull + for someone though, so it'S more then just a dummy. + """ + if self._meta.get('swappable', False): + model_label = '%s.%s' % (self.app_label, self.object_name.lower()) + swapped_for = getattr(settings, self.swappable, None) + if swapped_for: + try: + swapped_label, swapped_object = swapped_for.split('.') + except ValueError: + # setting not in the format app_label.model_name + # raising ImproperlyConfigured here causes problems with + # test cleanup code - instead it is raised in get_user_model + # or as part of validation. + return swapped_for + + if '%s.%s' % (swapped_label, swapped_object.lower()) not in (None, model_label): + return swapped_for + return None + def __getattr__(self, name): try: return self._meta[name] except KeyError: raise AttributeError - + def __setattr__(self, name, value): if not hasattr(self, name): self._meta[name] = value From 6a7d10d30fa223923409f23016c98ac451d482c9 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 15 Jul 2013 19:04:07 +0200 Subject: [PATCH 088/136] Initial setting on fields --- mongodbforms/fieldgenerator.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 7020e68b..4433764f 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -133,10 +133,14 @@ def get_field_default(self, field): f = field.field else: f = field + d = {} if isinstance(f.default, collections.Callable): + d['initial'] = field.default() + d['show_hidden_initial'] = True return f.default() else: - return f.default + d['initial'] = field.default + return f.default def _check_widget(self, map_key): if map_key in self.widget_override_map: @@ -341,6 +345,10 @@ def generate_listfield(self, field, **kwargs): return form_class(**defaults) def generate_mapfield(self, field, **kwargs): + # we can't really handle embedded documents here. So we just ignore them + if isinstance(field.field, MongoEmbeddedDocumentField): + return + map_key = 'mapfield' form_field = self.generate(field.field) defaults = { From 6853342350a124e1eaeb4c2e79a9e4a4f8527aaf Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 17 Jul 2013 16:25:27 +0200 Subject: [PATCH 089/136] Support unbound function in patch_document --- mongodbforms/documentoptions.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 6cebaf6b..e72dcef5 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -8,8 +8,12 @@ from mongoengine.fields import ReferenceField -def patch_document(function, instance): - setattr(instance, function.__name__, MethodType(function, instance)) +def patch_document(function, instance, bound=True): + if bound: + method = MethodType(function, instance) + else: + method = function + setattr(instance, function.__name__, method) def create_verbose_name(name): name = get_verbose_name(name) @@ -49,6 +53,7 @@ class DocumentMetaWrapper(MutableMapping): document = None _meta = None concrete_model = None + concrete_managers = [] def __init__(self, document): super(DocumentMetaWrapper, self).__init__() From fb57c721d56d6b8f78c03841da74e62301f035e9 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 19 Jul 2013 17:25:21 +0200 Subject: [PATCH 090/136] Generate a verbose_name for fields if it's None too --- mongodbforms/documentoptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index e72dcef5..db3bcbdb 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -87,7 +87,7 @@ def _setup_document_fields(self): if not hasattr(f, 'rel'): # need a bit more for actual reference fields here f.rel = None - if not hasattr(f, 'verbose_name'): + if not hasattr(f, 'verbose_name') or f.verbose_name is None: f.verbose_name = capfirst(create_verbose_name(f.name)) if not hasattr(f, 'flatchoices'): flat = [] From 38e2a28355e506165509ce7e67567de69da51601 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 22 Jul 2013 14:26:26 +0200 Subject: [PATCH 091/136] Remove _delete_bofore_save stuff for now. Seems to be unnecessary according to #47 --- mongodbforms/documents.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index cc474577..dc6d9238 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -158,20 +158,20 @@ def save_instance(form, instance, fields=None, fail_message='saved', if commit and hasattr(instance, 'save'): # see BaseDocumentForm._post_clean for an explanation - if hasattr(form, '_delete_before_save'): - data = instance._data - new_data = dict([(n, f) for n, f in data.items() if not n in form._delete_before_save]) - if hasattr(instance, '_changed_fields'): - for field in form._delete_before_save: - try: - instance._changed_fields.remove(field) - except ValueError: - pass - instance._data = new_data - instance.save() - instance._data = data - else: - instance.save() + #if hasattr(form, '_delete_before_save'): + # data = instance._data + # new_data = dict([(n, f) for n, f in data.items() if not n in form._delete_before_save]) + # if hasattr(instance, '_changed_fields'): + # for field in form._delete_before_save: + # try: + # instance._changed_fields.remove(field) + # except ValueError: + # pass + # instance._data = new_data + # instance.save() + # instance._data = data + #else: + instance.save() return instance @@ -392,17 +392,17 @@ def _post_clean(self): exclude = self._get_validation_exclusions() # Clean the model instance's fields. - to_delete = [] + #to_delete = [] try: for f in self.instance._fields.values(): value = getattr(self.instance, f.name) if f.name not in exclude: f.validate(value) - elif value in EMPTY_VALUES: + #elif value in EMPTY_VALUES: # mongoengine chokes on empty strings for fields # that are not required. Clean them up here, though # this is maybe not the right place :-) - to_delete.append(f.name) + #to_delete.append(f.name) except ValidationError as e: err = {f.name: [e.message]} self._update_errors(err) @@ -413,7 +413,7 @@ def _post_clean(self): # cached and the removed field is then missing on subsequent edits. # To avoid that it has to be added to the instance after the instance # has been saved. Kinda ugly. - self._delete_before_save = to_delete + #self._delete_before_save = to_delete # Call the model instance's clean method. if hasattr(self.instance, 'clean'): From ff225636f038ef2c442d9101a54d3ed44ad5b73b Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 30 Jul 2013 20:22:19 +0200 Subject: [PATCH 092/136] ReferenceFields('my_class') don't run until max recursion depth is reached. --- mongodbforms/documentoptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index db3bcbdb..efdc2a04 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -98,7 +98,8 @@ def _setup_document_fields(self): else: flat.append((choice,value)) f.flatchoices = flat - if isinstance(f, ReferenceField) and not isinstance(f.document_type._meta, DocumentMetaWrapper): + if isinstance(f, ReferenceField) and not isinstance(f.document_type._meta, DocumentMetaWrapper) \ + and self.document != f.document_type: f.document_type._meta = DocumentMetaWrapper(f.document_type) def _init_pk(self): From 34cd9bd883907c946c3f23fef710e0b7a23fa5cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Corte=CC=80s?= Date: Thu, 1 Aug 2013 11:26:15 +0200 Subject: [PATCH 093/136] Fix http://dev.1flow.net/webapps/1flow/group/4093/ --- mongodbforms/fields.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index 74b7abe5..e48a15ca 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -95,11 +95,12 @@ def label_from_instance(self, obj): return smart_unicode(obj) def clean(self, value): - # Check for empty values. - if value in EMPTY_VALUES and self-required: - raise forms.ValidationError(self.error_messages['required']) - elif value in EMPTY_VALUES and not self-required: - return None + # Check for empty values. + if value in EMPTY_VALUES: + if self.required: + raise forms.ValidationError(self.error_messages['required']) + else: + return None oid = super(ReferenceField, self).clean(value) From 26f656bc4e33f812c43ee7c58969edae43c5a077 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Thu, 8 Aug 2013 14:41:27 +0200 Subject: [PATCH 094/136] Data and files argument before instance or position for EmbeddedDocumentForms. Should avoid the problems in #50 --- mongodbforms/documents.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index dc6d9238..5bcf9920 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -530,12 +530,14 @@ def documentform_factory(document, form=DocumentForm, fields=None, exclude=None, class EmbeddedDocumentForm(with_metaclass(DocumentFormMetaclass, BaseDocumentForm)): - - def __init__(self, parent_document, instance=None, position=None, *args, **kwargs): + + def __init__(self, parent_document, data=None, files=None, position=None, *args, **kwargs): if self._meta.embedded_field is not None and not \ self._meta.embedded_field in parent_document._fields: raise FieldError("Parent document must have field %s" % self._meta.embedded_field) + instance = kwargs.get('instance', None) + if isinstance(parent_document._fields.get(self._meta.embedded_field), ListField): # if we received a list position of the instance and no instance # load the instance from the parent document and proceed as normal @@ -549,7 +551,7 @@ def __init__(self, parent_document, instance=None, position=None, *args, **kwarg emb_list = getattr(parent_document, self._meta.embedded_field) position = next((i for i, obj in enumerate(emb_list) if obj == instance), None) - super(EmbeddedDocumentForm, self).__init__(instance=instance, *args, **kwargs) + super(EmbeddedDocumentForm, self).__init__(data=data, files=files, instance=instance, *args, **kwargs) self.parent_document = parent_document self.position = position From 5cb3a5b86b455f6541068b49d9a007100f63c08f Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Thu, 8 Aug 2013 22:41:05 +0200 Subject: [PATCH 095/136] Hopefully fixes for #47 and #48 in one go --- mongodbforms/documents.py | 39 +++++++++++++-------------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 5bcf9920..67dd2105 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -158,20 +158,14 @@ def save_instance(form, instance, fields=None, fail_message='saved', if commit and hasattr(instance, 'save'): # see BaseDocumentForm._post_clean for an explanation - #if hasattr(form, '_delete_before_save'): - # data = instance._data - # new_data = dict([(n, f) for n, f in data.items() if not n in form._delete_before_save]) - # if hasattr(instance, '_changed_fields'): - # for field in form._delete_before_save: - # try: - # instance._changed_fields.remove(field) - # except ValueError: - # pass - # instance._data = new_data - # instance.save() - # instance._data = data - #else: - instance.save() + if len(form._meta._dont_save) > 0: + data = instance._data + new_data = dict([(n, f) for n, f in data.items() if not n in form._meta._dont_save]) + instance._data = new_data + instance.save() + instance._data = data + else: + instance.save() return instance @@ -258,6 +252,8 @@ def __init__(self, options=None): self.embedded_field = getattr(options, 'embedded_field_name', None) self.formfield_generator = getattr(options, 'formfield_generator', _fieldgenerator) + self._dont_save = [] + class DocumentFormMetaclass(type): def __new__(cls, name, bases, attrs): @@ -391,29 +387,20 @@ def _post_clean(self): self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude) exclude = self._get_validation_exclusions() - # Clean the model instance's fields. - #to_delete = [] try: for f in self.instance._fields.values(): value = getattr(self.instance, f.name) if f.name not in exclude: f.validate(value) - #elif value in EMPTY_VALUES: + elif value in EMPTY_VALUES and \ + f.name not in self.instance._changed_fields: # mongoengine chokes on empty strings for fields # that are not required. Clean them up here, though # this is maybe not the right place :-) - #to_delete.append(f.name) + opts._dont_save.append(f.name) except ValidationError as e: err = {f.name: [e.message]} self._update_errors(err) - - # Add to_delete list to instance. It is removed in save instance - # The reason for this is, that the field must be deleted from the - # instance before the instance gets saved. The changed instance gets - # cached and the removed field is then missing on subsequent edits. - # To avoid that it has to be added to the instance after the instance - # has been saved. Kinda ugly. - #self._delete_before_save = to_delete # Call the model instance's clean method. if hasattr(self.instance, 'clean'): From 0c50effce3f5a048f17231e123c407e6aded1afb Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 9 Aug 2013 13:57:09 +0200 Subject: [PATCH 096/136] Handle missing _changed_fields. Fix for #51. --- mongodbforms/documents.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 67dd2105..8d90ea6f 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -383,6 +383,8 @@ def clean(self): def _post_clean(self): opts = self._meta + changed_fields = getattr(self.instance, '_changed_fields', []) + # Update the model instance with self.cleaned_data. self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude) @@ -392,8 +394,7 @@ def _post_clean(self): value = getattr(self.instance, f.name) if f.name not in exclude: f.validate(value) - elif value in EMPTY_VALUES and \ - f.name not in self.instance._changed_fields: + elif value in EMPTY_VALUES and f.name not in changed_fields: # mongoengine chokes on empty strings for fields # that are not required. Clean them up here, though # this is maybe not the right place :-) From 1bba2adbf9ebb8a4bb414ded764c1f3b73804255 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 9 Aug 2013 14:00:25 +0200 Subject: [PATCH 097/136] create instance first then access _changed_fields --- mongodbforms/documents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 8d90ea6f..7466d7ba 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -383,10 +383,10 @@ def clean(self): def _post_clean(self): opts = self._meta - changed_fields = getattr(self.instance, '_changed_fields', []) # Update the model instance with self.cleaned_data. self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude) + changed_fields = getattr(self.instance, '_changed_fields', []) exclude = self._get_validation_exclusions() try: From 1224196beeaec1d6937705fd720f83f79e721089 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 9 Aug 2013 20:14:31 +0200 Subject: [PATCH 098/136] Hrmpf. Close #52. It's getting emerassing... --- mongodbforms/documents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 7466d7ba..9ddae12f 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -524,7 +524,7 @@ def __init__(self, parent_document, data=None, files=None, position=None, *args, self._meta.embedded_field in parent_document._fields: raise FieldError("Parent document must have field %s" % self._meta.embedded_field) - instance = kwargs.get('instance', None) + instance = kwargs.pop('instance', None) if isinstance(parent_document._fields.get(self._meta.embedded_field), ListField): # if we received a list position of the instance and no instance From e1185261fe5895eb8e47cd6dc7eee612842e4ba1 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 16 Aug 2013 18:20:23 +0200 Subject: [PATCH 099/136] Fix for #53 from @Prydie. Cheers --- mongodbforms/documentoptions.py | 13 +++++++++++-- mongodbforms/widgets.py | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index efdc2a04..2268eb16 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -39,10 +39,13 @@ class DocumentMetaWrapper(MutableMapping): Used to store mongoengine's _meta dict to make the document admin as compatible as possible to django's meta class on models. """ + # attributes Django deprecated. Not really sure when to remove them + _deprecated_attrs = {'module_name': 'model_name'} + pk = None pk_name = None _app_label = None - module_name = None + model_name = None _verbose_name = None has_auto_field = False object_name = None @@ -56,6 +59,9 @@ class DocumentMetaWrapper(MutableMapping): concrete_managers = [] def __init__(self, document): + #if isinstance(document._meta, DocumentMetaWrapper): + # return + super(DocumentMetaWrapper, self).__init__() self.document = document @@ -69,7 +75,7 @@ def __init__(self, document): except AttributeError: self.object_name = self.document.__class__.__name__ - self.module_name = self.object_name.lower() + self.model_name = self.object_name.lower() # add the gluey stuff to the document and it's fields to make # everything play nice with Django @@ -216,6 +222,9 @@ def swapped(self): return None def __getattr__(self, name): + if name in self._deprecated_attrs: + return getattr(self, self._deprecated_attrs.get(name)) + try: return self._meta[name] except KeyError: diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index ad39fb2f..f9035602 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -18,6 +18,7 @@ def render(self, name, value, attrs=None): raise TypeError("Value supplied for %s must be a list or tuple." % name) output = [] + value = [] if value is None else value final_attrs = self.build_attrs(attrs) id_ = final_attrs.get('id', None) value.append('') From 053624fd28a6bcdc5731821672552ac66c26948a Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Sat, 31 Aug 2013 15:12:20 +0200 Subject: [PATCH 100/136] Allow passing of a pre initialised contained field into List and MapFields --- mongodbforms/fieldgenerator.py | 8 ++--- mongodbforms/fields.py | 54 +++++++++++++++++++--------------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 4433764f..488429f2 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -117,14 +117,14 @@ 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 is not None: return capfirst(get_verbose_name(field.name)) return '' def get_field_help_text(self, field): if field.help_text: - return field.help_text.capitalize() + return field.help_text else: return '' @@ -337,7 +337,7 @@ def generate_listfield(self, field, **kwargs): map_key = 'listfield' form_field = self.generate(field.field) defaults.update({ - 'field_type': form_field.__class__, + 'contained_field': form_field.__class__, }) form_class = self.form_field_map.get(map_key) defaults.update(self._check_widget(map_key)) @@ -355,7 +355,7 @@ def generate_mapfield(self, field, **kwargs): 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field), 'required': field.required, - 'field_type': form_field.__class__, + 'contained_field': form_field.__class__, } form_class = self.form_field_map.get(map_key) defaults.update(self._check_widget(map_key)) diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index e48a15ca..dddaccba 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -165,13 +165,15 @@ class ListField(forms.Field): widget = ListWidget hidden_widget = forms.MultipleHiddenInput - def __init__(self, field_type, *args, **kwargs): + def __init__(self, contained_field, *args, **kwargs): if 'widget' in kwargs: self.widget = kwargs.pop('widget') - self.field_type = field_type - - contained_widget = self.field_type().widget + if isinstance(contained_field, type): + contained_widget = contained_field().widget + else: + contained_widget = contained_field.widget + if isinstance(contained_widget, type): w_type = contained_widget else: @@ -180,6 +182,11 @@ def __init__(self, field_type, *args, **kwargs): super(ListField, self).__init__(*args, **kwargs) + if isinstance(contained_field, type): + self.contained_field = contained_field(required=self.required) + else: + self.contained_field = contained_field + if not hasattr(self, 'empty_values'): self.empty_values = list(EMPTY_VALUES) @@ -198,17 +205,16 @@ def clean(self, value): else: raise ValidationError(self.error_messages['invalid']) - field = self.field_type(required=self.required) for field_value in value: try: - clean_data.append(field.clean(field_value)) + clean_data.append(self.contained_field.clean(field_value)) except ValidationError as e: # Collect all validation errors in a single list, which we'll # raise at the end of clean(), rather than raising a single # exception for the first error we encounter. errors.extend(e.messages) - if field.required: - field.required = False + if self.contained_field.required: + self.contained_field.required = False if errors: raise ValidationError(errors) @@ -220,9 +226,8 @@ def _has_changed(self, initial, data): if initial is None: initial = ['' for x in range(0, len(data))] - field = self.field_type(required=self.required) for initial, data in zip(initial, data): - if field._has_changed(initial, data): + if self.contained_field._has_changed(initial, data): return True return False @@ -233,13 +238,17 @@ class MapField(forms.Field): } widget = MapWidget - def __init__(self, field_type, max_key_length=None, min_key_length=None, + def __init__(self, contained_field, max_key_length=None, min_key_length=None, key_validators=[], field_kwargs={}, *args, **kwargs): if 'widget' in kwargs: self.widget = kwargs.pop('widget') - contained_widget = field_type().widget + if isinstance(contained_field, type): + contained_widget = contained_field().widget + else: + contained_widget = contained_field.widget + if isinstance(contained_widget, type): w_type = contained_widget else: @@ -248,6 +257,12 @@ def __init__(self, field_type, max_key_length=None, min_key_length=None, super(MapField, self).__init__(*args, **kwargs) + if isinstance(contained_field, type): + field_kwargs['required'] = self.required + self.contained_field = contained_field(**field_kwargs) + else: + self.contained_field = contained_field + self.key_validators = key_validators if min_key_length is not None: self.key_validators.append(MinLengthValidator(int(min_key_length))) @@ -255,10 +270,6 @@ def __init__(self, field_type, max_key_length=None, min_key_length=None, self.key_validators.append(MaxLengthValidator(int(max_key_length))) # type of field used to store the dicts value - self.field_type = field_type - field_kwargs['required'] = self.required - self.field_kwargs = field_kwargs - if not hasattr(self, 'empty_values'): self.empty_values = list(EMPTY_VALUES) @@ -294,15 +305,13 @@ def clean(self, value): raise ValidationError(self.error_messages['invalid']) # sort out required => at least one element must be in there - - data_field = self.field_type(**self.field_kwargs) for key, val in value.items(): # ignore empties. Can they even come up here? if key in self.empty_values and val in self.empty_values: continue try: - val = data_field.clean(val) + val = self.contained_field.clean(val) except ValidationError as e: # Collect all validation errors in a single list, which we'll # raise at the end of clean(), rather than raising a single @@ -319,8 +328,8 @@ def clean(self, value): clean_data[key] = val - if data_field.required: - data_field.required = False + if self.contained_field.required: + self.contained_field.required = False if errors: raise ValidationError(errors) @@ -330,7 +339,6 @@ def clean(self, value): return clean_data def _has_changed(self, initial, data): - field = self.field_type(**self.field_kwargs) for k, v in data.items(): if initial is None: init_val = '' @@ -339,7 +347,7 @@ def _has_changed(self, initial, data): init_val = initial[k] except KeyError: return True - if field._has_changed(init_val, v): + if self.contained_field._has_changed(init_val, v): return True return False From 6724f568617ed075f4ae17884875897e0d57ac64 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Sat, 31 Aug 2013 16:27:28 +0200 Subject: [PATCH 101/136] Stop passing around widget classes and use objects --- mongodbforms/fields.py | 21 ++++++++++++--------- mongodbforms/widgets.py | 23 ++++++++++++----------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index dddaccba..3b5f3636 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -65,8 +65,8 @@ class ReferenceField(forms.ChoiceField): """ def __init__(self, queryset, empty_label="---------", *args, **kwargs): forms.Field.__init__(self, *args, **kwargs) - self.queryset = queryset self.empty_label = empty_label + self.queryset = queryset def _get_queryset(self): return self._queryset.clone() @@ -175,10 +175,8 @@ def __init__(self, contained_field, *args, **kwargs): contained_widget = contained_field.widget if isinstance(contained_widget, type): - w_type = contained_widget - else: - w_type = contained_widget.__class__ - self.widget = self.widget(w_type) + contained_widget = contained_widget() + self.widget = self.widget(contained_widget) super(ListField, self).__init__(*args, **kwargs) @@ -230,6 +228,13 @@ def _has_changed(self, initial, data): if self.contained_field._has_changed(initial, data): return True return False + + def prepare_value(self, value): + value = super(ListField, self).prepare_value(value) + prep_val = [] + for v in value: + prep_val.append(self.contained_field.prepare_value(v)) + return prep_val class MapField(forms.Field): default_error_messages = { @@ -250,10 +255,8 @@ def __init__(self, contained_field, max_key_length=None, min_key_length=None, contained_widget = contained_field.widget if isinstance(contained_widget, type): - w_type = contained_widget - else: - w_type = contained_widget.__class__ - self.widget = self.widget(w_type) + contained_widget = contained_widget() + self.widget = self.widget(contained_widget) super(MapField, self).__init__(*args, **kwargs) diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index f9035602..517590f0 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -6,9 +6,10 @@ from django.forms.util import flatatt class ListWidget(Widget): - def __init__(self, widget_type, attrs=None): - self.widget_type = widget_type - self.widget = widget_type() + def __init__(self, contained_widget, attrs=None): + self.contained_widget = contained_widget + if isinstance(contained_widget, type): + self.contained_widget = self.contained_widget() if self.is_localized: self.widget.is_localized = self.is_localized super(ListWidget, self).__init__(attrs) @@ -25,7 +26,7 @@ def render(self, name, value, attrs=None): for i, widget_value in enumerate(value): if id_: final_attrs = dict(final_attrs, id='%s_%s' % (id_, i)) - output.append(self.widget.render(name + '_%s' % i, widget_value, final_attrs)) + output.append(self.contained_widget.render(name + '_%s' % i, widget_value, final_attrs)) return mark_safe(self.format_output(output)) def id_for_label(self, id_): @@ -35,7 +36,7 @@ def id_for_label(self, id_): return id_ def value_from_datadict(self, data, files, name): - widget = self.widget_type() + widget = self.contained_widget i = 0 ret = [] while (name + '_%s' % i) in data or (name + '_%s' % i) in files: @@ -69,8 +70,8 @@ def _get_media(self): def __deepcopy__(self, memo): obj = super(ListWidget, self).__deepcopy__(memo) - obj.widget = copy.deepcopy(self.widget) - obj.widget_type = copy.deepcopy(self.widget_type) + obj.contained_widget = copy.deepcopy(self.contained_widget) + #obj.widget_type = copy.deepcopy(self.widget_type) return obj class DynamicListWidget(ListWidget): @@ -135,11 +136,12 @@ class MapWidget(Widget): You'll probably want to use this class with MultiValueField. """ - def __init__(self, widget_type, attrs=None): - self.widget_type = widget_type + def __init__(self, contained_widget, attrs=None): self.key_widget = TextInput() self.key_widget.is_localized = self.is_localized - self.data_widget = self.widget_type() + if isinstance(contained_widget, type): + contained_widget = contained_widget() + self.data_widget = contained_widget self.data_widget.is_localized = self.is_localized super(MapWidget, self).__init__(attrs) @@ -209,7 +211,6 @@ def _get_media(self): def __deepcopy__(self, memo): obj = super(MapWidget, self).__deepcopy__(memo) - obj.widget_type = copy.deepcopy(self.widget_type) obj.key_widget = copy.deepcopy(self.key_widget) obj.data_widget = copy.deepcopy(self.data_widget) return obj From a41eb4288cb55b6191ec74090452ec0f0cb4e5de Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Sat, 31 Aug 2013 17:17:02 +0200 Subject: [PATCH 102/136] Handle value == None in prepare_value --- mongodbforms/fields.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index 3b5f3636..a2c4beec 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -230,6 +230,7 @@ def _has_changed(self, initial, data): return False def prepare_value(self, value): + value = [] if value is None else value value = super(ListField, self).prepare_value(value) prep_val = [] for v in value: From efc4d7ab1c5de825edb005ce7b3981a32cee63f7 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Thu, 5 Sep 2013 02:16:38 +0200 Subject: [PATCH 103/136] Proper fieldname generation --- mongodbforms/documentoptions.py | 2 +- mongodbforms/fieldgenerator.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 2268eb16..6fb2e7c8 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -17,7 +17,7 @@ def patch_document(function, instance, bound=True): def create_verbose_name(name): name = get_verbose_name(name) - name.replace('_', ' ') + name = name.replace('_', ' ') return name class PkWrapper(object): diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 488429f2..9950f994 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -15,7 +15,6 @@ from django.utils.encoding import smart_unicode except ImportError: from django.forms.util import smart_unicode -from django.db.models.options import get_verbose_name from django.utils.text import capfirst from mongoengine import ReferenceField as MongoReferenceField, EmbeddedDocumentField as MongoEmbeddedDocumentField, \ @@ -23,6 +22,7 @@ from .fields import MongoCharField, ReferenceField, DocumentMultipleChoiceField, ListField, MapField from .widgets import DynamicListWidget +from .documentoptions import create_verbose_name BLANK_CHOICE_DASH = [("", "---------")] @@ -119,7 +119,7 @@ def get_field_label(self, field): if field.verbose_name: return capfirst(field.verbose_name) if field.name is not None: - return capfirst(get_verbose_name(field.name)) + return capfirst(create_verbose_name(field.name)) return '' def get_field_help_text(self, field): From 147a7c212aaf4a983bcd87e41f5858227015d49c Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 11 Sep 2013 01:04:30 +0200 Subject: [PATCH 104/136] Basic support for a Django style rel attribute on ReferenceFields --- mongodbforms/documentoptions.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 6fb2e7c8..3fcf9f6e 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -19,6 +19,23 @@ def create_verbose_name(name): name = get_verbose_name(name) name = name.replace('_', ' ') return name + +class Relation(object): + # just an empty dict to make it useable with Django + # mongoengine has no notion of this + limit_choices_to = {} + def __init__(self, to): + self._to = to + + @property + def to(self): + if not isinstance(self._to._meta, DocumentMetaWrapper): + self._to._meta = DocumentMetaWrapper(document) + return self._to + + @to.setter + def to(self, value): + self._to = value class PkWrapper(object): def __init__(self, wrapped): @@ -92,7 +109,10 @@ def _setup_document_fields(self): # at least in the admin, probably in more places. if not hasattr(f, 'rel'): # need a bit more for actual reference fields here - f.rel = None + if isinstance(f, ReferenceField): + f.rel = Relation(f.document_type) + else: + f.rel = None if not hasattr(f, 'verbose_name') or f.verbose_name is None: f.verbose_name = capfirst(create_verbose_name(f.name)) if not hasattr(f, 'flatchoices'): From 0516d8e3efb064709d55072ec4b718150311c4b8 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 11 Sep 2013 01:54:22 +0200 Subject: [PATCH 105/136] Support rel attribute for ListField(ReferenceFields()) --- mongodbforms/documentoptions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 3fcf9f6e..71b1dfc9 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -6,7 +6,7 @@ from django.utils.text import capfirst from django.db.models.options import get_verbose_name -from mongoengine.fields import ReferenceField +from mongoengine.fields import ReferenceField, ListField def patch_document(function, instance, bound=True): if bound: @@ -111,6 +111,8 @@ def _setup_document_fields(self): # need a bit more for actual reference fields here if isinstance(f, ReferenceField): f.rel = Relation(f.document_type) + elif isinstance(f, ListField) and isinstance(f.field, ReferenceField): + f.field.rel = Relation(f.field.document_type) else: f.rel = None if not hasattr(f, 'verbose_name') or f.verbose_name is None: From 1af07ae997e65be20abe5e170a3fc0d40f6f4a49 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 13 Sep 2013 22:42:16 +0200 Subject: [PATCH 106/136] Fix one of the stupid copy & paste oversights for #22. --- mongodbforms/documentoptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 71b1dfc9..1c08250e 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -30,7 +30,7 @@ def __init__(self, to): @property def to(self): if not isinstance(self._to._meta, DocumentMetaWrapper): - self._to._meta = DocumentMetaWrapper(document) + self._to._meta = DocumentMetaWrapper(self._to) return self._to @to.setter From 8afa89077aec0a94a0e0d1b498451bf1f02a81a0 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 16 Sep 2013 14:42:02 +0200 Subject: [PATCH 107/136] Get Django compatible error messages from mongoengine ValidationError. For #57 --- mongodbforms/documents.py | 5 +++-- mongodbforms/util.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 9ddae12f..14509348 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -23,7 +23,7 @@ from gridfs import GridFS from .documentoptions import DocumentMetaWrapper -from .util import with_metaclass, load_field_generator +from .util import with_metaclass, load_field_generator, format_mongo_validation_errors _fieldgenerator = load_field_generator() @@ -408,7 +408,8 @@ def _post_clean(self): try: self.instance.clean() except ValidationError as e: - self._update_errors({NON_FIELD_ERRORS: e.messages}) + messages = format_mongo_validation_errors(e) + self._update_errors({NON_FIELD_ERRORS: messages}) # Validate uniqueness if needed. if self._validate_unique: diff --git a/mongodbforms/util.py b/mongodbforms/util.py index 70c2aaa2..866a59a6 100644 --- a/mongodbforms/util.py +++ b/mongodbforms/util.py @@ -1,3 +1,5 @@ +from collections import defaultdict + from django.conf import settings from .documentoptions import DocumentMetaWrapper @@ -50,6 +52,23 @@ def init_document_options(document): def get_document_options(document): return DocumentMetaWrapper(document) +def format_mongo_validation_errors(validation_exception): + """Returns a string listing all errors within a document""" + + def generate_key(value, prefix=''): + if isinstance(value, list): + value = ' '.join([generate_key(k) for k in value]) + if isinstance(value, dict): + value = ' '.join([generate_key(v, k) for k, v in value.iteritems()]) + + results = "%s.%s" % (prefix, value) if prefix else value + return results + + error_dict = defaultdict(list) + for k, v in validation_exception.to_dict().iteritems(): + error_dict[generate_key(v)].append(k) + return ["%s: %s" % (k, v) for k, v in error_dict.iteritems()] + # Taken from six (https://pypi.python.org/pypi/six) # by "Benjamin Peterson " # From 3d099bb89be37443d39d3b69ae1a95b8ff5828b2 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 16 Sep 2013 15:39:24 +0200 Subject: [PATCH 108/136] Hrmpf. More error handling for #57 --- mongodbforms/documents.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 14509348..25da25c0 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -20,6 +20,7 @@ from mongoengine.errors import ValidationError from mongoengine.queryset import OperationError, Q from mongoengine.connection import get_db, DEFAULT_CONNECTION_NAME +from mongoengine.base import NON_FIELD_ERRORS as MONGO_NON_FIELD_ERRORS from gridfs import GridFS from .documentoptions import DocumentMetaWrapper @@ -408,8 +409,11 @@ def _post_clean(self): try: self.instance.clean() except ValidationError as e: - messages = format_mongo_validation_errors(e) - self._update_errors({NON_FIELD_ERRORS: messages}) + if MONGO_NON_FIELD_ERRORS in e.errors: + error = e.errors.get(MONGO_NON_FIELD_ERRORS) + else: + error = e.message + self._update_errors({NON_FIELD_ERRORS: error}) # Validate uniqueness if needed. if self._validate_unique: From 39262c20ef455fea54551a17119d0b2b2c74f208 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 16 Sep 2013 17:11:53 +0200 Subject: [PATCH 109/136] Hmm, hopefully get a useable message for #57 now. --- mongodbforms/documents.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 25da25c0..0d75f4ab 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -413,7 +413,7 @@ def _post_clean(self): error = e.errors.get(MONGO_NON_FIELD_ERRORS) else: error = e.message - self._update_errors({NON_FIELD_ERRORS: error}) + self._update_errors({NON_FIELD_ERRORS: [error, ]}) # Validate uniqueness if needed. if self._validate_unique: @@ -464,8 +464,6 @@ def validate_unique(self): return errors - - def save(self, commit=True): """ Saves this ``form``'s cleaned_data into model instance @@ -487,6 +485,7 @@ def save(self, commit=True): return obj save.alters_data = True + class DocumentForm(with_metaclass(DocumentFormMetaclass, BaseDocumentForm)): pass From 36ce932772a200ff404265385e2ce8da5000b87c Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 25 Sep 2013 18:46:17 +0200 Subject: [PATCH 110/136] Properly call document validation excluding untouched fields --- mongodbforms/documents.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 0d75f4ab..d7c296d9 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -404,16 +404,21 @@ def _post_clean(self): err = {f.name: [e.message]} self._update_errors(err) + # Call the model instance's clean method. - if hasattr(self.instance, 'clean'): - try: - self.instance.clean() - except ValidationError as e: - if MONGO_NON_FIELD_ERRORS in e.errors: - error = e.errors.get(MONGO_NON_FIELD_ERRORS) - else: - error = e.message - self._update_errors({NON_FIELD_ERRORS: [error, ]}) + original_fields = self.instance._fields_ordered + to_check = [f for f in original_fields if f not in exclude] + self.instance._fields_ordered = to_check + try: + self.instance.validate() + except ValidationError as e: + if MONGO_NON_FIELD_ERRORS in e.errors: + error = e.errors.get(MONGO_NON_FIELD_ERRORS) + else: + error = e.message + self._update_errors({NON_FIELD_ERRORS: [error, ]}) + finally: + self.instance._fields_ordered = original_fields # Validate uniqueness if needed. if self._validate_unique: From ec80e4c16063cbc5335d2f4d46eaf1394fb295a6 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 25 Sep 2013 18:48:25 +0200 Subject: [PATCH 111/136] Use a tuple in the last commit instead of a list --- mongodbforms/documents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index d7c296d9..2977f3a4 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -407,7 +407,7 @@ def _post_clean(self): # Call the model instance's clean method. original_fields = self.instance._fields_ordered - to_check = [f for f in original_fields if f not in exclude] + to_check = tuple([f for f in original_fields if f not in exclude]) self.instance._fields_ordered = to_check try: self.instance.validate() From d9bf32cdc1353a8131d069f8b8a4505c22d082c4 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 21 Oct 2013 16:50:25 +0200 Subject: [PATCH 112/136] Html5 widget support; Hopefully got rid of the need for _dont_save stuff on forms --- README.txt | 4 +- mongodbforms/documentoptions.py | 2 +- mongodbforms/documents.py | 31 +++++++++----- mongodbforms/fieldgenerator.py | 72 +++++++++++++++++++++++---------- mongodbforms/widgets.py | 46 +++++---------------- setup.py | 2 +- 6 files changed, 86 insertions(+), 71 deletions(-) diff --git a/README.txt b/README.txt index 6f76ce4f..22b35f64 100644 --- a/README.txt +++ b/README.txt @@ -7,8 +7,8 @@ documents. Requirements ------------ -- Django >= 1.3 -- `mongoengine `__ >= 0.6 +- Django >= 1.4 +- `mongoengine `__ >= 0.8.3 Supported field types --------------------- diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 1c08250e..5efcb277 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -224,7 +224,7 @@ def swapped(self): NOTE: Not sure this is actually usefull for documents. So at the moment it's really only here because the admin wants it. It might prove usefull - for someone though, so it'S more then just a dummy. + for someone though, so it's more then just a dummy. """ if self._meta.get('swappable', False): model_label = '%s.%s' % (self.app_label, self.object_name.lower()) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 2977f3a4..e1fd168d 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -1,9 +1,8 @@ import os import itertools -from collections import Callable +from collections import Callable, OrderedDict from functools import reduce -from django.utils.datastructures import SortedDict from django.forms.forms import BaseForm, get_declared_fields, NON_FIELD_ERRORS, pretty_name from django.forms.widgets import media_property from django.core.exceptions import FieldError @@ -166,8 +165,7 @@ def save_instance(form, instance, fields=None, fail_message='saved', instance.save() instance._data = data else: - instance.save() - + instance.save() return instance def document_to_dict(instance, fields=None, exclude=None): @@ -230,8 +228,15 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, \ if formfield: field_list.append((f.name, formfield)) + + field_dict = OrderedDict(field_list) + if fields: + field_dict = OrderedDict( + [(f, field_dict.get(f)) for f in fields + if ((not exclude) or (exclude and f not in exclude))] + ) - return SortedDict(field_list) + return field_dict @@ -399,16 +404,24 @@ def _post_clean(self): # mongoengine chokes on empty strings for fields # that are not required. Clean them up here, though # this is maybe not the right place :-) - opts._dont_save.append(f.name) + print "Setting %s to None" % f.name + setattr(self.instance, f.name, None) + #opts._dont_save.append(f.name) except ValidationError as e: err = {f.name: [e.message]} self._update_errors(err) - # Call the model instance's clean method. + # Call validate() on the document. Since mongoengine + # does not provide an argument to specify which fields + # should be excluded during validation, we replace + # instance._fields_ordered with a version that does + # not include excluded fields. The attribute gets + # restored after validation. original_fields = self.instance._fields_ordered - to_check = tuple([f for f in original_fields if f not in exclude]) - self.instance._fields_ordered = to_check + self.instance._fields_ordered = tuple( + [f for f in original_fields if f not in exclude] + ) try: self.instance.validate() except ValidationError as e: diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 9950f994..38bc2bf1 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -21,7 +21,7 @@ ListField as MongoListField, MapField as MongoMapField from .fields import MongoCharField, ReferenceField, DocumentMultipleChoiceField, ListField, MapField -from .widgets import DynamicListWidget +from .widgets import Html5SplitDateTimeWidget from .documentoptions import create_verbose_name BLANK_CHOICE_DASH = [("", "---------")] @@ -142,7 +142,7 @@ def get_field_default(self, field): d['initial'] = field.default return f.default - def _check_widget(self, map_key): + def check_widget(self, map_key): if map_key in self.widget_override_map: return {'widget': self.widget_override_map.get(map_key)} else: @@ -176,7 +176,7 @@ def generate_stringfield(self, field, **kwargs): defaults['regex'] = field.regex form_class = self.form_field_map.get(map_key) - defaults.update(self._check_widget(map_key)) + defaults.update(self.check_widget(map_key)) defaults.update(kwargs) return form_class(**defaults) @@ -190,7 +190,7 @@ def generate_emailfield(self, field, **kwargs): 'label': self.get_field_label(field), 'help_text': self.get_field_help_text(field) } - defaults.update(self._check_widget(map_key)) + defaults.update(self.check_widget(map_key)) form_class = self.form_field_map.get(map_key) defaults.update(kwargs) return form_class(**defaults) @@ -206,7 +206,7 @@ def generate_urlfield(self, field, **kwargs): 'help_text': self.get_field_help_text(field) } form_class = self.form_field_map.get(map_key) - defaults.update(self._check_widget(map_key)) + defaults.update(self.check_widget(map_key)) defaults.update(kwargs) return form_class(**defaults) @@ -231,7 +231,7 @@ def generate_intfield(self, field, **kwargs): 'max_value': field.max_value, }) form_class = self.form_field_map.get(map_key) - defaults.update(self._check_widget(map_key)) + defaults.update(self.check_widget(map_key)) defaults.update(kwargs) return form_class(**defaults) @@ -246,7 +246,7 @@ def generate_floatfield(self, field, **kwargs): 'help_text': self.get_field_help_text(field) } form_class = self.form_field_map.get(map_key) - defaults.update(self._check_widget(map_key)) + defaults.update(self.check_widget(map_key)) defaults.update(kwargs) return form_class(**defaults) @@ -262,7 +262,7 @@ def generate_decimalfield(self, field, **kwargs): 'help_text': self.get_field_help_text(field) } form_class = self.form_field_map.get(map_key) - defaults.update(self._check_widget(map_key)) + defaults.update(self.check_widget(map_key)) defaults.update(kwargs) return form_class(**defaults) @@ -283,7 +283,7 @@ def generate_booleanfield(self, field, **kwargs): else: map_key = 'booleanfield' form_class = self.form_field_map.get(map_key) - defaults.update(self._check_widget(map_key)) + defaults.update(self.check_widget(map_key)) defaults.update(kwargs) return form_class(**defaults) @@ -295,7 +295,7 @@ def generate_datetimefield(self, field, **kwargs): 'label': self.get_field_label(field), } form_class = self.form_field_map.get(map_key) - defaults.update(self._check_widget(map_key)) + defaults.update(self.check_widget(map_key)) defaults.update(kwargs) return form_class(**defaults) @@ -308,7 +308,7 @@ def generate_referencefield(self, field, **kwargs): 'queryset': field.document_type.objects.clone(), } form_class = self.form_field_map.get(map_key) - defaults.update(self._check_widget(map_key)) + defaults.update(self.check_widget(map_key)) defaults.update(kwargs) return form_class(**defaults) @@ -340,7 +340,7 @@ def generate_listfield(self, field, **kwargs): 'contained_field': form_field.__class__, }) form_class = self.form_field_map.get(map_key) - defaults.update(self._check_widget(map_key)) + defaults.update(self.check_widget(map_key)) defaults.update(kwargs) return form_class(**defaults) @@ -358,7 +358,7 @@ def generate_mapfield(self, field, **kwargs): 'contained_field': form_field.__class__, } form_class = self.form_field_map.get(map_key) - defaults.update(self._check_widget(map_key)) + defaults.update(self.check_widget(map_key)) defaults.update(kwargs) return form_class(**defaults) @@ -371,7 +371,7 @@ def generate_filefield(self, field, **kwargs): 'help_text': self.get_field_help_text(field) } form_class = self.form_field_map.get(map_key) - defaults.update(self._check_widget(map_key)) + defaults.update(self.check_widget(map_key)) defaults.update(kwargs) return form_class(**defaults) @@ -384,7 +384,7 @@ def generate_imagefield(self, field, **kwargs): 'help_text': self.get_field_help_text(field) } form_class = self.form_field_map.get(map_key) - defaults.update(self._check_widget(map_key)) + defaults.update(self.check_widget(map_key)) defaults.update(kwargs) return form_class(**defaults) @@ -416,10 +416,38 @@ def generate(self, field, **kwargs): defaults.update(kwargs) return forms.CharField(**defaults) - -class DynamicFormFieldGenerator(MongoDefaultFormFieldGenerator): - widget_override_map = { - 'stringfield_long': forms.Textarea, - 'listfield': DynamicListWidget, - } - + +class Html5FormFieldGenerator(MongoDefaultFormFieldGenerator): + def check_widget(self, map_key): + override = super(Html5FormFieldGenerator, self).check_widget(map_key) + if override != {}: + return override + + chunks = map_key.split('field') + kind = chunks[0] + + if kind == 'email': + if hasattr(forms, 'EmailInput'): + return {'widget': forms.EmailInput} + else: + input = forms.TextInput + input.input_type = 'email' + return {'widget': input} + elif kind in ['int', 'float'] and len(chunks) < 2: + if hasattr(forms, 'NumberInput'): + return {'widget': forms.NumberInput} + else: + input = forms.TextInput + input.input_type = 'number' + return {'widget': input} + elif kind == 'url': + if hasattr(forms, 'URLInput'): + return {'widget': forms.URLInput} + else: + input = forms.TextInput + input.input_type = 'url' + return {'widget': input} + elif kind == 'datetime': + return {'widget': Html5SplitDateTimeWidget} + else: + return {} \ No newline at end of file diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index 517590f0..e8f1a69e 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -1,10 +1,19 @@ import copy -from django.forms.widgets import Widget, Media, TextInput +from django.forms.widgets import Widget, Media, TextInput, SplitDateTimeWidget, DateInput, TimeInput, MultiWidget from django.utils.safestring import mark_safe from django.core.validators import EMPTY_VALUES from django.forms.util import flatatt +class Html5SplitDateTimeWidget(SplitDateTimeWidget): + def __init__(self, attrs=None, date_format=None, time_format=None): + date_input = DateInput(attrs=attrs, format=date_format) + date_input.input_type = 'date' + time_input = TimeInput(attrs=attrs, format=time_format) + time_input.input_type = 'time' + widgets = (date_input, time_input) + MultiWidget.__init__(self, widgets, attrs) + class ListWidget(Widget): def __init__(self, contained_widget, attrs=None): self.contained_widget = contained_widget @@ -73,41 +82,6 @@ def __deepcopy__(self, memo): obj.contained_widget = copy.deepcopy(self.contained_widget) #obj.widget_type = copy.deepcopy(self.widget_type) return obj - -class DynamicListWidget(ListWidget): - def __init__(self, widget_type, attrs=None): - class_attr = {'class': 'dynamiclistwidget'} - if attrs is None: - attrs = class_attr - elif 'class' in attrs: - attrs['class'] = '%s %s' % (attrs['class'], class_attr['class']) - else: - attrs.update(class_attr) - super(DynamicListWidget, self).__init__(widget_type, attrs) - - def format_output(self, rendered_widgets): - """ - Given a list of rendered widgets (as strings), returns a Unicode string - representing the HTML for the whole lot. - - This hook allows you to format the HTML design of the widgets, if - needed. - """ - #print(rendered_widgets) - output = [] - for widget in rendered_widgets: - output.append("

%s

" % widget) - #output.append('' % self._name) - return ''.join(output) - - def _get_media(self): - "Media for a multiwidget is the combination of all media of the subwidgets" - media = Media(js=('mongodbforms/dynamiclistwidget.js',)) - for w in self.widgets: - media = media + w.media - return media - media = property(_get_media) - class MapWidget(Widget): """ diff --git a/setup.py b/setup.py index f61e49f5..60d87bd8 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def convert_readme(): return open('README.txt').read() setup(name='mongodbforms', - version='0.2.2', + version='0.3', description="An implementation of django forms using mongoengine.", author='Jan Schrewe', author_email='jan@schafproductions.com', From 7a3f4f025cedf250e118e7629a8f0cf37cb85d1e Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Mon, 21 Oct 2013 16:57:33 +0200 Subject: [PATCH 113/136] Remove debug print statemant --- mongodbforms/documents.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index e1fd168d..63db2702 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -404,7 +404,6 @@ def _post_clean(self): # mongoengine chokes on empty strings for fields # that are not required. Clean them up here, though # this is maybe not the right place :-) - print "Setting %s to None" % f.name setattr(self.instance, f.name, None) #opts._dont_save.append(f.name) except ValidationError as e: From df67df4373eac349944b58f93ea74439d4444c46 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Fri, 25 Oct 2013 00:57:53 +0200 Subject: [PATCH 114/136] PEP8 and a couple new form fields that return None instead of '' --- .gitignore | 1 + mongodbforms/documentoptions.py | 79 +-- mongodbforms/documents.py | 478 ++++++++---------- mongodbforms/fieldgenerator.py | 47 +- mongodbforms/fields.py | 96 ++-- .../static/mongodbforms/dynamiclistwidget.js | 383 -------------- mongodbforms/util.py | 30 +- mongodbforms/widgets.py | 57 ++- 8 files changed, 396 insertions(+), 775 deletions(-) delete mode 100644 mongodbforms/static/mongodbforms/dynamiclistwidget.js diff --git a/.gitignore b/.gitignore index 2a9e5a46..03e61266 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ build/* *.bak .settings/* *.tmproj +.tm_properties diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 5efcb277..5b5bb15f 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -8,6 +8,7 @@ from mongoengine.fields import ReferenceField, ListField + def patch_document(function, instance, bound=True): if bound: method = MethodType(function, instance) @@ -15,50 +16,55 @@ def patch_document(function, instance, bound=True): method = function setattr(instance, function.__name__, method) + def create_verbose_name(name): name = get_verbose_name(name) name = name.replace('_', ' ') return name - + + class Relation(object): # just an empty dict to make it useable with Django # mongoengine has no notion of this limit_choices_to = {} + def __init__(self, to): self._to = to - - @property + + @property def to(self): if not isinstance(self._to._meta, DocumentMetaWrapper): self._to._meta = DocumentMetaWrapper(self._to) return self._to - + @to.setter def to(self, value): self._to = value + class PkWrapper(object): def __init__(self, wrapped): self.obj = wrapped - + def __getattr__(self, attr): if attr in dir(self.obj): return getattr(self.obj, attr) raise AttributeError - + def __setattr__(self, attr, value): if attr != 'obj' and hasattr(self.obj, attr): setattr(self.obj, attr, value) - super(PkWrapper, self).__setattr__(attr, value) - + super(PkWrapper, self).__setattr__(attr, value) + + class DocumentMetaWrapper(MutableMapping): """ Used to store mongoengine's _meta dict to make the document admin - as compatible as possible to django's meta class on models. + as compatible as possible to django's meta class on models. """ # attributes Django deprecated. Not really sure when to remove them _deprecated_attrs = {'module_name': 'model_name'} - + pk = None pk_name = None _app_label = None @@ -74,26 +80,23 @@ class DocumentMetaWrapper(MutableMapping): _meta = None concrete_model = None concrete_managers = [] - + def __init__(self, document): - #if isinstance(document._meta, DocumentMetaWrapper): - # return - super(DocumentMetaWrapper, self).__init__() - + self.document = document # used by Django to distinguish between abstract and concrete models # here for now always the document self.concrete_model = document self._meta = getattr(document, '_meta', {}) - + try: self.object_name = self.document.__name__ except AttributeError: self.object_name = self.document.__class__.__name__ - + self.model_name = self.object_name.lower() - + # add the gluey stuff to the document and it's fields to make # everything play nice with Django self._setup_document_fields() @@ -102,7 +105,7 @@ def __init__(self, document): if 'id_field' in self._meta: self.pk_name = self._meta['id_field'] self._init_pk() - + def _setup_document_fields(self): for f in self.document._fields.values(): # Yay, more glue. Django expects fields to have a couple attributes @@ -111,7 +114,8 @@ def _setup_document_fields(self): # need a bit more for actual reference fields here if isinstance(f, ReferenceField): f.rel = Relation(f.document_type) - elif isinstance(f, ListField) and isinstance(f.field, ReferenceField): + elif isinstance(f, ListField) and \ + isinstance(f.field, ReferenceField): f.field.rel = Relation(f.field.document_type) else: f.rel = None @@ -124,16 +128,17 @@ def _setup_document_fields(self): if isinstance(value, (list, tuple)): flat.extend(value) else: - flat.append((choice,value)) + flat.append((choice, value)) f.flatchoices = flat - if isinstance(f, ReferenceField) and not isinstance(f.document_type._meta, DocumentMetaWrapper) \ - and self.document != f.document_type: + if isinstance(f, ReferenceField) and not \ + isinstance(f.document_type._meta, DocumentMetaWrapper) \ + and self.document != f.document_type: f.document_type._meta = DocumentMetaWrapper(f.document_type) - + def _init_pk(self): """ - Adds a wrapper around the documents pk field. The wrapper object gets the attributes - django expects on the pk field, like name and attname. + Adds a wrapper around the documents pk field. The wrapper object gets + the attributes django expects on the pk field, like name and attname. The function also adds a _get_pk_val method to the document. """ @@ -143,6 +148,7 @@ def _init_pk(self): self.pk.attname = self.pk_name self.document._pk_val = pk_field + def _get_pk_val(self): return self._pk_val patch_document(_get_pk_val, self.document) @@ -159,7 +165,7 @@ def verbose_name(self): """ Returns the verbose name of the document. - Checks the original meta dict first. If it is not found + Checks the original meta dict first. If it is not found then generates a verbose name from from the object name. """ if self._verbose_name is None: @@ -204,8 +210,8 @@ def get_field_by_name(self, name): else: return (field, None, True, False) else: - raise FieldDoesNotExist('%s has no field named %r' - % (self.object_name, name)) + raise FieldDoesNotExist('%s has no field named %r' % + (self.object_name, name)) def get_field(self, name, many_to_many=True): """ @@ -222,9 +228,9 @@ def swapped(self): For historical reasons, model name lookups using get_model() are case insensitive, so we make sure we are case insensitive here. - NOTE: Not sure this is actually usefull for documents. So at the moment - it's really only here because the admin wants it. It might prove usefull - for someone though, so it's more then just a dummy. + NOTE: Not sure this is actually usefull for documents. So at the + moment it's really only here because the admin wants it. It might + prove usefull for someone though, so it's more then just a dummy. """ if self._meta.get('swappable', False): model_label = '%s.%s' % (self.app_label, self.object_name.lower()) @@ -235,11 +241,12 @@ def swapped(self): except ValueError: # setting not in the format app_label.model_name # raising ImproperlyConfigured here causes problems with - # test cleanup code - instead it is raised in get_user_model - # or as part of validation. + # test cleanup code - instead it is raised in + # get_user_model or as part of validation. return swapped_for - if '%s.%s' % (swapped_label, swapped_object.lower()) not in (None, model_label): + if '%s.%s' % (swapped_label, swapped_object.lower()) \ + not in (None, model_label): return swapped_for return None @@ -258,7 +265,7 @@ def __setattr__(self, name, value): else: super(DocumentMetaWrapper, self).__setattr__(name, value) - def __contains__(self,key): + def __contains__(self, key): return key in self._meta def __getitem__(self, key): diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 63db2702..60bd5d80 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -3,16 +3,18 @@ from collections import Callable, OrderedDict from functools import reduce -from django.forms.forms import BaseForm, get_declared_fields, NON_FIELD_ERRORS, pretty_name +from django.forms.forms import (BaseForm, get_declared_fields, + NON_FIELD_ERRORS, pretty_name) from django.forms.widgets import media_property from django.core.exceptions import FieldError from django.core.validators import EMPTY_VALUES from django.forms.util import ErrorList from django.forms.formsets import BaseFormSet, formset_factory from django.utils.translation import ugettext_lazy as _, ugettext -from django.utils.text import capfirst +from django.utils.text import capfirst, get_valid_filename -from mongoengine.fields import ObjectIdField, ListField, ReferenceField, FileField, MapField +from mongoengine.fields import (ObjectIdField, ListField, ReferenceField, + FileField, MapField) try: from mongoengine.base import ValidationError except ImportError: @@ -20,22 +22,27 @@ from mongoengine.queryset import OperationError, Q from mongoengine.connection import get_db, DEFAULT_CONNECTION_NAME from mongoengine.base import NON_FIELD_ERRORS as MONGO_NON_FIELD_ERRORS + from gridfs import GridFS from .documentoptions import DocumentMetaWrapper -from .util import with_metaclass, load_field_generator, format_mongo_validation_errors +from .util import (with_metaclass, load_field_generator, + format_mongo_validation_errors) _fieldgenerator = load_field_generator() -def _get_unique_filename(name, db_alias=DEFAULT_CONNECTION_NAME, collection_name='fs'): + +def _get_unique_filename(name, db_alias=DEFAULT_CONNECTION_NAME, + collection_name='fs'): fs = GridFS(get_db(db_alias), collection_name) - file_root, file_ext = os.path.splitext(name) + file_root, file_ext = os.path.splitext(get_valid_filename(name)) count = itertools.count(1) while fs.exists(filename=name): # file_ext includes the dot. name = os.path.join("%s_%s%s" % (file_root, next(count), file_ext)) return name + def _save_iterator_file(field, instance, uploaded_file, file_data=None): """ Takes care of saving a file for a list field. Returns a Mongoengine @@ -43,7 +50,8 @@ def _save_iterator_file(field, instance, uploaded_file, file_data=None): """ # for a new file we need a new proxy object if file_data is None: - file_data = field.field.get_proxy_obj(key=field.name, instance=instance) + file_data = field.field.get_proxy_obj(key=field.name, + instance=instance) if file_data.instance is None: file_data.instance = instance @@ -54,13 +62,16 @@ def _save_iterator_file(field, instance, uploaded_file, file_data=None): file_data.delete() uploaded_file.seek(0) - filename = _get_unique_filename(uploaded_file.name, field.field.db_alias, field.field.collection_name) - file_data.put(uploaded_file, content_type=uploaded_file.content_type, filename=filename) + filename = _get_unique_filename(uploaded_file.name, field.field.db_alias, + field.field.collection_name) + file_data.put(uploaded_file, content_type=uploaded_file.content_type, + filename=filename) file_data.close() return file_data -def construct_instance(form, instance, fields=None, exclude=None, ignore=None): + +def construct_instance(form, instance, fields=None, exclude=None): """ Constructs and returns a document instance from the bound ``form``'s ``cleaned_data``, but does not save the returned instance to the @@ -84,7 +95,9 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): continue # Defer saving file-type fields until after the other fields, so a # callable upload_to can use the values from other fields. - if isinstance(f, FileField) or (isinstance(f, (MapField, ListField)) and isinstance(f.field, FileField)): + if isinstance(f, FileField) or \ + (isinstance(f, (MapField, ListField)) and + isinstance(f.field, FileField)): file_field_list.append(f) else: setattr(instance, f.name, cleaned_data.get(f.name)) @@ -97,7 +110,8 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): if uploaded_file is None: continue file_data = map_field.get(key, None) - map_field[key] = _save_iterator_file(f, instance, uploaded_file, file_data) + map_field[key] = _save_iterator_file(f, instance, + uploaded_file, file_data) setattr(instance, f.name, map_field) elif isinstance(f, ListField): list_field = getattr(instance, f.name) @@ -109,7 +123,8 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): file_data = list_field[i] except IndexError: file_data = None - file_obj = _save_iterator_file(f, instance, uploaded_file, file_data) + file_obj = _save_iterator_file(f, instance, + uploaded_file, file_data) try: list_field[i] = file_obj except IndexError: @@ -126,8 +141,10 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): # delete first to get the names right if field.grid_id: field.delete() - filename = _get_unique_filename(upload.name, f.db_alias, f.collection_name) - field.put(upload, content_type=upload.content_type, filename=filename) + filename = _get_unique_filename(upload.name, f.db_alias, + f.collection_name) + field.put(upload, content_type=upload.content_type, + filename=filename) setattr(instance, f.name, field) except AttributeError: # file was already uploaded and not changed during edit. @@ -141,7 +158,7 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=None): def save_instance(form, instance, fields=None, fail_message='saved', commit=True, exclude=None, construct=True): """ - Saves bound Form ``form``'s cleaned_data into document instance ``instance``. + Saves bound Form ``form``'s cleaned_data into document ``instance``. If commit=True, then the changes to ``instance`` will be saved to the database. Returns ``instance``. @@ -154,20 +171,23 @@ def save_instance(form, instance, fields=None, fail_message='saved', if form.errors: raise ValueError("The %s could not be %s because the data didn't" - " validate." % (instance.__class__.__name__, fail_message)) + " validate." % (instance.__class__.__name__, + fail_message)) if commit and hasattr(instance, 'save'): # see BaseDocumentForm._post_clean for an explanation - if len(form._meta._dont_save) > 0: - data = instance._data - new_data = dict([(n, f) for n, f in data.items() if not n in form._meta._dont_save]) - instance._data = new_data - instance.save() - instance._data = data - else: - instance.save() + #if len(form._meta._dont_save) > 0: + # data = instance._data + # new_data = dict([(n, f) for n, f in data.items() if not n \ + # in form._meta._dont_save]) + # instance._data = new_data + # instance.save() + # instance._data = data + #else: + 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 @@ -189,8 +209,10 @@ def document_to_dict(instance, fields=None, exclude=None): data[f.name] = getattr(instance, f.name, '') return data -def fields_for_document(document, fields=None, exclude=None, widgets=None, \ - formfield_callback=None, field_generator=_fieldgenerator): + +def fields_for_document(document, fields=None, exclude=None, widgets=None, + formfield_callback=None, + field_generator=_fieldgenerator): """ Returns a ``SortedDict`` containing form fields for the given model. @@ -224,7 +246,7 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, \ if formfield_callback: formfield = formfield_callback(f, **kwargs) else: - formfield = field_generator.generate(f, **kwargs) + formfield = field_generator.generate(f, **kwargs) if formfield: field_list.append((f.name, formfield)) @@ -239,7 +261,6 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, \ return field_dict - class ModelFormOptions(object): def __init__(self, options=None): # document class can be declared with 'document =' or 'model =' @@ -250,13 +271,15 @@ def __init__(self, options=None): self.model = self.document meta = getattr(self.document, '_meta', {}) # set up the document meta wrapper if document meta is a dict - if self.document is not None and not isinstance(meta, DocumentMetaWrapper): + if self.document is not None and \ + not isinstance(meta, DocumentMetaWrapper): self.document._meta = DocumentMetaWrapper(self.document) 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.formfield_generator = getattr(options, 'formfield_generator', _fieldgenerator) + self.formfield_generator = getattr(options, 'formfield_generator', + _fieldgenerator) self._dont_save = [] @@ -265,29 +288,40 @@ class DocumentFormMetaclass(type): def __new__(cls, name, bases, attrs): formfield_callback = attrs.pop('formfield_callback', None) try: - parents = [b for b in bases if issubclass(b, DocumentForm) or issubclass(b, EmbeddedDocumentForm)] + parents = [ + b for b in bases + if issubclass(b, DocumentForm) or + issubclass(b, EmbeddedDocumentForm) + ] except NameError: # We are defining DocumentForm itself. parents = None declared_fields = get_declared_fields(bases, attrs, False) - new_class = super(DocumentFormMetaclass, cls).__new__(cls, name, bases, attrs) + 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 = ModelFormOptions(getattr(new_class, 'Meta', None)) + opts = new_class._meta = ModelFormOptions( + getattr(new_class, 'Meta', None) + ) if opts.document: - formfield_generator = getattr(opts, 'formfield_generator', _fieldgenerator) + formfield_generator = getattr(opts, + 'formfield_generator', + _fieldgenerator) # If a model is defined, extract form fields from it. fields = fields_for_document(opts.document, opts.fields, - opts.exclude, opts.widgets, formfield_callback, formfield_generator) + opts.exclude, opts.widgets, + formfield_callback, + formfield_generator) # make sure opts.fields doesn't specify an invalid field none_document_fields = [k for k, v in fields.items() if not v] - missing_fields = set(none_document_fields) - \ - set(declared_fields.keys()) + 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), @@ -313,7 +347,7 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, if instance is None: if opts.document is None: - raise ValueError('DocumentForm has no document class specified.') + raise ValueError('A document class must be provided.') # if we didn't get an instance, instantiate a new one self.instance = opts.document object_data = {} @@ -329,19 +363,21 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, # It is False by default so overriding self.clean() and failing to call # super will stop validate_unique from being called. self._validate_unique = False - super(BaseDocumentForm, self).__init__(data, files, auto_id, prefix, object_data, - error_class, label_suffix, empty_permitted) + super(BaseDocumentForm, self).__init__(data, files, auto_id, prefix, + object_data, error_class, + label_suffix, empty_permitted) def _update_errors(self, message_dict): for k, v in list(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 + # Remove the invalid data from the cleaned_data dict 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) + self._errors.setdefault(NON_FIELD_ERRORS, + self.error_class()).extend(messages) def _get_validation_exclusions(self): """ @@ -391,7 +427,8 @@ def _post_clean(self): opts = self._meta # Update the model instance with self.cleaned_data. - self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude) + self.instance = construct_instance(self, self.instance, opts.fields, + opts.exclude) changed_fields = getattr(self.instance, '_changed_fields', []) exclude = self._get_validation_exclusions() @@ -410,7 +447,6 @@ def _post_clean(self): err = {f.name: [e.message]} self._update_errors(err) - # Call validate() on the document. Since mongoengine # does not provide an argument to specify which fields # should be excluded during validation, we replace @@ -419,7 +455,7 @@ def _post_clean(self): # restored after validation. original_fields = self.instance._fields_ordered self.instance._fields_ordered = tuple( - [f for f in original_fields if f not in exclude] + [f for f in original_fields if f not in exclude] ) try: self.instance.validate() @@ -455,26 +491,28 @@ def validate_unique(self): u_with_attr = getattr(self.instance, u_with) # handling ListField(ReferenceField()) sucks big time # What we need to do is construct a Q object that - # queries for the pk of every list entry and only accepts - # lists with the same length as the list we have + # queries for the pk of every list entry and only + # accepts lists with the same length as our list if isinstance(u_with_field, ListField) and \ isinstance(u_with_field.field, ReferenceField): - q = reduce(lambda x, y: x & y, [Q(**{u_with: k.pk}) for k in u_with_attr]) + q_list = [Q(**{u_with: k.pk}) for k in u_with_attr] + q = reduce(lambda x, y: x & y, q_list) size_key = '%s__size' % u_with q = q & Q(**{size_key: len(u_with_attr)}) filter_kwargs['q_obj'] = q & filter_kwargs['q_obj'] else: filter_kwargs[u_with] = u_with_attr - qs = self.instance.__class__.objects.no_dereference().filter(**filter_kwargs) - # Exclude the current object from the query if we are editing an - # instance (as opposed to creating a new one) + qs = self.instance.__class__.objects + qs.no_dereference().filter(**filter_kwargs) + # Exclude the current object from the query if we are editing + # an instance (as opposed to creating a new one) if self.instance.pk is not None: qs = qs.filter(pk__ne=self.instance.pk) if qs.count() > 0: - message = _("%(model_name)s with this %(field_label)s already exists.") % { - 'model_name': str(capfirst(self.instance._meta.verbose_name)), - 'field_label': str(pretty_name(f.name)) - } + message = _("%s with this %s already exists.") % ( + str(capfirst(self.instance._meta.verbose_name)), + str(pretty_name(f.name)) + ) err_dict = {f.name: [message]} self._update_errors(err_dict) errors.append(err_dict) @@ -497,7 +535,7 @@ def save(self, commit=True): except (KeyError, AttributeError): fail_message = 'embedded document saved' obj = save_instance(self, self.instance, self._meta.fields, - fail_message, commit, construct=False) + fail_message, commit, construct=False) return obj save.alters_data = True @@ -506,8 +544,9 @@ def save(self, commit=True): class DocumentForm(with_metaclass(DocumentFormMetaclass, BaseDocumentForm)): pass -def documentform_factory(document, form=DocumentForm, fields=None, exclude=None, - formfield_callback=None): + +def documentform_factory(document, form=DocumentForm, fields=None, + exclude=None, formfield_callback=None): # Build up a list of attributes that the Meta object will have. attrs = {'document': document, 'model': document} if fields is not None: @@ -538,29 +577,39 @@ def documentform_factory(document, form=DocumentForm, fields=None, exclude=None, return DocumentFormMetaclass(class_name, (form,), form_class_attrs) -class EmbeddedDocumentForm(with_metaclass(DocumentFormMetaclass, BaseDocumentForm)): - - def __init__(self, parent_document, data=None, files=None, position=None, *args, **kwargs): +class EmbeddedDocumentForm(with_metaclass(DocumentFormMetaclass, + BaseDocumentForm)): + def __init__(self, parent_document, data=None, files=None, position=None, + *args, **kwargs): if self._meta.embedded_field is not None and not \ self._meta.embedded_field in parent_document._fields: - raise FieldError("Parent document must have field %s" % self._meta.embedded_field) + raise FieldError("Parent document must have field %s" % + self._meta.embedded_field) instance = kwargs.pop('instance', None) - if isinstance(parent_document._fields.get(self._meta.embedded_field), ListField): + if isinstance(parent_document._fields.get(self._meta.embedded_field), + ListField): # if we received a list position of the instance and no instance # load the instance from the parent document and proceed as normal if instance is None and position is not None: - instance = getattr(parent_document, self._meta.embedded_field)[position] + instance = getattr(parent_document, + self._meta.embedded_field)[position] - # same as above only the other way around. Note: Mongoengine defines equality - # as having the same data, so if you have 2 objects with the same data the first - # one will be edited. That may or may not be the right one. + # same as above only the other way around. Note: Mongoengine + # defines equality as having the same data, so if you have 2 + # objects with the same data the first one will be edited. That + # may or may not be the right one. if instance is not None and position is None: emb_list = getattr(parent_document, self._meta.embedded_field) - position = next((i for i, obj in enumerate(emb_list) if obj == instance), None) + position = next( + (i for i, obj in enumerate(emb_list) if obj == instance), + None + ) - super(EmbeddedDocumentForm, self).__init__(data=data, files=files, instance=instance, *args, **kwargs) + super(EmbeddedDocumentForm, self).__init__(data=data, files=files, + instance=instance, *args, + **kwargs) self.parent_document = parent_document self.position = position @@ -570,29 +619,38 @@ def save(self, commit=True): embedded is returned as usual. """ if self.errors: - raise ValueError("The %s could not be saved because the data didn't" - " validate." % self.instance.__class__.__name__) + raise ValueError("The %s could not be saved because the data" + "didn't validate." % + self.instance.__class__.__name__) if commit: field = self.parent_document._fields.get(self._meta.embedded_field) if isinstance(field, ListField) and self.position is None: # no position given, simply appending to ListField try: - self.parent_document.update(**{"push__" + self._meta.embedded_field: self.instance}) + self.parent_document.update(**{ + "push__" + self._meta.embedded_field: self.instance + }) except: - raise OperationError("The %s could not be appended." % self.instance.__class__.__name__) + raise OperationError("The %s could not be appended." % + self.instance.__class__.__name__) elif isinstance(field, ListField) and self.position is not None: # updating ListField at given position try: - self.parent_document.update(**{"__".join(("set", self._meta.embedded_field, - str(self.position))): self.instance}) + self.parent_document.update(**{ + "__".join(("set", self._meta.embedded_field, + str(self.position))): self.instance + }) except: - raise OperationError("The %s could not be updated at position " - "%d." % (self.instance.__class__.__name__, self.position)) + raise OperationError("The %s could not be updated at " + "position %d." % + (self.instance.__class__.__name__, + self.position)) else: # not a listfield on parent, treat as an embedded field - setattr(self.parent_document, self._meta.embedded_field, self.instance) - self.parent_document.save() + setattr(self.parent_document, self._meta.embedded_field, + self.instance) + self.parent_document.save() return self.instance @@ -606,7 +664,7 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, self.queryset = queryset self._queryset = self.queryset self.initial = self.construct_initial() - defaults = {'data': data, 'files': files, 'auto_id': auto_id, + defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix, 'initial': self.initial} defaults.update(kwargs) super(BaseDocumentFormSet, self).__init__(**defaults) @@ -617,7 +675,7 @@ def construct_initial(self): for d in self.get_queryset(): initial.append(document_to_dict(d)) except TypeError: - pass + pass return initial def initial_form_count(self): @@ -637,7 +695,7 @@ def save(self, commit=True): """ Saves model instances for every form, adding and changing instances as necessary, and returns the list of instances. - """ + """ saved = [] for form in self.forms: if not form.has_changed() and not form in self.initial_forms: @@ -647,13 +705,12 @@ def save(self, commit=True): try: obj.delete() except AttributeError: - # if it has no delete method it is an - # embedded object. We just don't add to the list - # and it's gone. Cool huh? + # if it has no delete method it is an embedded object. We + # just don't add to the list and it's gone. Cool huh? continue if commit: obj.save() - saved.append(obj) + saved.append(obj) return saved def clean(self): @@ -671,7 +728,8 @@ def validate_unique(self): def get_date_error_message(self, date_check): return ugettext("Please correct the duplicate data for %(field_name)s " - "which must be unique for the %(lookup)s in %(date_field)s.") % { + "which must be unique for the %(lookup)s " + "in %(date_field)s.") % { 'field_name': date_check[2], 'date_field': date_check[3], 'lookup': str(date_check[1]), @@ -680,15 +738,18 @@ def get_date_error_message(self, date_check): def get_form_error(self): return ugettext("Please correct the duplicate values below.") -def documentformset_factory(document, form=DocumentForm, formfield_callback=None, - formset=BaseDocumentFormSet, - extra=1, can_delete=False, can_order=False, - max_num=None, fields=None, exclude=None): + +def documentformset_factory(document, form=DocumentForm, + formfield_callback=None, + formset=BaseDocumentFormSet, + extra=1, can_delete=False, can_order=False, + max_num=None, fields=None, exclude=None): """ Returns a FormSet class for the given Django model class. """ - form = documentform_factory(document, form=form, fields=fields, exclude=exclude, - formfield_callback=formfield_callback) + form = documentform_factory(document, form=form, fields=fields, + exclude=exclude, + formfield_callback=formfield_callback) FormSet = formset_factory(form, formset, extra=extra, max_num=max_num, can_order=can_order, can_delete=can_delete) FormSet.model = document @@ -707,7 +768,10 @@ def __init__(self, data=None, files=None, instance=None, self.instance = instance self.save_as_new = save_as_new - super(BaseInlineDocumentFormSet, self).__init__(data, files, prefix=prefix, queryset=queryset, **kwargs) + super(BaseInlineDocumentFormSet, self).__init__(data, files, + prefix=prefix, + queryset=queryset, + **kwargs) def initial_form_count(self): if self.save_as_new: @@ -719,7 +783,6 @@ def get_default_prefix(cls): return cls.document.__name__.lower() get_default_prefix = classmethod(get_default_prefix) - def add_fields(self, form, index): super(BaseInlineDocumentFormSet, self).add_fields(form, index) @@ -731,15 +794,19 @@ def add_fields(self, form, index): #form._meta.fields.append(self.fk.name) def get_unique_error_message(self, unique_check): - unique_check = [field for field in unique_check if field != self.fk.name] - return super(BaseInlineDocumentFormSet, self).get_unique_error_message(unique_check) + unique_check = [ + field for field in unique_check if field != self.fk.name + ] + return super(BaseInlineDocumentFormSet, self).get_unique_error_message( + unique_check + ) def inlineformset_factory(document, form=DocumentForm, formset=BaseInlineDocumentFormSet, fields=None, exclude=None, - extra=1, can_order=False, can_delete=True, max_num=None, - formfield_callback=None): + extra=1, can_order=False, can_delete=True, + max_num=None, formfield_callback=None): """ Returns an ``InlineFormSet`` for the given kwargs. @@ -761,179 +828,28 @@ def inlineformset_factory(document, form=DocumentForm, return FormSet -#class BaseInlineDocumentFormSet(BaseDocumentFormSet): -# """A formset for child objects related to a parent.""" -# def __init__(self, data=None, files=None, instance=None, -# save_as_new=False, prefix=None, queryset=None, **kwargs): -# if instance is None: -# self.instance = self.rel_field.name -# else: -# self.instance = instance -# self.save_as_new = save_as_new -# if queryset is None: -# queryset = self.model._default_manager -# if self.instance.pk: -# qs = queryset.filter(**{self.fk.name: self.instance}) -# else: -# qs = queryset.none() -# super(BaseInlineDocumentFormSet, self).__init__(data, files, prefix=prefix, -# queryset=qs, **kwargs) -# -# def initial_form_count(self): -# if self.save_as_new: -# return 0 -# return super(BaseInlineDocumentFormSet, self).initial_form_count() -# -# -# def _construct_form(self, i, **kwargs): -# form = super(BaseInlineDocumentFormSet, self)._construct_form(i, **kwargs) -# if self.save_as_new: -# # Remove the primary key from the form's data, we are only -# # creating new instances -# form.data[form.add_prefix(self._pk_field.name)] = None -# -# # Remove the foreign key from the form's data -# form.data[form.add_prefix(self.fk.name)] = None -# -# # Set the fk value here so that the form can do its validation. -# setattr(form.instance, self.fk.get_attname(), self.instance.pk) -# return form -# -# @classmethod -# def get_default_prefix(cls): -# from django.db.models.fields.related import RelatedObject -# return RelatedObject(cls.fk.rel.to, cls.model, cls.fk).get_accessor_name().replace('+','') -# -# def save_new(self, form, commit=True): -# # Use commit=False so we can assign the parent key afterwards, then -# # save the object. -# obj = form.save(commit=False) -# pk_value = getattr(self.instance, self.fk.rel.field_name) -# setattr(obj, self.fk.get_attname(), getattr(pk_value, 'pk', pk_value)) -# if commit: -# obj.save() -# # form.save_m2m() can be called via the formset later on if commit=False -# if commit and hasattr(form, 'save_m2m'): -# form.save_m2m() -# return obj -# -# def add_fields(self, form, index): -# super(BaseInlineDocumentFormSet, self).add_fields(form, index) -# if self._pk_field == self.fk: -# name = self._pk_field.name -# kwargs = {'pk_field': True} -# else: -# # The foreign key field might not be on the form, so we poke at the -# # Model field to get the label, since we need that for error messages. -# name = self.fk.name -# kwargs = { -# 'label': getattr(form.fields.get(name), 'label', capfirst(self.fk.verbose_name)) -# } -# if self.fk.rel.field_name != self.fk.rel.to._meta.pk.name: -# kwargs['to_field'] = self.fk.rel.field_name -# -# form.fields[name] = InlineForeignKeyField(self.instance, **kwargs) -# -# # Add the generated field to form._meta.fields if it's defined to make -# # sure validation isn't skipped on that field. -# if form._meta.fields: -# if isinstance(form._meta.fields, tuple): -# form._meta.fields = list(form._meta.fields) -# form._meta.fields.append(self.fk.name) -# -# def get_unique_error_message(self, unique_check): -# unique_check = [field for field in unique_check if field != self.fk.name] -# return super(BaseInlineDocumentFormSet, self).get_unique_error_message(unique_check) -# -# -#def _get_rel_field(parent_document, model, rel_name=None, can_fail=False): -# """ -# Finds and returns the ForeignKey from model to parent if there is one -# (returns None if can_fail is True and no such field exists). If fk_name is -# provided, assume it is the name of the ForeignKey field. Unles can_fail is -# True, an exception is raised if there is no ForeignKey from model to -# parent_document. -# """ -# #opts = model._meta -# fields = model._fields -# if rel_name: -# if rel_name not in fields: -# raise Exception("%s has no field named '%s'" % (model, rel_name)) -# -# rel_model = getattr(model, rel_name, None) -# ref_field = fields.get(rel_name) -# if not isinstance(ref_field, ReferenceField) or \ -# rel_model != parent_document: -# raise Exception("rel_name '%s' is not a reference to %s" % (rel_name, parent_document)) -# else: -# # Try to discover what the ForeignKey from model to parent_document is -# rel_to_parent = [ -# f for f in fields -# if isinstance(f, ReferenceField) -# and getattr(model, f.name) == parent_document -# ] -# if len(rel_to_parent) == 1: -# ref_field = rel_to_parent[0] -# elif len(rel_to_parent) == 0: -# if can_fail: -# return -# raise Exception("%s has no relation to %s" % (model, parent_document)) -# else: -# raise Exception("%s has more than 1 relation to %s" % (model, parent_document)) -# return ref_field -# -# -#def inlineformset_factory(parent_document, model, form=ModelForm, -# formset=BaseInlineFormSet, fk_name=None, -# fields=None, exclude=None, extra=3, can_order=False, -# can_delete=True, max_num=None, formfield_callback=None, -# widgets=None, validate_max=False, localized_fields=None): -# """ -# Returns an ``InlineFormSet`` for the given kwargs. -# -# You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` -# to ``parent_document``. -# """ -# rel_field = _get_rel_field(parent_document, model, fk_name=fk_name) -# # You can't have more then reference in a ReferenceField -# # so max_num is always one for now (maybe -# # ListFields(ReferenceFields) will be supported one day). -# max_num = 1 -# kwargs = { -# 'form': form, -# 'formfield_callback': formfield_callback, -# 'formset': formset, -# 'extra': extra, -# 'can_delete': can_delete, -# 'can_order': can_order, -# 'fields': fields, -# 'exclude': exclude, -# 'max_num': max_num, -# 'widgets': widgets, -# 'validate_max': validate_max, -# 'localized_fields': localized_fields, -# } -# FormSet = documentformset_factory(model, **kwargs) -# FormSet.rel_field = rel_field -# return FormSet - class EmbeddedDocumentFormSet(BaseDocumentFormSet): - def __init__(self, data=None, files=None, save_as_new=False, + def __init__(self, data=None, files=None, save_as_new=False, prefix=None, queryset=[], parent_document=None, **kwargs): self.parent_document = parent_document - super(EmbeddedDocumentFormSet, self).__init__(data, files, save_as_new, prefix, queryset, **kwargs) + + super(EmbeddedDocumentFormSet, self).__init__(data, files, save_as_new, + prefix, queryset, + **kwargs) def _construct_form(self, i, **kwargs): defaults = {'parent_document': self.parent_document} # add position argument to the form. Otherwise we will spend # a huge amount of time iterating over the list field on form __init__ - emb_list = getattr(self.parent_document, self.form._meta.embedded_field) + emb_list = getattr(self.parent_document, + self.form._meta.embedded_field) if emb_list is not None and len(emb_list) < i: defaults['position'] = i defaults.update(kwargs) - form = super(EmbeddedDocumentFormSet, self)._construct_form(i, **defaults) + form = super(EmbeddedDocumentFormSet, self)._construct_form( + i, **defaults) return form @property @@ -951,35 +867,37 @@ def save(self, commit=True): # Don't try to save the new documents. Embedded objects don't have # a save method anyway. objs = super(EmbeddedDocumentFormSet, self).save(commit=False) + objs = objs or [] if commit and self.parent_document is not None: form = self.empty_form - # The thing about formsets is that the base use case is to edit *all* - # of the associated objects on a model. As written, using these FormSets this - # way will cause the existing embedded documents to get saved along with a - # copy of themselves plus any new ones you added. + # The thing about formsets is that the base use case is to edit + # *all* of the associated objects on a model. As written, using + # these FormSets this way will cause the existing embedded + # documents to get saved along with a copy of themselves plus any + # new ones you added. # - # The only way you could do "updates" of existing embedded document fields is - # if those embedded documents had ObjectIDs of their own, which they don't - # by default in Mongoengine. + # The only way you could do "updates" of existing embedded document + # fields is if those embedded documents had ObjectIDs of their own, + # which they don't by default in Mongoengine. # - # In this case it makes the most sense to simply replace the embedded field - # with the new values gathered form the formset, rather than adding the new - # values to the existing values, because the new values will almost always - # contain the old values (with the default use case.) - # - # attr_data = getattr(self.parent_document, form._meta.embedded_field, []) - setattr(self.parent_document, form._meta.embedded_field, objs or []) + # In this case it makes the most sense to simply replace the + # embedded field with the new values gathered form the formset, + # rather than adding the new values to the existing values, because + # the new values will almost always contain the old values (with + # the default use case.) + setattr(self.parent_document, form._meta.embedded_field, objs) self.parent_document.save() - return objs + return objs -def embeddedformset_factory(document, parent_document, form=EmbeddedDocumentForm, - formset=EmbeddedDocumentFormSet, - fields=None, exclude=None, - extra=1, can_order=False, can_delete=True, max_num=None, - formfield_callback=None): +def embeddedformset_factory(document, parent_document, + form=EmbeddedDocumentForm, + formset=EmbeddedDocumentFormSet, + fields=None, exclude=None, + extra=1, can_order=False, can_delete=True, + max_num=None, formfield_callback=None): """ Returns an ``InlineFormSet`` for the given kwargs. diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 38bc2bf1..82e4cfcc 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -17,15 +17,21 @@ from django.forms.util import smart_unicode from django.utils.text import capfirst -from mongoengine import ReferenceField as MongoReferenceField, EmbeddedDocumentField as MongoEmbeddedDocumentField, \ - ListField as MongoListField, MapField as MongoMapField - -from .fields import MongoCharField, ReferenceField, DocumentMultipleChoiceField, ListField, MapField +from mongoengine import (ReferenceField as MongoReferenceField, + EmbeddedDocumentField as MongoEmbeddedDocumentField, + ListField as MongoListField, + MapField as MongoMapField) + +from mongodbforms.fields import (MongoCharField, MongoEmailField, + MongoURLField, ReferenceField, + DocumentMultipleChoiceField, ListField, + MapField) from .widgets import Html5SplitDateTimeWidget from .documentoptions import create_verbose_name BLANK_CHOICE_DASH = [("", "---------")] + class MongoFormFieldGenerator(object): """This class generates Django form-fields for mongoengine-fields.""" @@ -40,8 +46,8 @@ class MongoFormFieldGenerator(object): 'stringfield': MongoCharField, 'stringfield_choices': forms.TypedChoiceField, 'stringfield_long': MongoCharField, - 'emailfield': forms.EmailField, - 'urlfield': forms.URLField, + 'emailfield': MongoEmailField, + 'urlfield': MongoURLField, 'intfield': forms.IntegerField, 'intfield_choices': forms.TypedChoiceField, 'floatfield': forms.FloatField, @@ -90,10 +96,11 @@ def generate(self, field, **kwargs): return getattr(self, attr_name)(field, **kwargs) if cls_name in self.form_field_map: - return getattr(self, self.generator_map.get(cls_name))(field, **kwargs) + attr = self.generator_map.get(cls_name) + return getattr(self, attr)(field, **kwargs) - raise NotImplementedError('%s is not supported by MongoForm' % \ - field.__class__.__name__) + raise NotImplementedError('%s is not supported by MongoForm' % + field.__class__.__name__) def get_field_choices(self, field, include_blank=True, blank_choice=BLANK_CHOICE_DASH): @@ -203,7 +210,7 @@ def generate_urlfield(self, field, **kwargs): 'max_length': field.max_length, 'initial': self.get_field_default(field), 'label': self.get_field_label(field), - 'help_text': self.get_field_help_text(field) + 'help_text': self.get_field_help_text(field) } form_class = self.form_field_map.get(map_key) defaults.update(self.check_widget(map_key)) @@ -313,7 +320,8 @@ def generate_referencefield(self, field, **kwargs): return form_class(**defaults) def generate_listfield(self, field, **kwargs): - # we can't really handle embedded documents here. So we just ignore them + # We can't really handle embedded documents here. + # So we just ignore them if isinstance(field.field, MongoEmbeddedDocumentField): return @@ -345,7 +353,8 @@ def generate_listfield(self, field, **kwargs): return form_class(**defaults) def generate_mapfield(self, field, **kwargs): - # we can't really handle embedded documents here. So we just ignore them + # We can't really handle embedded documents here. + # So we just ignore them if isinstance(field.field, MongoEmbeddedDocumentField): return @@ -365,8 +374,8 @@ def generate_mapfield(self, field, **kwargs): def generate_filefield(self, field, **kwargs): map_key = 'filefield' defaults = { - 'required':field.required, - 'label':self.get_field_label(field), + 'required': field.required, + 'label': self.get_field_label(field), 'initial': self.get_field_default(field), 'help_text': self.get_field_help_text(field) } @@ -378,8 +387,8 @@ def generate_filefield(self, field, **kwargs): def generate_imagefield(self, field, **kwargs): map_key = 'imagefield' defaults = { - 'required':field.required, - 'label':self.get_field_label(field), + 'required': field.required, + 'label': self.get_field_label(field), 'initial': self.get_field_default(field), 'help_text': self.get_field_help_text(field) } @@ -398,7 +407,8 @@ def generate(self, field, **kwargs): can be found. """ try: - return super(MongoDefaultFormFieldGenerator, self).generate(field, **kwargs) + sup = super(MongoDefaultFormFieldGenerator, self) + return sup.generate(field, **kwargs) except NotImplementedError: # a normal charfield is always a good guess # for a widget. @@ -417,6 +427,7 @@ def generate(self, field, **kwargs): defaults.update(kwargs) return forms.CharField(**defaults) + class Html5FormFieldGenerator(MongoDefaultFormFieldGenerator): def check_widget(self, map_key): override = super(Html5FormFieldGenerator, self).check_widget(map_key) @@ -450,4 +461,4 @@ def check_widget(self, map_key): elif kind == 'datetime': return {'widget': Html5SplitDateTimeWidget} else: - return {} \ No newline at end of file + return {} diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index a2c4beec..cd24dcee 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -7,7 +7,8 @@ import copy from django import forms -from django.core.validators import EMPTY_VALUES, MinLengthValidator, MaxLengthValidator +from django.core.validators import (EMPTY_VALUES, MinLengthValidator, + MaxLengthValidator) try: from django.utils.encoding import force_text as force_unicode @@ -33,7 +34,8 @@ from pymongo.objectid import ObjectId from pymongo.errors import InvalidId -from .widgets import ListWidget, MapWidget +from mongodbforms.widgets import ListWidget, MapWidget + class MongoChoiceIterator(object): def __init__(self, field): @@ -51,17 +53,43 @@ def __len__(self): return len(self.queryset) def choice(self, obj): - return (self.field.prepare_value(obj), self.field.label_from_instance(obj)) + return (self.field.prepare_value(obj), + self.field.label_from_instance(obj)) + -class MongoCharField(forms.CharField): +class NormalizeValueMixin(object): + """ + mongoengine doesn't treat fields that return an empty string + as empty. This mixins can be used to create fields that return + None instead of an empty string. + """ def to_python(self, value): + value = super(NormalizeValueMixin, self).to_python(value) if value in EMPTY_VALUES: return None - return smart_unicode(value) + return value + + +class MongoCharField(NormalizeValueMixin, forms.CharField): + pass + + +class MongoEmailField(NormalizeValueMixin, forms.EmailField): + pass + + +class MongoSlugField(NormalizeValueMixin, forms.SlugField): + pass + + +class MongoURLField(NormalizeValueMixin, forms.URLField): + pass + 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="---------", *args, **kwargs): forms.Field.__init__(self, *args, **kwargs) @@ -89,8 +117,9 @@ def _get_choices(self): def label_from_instance(self, obj): """ This method is used to convert objects into strings; it's used to - generate the labels for the choices presented by this object. Subclasses - can override this method to customize the display of the choices. + generate the labels for the choices presented by this object. + Subclasses can override this method to customize the display of + the choices. """ return smart_unicode(obj) @@ -107,15 +136,18 @@ def clean(self, value): try: obj = self.queryset.get(pk=oid) except (TypeError, InvalidId, self.queryset._document.DoesNotExist): - raise forms.ValidationError(self.error_messages['invalid_choice'] % {'value':value}) + raise forms.ValidationError( + self.error_messages['invalid_choice'] % {'value': value} + ) return obj def __deepcopy__(self, memo): result = super(forms.ChoiceField, self).__deepcopy__(memo) - result.queryset = self.queryset # self.queryset calls clone() + result.queryset = self.queryset # self.queryset calls clone() result.empty_label = copy.deepcopy(self.empty_label) return result + class DocumentMultipleChoiceField(ReferenceField): """A MultipleChoiceField whose choices are a model QuerySet.""" widget = forms.SelectMultiple @@ -128,7 +160,9 @@ class DocumentMultipleChoiceField(ReferenceField): } def __init__(self, queryset, *args, **kwargs): - super(DocumentMultipleChoiceField, self).__init__(queryset, empty_label=None, *args, **kwargs) + super(DocumentMultipleChoiceField, self).__init__( + queryset, empty_label=None, *args, **kwargs + ) def clean(self, value): if self.required and not value: @@ -142,11 +176,15 @@ def clean(self, value): try: qs = qs.filter(pk__in=value) except ValidationError: - raise forms.ValidationError(self.error_messages['invalid_pk_value'] % str(value)) + raise forms.ValidationError( + self.error_messages['invalid_pk_value'] % str(value) + ) pks = set([force_unicode(getattr(o, 'pk')) for o in qs]) for val in value: if force_unicode(val) not in pks: - raise forms.ValidationError(self.error_messages['invalid_choice'] % val) + raise forms.ValidationError( + self.error_messages['invalid_choice'] % val + ) # Since this overrides the inherited ModelChoiceField.clean # we run custom validators here self.run_validators(value) @@ -154,7 +192,8 @@ def clean(self, value): def prepare_value(self, value): if hasattr(value, '__iter__') and not hasattr(value, '_meta'): - return [super(DocumentMultipleChoiceField, self).prepare_value(v) for v in value] + sup = super(DocumentMultipleChoiceField, self) + return [sup.prepare_value(v) for v in value] return super(DocumentMultipleChoiceField, self).prepare_value(value) @@ -195,7 +234,9 @@ def clean(self, value): clean_data = [] errors = ErrorList() if not value or isinstance(value, (list, tuple)): - if not value or not [v for v in value if v not in self.empty_values]: + if not value or not [ + v for v in value if v not in self.empty_values + ]: if self.required: raise ValidationError(self.error_messages['required']) else: @@ -237,6 +278,7 @@ def prepare_value(self, value): prep_val.append(self.contained_field.prepare_value(v)) return prep_val + class MapField(forms.Field): default_error_messages = { 'invalid': _('Enter a list of values.'), @@ -244,8 +286,9 @@ class MapField(forms.Field): } widget = MapWidget - def __init__(self, contained_field, max_key_length=None, min_key_length=None, - key_validators=[], field_kwargs={}, *args, **kwargs): + def __init__(self, contained_field, max_key_length=None, + min_key_length=None, key_validators=[], field_kwargs={}, + *args, **kwargs): if 'widget' in kwargs: self.widget = kwargs.pop('widget') @@ -279,7 +322,8 @@ def __init__(self, contained_field, max_key_length=None, min_key_length=None, def _validate_key(self, key): if key in self.empty_values and self.required: - raise ValidationError(self.error_messages['key_required'], code='key_required') + raise ValidationError(self.error_messages['key_required'], + code='key_required') errors = [] for v in self.key_validators: try: @@ -300,7 +344,9 @@ def clean(self, value): clean_data = {} errors = ErrorList() if not value or isinstance(value, dict): - if not value or not [v for v in value.values() if v not in self.empty_values]: + if not value or not [ + v for v in value.values() if v not in self.empty_values + ]: if self.required: raise ValidationError(self.error_messages['required']) else: @@ -354,15 +400,3 @@ def _has_changed(self, initial, data): if self.contained_field._has_changed(init_val, v): return True return False - - - - - - - - - - - - diff --git a/mongodbforms/static/mongodbforms/dynamiclistwidget.js b/mongodbforms/static/mongodbforms/dynamiclistwidget.js deleted file mode 100644 index 55210435..00000000 --- a/mongodbforms/static/mongodbforms/dynamiclistwidget.js +++ /dev/null @@ -1,383 +0,0 @@ -(function(context) { - window.mdbf = {} - /* by @k33g_org */ - var root = this; - - (function(speculoos) { - - var Class = (function() { - function Class(definition) { - /* from CoffeeScript */ - var __hasProp = Object.prototype.hasOwnProperty, m, F; - - this["extends"] = function(child, parent) { - for (m in parent) { - if (__hasProp.call(parent, m)) - child[m] = parent[m]; - } - function ctor() { - this.constructor = child; - } - ctor.prototype = parent.prototype; - child.prototype = new ctor; - child.__super__ = child["super"] = parent.prototype; - return child; - } - - if (definition.constructor.name === "Object") { - F = function() { - }; - } else { - F = definition.constructor; - } - - /* inheritance */ - if (definition["extends"]) { - this["extends"](F, definition["extends"]) - } - for (m in definition) { - if (m != 'constructor' && m != 'extends') { - if (m[0] != '$') { - F.prototype[m] = definition[m]; - } else { /* static members */ - F[m.split('$')[1]] = definition[m]; - } - } - } - - return F; - } - return Class; - })(); - - speculoos.Class = Class; - - })(mdbf); - - /* - * ! onDomReady.js 1.2 (c) 2012 Tubal Martin - MIT license - */ - !function(definition) { - if (typeof define === "function" && define.amd) { - // Register as an AMD module. - define(definition); - } else { - // Browser globals - window.mdbf.onDomReady = definition(); - } - } - (function() { - - 'use strict'; - - var win = window, doc = win.document, docElem = doc.documentElement, - - FALSE = false, COMPLETE = "complete", READYSTATE = "readyState", ATTACHEVENT = "attachEvent", ADDEVENTLISTENER = "addEventListener", DOMCONTENTLOADED = "DOMContentLoaded", ONREADYSTATECHANGE = "onreadystatechange", - - // W3C Event model - w3c = ADDEVENTLISTENER in doc, top = FALSE, - - // isReady: Is the DOM ready to be used? Set to true once it - // occurs. - isReady = FALSE, - - // Callbacks pending execution until DOM is ready - callbacks = []; - - // Handle when the DOM is ready - function ready(fn) { - if (!isReady) { - - // Make sure body exists, at least, in case IE gets a - // little overzealous (ticket #5443). - if (!doc.body) { - return defer(ready); - } - - // Remember that the DOM is ready - isReady = true; - - // Execute all callbacks - while (fn = callbacks.shift()) { - defer(fn); - } - } - } - - // The document ready event handler - function DOMContentLoadedHandler() { - if (w3c) { - doc.removeEventListener(DOMCONTENTLOADED, - DOMContentLoadedHandler, FALSE); - ready(); - } else if (doc[READYSTATE] === COMPLETE) { - // we're here because readyState === "complete" in oldIE - // which is good enough for us to call the dom ready! - doc.detachEvent(ONREADYSTATECHANGE, - DOMContentLoadedHandler); - ready(); - } - } - - // Defers a function, scheduling it to run after the current - // call stack has cleared. - function defer(fn, wait) { - // Allow 0 to be passed - setTimeout(fn, +wait >= 0 ? wait : 1); - } - - // Attach the listeners: - - // Catch cases where onDomReady is called after the browser - // event has already occurred. - // we once tried to use readyState "interactive" here, but it - // caused issues like the one - // discovered by ChrisS here: - // http://bugs.jquery.com/ticket/12282#comment:15 - if (doc[READYSTATE] === COMPLETE) { - // Handle it asynchronously to allow scripts the opportunity - // to delay ready - defer(ready); - - // Standards-based browsers support DOMContentLoaded - } else if (w3c) { - // Use the handy event callback - doc[ADDEVENTLISTENER](DOMCONTENTLOADED, - DOMContentLoadedHandler, FALSE); - - // A fallback to window.onload, that will always work - win[ADDEVENTLISTENER]("load", ready, FALSE); - - // If IE event model is used - } else { - // ensure firing before onload, - // maybe late but safe also for iframes - doc[ATTACHEVENT](ONREADYSTATECHANGE, - DOMContentLoadedHandler); - - // A fallback to window.onload, that will always work - win[ATTACHEVENT]("onload", ready); - - // If IE and not a frame - // continually check to see if the document is ready - try { - top = win.frameElement == null && docElem; - } catch (e) { - } - - if (top && top.doScroll) { - (function doScrollCheck() { - if (!isReady) { - try { - // Use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - top.doScroll("left"); - } catch (e) { - return defer(doScrollCheck, 50); - } - - // and execute any waiting functions - ready(); - } - })(); - } - } - - function onDomReady(fn) { - // If DOM is ready, execute the function (async), otherwise - // wait - isReady ? defer(fn) : callbacks.push(fn); - } - - // Add version - onDomReady.version = "1.2"; - - return onDomReady; - }); - /* - * nut, the concise CSS selector engine - * - * Version : 0.2.0 Author : Aurélien Delogu (dev@dreamysource.fr) Homepage : - * https://github.com/pyrsmk/nut License : MIT - */ - - window.mdbf.nut = function() { - - var doc = document, firstChild = 'firstChild', nextSibling = 'nextSibling', getElementsByClassName = 'getElementsByClassName', length = 'length', - - /* - * Get id node - * - * Parameters String selector : one selector Object context : one - * context - * - * Return Array : nodes - */ - getNodesFromIdSelector = function(selector, context) { - var node = doc.getElementById(selector); - if (!node) { - return []; - } else { - return [ node ]; - } - }, - - /* - * Get nodes corresponding to one class name (for IE<9) - * - * Parameters String selector : one selector Object context : one - * context - * - * Return Array : nodes - */ - getNodesByClassName = function(name, context) { - // Init vars - var node = context[firstChild], nodes = [], elements; - // Browse children - if (node) { - do { - if (node.nodeType == 1) { - // Match the class - if (node.className - && node.className.match('\\b' + name + '\\b')) { - nodes.push(node); - } - // Get nodes from node's children - if ((elements = getNodesByClassName(name, node))[length]) { - nodes = nodes.concat(elements); - } - } - } while (node = node[nextSibling]); - } - return nodes; - }, - - /* - * Get nodes from a class selector - * - * Parameters String selector : one selector Object context : one - * context - * - * Return Array : nodes - */ - getNodesFromClassSelector = function(selector, context) { - if (context[getElementsByClassName]) { - return context[getElementsByClassName](selector); - } else { - return getNodesByClassName(selector, context); - } - }, - - /* - * Get nodes from a tag selector - * - * Parameters String selector : one selector Object context : one - * context - * - * Return Array : nodes - */ - getNodesFromTagSelector = function(selector, context) { - return context.getElementsByTagName(selector); - }; - - /* - * Select DOM nodes - * - * Parameters String selectors : CSS selectors Array, Object context : - * contextual node - * - * Return Array : found nodes - */ - return function(selectors, context) { - // Format - if (!context) { - context = doc; - } - if (typeof context == 'object' && context.pop) { - context = context[0]; - } - // Init vars - var local_contexts, future_local_contexts, selector, elements, nodes = [], j, k, l, m, n, o, getNodesFromSelector; - // Prepare selectors - selectors = selectors.split(','); - n = -1; - while (selector = selectors[++n]) { - selectors[n] = selector.split(/\s+/); - } - // Evaluate selectors for each global context - j = selectors[length]; - while (j) { - // Init local context - local_contexts = [ context ]; - // Evaluate selectors - k = -1; - l = selectors[--j][length]; - while (++k < l) { - // Drop empty selectors - if (selector = selectors[j][k]) { - // Id - if (selector.charAt(0) == '#') { - selector = selector.substr(1); - getNodesFromSelector = getNodesFromIdSelector; - } - // Class - else if (selector.charAt(0) == '.') { - selector = selector.substr(1); - getNodesFromSelector = getNodesFromClassSelector; - } - // Tag - else { - getNodesFromSelector = getNodesFromTagSelector; - } - // Evaluate current selector for each local context - future_local_contexts = []; - m = -1; - while (local_contexts[++m]) { - elements = getNodesFromSelector(selector, - local_contexts[m]); - n = -1; - o = elements[length]; - while (++n < o) { - future_local_contexts.push(elements[n]); - } - } - // Set new local contexts - local_contexts = future_local_contexts; - } - } - // Append new nodes - nodes = nodes.concat(local_contexts); - } - return nodes; - }; - - }(); - - window.mdbf.EmptyInput = mdbf.Class({ - constructor: function EmptyInput(elem) { - - }, - }); - - window.mdbf.FieldClass = mdbf.Class({ - constructor: function FieldClass(class_name) { - // name of the field. used to generate additional fields - this.name = class_name.split('-')[1]; - this.context = mdbf.nut('.' + class_name); - this.inputs = mdbf.nut('input', context); - // contains the max number in use for the inputs id and name - this.input_counter = this.inputs.length - 1; - // base to add more fields on click - this.empty_input = this.inputs[0].cloneNode(true); - this.empty_input.value = ''; - this.empty_input.id = ''; - this.empty_input.name = ''; - console.log(this.inputs); - }, - - }); - - window.mdbf.init = function(class_name) { - var field = mdbf.FieldClass(class_name); - } -}()); diff --git a/mongodbforms/util.py b/mongodbforms/util.py index 866a59a6..1ec1c774 100644 --- a/mongodbforms/util.py +++ b/mongodbforms/util.py @@ -15,43 +15,50 @@ from django.core.exceptions import ImproperlyConfigured from django.utils.importlib import import_module from django.utils import six + def import_by_path(dotted_path, error_prefix=''): """ - Import a dotted module path and return the attribute/class designated by the - last name in the path. Raise ImproperlyConfigured if something goes wrong. + Import a dotted module path and return the attribute/class designated + by the last name in the path. Raise ImproperlyConfigured if something + goes wrong. """ try: module_path, class_name = dotted_path.rsplit('.', 1) except ValueError: - raise ImproperlyConfigured("%s%s doesn't look like a module path" % ( - error_prefix, dotted_path)) + raise ImproperlyConfigured("%s%s doesn't look like a module path" % + (error_prefix, dotted_path)) try: module = import_module(module_path) except ImportError as e: msg = '%sError importing module %s: "%s"' % ( error_prefix, module_path, e) six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg), - sys.exc_info()[2]) + sys.exc_info()[2]) try: attr = getattr(module, class_name) except AttributeError: - raise ImproperlyConfigured('%sModule "%s" does not define a "%s" attribute/class' % ( - error_prefix, module_path, class_name)) + raise ImproperlyConfigured( + '%sModule "%s" does not define a "%s" attribute/class' % + (error_prefix, module_path, class_name)) return attr + def load_field_generator(): if hasattr(settings, 'MONGODBFORMS_FIELDGENERATOR'): return import_by_path(settings.MONGODBFORMS_FIELDGENERATOR) return MongoDefaultFormFieldGenerator + def init_document_options(document): if not isinstance(document._meta, DocumentMetaWrapper): document._meta = DocumentMetaWrapper(document) return document + def get_document_options(document): return DocumentMetaWrapper(document) + def format_mongo_validation_errors(validation_exception): """Returns a string listing all errors within a document""" @@ -59,7 +66,9 @@ def generate_key(value, prefix=''): if isinstance(value, list): value = ' '.join([generate_key(k) for k in value]) if isinstance(value, dict): - value = ' '.join([generate_key(v, k) for k, v in value.iteritems()]) + value = ' '.join([ + generate_key(v, k) for k, v in value.iteritems() + ]) results = "%s.%s" % (prefix, value) if prefix else value return results @@ -69,6 +78,7 @@ def generate_key(value, prefix=''): error_dict[generate_key(v)].append(k) return ["%s: %s" % (k, v) for k, v in error_dict.iteritems()] + # Taken from six (https://pypi.python.org/pypi/six) # by "Benjamin Peterson " # @@ -81,8 +91,8 @@ def generate_key(value, prefix=''): # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index e8f1a69e..af597df0 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -1,10 +1,13 @@ import copy -from django.forms.widgets import Widget, Media, TextInput, SplitDateTimeWidget, DateInput, TimeInput, MultiWidget +from django.forms.widgets import (Widget, Media, TextInput, + SplitDateTimeWidget, DateInput, TimeInput, + MultiWidget) from django.utils.safestring import mark_safe from django.core.validators import EMPTY_VALUES from django.forms.util import flatatt + class Html5SplitDateTimeWidget(SplitDateTimeWidget): def __init__(self, attrs=None, date_format=None, time_format=None): date_input = DateInput(attrs=attrs, format=date_format) @@ -14,6 +17,7 @@ def __init__(self, attrs=None, date_format=None, time_format=None): widgets = (date_input, time_input) MultiWidget.__init__(self, widgets, attrs) + class ListWidget(Widget): def __init__(self, contained_widget, attrs=None): self.contained_widget = contained_widget @@ -25,7 +29,9 @@ def __init__(self, contained_widget, attrs=None): def render(self, name, value, attrs=None): if value is not None and not isinstance(value, (list, tuple)): - raise TypeError("Value supplied for %s must be a list or tuple." % name) + raise TypeError( + "Value supplied for %s must be a list or tuple." % name + ) output = [] value = [] if value is None else value @@ -35,7 +41,9 @@ def render(self, name, value, attrs=None): for i, widget_value in enumerate(value): if id_: final_attrs = dict(final_attrs, id='%s_%s' % (id_, i)) - output.append(self.contained_widget.render(name + '_%s' % i, widget_value, final_attrs)) + output.append(self.contained_widget.render( + name + '_%s' % i, widget_value, final_attrs) + ) return mark_safe(self.format_output(output)) def id_for_label(self, id_): @@ -51,9 +59,9 @@ def value_from_datadict(self, data, files, name): while (name + '_%s' % i) in data or (name + '_%s' % i) in files: value = widget.value_from_datadict(data, files, name + '_%s' % i) # we need a different list if we handle files. Basicly Django sends - # back the initial values if we're not dealing with files. If we store - # files on the list, we need to add empty values to the clean data, - # so the list positions are kept. + # back the initial values if we're not dealing with files. If we + # store files on the list, we need to add empty values to the clean + # data, so the list positions are kept. if value not in EMPTY_VALUES or (value is None and len(files) > 0): ret.append(value) i = i + 1 @@ -70,7 +78,10 @@ def format_output(self, rendered_widgets): return ''.join(rendered_widgets) def _get_media(self): - "Media for a multiwidget is the combination of all media of the subwidgets" + """ + Media for a multiwidget is the combination of all media of + the subwidgets + """ media = Media() for w in self.widgets: media = media + w.media @@ -83,6 +94,7 @@ def __deepcopy__(self, memo): #obj.widget_type = copy.deepcopy(self.widget_type) return obj + class MapWidget(Widget): """ A widget that is composed of multiple widgets. @@ -128,21 +140,28 @@ def render(self, name, value, attrs=None): id_ = final_attrs.get('id', None) fieldset_attr = {} - value = list(value.items()) # in Python 3.X dict.items() returns dynamic *view objects* + # in Python 3.X dict.items() returns dynamic *view objects* + value = list(value.items()) value.append(('', '')) for i, (key, widget_value) in enumerate(value): if id_: - fieldset_attr = dict(final_attrs, id='fieldset_%s_%s' % (id_, i)) + fieldset_attr = dict( + final_attrs, id='fieldset_%s_%s' % (id_, i) + ) group = [] group.append(mark_safe('
' % flatatt(fieldset_attr))) if id_: final_attrs = dict(final_attrs, id='%s_key_%s' % (id_, i)) - group.append(self.key_widget.render(name + '_key_%s' % i, key, final_attrs)) + group.append(self.key_widget.render( + name + '_key_%s' % i, key, final_attrs) + ) if id_: final_attrs = dict(final_attrs, id='%s_value_%s' % (id_, i)) - group.append(self.data_widget.render(name + '_value_%s' % i, widget_value, final_attrs)) + group.append(self.data_widget.render( + name + '_value_%s' % i, widget_value, final_attrs) + ) group.append(mark_safe('
')) output.append(mark_safe(''.join(group))) @@ -158,8 +177,12 @@ def value_from_datadict(self, data, files, name): i = 0 ret = {} while (name + '_key_%s' % i) in data: - key = self.key_widget.value_from_datadict(data, files, name + '_key_%s' % i) - value = self.data_widget.value_from_datadict(data, files, name + '_value_%s' % i) + key = self.key_widget.value_from_datadict( + data, files, name + '_key_%s' % i + ) + value = self.data_widget.value_from_datadict( + data, files, name + '_value_%s' % i + ) if key not in EMPTY_VALUES: ret.update(((key, value), )) i = i + 1 @@ -176,7 +199,10 @@ def format_output(self, rendered_widgets): return ''.join(rendered_widgets) def _get_media(self): - "Media for a multiwidget is the combination of all media of the subwidgets" + """ + Media for a multiwidget is the combination of all media of + the subwidgets. + """ media = Media() for w in self.widgets: media = media + w.media @@ -188,6 +214,3 @@ def __deepcopy__(self, memo): obj.key_widget = copy.deepcopy(self.key_widget) obj.data_widget = copy.deepcopy(self.data_widget) return obj - - - From d8c16b51613aecdbfb98abb756ae7ef46d27178e Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Thu, 31 Oct 2013 22:34:09 +0100 Subject: [PATCH 115/136] Fix a bug in unique_with queryset generation --- mongodbforms/documents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 60bd5d80..2ac2d298 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -502,8 +502,8 @@ def validate_unique(self): filter_kwargs['q_obj'] = q & filter_kwargs['q_obj'] else: filter_kwargs[u_with] = u_with_attr - qs = self.instance.__class__.objects - qs.no_dereference().filter(**filter_kwargs) + qs = self.instance.__class__.objects.clone() + qs = qs.no_dereference().filter(**filter_kwargs) # Exclude the current object from the query if we are editing # an instance (as opposed to creating a new one) if self.instance.pk is not None: From efa1bb160caec3ef20b05e48c77be0a0eec97c92 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Sat, 9 Nov 2013 18:15:10 +0100 Subject: [PATCH 116/136] We don't need no recursion for #60 --- mongodbforms/documentoptions.py | 67 ++++++++++++++++++++++++++++++--- mongodbforms/util.py | 6 ++- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 5b5bb15f..e8a6fdbe 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -5,6 +5,7 @@ from django.db.models.fields import FieldDoesNotExist from django.utils.text import capfirst from django.db.models.options import get_verbose_name +from django.utils.functional import LazyObject, empty from mongoengine.fields import ReferenceField, ListField @@ -57,6 +58,56 @@ def __setattr__(self, attr, value): super(PkWrapper, self).__setattr__(attr, value) +class LazyDocumentMetaWrapper(LazyObject): + _document = None + _meta = None + + def __init__(self, document): + self._document = document + self._meta = document._meta + super(LazyDocumentMetaWrapper, self).__init__() + + def _setup(self): + self._wrapped = DocumentMetaWrapper(self._document, self._meta) + + def __setattr__(self, name, value): + if name in ["_document", "_meta",]: + # Assign to __dict__ to avoid infinite __setattr__ loops. + object.__setattr__(self, name, value) + else: + super(LazyDocumentMetaWrapper, self).__setattr__(name, value) + + def __dir__(self): + if self._wrapped is empty: + self._setup() + return self._wrapped.__dir__() + + def __getitem__(self, key): + if self._wrapped is empty: + self._setup() + return self._wrapped.__getitem__(key) + + def __setitem__(self, key, value): + if self._wrapped is empty: + self._setup() + return self._wrapped.__getitem__(key, value) + + def __delitem__(self, key): + if self._wrapped is empty: + self._setup() + return self._wrapped.__delitem__(key) + + def __len__(self): + if self._wrapped is empty: + self._setup() + return self._wrapped.__len__() + + def __contains__(self, key): + if self._wrapped is empty: + self._setup() + return self._wrapped.__contains__(key) + + class DocumentMetaWrapper(MutableMapping): """ Used to store mongoengine's _meta dict to make the document admin @@ -81,14 +132,19 @@ class DocumentMetaWrapper(MutableMapping): concrete_model = None concrete_managers = [] - def __init__(self, document): + def __init__(self, document, meta=None): super(DocumentMetaWrapper, self).__init__() + print "__init__'s document is:" + print type(document) self.document = document # used by Django to distinguish between abstract and concrete models # here for now always the document self.concrete_model = document - self._meta = getattr(document, '_meta', {}) + if meta is None: + self._meta = getattr(document, '_meta', {}) + else: + self._meta = meta try: self.object_name = self.document.__name__ @@ -131,9 +187,10 @@ def _setup_document_fields(self): flat.append((choice, value)) f.flatchoices = flat if isinstance(f, ReferenceField) and not \ - isinstance(f.document_type._meta, DocumentMetaWrapper) \ - and self.document != f.document_type: - f.document_type._meta = DocumentMetaWrapper(f.document_type) + isinstance(f.document_type._meta, DocumentMetaWrapper) and not \ + isinstance(f.document_type._meta, LazyDocumentMetaWrapper) and \ + self.document != f.document_type: + f.document_type._meta = LazyDocumentMetaWrapper(f.document_type) def _init_pk(self): """ diff --git a/mongodbforms/util.py b/mongodbforms/util.py index 1ec1c774..1b658827 100644 --- a/mongodbforms/util.py +++ b/mongodbforms/util.py @@ -2,7 +2,7 @@ from django.conf import settings -from .documentoptions import DocumentMetaWrapper +from mongodbforms.documentoptions import DocumentMetaWrapper, LazyDocumentMetaWrapper from .fieldgenerator import MongoDefaultFormFieldGenerator try: @@ -50,7 +50,9 @@ def load_field_generator(): def init_document_options(document): - if not isinstance(document._meta, DocumentMetaWrapper): + if not (isinstance(document._meta, DocumentMetaWrapper) or + isinstance(document._meta, LazyDocumentMetaWrapper)): + print "setting up wrapper for meta of type %s" % type(document._meta) document._meta = DocumentMetaWrapper(document) return document From 164ef613564a596071a37b4c3cbb0abfd461f3d6 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Sat, 9 Nov 2013 18:18:56 +0100 Subject: [PATCH 117/136] Hmpf. Print isn't supposed to be there... --- mongodbforms/documentoptions.py | 2 -- mongodbforms/util.py | 1 - 2 files changed, 3 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index e8a6fdbe..5ff46ee8 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -135,8 +135,6 @@ class DocumentMetaWrapper(MutableMapping): def __init__(self, document, meta=None): super(DocumentMetaWrapper, self).__init__() - print "__init__'s document is:" - print type(document) self.document = document # used by Django to distinguish between abstract and concrete models # here for now always the document diff --git a/mongodbforms/util.py b/mongodbforms/util.py index 1b658827..c240d3e2 100644 --- a/mongodbforms/util.py +++ b/mongodbforms/util.py @@ -52,7 +52,6 @@ def load_field_generator(): def init_document_options(document): if not (isinstance(document._meta, DocumentMetaWrapper) or isinstance(document._meta, LazyDocumentMetaWrapper)): - print "setting up wrapper for meta of type %s" % type(document._meta) document._meta = DocumentMetaWrapper(document) return document From 058ce02d9be144a8e9d3ba15c84fc9dc662e9799 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 12 Nov 2013 04:28:41 +0100 Subject: [PATCH 118/136] Cleanup of container widgets and code for LazyDocumentWrapper --- mongodbforms/documentoptions.py | 22 +---- mongodbforms/util.py | 3 +- mongodbforms/widgets.py | 143 +++++++++++--------------------- 3 files changed, 52 insertions(+), 116 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 5ff46ee8..f1c2146f 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -34,7 +34,7 @@ def __init__(self, to): @property def to(self): - if not isinstance(self._to._meta, DocumentMetaWrapper): + if not isinstance(self._to._meta, (DocumentMetaWrapper, LazyDocumentMetaWrapper)): self._to._meta = DocumentMetaWrapper(self._to) return self._to @@ -72,39 +72,26 @@ def _setup(self): def __setattr__(self, name, value): if name in ["_document", "_meta",]: - # Assign to __dict__ to avoid infinite __setattr__ loops. object.__setattr__(self, name, value) else: super(LazyDocumentMetaWrapper, self).__setattr__(name, value) def __dir__(self): - if self._wrapped is empty: - self._setup() return self._wrapped.__dir__() - + def __getitem__(self, key): - if self._wrapped is empty: - self._setup() return self._wrapped.__getitem__(key) def __setitem__(self, key, value): - if self._wrapped is empty: - self._setup() return self._wrapped.__getitem__(key, value) def __delitem__(self, key): - if self._wrapped is empty: - self._setup() return self._wrapped.__delitem__(key) def __len__(self): - if self._wrapped is empty: - self._setup() return self._wrapped.__len__() def __contains__(self, key): - if self._wrapped is empty: - self._setup() return self._wrapped.__contains__(key) @@ -185,8 +172,7 @@ def _setup_document_fields(self): flat.append((choice, value)) f.flatchoices = flat if isinstance(f, ReferenceField) and not \ - isinstance(f.document_type._meta, DocumentMetaWrapper) and not \ - isinstance(f.document_type._meta, LazyDocumentMetaWrapper) and \ + isinstance(f.document_type._meta, (DocumentMetaWrapper, LazyDocumentMetaWrapper)) and \ self.document != f.document_type: f.document_type._meta = LazyDocumentMetaWrapper(f.document_type) @@ -221,7 +207,7 @@ def verbose_name(self): Returns the verbose name of the document. Checks the original meta dict first. If it is not found - then generates a verbose name from from the object name. + then generates a verbose name from the object name. """ if self._verbose_name is None: verbose_name = self._meta.get('verbose_name', self.object_name) diff --git a/mongodbforms/util.py b/mongodbforms/util.py index c240d3e2..3ea4e91c 100644 --- a/mongodbforms/util.py +++ b/mongodbforms/util.py @@ -50,8 +50,7 @@ def load_field_generator(): def init_document_options(document): - if not (isinstance(document._meta, DocumentMetaWrapper) or - isinstance(document._meta, LazyDocumentMetaWrapper)): + if not isinstance(document._meta, (DocumentMetaWrapper, LazyDocumentMetaWrapper)): document._meta = DocumentMetaWrapper(document) return document diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index af597df0..c643550e 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -18,15 +18,47 @@ def __init__(self, attrs=None, date_format=None, time_format=None): MultiWidget.__init__(self, widgets, attrs) -class ListWidget(Widget): - def __init__(self, contained_widget, attrs=None): - self.contained_widget = contained_widget - if isinstance(contained_widget, type): - self.contained_widget = self.contained_widget() - if self.is_localized: - self.widget.is_localized = self.is_localized - super(ListWidget, self).__init__(attrs) +class BaseContainerWidget(Widget): + def __init__(self, data_widget, attrs=None): + if isinstance(data_widget, type): + data_widget = data_widget() + self.data_widget = data_widget + self.data_widget.is_localized = self.is_localized + super(BaseContainerWidget, self).__init__(attrs) + + def id_for_label(self, id_): + # See the comment for RadioSelect.id_for_label() + if id_: + id_ += '_0' + return id_ + + def format_output(self, rendered_widgets): + """ + Given a list of rendered widgets (as strings), returns a Unicode string + representing the HTML for the whole lot. + + This hook allows you to format the HTML design of the widgets, if + needed. + """ + return ''.join(rendered_widgets) + def _get_media(self): + """ + Media for a multiwidget is the combination of all media of + the subwidgets. + """ + media = Media() + media = media + self.data_widget.media + return media + media = property(_get_media) + + def __deepcopy__(self, memo): + obj = super(BaseContainerWidget, self).__deepcopy__(memo) + obj.data_widget = copy.deepcopy(self.data_widget) + return obj + + +class ListWidget(BaseContainerWidget): def render(self, name, value, attrs=None): if value is not None and not isinstance(value, (list, tuple)): raise TypeError( @@ -41,19 +73,13 @@ def render(self, name, value, attrs=None): for i, widget_value in enumerate(value): if id_: final_attrs = dict(final_attrs, id='%s_%s' % (id_, i)) - output.append(self.contained_widget.render( + output.append(self.data_widget.render( name + '_%s' % i, widget_value, final_attrs) ) return mark_safe(self.format_output(output)) - def id_for_label(self, id_): - # See the comment for RadioSelect.id_for_label() - if id_: - id_ += '_0' - return id_ - def value_from_datadict(self, data, files, name): - widget = self.contained_widget + widget = self.data_widget i = 0 ret = [] while (name + '_%s' % i) in data or (name + '_%s' % i) in files: @@ -67,69 +93,12 @@ def value_from_datadict(self, data, files, name): i = i + 1 return ret - def format_output(self, rendered_widgets): - """ - Given a list of rendered widgets (as strings), returns a Unicode string - representing the HTML for the whole lot. - - This hook allows you to format the HTML design of the widgets, if - needed. - """ - return ''.join(rendered_widgets) - - def _get_media(self): - """ - Media for a multiwidget is the combination of all media of - the subwidgets - """ - media = Media() - for w in self.widgets: - media = media + w.media - return media - media = property(_get_media) - - def __deepcopy__(self, memo): - obj = super(ListWidget, self).__deepcopy__(memo) - obj.contained_widget = copy.deepcopy(self.contained_widget) - #obj.widget_type = copy.deepcopy(self.widget_type) - return obj - - -class MapWidget(Widget): - """ - A widget that is composed of multiple widgets. - - Its render() method is different than other widgets', because it has to - figure out how to split a single value for display in multiple widgets. - The ``value`` argument can be one of two things: - - * A list. - * A normal value (e.g., a string) that has been "compressed" from - a list of values. - - In the second case -- i.e., if the value is NOT a list -- render() will - first "decompress" the value into a list before rendering it. It does so by - calling the decompress() method, which MultiWidget subclasses must - implement. This method takes a single "compressed" value and returns a - list. - - When render() does its HTML rendering, each value in the list is rendered - with the corresponding widget -- the first value is rendered in the first - widget, the second value is rendered in the second widget, etc. - Subclasses may implement format_output(), which takes the list of rendered - widgets and returns a string of HTML that formats them any way you'd like. - - You'll probably want to use this class with MultiValueField. - """ - def __init__(self, contained_widget, attrs=None): +class MapWidget(BaseContainerWidget): + def __init__(self, data_widget, attrs=None): self.key_widget = TextInput() self.key_widget.is_localized = self.is_localized - if isinstance(contained_widget, type): - contained_widget = contained_widget() - self.data_widget = contained_widget - self.data_widget.is_localized = self.is_localized - super(MapWidget, self).__init__(attrs) + super(MapWidget, self).__init__(data_widget, attrs) def render(self, name, value, attrs=None): if value is not None and not isinstance(value, dict): @@ -167,12 +136,6 @@ def render(self, name, value, attrs=None): output.append(mark_safe(''.join(group))) return mark_safe(self.format_output(output)) - def id_for_label(self, id_): - # See the comment for RadioSelect.id_for_label() - if id_: - id_ += '_0' - return id_ - def value_from_datadict(self, data, files, name): i = 0 ret = {} @@ -188,29 +151,17 @@ def value_from_datadict(self, data, files, name): i = i + 1 return ret - def format_output(self, rendered_widgets): - """ - Given a list of rendered widgets (as strings), returns a Unicode string - representing the HTML for the whole lot. - - This hook allows you to format the HTML design of the widgets, if - needed. - """ - return ''.join(rendered_widgets) - def _get_media(self): """ Media for a multiwidget is the combination of all media of the subwidgets. """ - media = Media() - for w in self.widgets: - media = media + w.media + media = super(MapWidget, self)._get_media() + media = media + self.key_widget.media return media media = property(_get_media) def __deepcopy__(self, memo): obj = super(MapWidget, self).__deepcopy__(memo) obj.key_widget = copy.deepcopy(self.key_widget) - obj.data_widget = copy.deepcopy(self.data_widget) return obj From 703965ccb3b432f4ee3d84ad55a3c2953285a362 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Thu, 14 Nov 2013 15:25:05 +0100 Subject: [PATCH 119/136] Absolute imports and proper handling of embedded field names for Formsets for #11 --- mongodbforms/documents.py | 59 ++++++++++++++++++++++++++++++---- mongodbforms/fieldgenerator.py | 4 +-- mongodbforms/util.py | 2 +- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 2ac2d298..aa53846a 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -14,7 +14,7 @@ from django.utils.text import capfirst, get_valid_filename from mongoengine.fields import (ObjectIdField, ListField, ReferenceField, - FileField, MapField) + FileField, MapField, EmbeddedDocumentField) try: from mongoengine.base import ValidationError except ImportError: @@ -25,8 +25,8 @@ from gridfs import GridFS -from .documentoptions import DocumentMetaWrapper -from .util import (with_metaclass, load_field_generator, +from mongodbforms.documentoptions import DocumentMetaWrapper +from mongodbforms.util import (with_metaclass, load_field_generator, format_mongo_validation_errors) _fieldgenerator = load_field_generator() @@ -685,7 +685,8 @@ def initial_form_count(self): return super(BaseDocumentFormSet, self).initial_form_count() def get_queryset(self): - return self._queryset + qs = self._queryset or [] + return qs def save_object(self, form): obj = form.save(commit=False) @@ -833,6 +834,9 @@ def __init__(self, data=None, files=None, save_as_new=False, prefix=None, queryset=[], parent_document=None, **kwargs): self.parent_document = parent_document + queryset = getattr(self.parent_document, + self.form._meta.embedded_field) + super(EmbeddedDocumentFormSet, self).__init__(data, files, save_as_new, prefix, queryset, **kwargs) @@ -851,6 +855,10 @@ def _construct_form(self, i, **kwargs): form = super(EmbeddedDocumentFormSet, self)._construct_form( i, **defaults) return form + + @classmethod + def get_default_prefix(cls): + return cls.document.__name__.lower() @property def empty_form(self): @@ -870,7 +878,6 @@ def save(self, commit=True): objs = objs or [] if commit and self.parent_document is not None: - form = self.empty_form # The thing about formsets is that the base use case is to edit # *all* of the associated objects on a model. As written, using # these FormSets this way will cause the existing embedded @@ -886,17 +893,51 @@ def save(self, commit=True): # rather than adding the new values to the existing values, because # the new values will almost always contain the old values (with # the default use case.) - setattr(self.parent_document, form._meta.embedded_field, objs) + setattr(self.parent_document, self.form._meta.embedded_field, objs) self.parent_document.save() return objs + +def _get_embedded_field(parent_doc, document, emb_name=None, can_fail=False): + if emb_name: + emb_fields = [f for f in parent_doc._fields.values() if f.name == emb_name] + if len(emb_fields) == 1: + field = emb_fields[0] + if not isinstance(field, (EmbeddedDocumentField, ListField)) or \ + (isinstance(field, EmbeddedDocumentField) and field.document_type != document) or \ + (isinstance(field, ListField) and + isinstance(field.field, EmbeddedDocumentField) and + field.field.document_type != document): + raise Exception("emb_name '%s' is not a EmbeddedDocumentField or not a ListField to %s" % (emb_name, document)) + elif len(emb_fields) == 0: + raise Exception("%s has no field named '%s'" % (parent_doc, emb_name)) + else: + emb_fields = [ + f for f in parent_doc._fields.values() + if (isinstance(field, EmbeddedDocumentField) and field.document_type == model) or \ + (isinstance(field, ListField) and + isinstance(field.field, EmbeddedDocumentField) and + field.field.document_type == document) + ] + if len(emb_fields) == 1: + field = emb_fields[0] + elif len(emb_fields) == 0: + if can_fail: + return + raise Exception("%s has no EmbeddedDocumentField or ListField to %s" % (parent_doc, document)) + else: + raise Exception("%s has more than 1 EmbeddedDocumentField to %s" % (parent_doc, document)) + + return field + def embeddedformset_factory(document, parent_document, form=EmbeddedDocumentForm, formset=EmbeddedDocumentFormSet, + embedded_name=None, fields=None, exclude=None, - extra=1, can_order=False, can_delete=True, + extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None): """ Returns an ``InlineFormSet`` for the given kwargs. @@ -904,6 +945,9 @@ def embeddedformset_factory(document, parent_document, You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` to ``parent_model``. """ + emb_field = _get_embedded_field(parent_document, document, emb_name=embedded_name) + if isinstance(emb_field, EmbeddedDocumentField): + max_num = 1 kwargs = { 'form': form, 'formfield_callback': formfield_callback, @@ -916,4 +960,5 @@ def embeddedformset_factory(document, parent_document, 'max_num': max_num, } FormSet = documentformset_factory(document, **kwargs) + FormSet.form._meta.embedded_field = emb_field.name return FormSet diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index 82e4cfcc..acbabffc 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -26,8 +26,8 @@ MongoURLField, ReferenceField, DocumentMultipleChoiceField, ListField, MapField) -from .widgets import Html5SplitDateTimeWidget -from .documentoptions import create_verbose_name +from mongodbforms.widgets import Html5SplitDateTimeWidget +from mongodbforms.documentoptions import create_verbose_name BLANK_CHOICE_DASH = [("", "---------")] diff --git a/mongodbforms/util.py b/mongodbforms/util.py index 3ea4e91c..7cd8498e 100644 --- a/mongodbforms/util.py +++ b/mongodbforms/util.py @@ -3,7 +3,7 @@ from django.conf import settings from mongodbforms.documentoptions import DocumentMetaWrapper, LazyDocumentMetaWrapper -from .fieldgenerator import MongoDefaultFormFieldGenerator +from mongodbforms.fieldgenerator import MongoDefaultFormFieldGenerator try: from django.utils.module_loading import import_by_path From 21fb038d0febea5deddf5facd30b2effd05fb702 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Thu, 21 Nov 2013 14:37:17 +0100 Subject: [PATCH 120/136] Make sure not to use LazyDocumentMetaWrapper as doc's meta for #61 --- mongodbforms/documentoptions.py | 41 +++++++++++++++++++++++---------- mongodbforms/documents.py | 36 ++++++++++++++--------------- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index f1c2146f..efbabf97 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -44,6 +44,9 @@ def to(self, value): class PkWrapper(object): + editable = False + fake = False + def __init__(self, wrapped): self.obj = wrapped @@ -118,6 +121,8 @@ class DocumentMetaWrapper(MutableMapping): _meta = None concrete_model = None concrete_managers = [] + virtual_fields = [] + auto_created = False def __init__(self, document, meta=None): super(DocumentMetaWrapper, self).__init__() @@ -127,9 +132,10 @@ def __init__(self, document, meta=None): # here for now always the document self.concrete_model = document if meta is None: - self._meta = getattr(document, '_meta', {}) - else: - self._meta = meta + meta = getattr(document, '_meta', {}) + if isinstance(meta, LazyDocumentMetaWrapper): + meta = meta._meta + self._meta = meta try: self.object_name = self.document.__name__ @@ -143,9 +149,9 @@ def __init__(self, document, meta=None): self._setup_document_fields() # Setup self.pk if the document has an id_field in it's meta # if it doesn't have one it's an embedded document - if 'id_field' in self._meta: - self.pk_name = self._meta['id_field'] - self._init_pk() + #if 'id_field' in self._meta: + # self.pk_name = self._meta['id_field'] + self._init_pk() def _setup_document_fields(self): for f in self.document._fields.values(): @@ -183,16 +189,27 @@ def _init_pk(self): The function also adds a _get_pk_val method to the document. """ - pk_field = getattr(self.document, self.pk_name) + if 'id_field' in self._meta: + self.pk_name = self._meta['id_field'] + pk_field = getattr(self.document, self.pk_name) + else: + pk_field = None self.pk = PkWrapper(pk_field) - self.pk.name = self.pk_name - self.pk.attname = self.pk_name - - self.document._pk_val = pk_field def _get_pk_val(self): return self._pk_val - patch_document(_get_pk_val, self.document) + + if pk_field is not None: + self.pk.name = self.pk_name + self.pk.attname = self.pk_name + self.document._pk_val = pk_field + patch_document(_get_pk_val, self.document) + else: + self.pk.fake = True + # this is used in the admin and used to determine if the admin + # needs to add a hidden pk field. It does not for embedded fields. + # So we pretend to have an editable pk field and just ignore it otherwise + self.pk.editable = True @property def app_label(self): diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index aa53846a..8c66c075 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -832,7 +832,13 @@ def inlineformset_factory(document, form=DocumentForm, class EmbeddedDocumentFormSet(BaseDocumentFormSet): def __init__(self, data=None, files=None, save_as_new=False, prefix=None, queryset=[], parent_document=None, **kwargs): - self.parent_document = parent_document + if parent_document is not None: + self.parent_document = parent_document + + if 'instance' in kwargs: + instance = kwargs.pop('instance') + if parent_document is None: + self.parent_document = instance queryset = getattr(self.parent_document, self.form._meta.embedded_field) @@ -848,7 +854,8 @@ def _construct_form(self, i, **kwargs): # a huge amount of time iterating over the list field on form __init__ emb_list = getattr(self.parent_document, self.form._meta.embedded_field) - if emb_list is not None and len(emb_list) < i: + + if emb_list is not None and len(emb_list) > i: defaults['position'] = i defaults.update(kwargs) @@ -878,22 +885,15 @@ def save(self, commit=True): objs = objs or [] if commit and self.parent_document is not None: - # The thing about formsets is that the base use case is to edit - # *all* of the associated objects on a model. As written, using - # these FormSets this way will cause the existing embedded - # documents to get saved along with a copy of themselves plus any - # new ones you added. - # - # The only way you could do "updates" of existing embedded document - # fields is if those embedded documents had ObjectIDs of their own, - # which they don't by default in Mongoengine. - # - # In this case it makes the most sense to simply replace the - # embedded field with the new values gathered form the formset, - # rather than adding the new values to the existing values, because - # the new values will almost always contain the old values (with - # the default use case.) - setattr(self.parent_document, self.form._meta.embedded_field, objs) + field = self.parent_document._fields.get(self.form._meta.embedded_field, None) + if isinstance(field, EmbeddedDocumentField): + try: + obj = objs[0] + except IndexError: + obj = None + setattr(self.parent_document, self.form._meta.embedded_field, obj) + else: + setattr(self.parent_document, self.form._meta.embedded_field, objs) self.parent_document.save() return objs From 2a265b8bc8463fb22f3d331556f60ade7bbcd588 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 26 Nov 2013 17:12:38 +0100 Subject: [PATCH 121/136] A couple more changes for Django 1.6 and hidden MapField widget --- mongodbforms/documents.py | 3 +++ mongodbforms/fields.py | 4 ++-- mongodbforms/widgets.py | 17 ++++++++++++++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 8c66c075..c1f8c630 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -20,6 +20,7 @@ except ImportError: from mongoengine.errors import ValidationError from mongoengine.queryset import OperationError, Q +from mongoengine.queryset.base import BaseQuerySet from mongoengine.connection import get_db, DEFAULT_CONNECTION_NAME from mongoengine.base import NON_FIELD_ERRORS as MONGO_NON_FIELD_ERRORS @@ -661,6 +662,8 @@ class BaseDocumentFormSet(BaseFormSet): def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, queryset=None, **kwargs): + if not isinstance(queryset, (list, BaseQuerySet)): + queryset = [queryset] self.queryset = queryset self._queryset = self.queryset self.initial = self.construct_initial() diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index cd24dcee..8f9af0a6 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -34,7 +34,7 @@ from pymongo.objectid import ObjectId from pymongo.errors import InvalidId -from mongodbforms.widgets import ListWidget, MapWidget +from mongodbforms.widgets import ListWidget, MapWidget, HiddenMapWidget class MongoChoiceIterator(object): @@ -285,11 +285,11 @@ class MapField(forms.Field): 'key_required': _('A key is required.'), } widget = MapWidget + hidden_widget = HiddenMapWidget def __init__(self, contained_field, max_key_length=None, min_key_length=None, key_validators=[], field_kwargs={}, *args, **kwargs): - if 'widget' in kwargs: self.widget = kwargs.pop('widget') diff --git a/mongodbforms/widgets.py b/mongodbforms/widgets.py index c643550e..77acbc12 100644 --- a/mongodbforms/widgets.py +++ b/mongodbforms/widgets.py @@ -2,7 +2,7 @@ from django.forms.widgets import (Widget, Media, TextInput, SplitDateTimeWidget, DateInput, TimeInput, - MultiWidget) + MultiWidget, HiddenInput) from django.utils.safestring import mark_safe from django.core.validators import EMPTY_VALUES from django.forms.util import flatatt @@ -118,7 +118,8 @@ def render(self, name, value, attrs=None): final_attrs, id='fieldset_%s_%s' % (id_, i) ) group = [] - group.append(mark_safe('
' % flatatt(fieldset_attr))) + if not self.is_hidden: + group.append(mark_safe('
' % flatatt(fieldset_attr))) if id_: final_attrs = dict(final_attrs, id='%s_key_%s' % (id_, i)) @@ -131,7 +132,8 @@ def render(self, name, value, attrs=None): group.append(self.data_widget.render( name + '_value_%s' % i, widget_value, final_attrs) ) - group.append(mark_safe('
')) + if not self.is_hidden: + group.append(mark_safe('
')) output.append(mark_safe(''.join(group))) return mark_safe(self.format_output(output)) @@ -165,3 +167,12 @@ def __deepcopy__(self, memo): obj = super(MapWidget, self).__deepcopy__(memo) obj.key_widget = copy.deepcopy(self.key_widget) return obj + + +class HiddenMapWidget(MapWidget): + is_hidden = True + + def __init__(self, attrs=None): + data_widget = HiddenInput() + super(MapWidget, self).__init__(data_widget, attrs) + self.key_widget = HiddenInput() From 58a1c36e6b7daa97e135b87c2e019516fbfad216 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Wed, 27 Nov 2013 17:06:33 +0100 Subject: [PATCH 122/136] Errors found by pyflakes --- mongodbforms/documentoptions.py | 3 ++- mongodbforms/documents.py | 5 ++--- mongodbforms/fields.py | 2 -- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index efbabf97..082e3440 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -5,7 +5,8 @@ from django.db.models.fields import FieldDoesNotExist from django.utils.text import capfirst from django.db.models.options import get_verbose_name -from django.utils.functional import LazyObject, empty +from django.utils.functional import LazyObject +from django.conf import settings from mongoengine.fields import ReferenceField, ListField diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index c1f8c630..1c4801c4 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -27,8 +27,7 @@ from gridfs import GridFS from mongodbforms.documentoptions import DocumentMetaWrapper -from mongodbforms.util import (with_metaclass, load_field_generator, - format_mongo_validation_errors) +from mongodbforms.util import with_metaclass, load_field_generator _fieldgenerator = load_field_generator() @@ -918,7 +917,7 @@ def _get_embedded_field(parent_doc, document, emb_name=None, can_fail=False): else: emb_fields = [ f for f in parent_doc._fields.values() - if (isinstance(field, EmbeddedDocumentField) and field.document_type == model) or \ + if (isinstance(field, EmbeddedDocumentField) and field.document_type == document) or \ (isinstance(field, ListField) and isinstance(field.field, EmbeddedDocumentField) and field.field.document_type == document) diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py index 8f9af0a6..6f4817d4 100644 --- a/mongodbforms/fields.py +++ b/mongodbforms/fields.py @@ -28,10 +28,8 @@ from django.core.exceptions import ValidationError try: # objectid was moved into bson in pymongo 1.9 - from bson.objectid import ObjectId from bson.errors import InvalidId except ImportError: - from pymongo.objectid import ObjectId from pymongo.errors import InvalidId from mongodbforms.widgets import ListWidget, MapWidget, HiddenMapWidget From d8cc4d4f9c6c08039bf4b0122ff15a5ccfd105c9 Mon Sep 17 00:00:00 2001 From: Naison Souza Date: Sat, 4 Oct 2014 15:45:39 -0300 Subject: [PATCH 123/136] get_verbose_name fix for new django 1.7 --- mongodbforms/documentoptions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 082e3440..06eb91e2 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -4,9 +4,12 @@ from django.db.models.fields import FieldDoesNotExist from django.utils.text import capfirst -from django.db.models.options import get_verbose_name from django.utils.functional import LazyObject from django.conf import settings +try: + from django.db.models.options import get_verbose_name +except ImportError: + from django.utils.text import camel_case_to_spaces as get_verbose_name from mongoengine.fields import ReferenceField, ListField From 9a581e4dae8710089613ace3db2254b37ea9b3a1 Mon Sep 17 00:00:00 2001 From: Laurent Payot Date: Fri, 5 Dec 2014 13:29:47 +0100 Subject: [PATCH 124/136] get_verbose_name fix for newer Django versions --- mongodbforms/documentoptions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 082e3440..06eb91e2 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -4,9 +4,12 @@ from django.db.models.fields import FieldDoesNotExist from django.utils.text import capfirst -from django.db.models.options import get_verbose_name from django.utils.functional import LazyObject from django.conf import settings +try: + from django.db.models.options import get_verbose_name +except ImportError: + from django.utils.text import camel_case_to_spaces as get_verbose_name from mongoengine.fields import ReferenceField, ListField From b3331fe821b90f5bff2ed2d68151172b950be7a5 Mon Sep 17 00:00:00 2001 From: Laurent Payot Date: Fri, 5 Dec 2014 13:35:59 +0100 Subject: [PATCH 125/136] mongoengine StringField regex fix --- mongodbforms/fieldgenerator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py index acbabffc..f4d842e3 100644 --- a/mongodbforms/fieldgenerator.py +++ b/mongodbforms/fieldgenerator.py @@ -7,7 +7,7 @@ import collections from django import forms -from django.core.validators import EMPTY_VALUES +from django.core.validators import EMPTY_VALUES, RegexValidator try: from django.utils.encoding import smart_text as smart_unicode except ImportError: @@ -180,7 +180,7 @@ def generate_stringfield(self, field, **kwargs): 'min_length': field.min_length, }) if field.regex: - defaults['regex'] = field.regex + defaults['validators'] = [RegexValidator(regex=field.regex)] form_class = self.form_field_map.get(map_key) defaults.update(self.check_widget(map_key)) From 7115aa0f6c9b2a54b28bf8d8fa7b86ea0bbc74f4 Mon Sep 17 00:00:00 2001 From: Oleg Churkin Date: Tue, 23 Dec 2014 14:55:42 +0300 Subject: [PATCH 126/136] LazyDocumentMetaWrapper getitem method fixed --- mongodbforms/documentoptions.py | 32 +++++++-------------- mongodbforms/tests.py | 50 +++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 22 deletions(-) create mode 100644 mongodbforms/tests.py diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 082e3440..4b515fae 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -5,7 +5,7 @@ from django.db.models.fields import FieldDoesNotExist from django.utils.text import capfirst from django.db.models.options import get_verbose_name -from django.utils.functional import LazyObject +from django.utils.functional import LazyObject, new_method_proxy from django.conf import settings from mongoengine.fields import ReferenceField, ListField @@ -65,40 +65,28 @@ def __setattr__(self, attr, value): class LazyDocumentMetaWrapper(LazyObject): _document = None _meta = None - + def __init__(self, document): self._document = document self._meta = document._meta super(LazyDocumentMetaWrapper, self).__init__() - + def _setup(self): self._wrapped = DocumentMetaWrapper(self._document, self._meta) - + def __setattr__(self, name, value): if name in ["_document", "_meta",]: object.__setattr__(self, name, value) else: super(LazyDocumentMetaWrapper, self).__setattr__(name, value) - - def __dir__(self): - return self._wrapped.__dir__() - - def __getitem__(self, key): - return self._wrapped.__getitem__(key) - - def __setitem__(self, key, value): - return self._wrapped.__getitem__(key, value) - - def __delitem__(self, key): - return self._wrapped.__delitem__(key) - - def __len__(self): - return self._wrapped.__len__() - + + __len__ = new_method_proxy(len) + + @new_method_proxy def __contains__(self, key): - return self._wrapped.__contains__(key) - + return key in self + class DocumentMetaWrapper(MutableMapping): """ Used to store mongoengine's _meta dict to make the document admin diff --git a/mongodbforms/tests.py b/mongodbforms/tests.py new file mode 100644 index 00000000..a6c26258 --- /dev/null +++ b/mongodbforms/tests.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from django.conf import settings + +settings.configure( + DEBUG=True, + DATABASES={ + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + } + }, + ROOT_URLCONF='', + INSTALLED_APPS=( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.admin', + 'mongodbforms', + ) +) + + +import mongoengine +from django.test import SimpleTestCase +from mongodbforms.documentoptions import LazyDocumentMetaWrapper + + +class TestDocument(mongoengine.Document): + meta = {'abstract': True} + + name = mongoengine.StringField() + + +class LazyWrapperTest(SimpleTestCase): + + def test_lazy_getitem(self): + meta = LazyDocumentMetaWrapper(TestDocument) + self.assertTrue(meta['abstract']) + + meta = LazyDocumentMetaWrapper(TestDocument) + self.assertTrue(meta.get('abstract')) + + meta = LazyDocumentMetaWrapper(TestDocument) + self.assertTrue('abstract' in meta) + + meta = LazyDocumentMetaWrapper(TestDocument) + self.assertEqual(len(meta), 1) + + meta = LazyDocumentMetaWrapper(TestDocument) + meta.custom = 'yes' + self.assertEqual(meta.custom, 'yes') From cef1d4918a99d589e3ab61b426de98a5db02a2ef Mon Sep 17 00:00:00 2001 From: Oleg Churkin Date: Tue, 23 Dec 2014 15:03:28 +0300 Subject: [PATCH 127/136] Version bump to 0.3.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 60d87bd8..91eb0b14 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def convert_readme(): return open('README.txt').read() setup(name='mongodbforms', - version='0.3', + version='0.3.1', description="An implementation of django forms using mongoengine.", author='Jan Schrewe', author_email='jan@schafproductions.com', From 0b4cc51a81b1d2ade9aecb0c9b846654b5c56544 Mon Sep 17 00:00:00 2001 From: mvergerdelbove Date: Thu, 5 Feb 2015 14:28:36 -0800 Subject: [PATCH 128/136] specify app_label in document meta --- mongodbforms/documentoptions.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 082e3440..0efc8c28 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -215,8 +215,11 @@ def _get_pk_val(self): @property def app_label(self): if self._app_label is None: - model_module = sys.modules[self.document.__module__] - self._app_label = model_module.__name__.split('.')[-2] + if self._meta.get('app_label'): + self._app_label = self._meta["app_label"] + else: + model_module = sys.modules[self.document.__module__] + self._app_label = model_module.__name__.split('.')[-2] return self._app_label @property From fdeedc0435183da07fcf8337967134a6c2369e77 Mon Sep 17 00:00:00 2001 From: Olivier Le Floch Date: Mon, 9 Mar 2015 22:06:58 -0700 Subject: [PATCH 129/136] Django 1.7+ compatibility: get_verbose_name -> camel_case_to_spaces --- mongodbforms/documentoptions.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 0efc8c28..0572223b 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -4,7 +4,14 @@ from django.db.models.fields import FieldDoesNotExist from django.utils.text import capfirst -from django.db.models.options import get_verbose_name + +try: + # New in Django 1.7+ + from django.utils.text import camel_case_to_spaces +except ImportError: + # Backwards compatibility + from django.db.models.options import get_verbose_name as camel_case_to_spaces + from django.utils.functional import LazyObject from django.conf import settings @@ -20,7 +27,7 @@ def patch_document(function, instance, bound=True): def create_verbose_name(name): - name = get_verbose_name(name) + name = camel_case_to_spaces(name) name = name.replace('_', ' ') return name From 50d92fe21c1a2c5c9a5167b927a1bffa993eb0f2 Mon Sep 17 00:00:00 2001 From: Olivier Le Floch Date: Tue, 10 Mar 2015 16:46:59 -0700 Subject: [PATCH 130/136] More Django 1.7 compatibility Fixes 'ModelFormOptions' object has no attribute 'labels' Thanks to https://github.com/carlware/django-mongodbforms/commit/09886bdaf99df624a06de7acda9ad5250358a432 --- mongodbforms/documents.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 1c4801c4..5c44a954 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -283,6 +283,9 @@ def __init__(self, options=None): self._dont_save = [] + self.labels = False + self.help_texts = False + class DocumentFormMetaclass(type): def __new__(cls, name, bases, attrs): From 5438a1dc206121e207d279cf0e5e3b3e55157d4a Mon Sep 17 00:00:00 2001 From: Olivier Le Floch Date: Mon, 4 May 2015 16:52:31 -0700 Subject: [PATCH 131/136] Django 1.8 support (OPS-4393) --- mongodbforms/documentoptions.py | 66 +++++++++--------- mongodbforms/documents.py | 120 ++++++++++++++++---------------- 2 files changed, 95 insertions(+), 91 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 0572223b..fe394850 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -54,7 +54,7 @@ def to(self, value): class PkWrapper(object): editable = False fake = False - + def __init__(self, wrapped): self.obj = wrapped @@ -72,39 +72,39 @@ def __setattr__(self, attr, value): class LazyDocumentMetaWrapper(LazyObject): _document = None _meta = None - + def __init__(self, document): self._document = document self._meta = document._meta super(LazyDocumentMetaWrapper, self).__init__() - + def _setup(self): self._wrapped = DocumentMetaWrapper(self._document, self._meta) - + def __setattr__(self, name, value): if name in ["_document", "_meta",]: object.__setattr__(self, name, value) else: super(LazyDocumentMetaWrapper, self).__setattr__(name, value) - + def __dir__(self): return self._wrapped.__dir__() - + def __getitem__(self, key): return self._wrapped.__getitem__(key) - + def __setitem__(self, key, value): return self._wrapped.__getitem__(key, value) - + def __delitem__(self, key): return self._wrapped.__delitem__(key) - + def __len__(self): return self._wrapped.__len__() - + def __contains__(self, key): return self._wrapped.__contains__(key) - + class DocumentMetaWrapper(MutableMapping): """ @@ -131,6 +131,7 @@ class DocumentMetaWrapper(MutableMapping): concrete_managers = [] virtual_fields = [] auto_created = False + _deferred = False def __init__(self, document, meta=None): super(DocumentMetaWrapper, self).__init__() @@ -169,11 +170,14 @@ def _setup_document_fields(self): # need a bit more for actual reference fields here if isinstance(f, ReferenceField): f.rel = Relation(f.document_type) + f.is_relation = True elif isinstance(f, ListField) and \ isinstance(f.field, ReferenceField): f.field.rel = Relation(f.field.document_type) + f.field.is_relation = True else: f.rel = None + f.is_relation = False if not hasattr(f, 'verbose_name') or f.verbose_name is None: f.verbose_name = capfirst(create_verbose_name(f.name)) if not hasattr(f, 'flatchoices'): @@ -206,7 +210,7 @@ def _init_pk(self): def _get_pk_val(self): return self._pk_val - + if pk_field is not None: self.pk.name = self.pk_name self.pk.attname = self.pk_name @@ -218,7 +222,7 @@ def _get_pk_val(self): # needs to add a hidden pk field. It does not for embedded fields. # So we pretend to have an editable pk field and just ignore it otherwise self.pk.editable = True - + @property def app_label(self): if self._app_label is None: @@ -228,12 +232,12 @@ def app_label(self): model_module = sys.modules[self.document.__module__] self._app_label = model_module.__name__.split('.')[-2] return self._app_label - + @property def verbose_name(self): """ Returns the verbose name of the document. - + Checks the original meta dict first. If it is not found then generates a verbose name from the object name. """ @@ -241,15 +245,15 @@ def verbose_name(self): verbose_name = self._meta.get('verbose_name', self.object_name) self._verbose_name = capfirst(create_verbose_name(verbose_name)) return self._verbose_name - + @property def verbose_name_raw(self): return self.verbose_name - + @property def verbose_name_plural(self): return "%ss" % self.verbose_name - + def get_add_permission(self): return 'add_%s' % self.object_name.lower() @@ -258,10 +262,10 @@ def get_change_permission(self): def get_delete_permission(self): return 'delete_%s' % self.object_name.lower() - + def get_ordered_objects(self): return [] - + def get_field_by_name(self, name): """ Returns the (field_object, model, direct, m2m), where field_object is @@ -281,13 +285,13 @@ def get_field_by_name(self, name): else: raise FieldDoesNotExist('%s has no field named %r' % (self.object_name, name)) - + def get_field(self, name, many_to_many=True): """ Returns the requested field by name. Raises FieldDoesNotExist on error. """ return self.get_field_by_name(name)[0] - + @property def swapped(self): """ @@ -296,7 +300,7 @@ def swapped(self): For historical reasons, model name lookups using get_model() are case insensitive, so we make sure we are case insensitive here. - + NOTE: Not sure this is actually usefull for documents. So at the moment it's really only here because the admin wants it. It might prove usefull for someone though, so it's more then just a dummy. @@ -318,28 +322,28 @@ def swapped(self): not in (None, model_label): return swapped_for return None - + def __getattr__(self, name): if name in self._deprecated_attrs: return getattr(self, self._deprecated_attrs.get(name)) - + try: return self._meta[name] except KeyError: raise AttributeError - + def __setattr__(self, name, value): if not hasattr(self, name): self._meta[name] = value else: super(DocumentMetaWrapper, self).__setattr__(name, value) - + def __contains__(self, key): return key in self._meta - + def __getitem__(self, key): return self._meta[key] - + def __setitem__(self, key, value): self._meta[key] = value @@ -357,10 +361,10 @@ def get(self, key, default=None): return self.__getitem__(key) except KeyError: return default - + def get_parent_list(self): return [] - + def get_all_related_objects(self, *args, **kwargs): return [] diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 5c44a954..cecabd0d 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -41,7 +41,7 @@ def _get_unique_filename(name, db_alias=DEFAULT_CONNECTION_NAME, # file_ext includes the dot. name = os.path.join("%s_%s%s" % (file_root, next(count), file_ext)) return name - + def _save_iterator_file(field, instance, uploaded_file, file_data=None): """ @@ -52,22 +52,22 @@ def _save_iterator_file(field, instance, uploaded_file, file_data=None): if file_data is None: file_data = field.field.get_proxy_obj(key=field.name, instance=instance) - + if file_data.instance is None: file_data.instance = instance if file_data.key is None: file_data.key = field.name - + if file_data.grid_id: file_data.delete() - + uploaded_file.seek(0) filename = _get_unique_filename(uploaded_file.name, field.field.db_alias, field.field.collection_name) file_data.put(uploaded_file, content_type=uploaded_file.content_type, filename=filename) file_data.close() - + return file_data @@ -79,11 +79,11 @@ def construct_instance(form, instance, fields=None, exclude=None): """ cleaned_data = form.cleaned_data file_field_list = [] - + # check wether object is instantiated if isinstance(instance, type): instance = instance() - + for f in instance._fields.values(): if isinstance(f, ObjectIdField): continue @@ -135,7 +135,7 @@ def construct_instance(form, instance, fields=None, exclude=None): upload = cleaned_data[f.name] if upload is None: continue - + try: upload.file.seek(0) # delete first to get the names right @@ -151,7 +151,7 @@ def construct_instance(form, instance, fields=None, exclude=None): # upload is already the gridfsproxy object we need. upload.get() setattr(instance, f.name, upload) - + return instance @@ -168,12 +168,12 @@ def save_instance(form, instance, fields=None, fail_message='saved', """ if construct: instance = construct_instance(form, instance, fields, exclude) - + if form.errors: raise ValueError("The %s could not be %s because the data didn't" " validate." % (instance.__class__.__name__, fail_message)) - + if commit and hasattr(instance, 'save'): # see BaseDocumentForm._post_clean for an explanation #if len(form._meta._dont_save) > 0: @@ -226,10 +226,10 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, field_list = [] if isinstance(field_generator, type): field_generator = field_generator() - + if formfield_callback and not isinstance(formfield_callback, Callable): raise TypeError('formfield_callback must be a function or callable') - + for name in document._fields_ordered: f = document._fields.get(name) if isinstance(f, ObjectIdField): @@ -250,7 +250,7 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, if formfield: field_list.append((f.name, formfield)) - + field_dict = OrderedDict(field_list) if fields: field_dict = OrderedDict( @@ -267,7 +267,7 @@ def __init__(self, options=None): self.document = getattr(options, 'document', None) if self.document is None: self.document = getattr(options, 'model', None) - + self.model = self.document meta = getattr(self.document, '_meta', {}) # set up the document meta wrapper if document meta is a dict @@ -280,13 +280,13 @@ def __init__(self, options=None): self.embedded_field = getattr(options, 'embedded_field_name', None) self.formfield_generator = getattr(options, 'formfield_generator', _fieldgenerator) - + self._dont_save = [] - - self.labels = False - self.help_texts = False - + self.labels = getattr(options, 'labels', None) + self.help_texts = getattr(options, 'help_texts', None) + + class DocumentFormMetaclass(type): def __new__(cls, name, bases, attrs): formfield_callback = attrs.pop('formfield_callback', None) @@ -307,7 +307,7 @@ def __new__(cls, name, bases, attrs): if 'media' not in attrs: new_class.media = media_property(new_class) - + opts = new_class._meta = ModelFormOptions( getattr(new_class, 'Meta', None) ) @@ -315,7 +315,7 @@ def __new__(cls, name, bases, attrs): formfield_generator = getattr(opts, 'formfield_generator', _fieldgenerator) - + # If a model is defined, extract form fields from it. fields = fields_for_document(opts.document, opts.fields, opts.exclude, opts.widgets, @@ -335,19 +335,19 @@ def __new__(cls, name, bases, attrs): fields.update(declared_fields) else: fields = declared_fields - + new_class.declared_fields = declared_fields new_class.base_fields = fields return new_class - - + + class BaseDocumentForm(BaseForm): 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): - + opts = self._meta - + if instance is None: if opts.document is None: raise ValueError('A document class must be provided.') @@ -357,11 +357,11 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, else: self.instance = instance 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 will be set to True by BaseModelForm.clean(). # It is False by default so overriding self.clean() and failing to call # super will stop validate_unique from being called. @@ -428,7 +428,7 @@ def clean(self): def _post_clean(self): opts = self._meta - + # Update the model instance with self.cleaned_data. self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude) @@ -519,9 +519,9 @@ def validate_unique(self): err_dict = {f.name: [message]} self._update_errors(err_dict) errors.append(err_dict) - + return errors - + def save(self, commit=True): """ Saves this ``form``'s cleaned_data into model instance @@ -546,7 +546,7 @@ def save(self, commit=True): class DocumentForm(with_metaclass(DocumentFormMetaclass, BaseDocumentForm)): pass - + def documentform_factory(document, form=DocumentForm, fields=None, exclude=None, formfield_callback=None): @@ -588,9 +588,9 @@ def __init__(self, parent_document, data=None, files=None, position=None, self._meta.embedded_field in parent_document._fields: raise FieldError("Parent document must have field %s" % self._meta.embedded_field) - + instance = kwargs.pop('instance', None) - + if isinstance(parent_document._fields.get(self._meta.embedded_field), ListField): # if we received a list position of the instance and no instance @@ -598,7 +598,7 @@ def __init__(self, parent_document, data=None, files=None, position=None, if instance is None and position is not None: instance = getattr(parent_document, self._meta.embedded_field)[position] - + # same as above only the other way around. Note: Mongoengine # defines equality as having the same data, so if you have 2 # objects with the same data the first one will be edited. That @@ -609,13 +609,13 @@ def __init__(self, parent_document, data=None, files=None, position=None, (i for i, obj in enumerate(emb_list) if obj == instance), None ) - + super(EmbeddedDocumentForm, self).__init__(data=data, files=files, instance=instance, *args, **kwargs) self.parent_document = parent_document self.position = position - + def save(self, commit=True): """If commit is True the embedded document is added to the parent document. Otherwise the parent_document is left untouched and the @@ -625,7 +625,7 @@ def save(self, commit=True): raise ValueError("The %s could not be saved because the data" "didn't validate." % self.instance.__class__.__name__) - + if commit: field = self.parent_document._fields.get(self._meta.embedded_field) if isinstance(field, ListField) and self.position is None: @@ -728,10 +728,10 @@ def validate_unique(self): if not hasattr(form, 'cleaned_data'): continue errors += form.validate_unique() - + if errors: raise ValidationError(errors) - + def get_date_error_message(self, date_check): return ugettext("Please correct the duplicate data for %(field_name)s " "which must be unique for the %(lookup)s " @@ -766,14 +766,14 @@ def documentformset_factory(document, form=DocumentForm, class BaseInlineDocumentFormSet(BaseDocumentFormSet): """ A formset for child objects related to a parent. - + self.instance -> the document containing the inline objects """ def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=[], **kwargs): self.instance = instance self.save_as_new = save_as_new - + super(BaseInlineDocumentFormSet, self).__init__(data, files, prefix=prefix, queryset=queryset, @@ -788,7 +788,7 @@ def initial_form_count(self): def get_default_prefix(cls): return cls.document.__name__.lower() get_default_prefix = classmethod(get_default_prefix) - + def add_fields(self, form, index): super(BaseInlineDocumentFormSet, self).add_fields(form, index) @@ -839,35 +839,35 @@ def __init__(self, data=None, files=None, save_as_new=False, prefix=None, queryset=[], parent_document=None, **kwargs): if parent_document is not None: self.parent_document = parent_document - + if 'instance' in kwargs: instance = kwargs.pop('instance') if parent_document is None: self.parent_document = instance - + queryset = getattr(self.parent_document, self.form._meta.embedded_field) - + super(EmbeddedDocumentFormSet, self).__init__(data, files, save_as_new, prefix, queryset, **kwargs) - + def _construct_form(self, i, **kwargs): defaults = {'parent_document': self.parent_document} - + # add position argument to the form. Otherwise we will spend # a huge amount of time iterating over the list field on form __init__ emb_list = getattr(self.parent_document, self.form._meta.embedded_field) - + if emb_list is not None and len(emb_list) > i: defaults['position'] = i defaults.update(kwargs) - + form = super(EmbeddedDocumentFormSet, self)._construct_form( i, **defaults) return form - + @classmethod def get_default_prefix(cls): return cls.document.__name__.lower() @@ -882,13 +882,13 @@ def empty_form(self): ) self.add_fields(form, None) return form - + def save(self, commit=True): # Don't try to save the new documents. Embedded objects don't have # a save method anyway. objs = super(EmbeddedDocumentFormSet, self).save(commit=False) objs = objs or [] - + if commit and self.parent_document is not None: field = self.parent_document._fields.get(self.form._meta.embedded_field, None) if isinstance(field, EmbeddedDocumentField): @@ -900,9 +900,9 @@ def save(self, commit=True): else: setattr(self.parent_document, self.form._meta.embedded_field, objs) self.parent_document.save() - + return objs - + def _get_embedded_field(parent_doc, document, emb_name=None, can_fail=False): if emb_name: @@ -912,7 +912,7 @@ def _get_embedded_field(parent_doc, document, emb_name=None, can_fail=False): if not isinstance(field, (EmbeddedDocumentField, ListField)) or \ (isinstance(field, EmbeddedDocumentField) and field.document_type != document) or \ (isinstance(field, ListField) and - isinstance(field.field, EmbeddedDocumentField) and + isinstance(field.field, EmbeddedDocumentField) and field.field.document_type != document): raise Exception("emb_name '%s' is not a EmbeddedDocumentField or not a ListField to %s" % (emb_name, document)) elif len(emb_fields) == 0: @@ -922,7 +922,7 @@ def _get_embedded_field(parent_doc, document, emb_name=None, can_fail=False): f for f in parent_doc._fields.values() if (isinstance(field, EmbeddedDocumentField) and field.document_type == document) or \ (isinstance(field, ListField) and - isinstance(field.field, EmbeddedDocumentField) and + isinstance(field.field, EmbeddedDocumentField) and field.field.document_type == document) ] if len(emb_fields) == 1: @@ -933,9 +933,9 @@ def _get_embedded_field(parent_doc, document, emb_name=None, can_fail=False): raise Exception("%s has no EmbeddedDocumentField or ListField to %s" % (parent_doc, document)) else: raise Exception("%s has more than 1 EmbeddedDocumentField to %s" % (parent_doc, document)) - + return field - + def embeddedformset_factory(document, parent_document, form=EmbeddedDocumentForm, From 64b10d421326a20559d710690a08b3766bf51f5e Mon Sep 17 00:00:00 2001 From: Olivier Le Floch Date: Mon, 4 May 2015 17:07:29 -0700 Subject: [PATCH 132/136] Set _deferred on the model properly (OPS-4393) --- mongodbforms/documentoptions.py | 1 - mongodbforms/util.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index fe394850..fcc00b46 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -131,7 +131,6 @@ class DocumentMetaWrapper(MutableMapping): concrete_managers = [] virtual_fields = [] auto_created = False - _deferred = False def __init__(self, document, meta=None): super(DocumentMetaWrapper, self).__init__() diff --git a/mongodbforms/util.py b/mongodbforms/util.py index 7cd8498e..7684337d 100644 --- a/mongodbforms/util.py +++ b/mongodbforms/util.py @@ -15,7 +15,7 @@ from django.core.exceptions import ImproperlyConfigured from django.utils.importlib import import_module from django.utils import six - + def import_by_path(dotted_path, error_prefix=''): """ Import a dotted module path and return the attribute/class designated @@ -52,6 +52,8 @@ def load_field_generator(): def init_document_options(document): if not isinstance(document._meta, (DocumentMetaWrapper, LazyDocumentMetaWrapper)): document._meta = DocumentMetaWrapper(document) + # Workaround for Django 1.7+ + document._deferred = False return document From 8f1e0786d05215ad88b55fcfd3e33c90c17406b9 Mon Sep 17 00:00:00 2001 From: mvergerdelbove Date: Wed, 6 May 2015 15:34:30 -0700 Subject: [PATCH 133/136] compute the document pk value on the fly --- mongodbforms/documentoptions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index 0572223b..240f5448 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -204,14 +204,14 @@ def _init_pk(self): pk_field = None self.pk = PkWrapper(pk_field) - def _get_pk_val(self): - return self._pk_val - + def _get_pk_val(obj): + return obj.pk + + patch_document(_get_pk_val, self.document, False) # document is a class... + if pk_field is not None: self.pk.name = self.pk_name self.pk.attname = self.pk_name - self.document._pk_val = pk_field - patch_document(_get_pk_val, self.document) else: self.pk.fake = True # this is used in the admin and used to determine if the admin From faa39753bbabcbc06590f6d1878693144c5d6ac8 Mon Sep 17 00:00:00 2001 From: Olivier Le Floch Date: Wed, 20 May 2015 17:14:14 +0200 Subject: [PATCH 134/136] Fix deletion in admin (django 1.7+) (OPS-4455) --- mongodbforms/documentoptions.py | 24 ++++++++++++++++++++++-- mongodbforms/util.py | 2 ++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py index fcc00b46..794c58e9 100644 --- a/mongodbforms/documentoptions.py +++ b/mongodbforms/documentoptions.py @@ -122,6 +122,7 @@ class DocumentMetaWrapper(MutableMapping): has_auto_field = False object_name = None proxy = [] + proxied_children = [] parents = {} many_to_many = [] _field_cache = None @@ -168,13 +169,21 @@ def _setup_document_fields(self): if not hasattr(f, 'rel'): # need a bit more for actual reference fields here if isinstance(f, ReferenceField): + # FIXME: Probably broken in Django 1.7 f.rel = Relation(f.document_type) f.is_relation = True - elif isinstance(f, ListField) and \ - isinstance(f.field, ReferenceField): + elif isinstance(f, ListField) and isinstance(f.field, ReferenceField): + # FIXME: Probably broken in Django 1.7 f.field.rel = Relation(f.field.document_type) f.field.is_relation = True else: + f.many_to_many = None + f.many_to_one = None + f.one_to_many = None + f.one_to_one = None + f.related_model = None + + # FIXME: No longer used in Django 1.7? f.rel = None f.is_relation = False if not hasattr(f, 'verbose_name') or f.verbose_name is None: @@ -192,6 +201,8 @@ def _setup_document_fields(self): isinstance(f.document_type._meta, (DocumentMetaWrapper, LazyDocumentMetaWrapper)) and \ self.document != f.document_type: f.document_type._meta = LazyDocumentMetaWrapper(f.document_type) + if not hasattr(f, 'auto_created'): + f.auto_created = False def _init_pk(self): """ @@ -291,6 +302,9 @@ def get_field(self, name, many_to_many=True): """ return self.get_field_by_name(name)[0] + def get_fields(self, include_hidden=False): + return self.document._fields.values() + @property def swapped(self): """ @@ -355,6 +369,12 @@ def __iter__(self): def __len__(self): return self._meta.__len__() + def __cmp__(self, other): + return hash(self) == hash(other) + + def __hash__(self): + return id(self) + def get(self, key, default=None): try: return self.__getitem__(key) diff --git a/mongodbforms/util.py b/mongodbforms/util.py index 7684337d..d2876621 100644 --- a/mongodbforms/util.py +++ b/mongodbforms/util.py @@ -54,6 +54,8 @@ def init_document_options(document): document._meta = DocumentMetaWrapper(document) # Workaround for Django 1.7+ document._deferred = False + # FIXME: Wrong implementation for Relations (https://github.com/django/django/blob/master/django/db/models/base.py#L601) + document.serializable_value = lambda self, field_name: self._meta.get_field(field_name) return document From 397cd92d4266f3bd36482d265183a527b98c5257 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 7 Jul 2015 00:19:14 +0200 Subject: [PATCH 135/136] PEP-8 cleanup. DocumentFormMetaclass now inherits from DeclarativeFieldsMetaclass instead of get_declared_fields to get rid of declaration warning --- mongodbforms/documents.py | 95 +++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index cecabd0d..55142afc 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -3,7 +3,7 @@ from collections import Callable, OrderedDict from functools import reduce -from django.forms.forms import (BaseForm, get_declared_fields, +from django.forms.forms import (BaseForm, DeclarativeFieldsMetaclass, NON_FIELD_ERRORS, pretty_name) from django.forms.widgets import media_property from django.core.exceptions import FieldError @@ -87,7 +87,7 @@ def construct_instance(form, instance, fields=None, exclude=None): for f in instance._fields.values(): if isinstance(f, ObjectIdField): continue - if not f.name in cleaned_data: + if f.name not in cleaned_data: continue if fields is not None and f.name not in fields: continue @@ -176,14 +176,14 @@ def save_instance(form, instance, fields=None, fail_message='saved', if commit and hasattr(instance, 'save'): # see BaseDocumentForm._post_clean for an explanation - #if len(form._meta._dont_save) > 0: + # if len(form._meta._dont_save) > 0: # data = instance._data # new_data = dict([(n, f) for n, f in data.items() if not n \ # in form._meta._dont_save]) # instance._data = new_data # instance.save() # instance._data = data - #else: + # else: instance.save() return instance @@ -202,7 +202,7 @@ def document_to_dict(instance, fields=None, exclude=None): """ data = {} for f in instance._fields.values(): - if fields and not f.name in fields: + if fields and f.name not in fields: continue if exclude and f.name in exclude: continue @@ -234,7 +234,7 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, f = document._fields.get(name) if isinstance(f, ObjectIdField): continue - if fields and not f.name in fields: + if fields and f.name not in fields: continue if exclude and f.name in exclude: continue @@ -262,6 +262,7 @@ def fields_for_document(document, fields=None, exclude=None, widgets=None, class ModelFormOptions(object): + def __init__(self, options=None): # document class can be declared with 'document =' or 'model =' self.document = getattr(options, 'document', None) @@ -287,7 +288,7 @@ def __init__(self, options=None): self.help_texts = getattr(options, 'help_texts', None) -class DocumentFormMetaclass(type): +class DocumentFormMetaclass(DeclarativeFieldsMetaclass): def __new__(cls, name, bases, attrs): formfield_callback = attrs.pop('formfield_callback', None) try: @@ -299,7 +300,6 @@ def __new__(cls, name, bases, attrs): except NameError: # We are defining DocumentForm itself. parents = None - declared_fields = get_declared_fields(bases, attrs, False) new_class = super(DocumentFormMetaclass, cls).__new__(cls, name, bases, attrs) if not parents: @@ -324,7 +324,7 @@ def __new__(cls, name, bases, attrs): # make sure opts.fields doesn't specify an invalid field none_document_fields = [k for k, v in fields.items() if not v] missing_fields = (set(none_document_fields) - - set(declared_fields.keys())) + set(new_class.declared_fields.keys())) if missing_fields: message = 'Unknown field(s) (%s) specified for %s' message = message % (', '.join(missing_fields), @@ -332,16 +332,16 @@ def __new__(cls, name, bases, attrs): raise FieldError(message) # Override default model fields with any custom declared ones # (plus, include all the other declared fields). - fields.update(declared_fields) + fields.update(new_class.declared_fields) else: - fields = declared_fields + fields = new_class.declared_fields - new_class.declared_fields = declared_fields new_class.base_fields = fields return new_class class BaseDocumentForm(BaseForm): + 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): @@ -445,7 +445,7 @@ def _post_clean(self): # that are not required. Clean them up here, though # this is maybe not the right place :-) setattr(self.instance, f.name, None) - #opts._dont_save.append(f.name) + # opts._dont_save.append(f.name) except ValidationError as e: err = {f.name: [e.message]} self._update_errors(err) @@ -582,10 +582,11 @@ def documentform_factory(document, form=DocumentForm, fields=None, class EmbeddedDocumentForm(with_metaclass(DocumentFormMetaclass, BaseDocumentForm)): + def __init__(self, parent_document, data=None, files=None, position=None, *args, **kwargs): - if self._meta.embedded_field is not None and not \ - self._meta.embedded_field in parent_document._fields: + if self._meta.embedded_field is not None and \ + self._meta.embedded_field not in parent_document._fields: raise FieldError("Parent document must have field %s" % self._meta.embedded_field) @@ -658,16 +659,17 @@ def save(self, commit=True): class BaseDocumentFormSet(BaseFormSet): + """ A ``FormSet`` for editing a queryset and/or adding new objects to it. """ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, - queryset=None, **kwargs): + queryset=[], **kwargs): + if not isinstance(queryset, (list, BaseQuerySet)): queryset = [queryset] self.queryset = queryset - self._queryset = self.queryset self.initial = self.construct_initial() defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix, 'initial': self.initial} @@ -690,7 +692,7 @@ def initial_form_count(self): return super(BaseDocumentFormSet, self).initial_form_count() def get_queryset(self): - qs = self._queryset or [] + qs = self.queryset or [] return qs def save_object(self, form): @@ -704,7 +706,7 @@ def save(self, commit=True): """ saved = [] for form in self.forms: - if not form.has_changed() and not form in self.initial_forms: + if not form.has_changed() and form not in self.initial_forms: continue obj = self.save_object(form) if form.cleaned_data.get("DELETE", False): @@ -764,11 +766,13 @@ def documentformset_factory(document, form=DocumentForm, class BaseInlineDocumentFormSet(BaseDocumentFormSet): + """ A formset for child objects related to a parent. self.instance -> the document containing the inline objects """ + def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=[], **kwargs): self.instance = instance @@ -784,7 +788,7 @@ def initial_form_count(self): return 0 return super(BaseInlineDocumentFormSet, self).initial_form_count() - #@classmethod + # @classmethod def get_default_prefix(cls): return cls.document.__name__.lower() get_default_prefix = classmethod(get_default_prefix) @@ -797,7 +801,7 @@ def add_fields(self, form, index): if form._meta.fields: if isinstance(form._meta.fields, tuple): form._meta.fields = list(form._meta.fields) - #form._meta.fields.append(self.fk.name) + # form._meta.fields.append(self.fk.name) def get_unique_error_message(self, unique_check): unique_check = [ @@ -835,6 +839,7 @@ def inlineformset_factory(document, form=DocumentForm, class EmbeddedDocumentFormSet(BaseDocumentFormSet): + def __init__(self, data=None, files=None, save_as_new=False, prefix=None, queryset=[], parent_document=None, **kwargs): if parent_document is not None: @@ -890,15 +895,18 @@ def save(self, commit=True): objs = objs or [] if commit and self.parent_document is not None: - field = self.parent_document._fields.get(self.form._meta.embedded_field, None) + field = self.parent_document._fields.get( + self.form._meta.embedded_field, None) if isinstance(field, EmbeddedDocumentField): try: obj = objs[0] except IndexError: obj = None - setattr(self.parent_document, self.form._meta.embedded_field, obj) + setattr( + self.parent_document, self.form._meta.embedded_field, obj) else: - setattr(self.parent_document, self.form._meta.embedded_field, objs) + setattr( + self.parent_document, self.form._meta.embedded_field, objs) self.parent_document.save() return objs @@ -906,33 +914,43 @@ def save(self, commit=True): def _get_embedded_field(parent_doc, document, emb_name=None, can_fail=False): if emb_name: - emb_fields = [f for f in parent_doc._fields.values() if f.name == emb_name] + emb_fields = [ + f for f in parent_doc._fields.values() if f.name == emb_name] if len(emb_fields) == 1: field = emb_fields[0] if not isinstance(field, (EmbeddedDocumentField, ListField)) or \ - (isinstance(field, EmbeddedDocumentField) and field.document_type != document) or \ - (isinstance(field, ListField) and - isinstance(field.field, EmbeddedDocumentField) and - field.field.document_type != document): - raise Exception("emb_name '%s' is not a EmbeddedDocumentField or not a ListField to %s" % (emb_name, document)) + (isinstance(field, EmbeddedDocumentField) and + field.document_type != document) or \ + (isinstance(field, ListField) and + isinstance(field.field, EmbeddedDocumentField) and + field.field.document_type != document): + raise Exception( + "emb_name '%s' is not a EmbeddedDocumentField or not a ListField to %s" % ( + emb_name, document + ) + ) elif len(emb_fields) == 0: - raise Exception("%s has no field named '%s'" % (parent_doc, emb_name)) + raise Exception("%s has no field named '%s'" % + (parent_doc, emb_name)) else: emb_fields = [ f for f in parent_doc._fields.values() - if (isinstance(field, EmbeddedDocumentField) and field.document_type == document) or \ - (isinstance(field, ListField) and - isinstance(field.field, EmbeddedDocumentField) and - field.field.document_type == document) + if (isinstance(field, EmbeddedDocumentField) and + field.document_type == document) or + (isinstance(field, ListField) and + isinstance(field.field, EmbeddedDocumentField) and + field.field.document_type == document) ] if len(emb_fields) == 1: field = emb_fields[0] elif len(emb_fields) == 0: if can_fail: return - raise Exception("%s has no EmbeddedDocumentField or ListField to %s" % (parent_doc, document)) + raise Exception( + "%s has no EmbeddedDocumentField or ListField to %s" % (parent_doc, document)) else: - raise Exception("%s has more than 1 EmbeddedDocumentField to %s" % (parent_doc, document)) + raise Exception( + "%s has more than 1 EmbeddedDocumentField to %s" % (parent_doc, document)) return field @@ -950,7 +968,8 @@ def embeddedformset_factory(document, parent_document, You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` to ``parent_model``. """ - emb_field = _get_embedded_field(parent_document, document, emb_name=embedded_name) + emb_field = _get_embedded_field( + parent_document, document, emb_name=embedded_name) if isinstance(emb_field, EmbeddedDocumentField): max_num = 1 kwargs = { From 9d7e48d88f613d517fe1f6d359cbc6ad10850bb5 Mon Sep 17 00:00:00 2001 From: Jan Schrewe Date: Tue, 7 Jul 2015 12:33:16 +0200 Subject: [PATCH 136/136] =?UTF-8?q?Figure=20out=20a=20decent=20=E2=80=98qu?= =?UTF-8?q?eryset=E2=80=99=20in=20EmbeddedDocumentFormSet.=5F=5Finit=5F=5F?= =?UTF-8?q?=20for=20a=20single=20EmbeddedDocumentField?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mongodbforms/documents.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mongodbforms/documents.py b/mongodbforms/documents.py index 55142afc..0a432a89 100644 --- a/mongodbforms/documents.py +++ b/mongodbforms/documents.py @@ -666,7 +666,6 @@ class BaseDocumentFormSet(BaseFormSet): def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, queryset=[], **kwargs): - if not isinstance(queryset, (list, BaseQuerySet)): queryset = [queryset] self.queryset = queryset @@ -842,6 +841,7 @@ class EmbeddedDocumentFormSet(BaseDocumentFormSet): def __init__(self, data=None, files=None, save_as_new=False, prefix=None, queryset=[], parent_document=None, **kwargs): + if parent_document is not None: self.parent_document = parent_document @@ -850,8 +850,11 @@ def __init__(self, data=None, files=None, save_as_new=False, if parent_document is None: self.parent_document = instance - queryset = getattr(self.parent_document, - self.form._meta.embedded_field) + queryset = getattr(self.parent_document, self.form._meta.embedded_field) + if not isinstance(queryset, list) and queryset is None: + queryset = [] + elif not isinstance(queryset, list): + queryset = [queryset, ] super(EmbeddedDocumentFormSet, self).__init__(data, files, save_as_new, prefix, queryset, @@ -968,8 +971,7 @@ def embeddedformset_factory(document, parent_document, You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` to ``parent_model``. """ - emb_field = _get_embedded_field( - parent_document, document, emb_name=embedded_name) + emb_field = _get_embedded_field(parent_document, document, emb_name=embedded_name) if isinstance(emb_field, EmbeddedDocumentField): max_num = 1 kwargs = {