diff --git a/.gitignore b/.gitignore
index 669100e2..03e61266 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,5 +7,9 @@ MANIFEST
build/*
*egg*
*.bak
+.settings/*
+*.tmproj
+.tm_properties
+
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
diff --git a/README.txt b/README.txt
index 803d2419..22b35f64 100644
--- a/README.txt
+++ b/README.txt
@@ -7,7 +7,38 @@ documents.
Requirements
------------
-- `mongoengine `_
+- Django >= 1.4
+- `mongoengine `__ >= 0.8.3
+
+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
-----
@@ -20,7 +51,7 @@ Normal documents
To use mongodbforms with normal documents replace djangos forms with
mongodbform forms.
-::
+.. code:: python
from mongodbforms import DocumentForm
@@ -32,64 +63,84 @@ 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 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 the embedded field is a plain embedded field the current object is
+simply overwritten.
-::
+.. code:: python
# forms.py
from mongodbforms import EmbeddedDocumentForm
-
+
class MessageForm(EmbeddedDocumentForm):
class Meta:
document = Message
embedded_field_name = 'messages'
-
+
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
-------------
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
~~~~~~~~~~~~~~~~~~~~~
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.
+
+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``.
-If you want to use your own generator class you can use the
-``formfield_generator`` option on the form's Meta class.
+.. code:: python
-::
+ # settings.py
+
+ # set the fieldgeneretor for the whole application
+ MONGODBFORMS_FIELDGENERATOR = 'myproject.fieldgenerator.GeneratorClass'
# generator.py
from mongodbforms.fieldgenerator import MongoFormFieldGenerator
-
+
class MyFieldGenerator(MongoFormFieldGenerator):
...
# forms.py
from mongodbforms import DocumentForm
-
+
from generator import MyFieldGenerator
-
+
class MessageForm(DocumentForm):
class Meta:
formfield_generator = MyFieldGenerator
diff --git a/mongodbforms/documentoptions.py b/mongodbforms/documentoptions.py
index 17e175a2..7cf4f388 100644
--- a/mongodbforms/documentoptions.py
+++ b/mongodbforms/documentoptions.py
@@ -1,126 +1,253 @@
import sys
from collections import MutableMapping
+from types import MethodType
from django.db.models.fields import FieldDoesNotExist
-from django.db.models.options import get_verbose_name
from django.utils.text import capfirst
+from django.utils.functional import LazyObject, new_method_proxy
+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.conf import settings
+from mongoengine.fields import ReferenceField, ListField
+
+
+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 = camel_case_to_spaces(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, LazyDocumentMetaWrapper)):
+ self._to._meta = DocumentMetaWrapper(self._to)
+ return self._to
+
+ @to.setter
+ def to(self, value):
+ self._to = value
-from mongoengine.fields import ReferenceField
class PkWrapper(object):
+ editable = False
+ fake = False
+
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)
+
+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)
+
+ __len__ = new_method_proxy(len)
+
+ @new_method_proxy
+ def __contains__(self, key):
+ return key in self
+
+
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.
"""
- _pk = None
+ # 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
proxy = []
+ proxied_children = []
parents = {}
many_to_many = []
_field_cache = None
document = None
_meta = None
-
- def __init__(self, document):
+ concrete_model = None
+ concrete_managers = []
+ virtual_fields = []
+ auto_created = False
+
+ def __init__(self, document, meta=None):
+ super(DocumentMetaWrapper, self).__init__()
+
self.document = document
- self._meta = getattr(document, '_meta', {})
-
+ # used by Django to distinguish between abstract and concrete models
+ # here for now always the document
+ self.concrete_model = document
+ if meta is None:
+ meta = getattr(document, '_meta', {})
+ if isinstance(meta, LazyDocumentMetaWrapper):
+ meta = meta._meta
+ self._meta = meta
+
try:
self.object_name = self.document.__name__
except AttributeError:
self.object_name = self.document.__class__.__name__
-
- self.module_name = self.object_name.lower()
-
- # EmbeddedDocuments don't have an id field.
- try:
+
+ 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()
+ # 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()
+
+ 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
+ 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):
+ # 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:
+ 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) and not \
+ 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):
+ """
+ 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 'id_field' in self._meta:
self.pk_name = self._meta['id_field']
- self._init_pk()
- except KeyError:
- pass
-
+ pk_field = getattr(self.document, self.pk_name)
+ else:
+ pk_field = None
+ self.pk = PkWrapper(pk_field)
+
+ 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
+ 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):
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
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.
+
+ Checks the original meta dict first. If it is not found
+ then generates a verbose name from the object name.
"""
if self._verbose_name is None:
- try:
- self._verbose_name = capfirst(get_verbose_name(self._meta['verbose_name']))
- except KeyError:
- self._verbose_name = capfirst(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
def verbose_name_raw(self):
return self.verbose_name
-
+
@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():
- 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()
@@ -129,10 +256,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
@@ -142,59 +269,78 @@ 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:
- 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():
- 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)
+ if name in self.document._fields:
+ field = self.document._fields[name]
+ if isinstance(field, ReferenceField):
+ return (field, field.document_type, False, False)
else:
- self._field_cache[f.name] = (f, None, True, False)
-
- return self._field_cache
-
+ return (field, None, True, False)
+ 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]
-
+
+ def get_fields(self, include_hidden=False):
+ return self.document._fields.values()
+
+ @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):
+ 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):
+
+ 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
@@ -207,15 +353,21 @@ 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)
except KeyError:
- return default
-
+ 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 227b5c12..0a432a89 100644
--- a/mongodbforms/documents.py
+++ b/mongodbforms/documents.py
@@ -1,41 +1,77 @@
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.forms import (BaseForm, DeclarativeFieldsMetaclass,
+ 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, ImageField
+from mongoengine.fields import (ObjectIdField, ListField, ReferenceField,
+ FileField, MapField, EmbeddedDocumentField)
try:
from mongoengine.base import ValidationError
except ImportError:
from mongoengine.errors import ValidationError
-from mongoengine.connection import _get_db
+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
+
from gridfs import GridFS
-from .fieldgenerator import MongoDefaultFormFieldGenerator
-from .documentoptions import DocumentMetaWrapper
+from mongodbforms.documentoptions import DocumentMetaWrapper
+from mongodbforms.util import with_metaclass, load_field_generator
-from .util import with_metaclass
+_fieldgenerator = load_field_generator()
-def _get_unique_filename(name):
- fs = GridFS(_get_db())
- file_root, file_ext = os.path.splitext(name)
+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(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 construct_instance(form, instance, fields=None, exclude=None, ignore=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.
+ """
+ # 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)
+
+ 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
+
+
+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
@@ -43,15 +79,15 @@ def construct_instance(form, instance, fields=None, exclude=None, ignore=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
- 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
@@ -59,34 +95,70 @@ 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)
+ 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)
+ 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
+ file_obj = _save_iterator_file(f, instance,
+ 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)
+ upload = cleaned_data[f.name]
+ if upload is None:
+ continue
+
+ try:
+ upload.file.seek(0)
+ # 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)
+ 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
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``.
@@ -94,27 +166,28 @@ 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))
-
+ " validate." % (instance.__class__.__name__,
+ fail_message))
+
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:
- instance._changed_fields.remove(field)
- 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
@@ -129,16 +202,17 @@ 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
- 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, \
- formfield_callback=None, field_generator=MongoDefaultFormFieldGenerator):
+
+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.
@@ -150,22 +224,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()
-
- # 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:
+
+ 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):
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 f.name not in fields:
continue
if exclude and f.name in exclude:
continue
@@ -174,75 +243,88 @@ 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)
+ field_dict = OrderedDict(field_list)
if fields:
- field_dict = SortedDict(
+ field_dict = OrderedDict(
[(f, field_dict.get(f)) for f in fields
- if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored)]
+ if ((not exclude) or (exclude and f not in exclude))]
)
- return field_dict
+ return field_dict
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)
-
+
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)
self.embedded_field = getattr(options, 'embedded_field_name', None)
- self.formfield_generator = getattr(options, 'formfield_generator', MongoDefaultFormFieldGenerator)
-
-
-class DocumentFormMetaclass(type):
+ self.formfield_generator = getattr(options, 'formfield_generator',
+ _fieldgenerator)
+
+ self._dont_save = []
+
+ self.labels = getattr(options, 'labels', None)
+ self.help_texts = getattr(options, 'help_texts', None)
+
+
+class DocumentFormMetaclass(DeclarativeFieldsMetaclass):
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', 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,
- 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(new_class.declared_fields.keys()))
if missing_fields:
message = 'Unknown field(s) (%s) specified for %s'
message = message % (', '.join(missing_fields),
@@ -250,53 +332,55 @@ 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
-
- new_class.declared_fields = declared_fields
+ fields = new_class.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('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 = {}
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.
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):
"""
@@ -308,23 +392,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
@@ -334,7 +417,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
@@ -345,40 +428,48 @@ 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)
+ self.instance = construct_instance(self, self.instance, opts.fields,
+ opts.exclude)
+ changed_fields = getattr(self.instance, '_changed_fields', [])
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 == '':
+ 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 :-)
- to_delete.append(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)
-
- # 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'):
- try:
- self.instance.clean()
- except ValidationError as e:
- self._update_errors({NON_FIELD_ERRORS: e.messages})
+
+ # 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
+ self.instance._fields_ordered = tuple(
+ [f for f in original_fields if f not in exclude]
+ )
+ 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:
@@ -394,28 +485,42 @@ 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)
- # Exclude the current object from the query if we are editing an
- # instance (as opposed to creating a new one)
+ 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 our list
+ if isinstance(u_with_field, ListField) and \
+ isinstance(u_with_field.field, ReferenceField):
+ 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.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:
qs = qs.filter(pk__ne=self.instance.pk)
- if len(qs) > 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))
- }
+ if qs.count() > 0:
+ 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)
-
+
return errors
-
-
def save(self, commit=True):
"""
@@ -433,16 +538,18 @@ 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
+
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:
@@ -473,52 +580,97 @@ 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, *args, **kwargs):
- super(EmbeddedDocumentForm, self).__init__(*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 \
+ self._meta.embedded_field not 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
+ # 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
+ # 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
+ )
+
+ super(EmbeddedDocumentForm, self).__init__(data=data, files=files,
+ instance=instance, *args,
+ **kwargs)
self.parent_document = parent_document
- 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)
-
+ 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
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 field.default is None:
- default = []
+ 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
+ })
+ 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:
- 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
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,
+ defaults = {'data': data, 'files': files, 'auto_id': auto_id,
'prefix': prefix, 'initial': self.initial}
defaults.update(kwargs)
super(BaseDocumentFormSet, self).__init__(**defaults)
@@ -529,7 +681,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):
@@ -539,7 +691,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)
@@ -549,23 +702,22 @@ 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:
+ 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):
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):
@@ -577,13 +729,14 @@ 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 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]),
@@ -592,15 +745,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
@@ -609,28 +765,32 @@ def documentformset_factory(document, form=DocumentForm, formfield_callback=None
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)
+
+ 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
+ # @classmethod
def get_default_prefix(cls):
- return cls.model.__name__.lower()
+ 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)
@@ -640,18 +800,22 @@ 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 = [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.
@@ -673,174 +837,49 @@ 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(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
- super(EmbeddedDocumentFormSet, self).__init__(data, files, instance, save_as_new, prefix, queryset, **kwargs)
-
+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
+
+ 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)
+ 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,
+ **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)
+
+ 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):
form = self.form(
@@ -851,46 +890,90 @@ 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:
- 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 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 [])
+ 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
+ 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 == 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))
+ 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,
- 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,
+ embedded_name=None,
+ fields=None, exclude=None,
+ extra=3, 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``.
"""
+ 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,
@@ -903,5 +986,5 @@ def embeddedformset_factory(document, parent_document, form=EmbeddedDocumentForm
'max_num': max_num,
}
FormSet = documentformset_factory(document, **kwargs)
- FormSet.parent_document = parent_document
+ FormSet.form._meta.embedded_field = emb_field.name
return FormSet
diff --git a/mongodbforms/fieldgenerator.py b/mongodbforms/fieldgenerator.py
index 7011fdd9..f4d842e3 100644
--- a/mongodbforms/fieldgenerator.py
+++ b/mongodbforms/fieldgenerator.py
@@ -4,9 +4,10 @@
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
+from django.core.validators import EMPTY_VALUES, RegexValidator
try:
from django.utils.encoding import smart_text as smart_unicode
except ImportError:
@@ -14,34 +15,92 @@
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
+from mongoengine import (ReferenceField as MongoReferenceField,
+ EmbeddedDocumentField as MongoEmbeddedDocumentField,
+ ListField as MongoListField,
+ MapField as MongoMapField)
-from .fields import MongoCharField, ReferenceField, DocumentMultipleChoiceField, ListField
+from mongodbforms.fields import (MongoCharField, MongoEmailField,
+ MongoURLField, ReferenceField,
+ DocumentMultipleChoiceField, ListField,
+ MapField)
+from mongodbforms.widgets import Html5SplitDateTimeWidget
+from mongodbforms.documentoptions import create_verbose_name
BLANK_CHOICE_DASH = [("", "---------")]
+
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.
+ generator_map = {
+ 'sortedlistfield': 'generate_listfield',
+ 'longfield': 'generate_intfield',
+ }
+
+ form_field_map = {
+ 'stringfield': MongoCharField,
+ 'stringfield_choices': forms.TypedChoiceField,
+ 'stringfield_long': MongoCharField,
+ 'emailfield': MongoEmailField,
+ 'urlfield': MongoURLField,
+ 'intfield': forms.IntegerField,
+ 'intfield_choices': forms.TypedChoiceField,
+ 'floatfield': forms.FloatField,
+ 'decimalfield': forms.DecimalField,
+ 'booleanfield': forms.BooleanField,
+ 'booleanfield_choices': forms.TypedChoiceField,
+ 'datetimefield': forms.SplitDateTimeField,
+ '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.
"""
- field_name = field.__class__.__name__.lower()
- if hasattr(self, 'generate_%s' % field_name):
- return getattr(self, 'generate_%s' % field_name)(field, **kwargs)
+ # 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
+
+ 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()
- if hasattr(self, 'generate_%s' % cls_name):
- return getattr(self, 'generate_%s' % cls_name)(field, **kwargs)
-
- raise NotImplementedError('%s is not supported by MongoForm' % \
- field.__class__.__name__)
+
+ 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:
+ attr = self.generator_map.get(cls_name)
+ return getattr(self, attr)(field, **kwargs)
+
+ 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):
@@ -65,220 +124,278 @@ 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 capfirst(create_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 ''
+
+ def get_field_default(self, field):
+ if isinstance(field, (MongoListField, MongoMapField)):
+ 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:
+ d['initial'] = field.default
+ return f.default
+
+ 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
-
+ defaults = {
+ 'label': self.get_field_label(field),
+ 'initial': self.get_field_default(field),
+ 'required': field.required,
+ 'help_text': self.get_field_help_text(field),
+ }
+ if field.choices:
+ map_key = 'stringfield_choices'
+ defaults.update({
+ 'choices': self.get_field_choices(field),
+ 'coerce': self.string_field,
+ })
+ 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['validators'] = [RegexValidator(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,
'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)
}
-
+ 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,
'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)
+ '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):
+ 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:
- defaults = {
+ map_key = 'intfield_choices'
+ defaults.update({
'coerce': self.integer_field,
'empty_value': None,
- 'required': field.required,
- 'initial': field.default,
- 'label': self.get_field_label(field),
'choices': self.get_field_choices(field),
- 'help_text': self.get_field_help_text(field)
- }
-
- defaults.update(kwargs)
- return forms.TypedChoiceField(**defaults)
+ })
else:
- defaults = {
- 'required': field.required,
+ map_key = 'intfield'
+ defaults.update({
'min_value': field.min_value,
'max_value': field.max_value,
- 'initial': field.default,
- 'label': self.get_field_label(field),
- 'help_text': self.get_field_help_text(field)
- }
-
- defaults.update(kwargs)
- 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': self.get_field_default(field),
+ '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': self.get_field_default(field),
+ '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)
+ defaults.update(self.check_widget(map_key))
defaults.update(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:
- defaults = {
+ map_key = 'booleanfield_choices'
+ defaults.update({
'coerce': self.boolean_field,
'empty_value': None,
- 'required': field.required,
- 'initial': field.default,
- 'label': self.get_field_label(field),
'choices': self.get_field_choices(field),
- 'help_text': self.get_field_help_text(field)
- }
-
- defaults.update(kwargs)
- return forms.TypedChoiceField(**defaults)
+ })
else:
- defaults = {
- 'required': field.required,
- 'initial': field.default,
- 'label': self.get_field_label(field),
- 'help_text': self.get_field_help_text(field)
- }
-
- defaults.update(kwargs)
- return forms.BooleanField(**defaults)
+ map_key = 'booleanfield'
+ 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,
+ 'initial': self.get_field_default(field),
'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.clone(),
}
-
+ 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
+
+ defaults = {
+ 'label': self.get_field_label(field),
+ 'help_text': self.get_field_help_text(field),
+ 'required': field.required,
+ }
if field.field.choices:
- defaults = {
+ map_key = 'listfield_choices'
+ defaults.update({
'choices': field.field.choices,
- 'required': field.required,
- 'label': self.get_field_label(field),
- 'help_text': self.get_field_help_text(field),
'widget': forms.CheckboxSelectMultiple
- }
-
- defaults.update(kwargs)
- return forms.MultipleChoiceField(**defaults)
+ })
elif isinstance(field.field, MongoReferenceField):
- defaults = {
- 'label': self.get_field_label(field),
- 'help_text': self.get_field_help_text(field),
- 'required': field.required
- }
-
- defaults.update(kwargs)
- f = DocumentMultipleChoiceField(field.field.document_type.objects, **defaults)
- return f
- 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
+ map_key = 'listfield_references'
+ defaults.update({
+ 'queryset': field.field.document_type.objects.clone(),
+ })
+ else:
+ map_key = 'listfield'
form_field = self.generate(field.field)
- f = ListField(form_field.__class__, **defaults)
- return f
+ defaults.update({
+ 'contained_field': form_field.__class__,
+ })
+ 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):
+ # 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 = {
+ 'label': self.get_field_label(field),
+ 'help_text': self.get_field_help_text(field),
+ 'required': field.required,
+ 'contained_field': form_field.__class__,
+ }
+ 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_filefield(self, field, **kwargs):
+ map_key = 'filefield'
defaults = {
- 'required':field.required,
- 'label':self.get_field_label(field),
- 'initial': field.default,
+ 'required': field.required,
+ 'label': self.get_field_label(field),
+ 'initial': self.get_field_default(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.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,
+ 'required': field.required,
+ 'label': self.get_field_label(field),
+ 'initial': self.get_field_default(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.ImageField(**defaults)
+ return form_class(**defaults)
class MongoDefaultFormFieldGenerator(MongoFormFieldGenerator):
@@ -290,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.
@@ -308,3 +426,39 @@ 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)
+ 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 {}
diff --git a/mongodbforms/fields.py b/mongodbforms/fields.py
index 078b6e90..6f4817d4 100644
--- a/mongodbforms/fields.py
+++ b/mongodbforms/fields.py
@@ -4,9 +4,11 @@
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
+from django.core.validators import (EMPTY_VALUES, MinLengthValidator,
+ MaxLengthValidator)
try:
from django.utils.encoding import force_text as force_unicode
@@ -26,13 +28,12 @@
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 .widgets import ListWidget
+from mongodbforms.widgets import ListWidget, MapWidget, HiddenMapWidget
+
class MongoChoiceIterator(object):
def __init__(self, field):
@@ -50,32 +51,55 @@ 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="---------",
- *aargs, **kwaargs):
-
- forms.Field.__init__(self, *aargs, **kwaargs)
- self.queryset = queryset
+ def __init__(self, queryset, empty_label="---------", *args, **kwargs):
+ forms.Field.__init__(self, *args, **kwargs)
self.empty_label = empty_label
+ self.queryset = queryset
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):
@@ -86,47 +110,42 @@ 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):
"""
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)
def clean(self, value):
- # Check for empty values.
+ # 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
+ 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})
+ 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):
"""A MultipleChoiceField whose choices are a model QuerySet."""
widget = forms.SelectMultiple
@@ -139,7 +158,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:
@@ -149,21 +170,19 @@ def clean(self, value):
if not isinstance(value, (list, tuple)):
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])
+ 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)
+ 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)
@@ -171,43 +190,38 @@ 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)
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 = ListWidget
+ hidden_widget = forms.MultipleHiddenInput
- def __init__(self, field_type, *args, **kwargs):
- self.field_type = field_type
- widget = self.field_type().widget
- if isinstance(widget, type):
- w_type = widget
+ def __init__(self, contained_field, *args, **kwargs):
+ if 'widget' in kwargs:
+ self.widget = kwargs.pop('widget')
+
+ if isinstance(contained_field, type):
+ contained_widget = contained_field().widget
else:
- w_type = widget.__class__
- self.widget = self.widget(w_type)
+ contained_widget = contained_field.widget
+
+ if isinstance(contained_widget, type):
+ contained_widget = contained_widget()
+ self.widget = self.widget(contained_widget)
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)
@@ -215,18 +229,12 @@ 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 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:
@@ -234,19 +242,16 @@ def clean(self, value):
else:
raise ValidationError(self.error_messages['invalid'])
- 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))
+ 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)
@@ -258,9 +263,138 @@ 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
+
+ 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:
+ 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.'),
+ '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')
+
+ if isinstance(contained_field, type):
+ contained_widget = contained_field().widget
+ else:
+ contained_widget = contained_field.widget
+
+ if isinstance(contained_widget, type):
+ contained_widget = contained_widget()
+ self.widget = self.widget(contained_widget)
+
+ 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)))
+ 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
+ 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):
+ 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
+ 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 = 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
+ # 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 self.contained_field.required:
+ self.contained_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):
+ for k, v in data.items():
+ if initial is None:
+ init_val = ''
+ else:
+ try:
+ init_val = initial[k]
+ except KeyError:
+ return True
+ if self.contained_field._has_changed(init_val, v):
+ return True
+ return False
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')
diff --git a/mongodbforms/util.py b/mongodbforms/util.py
index 4ca4e7b8..d2876621 100644
--- a/mongodbforms/util.py
+++ b/mongodbforms/util.py
@@ -1,21 +1,88 @@
-from types import MethodType
+from collections import defaultdict
-from .documentoptions import DocumentMetaWrapper
+from django.conf import settings
+
+from mongodbforms.documentoptions import DocumentMetaWrapper, LazyDocumentMetaWrapper
+from mongodbforms.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))
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, LazyDocumentMetaWrapper)):
+ 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
+
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 "
#
@@ -28,8 +95,8 @@ def get_document_options(document):
# 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 4dd43ead..77acbc12 100644
--- a/mongodbforms/widgets.py
+++ b/mongodbforms/widgets.py
@@ -1,112 +1,178 @@
import copy
-from django.forms.widgets import Widget, Media
+from django.forms.widgets import (Widget, Media, TextInput,
+ SplitDateTimeWidget, DateInput, TimeInput,
+ MultiWidget, HiddenInput)
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 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)
-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.
+ 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
- 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(ListWidget, self).__init__(attrs)
+class ListWidget(BaseContainerWidget):
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)
-
- 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 = []
+ value = [] if value is None else value
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.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.widget_type()
+ widget = self.data_widget
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
- 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)
+class MapWidget(BaseContainerWidget):
+ def __init__(self, data_widget, attrs=None):
+ self.key_widget = TextInput()
+ self.key_widget.is_localized = self.is_localized
+ super(MapWidget, self).__init__(data_widget, 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 = {}
+
+ # 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)
+ )
+ group = []
+ if not self.is_hidden:
+ group.append(mark_safe(''))
+
+ output.append(mark_safe(''.join(group)))
+ return mark_safe(self.format_output(output))
+
+ 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 _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 for a multiwidget is the combination of all media of
+ the subwidgets.
+ """
+ media = super(MapWidget, self)._get_media()
+ media = media + self.key_widget.media
return media
media = property(_get_media)
def __deepcopy__(self, memo):
- obj = super(ListWidget, self).__deepcopy__(memo)
- obj.widgets = copy.deepcopy(self.widgets)
- obj.widget_type = copy.deepcopy(self.widget_type)
+ obj = super(MapWidget, self).__deepcopy__(memo)
+ obj.key_widget = copy.deepcopy(self.key_widget)
return obj
+
+
+class HiddenMapWidget(MapWidget):
+ is_hidden = True
-
-
\ No newline at end of file
+ def __init__(self, attrs=None):
+ data_widget = HiddenInput()
+ super(MapWidget, self).__init__(data_widget, attrs)
+ self.key_widget = HiddenInput()
diff --git a/readme.md b/readme.md
index 318dda40..17e8c781 100644
--- a/readme.md
+++ b/readme.md
@@ -4,8 +4,22 @@ 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
+
+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
@@ -15,29 +29,39 @@ 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
-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 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 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 the embedded field is a plain embedded field the current object is simply 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',]
+ fields = ['subject', 'sender', 'message',]
+
+# views.py
- # views.py
- form = MessageForm(parent_document=some_document, ...)
+# 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
@@ -45,24 +69,33 @@ 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.
+
+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
-If you want to use your own generator class you can use the ``formfield_generator`` option on the form's Meta class.
+# set the fieldgeneretor for the whole application
+MONGODBFORMS_FIELDGENERATOR = 'myproject.fieldgenerator.GeneratorClass'
- # generator.py
- from mongodbforms.fieldgenerator import MongoFormFieldGenerator
+# 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
+```
diff --git a/setup.py b/setup.py
index a41ae33d..91eb0b14 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.1.5',
+ version='0.3.1',
description="An implementation of django forms using mongoengine.",
author='Jan Schrewe',
author_email='jan@schafproductions.com',
@@ -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',],
)