diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 7d033a1..65a73eb 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,14 +13,13 @@ jobs:
strategy:
matrix:
python:
- - "3.8"
- "3.9"
- "3.10"
- "3.11"
+ - "3.12"
- "pypy3.9"
- "pypy3.10"
django:
- - "Django>=3.2,<3.3"
- "Django>=4.2,<4.3"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/README.rst b/README.rst
index 815b67a..74bfea5 100644
--- a/README.rst
+++ b/README.rst
@@ -24,7 +24,7 @@ Right now we're targeting to get things working on Django 4.2;
WARNING: This project is not in good state, and is likely to break with django updates.
It's better to use raw mongoengine.
-Working / Django 3.2-4.2
+Working / Django 4.2
------------------------
* [ok] sessions
@@ -50,9 +50,9 @@ It get's replaced after class creation via some metaclass magick.
Fields notes
------------
-* mongo defaults Field(required=False), changed to django-style defaults
- -> Field(blank=False), and setting required = not blank in Field.__init__
-
+* Project uses mongoengine style argument `required=False`, not django style `blank=False`,
+ to be compatible with mongo-types.
+ **All your fields are optional by default.**
TODO
diff --git a/django_mongoengine/document.py b/django_mongoengine/document.py
index 973d818..4af0d0f 100644
--- a/django_mongoengine/document.py
+++ b/django_mongoengine/document.py
@@ -2,7 +2,7 @@
from __future__ import annotations
from functools import partial
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
from bson.objectid import ObjectId
from django.db.models import Model
@@ -23,7 +23,7 @@
# TopLevelDocumentMetaclass is using ObjectIdField to create default pk field,
# if one's not set explicitly.
# We need to know it's not editable and auto_created.
-mtc.ObjectIdField = partial(ObjectIdField, editable=False, auto_created=True, blank=True)
+mtc.ObjectIdField = partial(ObjectIdField, editable=False, auto_created=True)
def django_meta(meta, *top_bases):
@@ -51,7 +51,7 @@ class DjangoFlavor:
objects = QuerySetManager["Self"]()
_default_manager = QuerySetManager["Self"]()
_get_pk_val = Model.__dict__["_get_pk_val"]
- _meta: DocumentMetaWrapper
+ _meta: DocumentMetaWrapper | dict[str, Any]
DoesNotExist: type[DoesNotExist]
def __init__(self, *args, **kwargs):
diff --git a/django_mongoengine/fields/djangoflavor.py b/django_mongoengine/fields/djangoflavor.py
index eb7d5f7..f764d47 100644
--- a/django_mongoengine/fields/djangoflavor.py
+++ b/django_mongoengine/fields/djangoflavor.py
@@ -10,13 +10,12 @@
from .internal import INTERNAL_DJANGO_FIELDS_MAP
-_field_defaults = (
- ("blank", False),
- ("null", False),
- ("help_text", ""),
- ("editable", True),
- ("auto_created", False),
-)
+# Add some default values, required for django.
+_field_defaults = {
+ "help_text": "",
+ "editable": True,
+ "auto_created": False,
+}
class DjangoField:
@@ -37,13 +36,13 @@ def _get_flatchoices(self):
flatchoices = property(_get_flatchoices)
def __init__(self, *args, **kwargs):
- for k, v in _field_defaults:
- kwargs.setdefault(k, v)
- if "required" in kwargs:
+ kwargs = _field_defaults | kwargs
+
+ if "blank" in kwargs:
raise ImproperlyConfigured(
- "`required` option is not supported. Use Django-style `blank` instead."
+ "`blank` option is not supported. Use Mongoengine-style `required` instead."
)
- kwargs["required"] = not kwargs["blank"]
+
if hasattr(self, "auto_created"):
kwargs.pop("auto_created")
self._verbose_name = kwargs.pop("verbose_name", None)
@@ -53,6 +52,9 @@ def __init__(self, *args, **kwargs):
self.remote_field = None
self.is_relation = self.remote_field is not None
+ # This is needed for django, but we use mongoengine style in __init__
+ # to not confuse type checker.
+ self.blank = not self.required
def _get_verbose_name(self):
return self._verbose_name or self.db_field.replace('_', ' ')
@@ -272,7 +274,7 @@ def formfield(self, **kwargs):
elif isinstance(self.field, fields.ReferenceField):
defaults = {
'form_class': formfields.DocumentMultipleChoiceField,
- 'queryset': self.field.document_type.objects,
+ 'queryset': self.field.document_type.objects, # type: ignore
}
else:
defaults = {}
diff --git a/django_mongoengine/mongo_auth/models.py b/django_mongoengine/mongo_auth/models.py
index 67b5c79..9f047c7 100644
--- a/django_mongoengine/mongo_auth/models.py
+++ b/django_mongoengine/mongo_auth/models.py
@@ -10,13 +10,13 @@
from django.contrib.contenttypes.models import ContentTypeManager
from django.db import models
from django.utils import timezone
-from django.utils.encoding import smart_str
from django.utils.translation import gettext_lazy as _
from mongoengine import ImproperlyConfigured
from django_mongoengine import document, fields
from django_mongoengine.queryset import QuerySetManager
+from django.contrib.auth.hashers import check_password, make_password
from .managers import MongoUserManager
@@ -34,32 +34,6 @@ def ct_init(self, *args, **kwargs):
),
)
-try:
- from django.contrib.auth.hashers import check_password, make_password
-except ImportError:
- """Handle older versions of Django"""
- from django.utils.hashcompat import md5_constructor, sha_constructor
-
- def get_hexdigest(algorithm, salt, raw_password):
- raw_password, salt = smart_str(raw_password), smart_str(salt)
- if algorithm == 'md5':
- return md5_constructor(salt + raw_password).hexdigest()
- elif algorithm == 'sha1':
- return sha_constructor(salt + raw_password).hexdigest()
- raise ValueError('Got unknown password algorithm type in password')
-
- def check_password(raw_password, password):
- algo, salt, hash = password.split('$')
- return hash == get_hexdigest(algo, salt, raw_password)
-
- def make_password(raw_password):
- from random import random
-
- algo = 'sha1'
- salt = get_hexdigest(algo, str(random()), str(random()))[:5]
- hash = get_hexdigest(algo, salt, raw_password)
- return '%s$%s$%s' % (algo, salt, hash)
-
class BaseUser:
is_anonymous = AbstractBaseUser.__dict__['is_anonymous']
@@ -88,7 +62,7 @@ class Meta:
# ordering = ('name',)
# unique_together = (('app_label', 'model'),)
- def __unicode__(self):
+ def __str__(self):
return self.name
def model_class(self):
@@ -158,7 +132,7 @@ class Meta:
# unique_together = (('content_type', 'codename'),)
# ordering = ('content_type__app_label', 'content_type__model', 'codename')
- def __unicode__(self):
+ def __str__(self):
return "%s | %s | %s" % (
self.content_type.app_label,
self.content_type,
@@ -195,7 +169,7 @@ class Meta:
verbose_name = _('group')
verbose_name_plural = _('groups')
- def __unicode__(self):
+ def __str__(self):
return self.name
@@ -208,22 +182,23 @@ class AbstractUser(BaseUser, document.Document):
max_length=150,
verbose_name=_('username'),
help_text=_("Required. 150 characters or fewer. Letters, numbers and @/./+/-/_ characters"),
+ required=True,
)
first_name = fields.StringField(
max_length=30,
- blank=True,
verbose_name=_('first name'),
)
- last_name = fields.StringField(max_length=30, blank=True, verbose_name=_('last name'))
- email = fields.EmailField(verbose_name=_('e-mail address'), blank=True)
+ last_name = fields.StringField(max_length=30, verbose_name=_('last name'))
+ email = fields.EmailField(verbose_name=_('e-mail address'), required=True)
password = fields.StringField(
max_length=128,
verbose_name=_('password'),
help_text=_(
"Use '[algo]$[iterations]$[salt]$[hexdigest]' or use the change password form."
),
+ required=True,
)
is_staff = fields.BooleanField(
default=False,
@@ -250,7 +225,6 @@ class AbstractUser(BaseUser, document.Document):
user_permissions = fields.ListField(
fields.ReferenceField(Permission),
verbose_name=_('user permissions'),
- blank=True,
help_text=_('Permissions for the user.'),
)
@@ -259,7 +233,7 @@ class AbstractUser(BaseUser, document.Document):
meta = {'abstract': True, 'indexes': [{'fields': ['username'], 'unique': True, 'sparse': True}]}
- def __unicode__(self):
+ def __str__(self):
return self.username
def get_full_name(self):
diff --git a/example/tumblelog/tumblelog/models.py b/example/tumblelog/tumblelog/models.py
index 1272249..429c5f2 100644
--- a/example/tumblelog/tumblelog/models.py
+++ b/example/tumblelog/tumblelog/models.py
@@ -1,7 +1,4 @@
-try:
- from django.urls import reverse
-except ImportError:
- from django.core.urlresolvers import reverse
+from django.urls import reverse
import datetime
@@ -13,9 +10,9 @@ class Comment(EmbeddedDocument):
default=datetime.datetime.now,
editable=False,
)
- author = fields.StringField(verbose_name="Name", max_length=255)
- email = fields.EmailField(verbose_name="Email", blank=True)
- body = fields.StringField(verbose_name="Comment")
+ author = fields.StringField(verbose_name="Name", max_length=255, required=True)
+ email = fields.EmailField(verbose_name="Email", required=True)
+ body = fields.StringField(verbose_name="Comment", required=True)
class Post(Document):
@@ -23,18 +20,21 @@ class Post(Document):
default=datetime.datetime.now,
editable=False,
)
- title = fields.StringField(max_length=255)
- slug = fields.StringField(max_length=255, primary_key=True)
+ title = fields.StringField(max_length=255, required=True)
+ slug = fields.StringField(max_length=255, primary_key=True, required=True)
comments = fields.ListField(
- fields.EmbeddedDocumentField('Comment'),
+ fields.EmbeddedDocumentField(Comment, required=True),
+ default=[],
+ )
+ strings = fields.ListField(
+ fields.StringField(required=True),
default=[],
- blank=True,
)
def get_absolute_url(self):
- return reverse('post', kwargs={"slug": self.slug})
+ return reverse("post", kwargs={"slug": self.slug})
- def __unicode__(self):
+ def __str__(self):
return self.title
@property
@@ -42,29 +42,29 @@ def post_type(self):
return self.__class__.__name__
meta = {
- 'indexes': ['-created_at', 'slug'],
- 'ordering': ['-created_at'],
- 'allow_inheritance': True,
+ "indexes": ["-created_at", "slug"],
+ "ordering": ["-created_at"],
+ "allow_inheritance": True,
}
class BlogPost(Post):
- body = fields.StringField()
+ body = fields.StringField(required=True)
class Video(Post):
- embed_code = fields.StringField()
+ embed_code = fields.StringField(required=True)
class Image(Post):
- image = fields.ImageField()
+ image = fields.ImageField(required=True)
class Quote(Post):
- body = fields.StringField()
- author = fields.StringField(verbose_name="Author Name", max_length=255)
+ body = fields.StringField(required=True)
+ author = fields.StringField(verbose_name="Author Name", max_length=255, required=True)
class Music(Post):
- url = fields.StringField(max_length=100, verbose_name="Music Url")
- music_parameters = fields.DictField(verbose_name="Music Parameters")
+ url = fields.StringField(max_length=100, verbose_name="Music Url", required=True)
+ music_parameters = fields.DictField(verbose_name="Music Parameters", required=True)
diff --git a/poetry.lock b/poetry.lock
index 01bb90c..19a51d1 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -264,6 +264,20 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"]
+[[package]]
+name = "django-types"
+version = "0.19.1"
+description = "Type stubs for Django"
+optional = false
+python-versions = ">=3.7,<4.0"
+files = [
+ {file = "django_types-0.19.1-py3-none-any.whl", hash = "sha256:b3f529de17f6374d41ca67232aa01330c531bbbaa3ac4097896f31ac33c96c30"},
+ {file = "django_types-0.19.1.tar.gz", hash = "sha256:5ae7988612cf6fbc357b018bbc3b3a878b65e04275cc46e0d35d66a708daff12"},
+]
+
+[package.dependencies]
+types-psycopg2 = ">=2.9.21.13"
+
[[package]]
name = "dnspython"
version = "2.1.0"
@@ -458,15 +472,30 @@ files = [
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
+[[package]]
+name = "mongo-types"
+version = "0.15.1"
+description = "Type stubs for mongoengine"
+optional = false
+python-versions = "^3.8"
+files = []
+develop = false
+
+[package.source]
+type = "git"
+url = "https://github.com/last-partizan/mongo-types.git"
+reference = "fix-embedded-types"
+resolved_reference = "1c9e66cbc80685c3085eaa5227d645460dbc68f0"
+
[[package]]
name = "mongoengine"
-version = "0.26.0"
+version = "0.27.0"
description = "MongoEngine is a Python Object-Document Mapper for working with MongoDB."
optional = false
python-versions = ">=3.7"
files = [
- {file = "mongoengine-0.26.0-py3-none-any.whl", hash = "sha256:020a0779d1830affc649f2760d8c408e998981f18898e425eb041915181d3a53"},
- {file = "mongoengine-0.26.0.tar.gz", hash = "sha256:3f284bdcbe8d1a3a9b8ab7d3c3ed672d10b8fd2e545447cd1d75e40d6e978332"},
+ {file = "mongoengine-0.27.0-py3-none-any.whl", hash = "sha256:c3523b8f886052f3deb200b3218bcc13e4b781661e3bea38587cc936c80ea358"},
+ {file = "mongoengine-0.27.0.tar.gz", hash = "sha256:8f38df7834dc4b192d89f2668dcf3091748d12f74d55648ce77b919167a4a49b"},
]
[package.dependencies]
@@ -1030,6 +1059,17 @@ files = [
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
+[[package]]
+name = "types-psycopg2"
+version = "2.9.21.16"
+description = "Typing stubs for psycopg2"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "types-psycopg2-2.9.21.16.tar.gz", hash = "sha256:44a3ae748173bb637cff31654d6bd12de9ad0c7ad73afe737df6152830ed82ed"},
+ {file = "types_psycopg2-2.9.21.16-py3-none-any.whl", hash = "sha256:e2f24b651239ccfda320ab3457099af035cf37962c36c9fa26a4dc65991aebed"},
+]
+
[[package]]
name = "typing-extensions"
version = "4.8.0"
@@ -1080,5 +1120,5 @@ files = [
[metadata]
lock-version = "2.0"
-python-versions = ">=3.8"
-content-hash = "80b087eaaabf55a7f91863cf6dae6377efef9b229608d03da49ecc6459d4972e"
+python-versions = ">=3.8,<4.0"
+content-hash = "6230e932eacccb9aba246b34836235a6410301f67dfda387d62f463a88fc4892"
diff --git a/pyproject.toml b/pyproject.toml
index a74a180..a60590c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ description = "Django support for MongoDB via MongoEngine"
authors = ["Ross Lawley "]
[tool.poetry.dependencies]
-python = ">=3.8"
+python = ">=3.8,<4.0"
django = ">=3.2,<5"
mongoengine = ">=0.14"
@@ -20,6 +20,9 @@ ruff = "^0.1.3"
sphinx = "*"
black = "^23.10.1"
typing-extensions = "^4.8.0"
+#mongo-types = { git = "https://github.com/sbdchd/mongo-types.git", branch = "main" }
+mongo-types = { git = "https://github.com/last-partizan/mongo-types.git", branch = "fix-embedded-types" }
+django-types = "^0.19.1"
[tool.black]
line-length = 100
diff --git a/setup.cfg b/setup.cfg
index eee9ea0..07d36ad 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -24,7 +24,7 @@ classifiers =
[options]
zip_safe = False
install_requires =
- Django>=3.2,<4.3
+ Django>=4.2,<4.3
mongoengine>=0.14
include_package_data = True
packages = find:
diff --git a/tests/forms/models.py b/tests/forms/models.py
index 3b29766..c36b478 100644
--- a/tests/forms/models.py
+++ b/tests/forms/models.py
@@ -4,8 +4,8 @@
class MongoDoc(Document):
- year = fields.IntField()
- file = fields.FileField(upload_to="test")
+ year = fields.IntField(required=True)
+ file = fields.FileField(upload_to="test", required=True)
class DjangoModel(models.Model):
diff --git a/tests/views/models.py b/tests/views/models.py
index 02bcd48..3c95ee5 100644
--- a/tests/views/models.py
+++ b/tests/views/models.py
@@ -1,57 +1,59 @@
-#!/usr/bin/env python
-
from bson.objectid import ObjectId
from django.urls import reverse
from django_mongoengine import Document, fields
+def object_id() -> str:
+ return str(ObjectId())
+
+
class Artist(Document):
- id = fields.StringField(primary_key=True, default=ObjectId)
- name = fields.StringField(max_length=100)
+ id = fields.StringField(primary_key=True, default=object_id)
+ name = fields.StringField(max_length=100, required=True)
class Meta:
- ordering = (['name'],)
- verbose_name = ('professional artist',)
- verbose_name_plural = 'professional artists'
+ ordering = (["name"],)
+ verbose_name = ("professional artist",)
+ verbose_name_plural = "professional artists"
- def __unicode__(self):
- return self.name or ''
+ def __str__(self):
+ return self.name or ""
def get_absolute_url(self):
- return reverse('artist_detail', args=(self.id,))
+ return reverse("artist_detail", args=(self.id,))
class Author(Document):
- id = fields.StringField(primary_key=True, default=ObjectId)
- name = fields.StringField(max_length=100)
- slug = fields.StringField()
+ id = fields.StringField(primary_key=True, default=object_id)
+ name = fields.StringField(max_length=100, required=True)
+ slug = fields.StringField(required=True)
- _meta = {"ordering": ['name'], "exclude": 'id'}
+ _meta = {"ordering": ["name"], "exclude": "id"}
- def __unicode__(self):
- return self.name or ''
+ def __str__(self):
+ return self.name or ""
class Book(Document):
- id = fields.StringField(primary_key=True, default=ObjectId)
- name = fields.StringField(max_length=300)
- slug = fields.StringField()
- pages = fields.IntField()
- authors = fields.ListField(fields.ReferenceField(Author))
- pubdate = fields.DateTimeField()
+ id = fields.StringField(primary_key=True, default=object_id)
+ name = fields.StringField(max_length=300, required=True)
+ slug = fields.StringField(required=True)
+ pages = fields.IntField(required=True)
+ authors = fields.ListField(fields.ReferenceField(Author, required=True))
+ pubdate = fields.DateTimeField(required=True)
- _meta = {"ordering": ['-pubdate']}
+ _meta = {"ordering": ["-pubdate"]}
- def __unicode__(self):
- return self.name or ''
+ def __str__(self):
+ return self.name or ""
class Page(Document):
- id = fields.StringField(primary_key=True, default=ObjectId)
- content = fields.StringField()
- template = fields.StringField(max_length=300)
+ id = fields.StringField(primary_key=True, default=object_id)
+ content = fields.StringField(required=True)
+ template = fields.StringField(max_length=300, required=True)
class City(Document):
- name = fields.StringField(max_length=100)
+ name = fields.StringField(max_length=100, required=True)