From 441f48ee37e8ccfc5abefe15821443dca5fd50ee Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Wed, 15 Nov 2023 21:18:21 +0200 Subject: [PATCH 1/4] chore: Replace old __unicode__ with __str__ --- django_mongoengine/mongo_auth/models.py | 8 ++++---- example/tumblelog/tumblelog/models.py | 2 +- tests/views/models.py | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/django_mongoengine/mongo_auth/models.py b/django_mongoengine/mongo_auth/models.py index 67b5c79..c381056 100644 --- a/django_mongoengine/mongo_auth/models.py +++ b/django_mongoengine/mongo_auth/models.py @@ -88,7 +88,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 +158,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 +195,7 @@ class Meta: verbose_name = _('group') verbose_name_plural = _('groups') - def __unicode__(self): + def __str__(self): return self.name @@ -259,7 +259,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..5e7cc90 100644 --- a/example/tumblelog/tumblelog/models.py +++ b/example/tumblelog/tumblelog/models.py @@ -34,7 +34,7 @@ class Post(Document): def get_absolute_url(self): return reverse('post', kwargs={"slug": self.slug}) - def __unicode__(self): + def __str__(self): return self.title @property diff --git a/tests/views/models.py b/tests/views/models.py index 02bcd48..3389f05 100644 --- a/tests/views/models.py +++ b/tests/views/models.py @@ -15,8 +15,8 @@ class Meta: 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,)) @@ -29,8 +29,8 @@ class Author(Document): _meta = {"ordering": ['name'], "exclude": 'id'} - def __unicode__(self): - return self.name or '' + def __str__(self): + return self.name or "" class Book(Document): @@ -43,8 +43,8 @@ class Book(Document): _meta = {"ordering": ['-pubdate']} - def __unicode__(self): - return self.name or '' + def __str__(self): + return self.name or "" class Page(Document): From 1d0e5e975fdb420ca64b31c17bc37bae48b29dc3 Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Wed, 15 Nov 2023 21:45:19 +0200 Subject: [PATCH 2/4] refactor: Use mongo-style init arguments --- django_mongoengine/document.py | 6 +-- django_mongoengine/fields/djangoflavor.py | 28 +++++++------ django_mongoengine/mongo_auth/models.py | 37 +++-------------- example/tumblelog/tumblelog/models.py | 44 ++++++++++---------- poetry.lock | 50 ++++++++++++++++++++--- pyproject.toml | 5 ++- tests/forms/models.py | 4 +- tests/views/models.py | 48 +++++++++++----------- 8 files changed, 121 insertions(+), 101 deletions(-) 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 c381056..ec53e46 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,33 +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'] is_authenticated = AbstractBaseUser.__dict__['is_authenticated'] @@ -208,22 +181,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 +224,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.'), ) diff --git a/example/tumblelog/tumblelog/models.py b/example/tumblelog/tumblelog/models.py index 5e7cc90..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,16 +20,19 @@ 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 __str__(self): return self.title @@ -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/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 3389f05..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 __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 __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 __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) From 4c1c757d277ac9caec2b34e7163190caff2e35cf Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Wed, 15 Nov 2023 21:51:42 +0200 Subject: [PATCH 3/4] chore: Update readme and test matrix --- .github/workflows/test.yml | 3 +-- README.rst | 8 ++++---- setup.cfg | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) 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/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: From ff11beaeb4b46ca8bd747e1c01f2e0f62ca521af Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Wed, 15 Nov 2023 21:52:50 +0200 Subject: [PATCH 4/4] black --- django_mongoengine/mongo_auth/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django_mongoengine/mongo_auth/models.py b/django_mongoengine/mongo_auth/models.py index ec53e46..9f047c7 100644 --- a/django_mongoengine/mongo_auth/models.py +++ b/django_mongoengine/mongo_auth/models.py @@ -34,6 +34,7 @@ def ct_init(self, *args, **kwargs): ), ) + class BaseUser: is_anonymous = AbstractBaseUser.__dict__['is_anonymous'] is_authenticated = AbstractBaseUser.__dict__['is_authenticated']