Source code for flask_mongoengine.documents

"""Extended version of :mod:`mongoengine.document`."""
import logging
from typing import Dict, List, Optional, Type, Union

import mongoengine
from flask import abort
from mongoengine.errors import DoesNotExist
from mongoengine.queryset import QuerySet

from flask_mongoengine.decorators import wtf_required
from flask_mongoengine.pagination import ListFieldPagination, Pagination

try:
    from flask_mongoengine.wtf.models import ModelForm
except ImportError:  # pragma: no cover
    ModelForm = None
logger = logging.getLogger("flask_mongoengine")


[docs]class BaseQuerySet(QuerySet): """Extends :class:`~mongoengine.queryset.QuerySet` class with handly methods."""
[docs] def _abort_404(self, _message_404): """Returns 404 error with message, if message provided. :param _message_404: Message for 404 comment """ abort(404, _message_404) if _message_404 else abort(404)
[docs] def get_or_404(self, *args, _message_404=None, **kwargs): """Get a document and raise a 404 Not Found error if it doesn't exist. :param _message_404: Message for 404 comment, not forwarded to :func:`~mongoengine.queryset.QuerySet.get` :param args: args list, silently forwarded to :func:`~mongoengine.queryset.QuerySet.get` :param kwargs: keywords arguments, silently forwarded to :func:`~mongoengine.queryset.QuerySet.get` """ try: return self.get(*args, **kwargs) except DoesNotExist: self._abort_404(_message_404)
[docs] def first_or_404(self, _message_404=None): """ Same as :func:`~BaseQuerySet.get_or_404`, but uses :func:`~mongoengine.queryset.QuerySet.first`, not :func:`~mongoengine.queryset.QuerySet.get`. :param _message_404: Message for 404 comment, not forwarded to :func:`~mongoengine.queryset.QuerySet.get` """ return self.first() or self._abort_404(_message_404)
[docs] def paginate(self, page, per_page): """ Paginate the QuerySet with a certain number of docs per page and return docs for a given page. """ return Pagination(self, page, per_page)
[docs] def paginate_field(self, field_name, doc_id, page, per_page, total=None): """ Paginate items within a list field from one document in the QuerySet. """ # TODO this doesn't sound useful at all - remove in next release? item = self.get(id=doc_id) count = getattr(item, f"{field_name}_count", "") total = total or count or len(getattr(item, field_name)) return ListFieldPagination( self, doc_id, field_name, page, per_page, total=total )
[docs]class WtfFormMixin: """Special mixin, for form generation functions."""
[docs] @classmethod def _get_fields_names( cls: Union["WtfFormMixin", mongoengine.document.BaseDocument], only: Optional[List[str]], exclude: Optional[List[str]], ): """ Filter fields names for further form generation. :param only: An optional iterable with the property names that should be included in the form. Only these properties will have fields. Fields are always appear in provided order, this allows user to change form fields ordering, without changing database model. :param exclude: An optional iterable with the property names that should be excluded from the form. All other properties will have fields. Fields are appears in order, defined in model, excluding provided fields names. For adjusting fields ordering, use :attr:`only`. """ field_names = cls._fields_ordered if only: field_names = [field for field in only if field in field_names] elif exclude: field_names = [field for field in field_names if field not in exclude] return field_names
[docs] @classmethod @wtf_required def to_wtf_form( cls: Union["WtfFormMixin", mongoengine.document.BaseDocument], base_class: Type[ModelForm] = ModelForm, only: Optional[List[str]] = None, exclude: Optional[List[str]] = None, fields_kwargs: Optional[Dict[str, Dict]] = None, ) -> Type[ModelForm]: """ Generate WTForm from Document model. :param base_class: Base form class to extend from. Must be a :class:`.ModelForm` subclass. :param only: An optional iterable with the property names that should be included in the form. Only these properties will have fields. Fields are always appear in provided order, this allows user to change form fields ordering, without changing database model. :param exclude: An optional iterable with the property names that should be excluded from the form. All other properties will have fields. Fields are appears in order, defined in model, excluding provided fields names. For adjusting fields ordering, use :attr:`only`. :param fields_kwargs: An optional dictionary of dictionaries, where field names mapping to keyword arguments used to construct each field object. Has the highest priority over all fields settings (made in Document field definition). Field options are directly passed to field generation, so must match WTForm Field keyword arguments. Support special field keyword option ``wtf_field_class``, that can be used for complete field class replacement. Dictionary format example:: dictionary = { "field_name":{ "label":"new", "default": "new", "wtf_field_class": wtforms.fields.StringField } } With such dictionary for field with name ``field_name`` :class:`wtforms.fields.StringField` will be called like:: field_name = wtforms.fields.StringField(label="new", default="new") """ form_fields_dict = {} fields_kwargs = fields_kwargs or {} fields_names = cls._get_fields_names(only, exclude) for field_name in fields_names: # noinspection PyUnresolvedReferences field_class = cls._fields[field_name] try: form_fields_dict[field_name] = field_class.to_wtf_field( model=cls, field_kwargs=fields_kwargs.get(field_name, {}), ) except (AttributeError, NotImplementedError): logger.warning( f"Field {field_name} ignored, field type does not have " f".to_wtf_field() method or method raised NotImplementedError." ) form_fields_dict["model_class"] = cls # noinspection PyTypeChecker return type(f"{cls.__name__}Form", (base_class,), form_fields_dict)
[docs]class Document(WtfFormMixin, mongoengine.Document): """Abstract Document with QuerySet and WTForms extra helpers.""" meta = {"abstract": True, "queryset_class": BaseQuerySet}
[docs] def paginate_field(self, field_name, page, per_page, total=None): """Paginate items within a list field.""" # TODO this doesn't sound useful at all - remove in next release? count = getattr(self, f"{field_name}_count", "") total = total or count or len(getattr(self, field_name)) return ListFieldPagination( self.__class__.objects, self.pk, field_name, page, per_page, total=total )
[docs]class DynamicDocument(WtfFormMixin, mongoengine.DynamicDocument): """Abstract DynamicDocument with QuerySet and WTForms extra helpers.""" meta = {"abstract": True, "queryset_class": BaseQuerySet}
[docs]class EmbeddedDocument(WtfFormMixin, mongoengine.EmbeddedDocument): """Abstract EmbeddedDocument document with extra WTForms helpers.""" meta = {"abstract": True}
[docs]class DynamicEmbeddedDocument(WtfFormMixin, mongoengine.DynamicEmbeddedDocument): """Abstract DynamicEmbeddedDocument document with extra WTForms helpers.""" meta = {"abstract": True}