Skip to content

[5.0.x.] Backports for 5.0.0b1 release #247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
68d4509
fix flaky test: model_fields_.test_embedded_model.ModelTests.test_pre…
timgraham Jan 30, 2025
2662c75
correct a TestCase.setUpTestData() to be a classmethod
timgraham Jan 30, 2025
e5c73d7
fix MongoDB version in evergreen build name
timgraham Jan 30, 2025
d4d1858
Add Docs link to README (#244)
Jibola Jan 31, 2025
020a94c
add tests for nested embedded model form fields
timgraham Jan 25, 2025
288fe24
fix form element names of nested embedded model fields
timgraham Jan 25, 2025
556ecf6
fix crash when rendering an invalid form with nested embedded model f…
timgraham Jan 29, 2025
0556474
Update the limitations link to new (#248)
Jibola Feb 3, 2025
bfb8711
provide django version check guidance (#245)
Jibola Feb 3, 2025
1656664
PYTHON-5047 Do not run nightly release check on forks (#251)
blink1073 Feb 6, 2025
515173c
Add @async_unsafe to DatabaseWrapper methods as needed
timgraham Feb 6, 2025
74f7be2
fix persistent connections
timgraham Feb 6, 2025
7c54410
Restructure docs per Diátaxis framework
timgraham Jan 23, 2025
8f81d58
Update links for repo move from mongodb-labs to mongodb
timgraham Feb 19, 2025
19cf494
INTPYTHON-509 Add db_name parameter to parse_uri()
timgraham Feb 4, 2025
b7f8154
Add release notes
timgraham Feb 20, 2025
8e7e8db
INTPYTHON-393 Remove ObjectIdAutoField acceptance of integer values
timgraham Feb 10, 2025
be4e4ad
Add a database router so dumpdata can ignore embedded models
timgraham Feb 22, 2025
18919da
Add release date for 5.0.0b1
timgraham Feb 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .evergreen/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ tasks:
- func: "run unit tests"

buildvariants:
- name: tests-5-noauth-nossl
- name: tests-6-noauth-nossl
display_name: Run Tests 6.0 NoAuth NoSSL
run_on: rhel87-small
expansions:
Expand All @@ -79,7 +79,7 @@ buildvariants:
tasks:
- name: run-tests

- name: tests-5-auth-ssl
- name: tests-6-auth-ssl
display_name: Run Tests 6.0 Auth SSL
run_on: rhel87-small
expansions:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/mongodb_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django_mongodb_backend import parse_uri

if mongodb_uri := os.getenv("MONGODB_URI"):
db_settings = parse_uri(mongodb_uri)
db_settings = parse_uri(mongodb_uri, db_name="dummy")

# Workaround for https://github.com/mongodb-labs/mongo-orchestration/issues/268
if db_settings["USER"] and db_settings["PASSWORD"]:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
pre-publish:
environment: release
runs-on: ubuntu-latest
if: github.repository_owner == 'mongodb' || github.event_name == 'workflow_dispatch'
permissions:
id-token: write
contents: write
Expand Down
36 changes: 22 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ This backend is currently in development and is not advised for production workf
changes may be made without notice. We welcome your feedback as we continue to
explore and build. The best way to share this is via our [MongoDB Community Forum](https://www.mongodb.com/community/forums/tag/python).

## Index
* [Documentation](https://www.mongodb.com/docs/languages/python/django-mongodb/current/)
* [Getting Started](https://www.mongodb.com/docs/languages/python/django-mongodb/current/get-started/)
* [Model Your Data](https://www.mongodb.com/docs/languages/python/django-mongodb/current/model-data/)
* [Limitations & Future Work](https://www.mongodb.com/docs/languages/python/django-mongodb/current/limitations-upcoming/)

The documentation in the "docs" directory is online at
https://django-mongodb-backend.readthedocs.io/en/latest/.

## Install

Use the version of `django-mongodb-backend` that corresponds to your version of
Expand All @@ -20,8 +29,14 @@ $ pip install --pre django-mongodb-backend==5.0.*

From your shell, run the following command to create a new Django project
called `example` using our custom template. Make sure the zipfile referenced
at the end of the template link corresponds to your
version of Django. The snippet below specifies `5.0.x.zip` at the end of
at the end of the template link corresponds to your version of Django.

You can check what version of Django you're using with:
```bash
$ django-admin --version
```

The snippet below specifies `5.0.x.zip` at the end of
the template url to get the template for any Django version matching 5.0:

```bash
Expand All @@ -31,24 +46,17 @@ $ django-admin startproject example --template https://github.com/mongodb-labs/d

### Connect to the database

Navigate to your `example/settings.py` file and find the variable named
`DATABASES` Replace the `DATABASES` variable with this:
Navigate to your `example/settings.py` file and replace the `DATABASES`
setting like so:

```python
DATABASES = {
"default": django_mongodb_backend.parse_uri("<CONNECTION_STRING_URI>"),
"default": django_mongodb_backend.parse_uri(
"<CONNECTION_STRING_URI>", db_name="example"
),
}
```

The MongoDB `<CONNECTION_STRING_URI>` must also specify a database for the
`parse_uri` function to work.
If not already included, make sure you provide a value for `<DATABASE_NAME>`
in your URI as shown in the example below:
```bash
mongodb+srv://myDatabaseUser:D1fficultP%[email protected]/<DATABASE_NAME>?retryWrites=true&w=majority
```


### Run the server
To verify that you installed Django MongoDB Backend and correctly configured your project, run the following command from your project root:
```bash
Expand Down
6 changes: 5 additions & 1 deletion django_mongodb_backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from django.core.exceptions import ImproperlyConfigured
from django.db.backends.base.base import BaseDatabaseWrapper
from django.utils.asyncio import async_unsafe
from pymongo.collection import Collection
from pymongo.driver_info import DriverInfo
from pymongo.mongo_client import MongoClient
Expand Down Expand Up @@ -172,6 +173,7 @@ def get_connection_params(self):
**settings_dict["OPTIONS"],
}

@async_unsafe
def get_new_connection(self, conn_params):
return MongoClient(**conn_params, driver=self._driver_info())

Expand All @@ -187,13 +189,15 @@ def _rollback(self):
pass

def set_autocommit(self, autocommit, force_begin_transaction_with_broken_autocommit=False):
pass
self.autocommit = autocommit

@async_unsafe
def close(self):
super().close()
with contextlib.suppress(AttributeError):
del self.database

@async_unsafe
def cursor(self):
return Cursor()

Expand Down
9 changes: 7 additions & 2 deletions django_mongodb_backend/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_temporal_subtraction = True
# MongoDB stores datetimes in UTC.
supports_timezones = False
# Not implemented: https://github.com/mongodb-labs/django-mongodb-backend/issues/7
# Not implemented: https://github.com/mongodb/django-mongodb-backend/issues/7
supports_transactions = False
supports_unspecified_pk = True
uses_savepoints = False
Expand Down Expand Up @@ -197,6 +197,8 @@ def django_test_expected_failures(self):
"lookup.tests.LookupTests.test_in_ignore_none_with_unhashable_items",
"m2m_through_regress.tests.ThroughLoadDataTestCase.test_sequence_creation",
"many_to_many.tests.ManyToManyTests.test_add_remove_invalid_type",
"many_to_one.tests.ManyToOneTests.test_fk_to_smallautofield",
"many_to_one.tests.ManyToOneTests.test_fk_to_bigautofield",
"migrations.test_operations.OperationTests.test_autofield__bigautofield_foreignfield_growth",
"migrations.test_operations.OperationTests.test_model_with_bigautofield",
"migrations.test_operations.OperationTests.test_smallfield_autofield_foreignfield_growth",
Expand All @@ -205,6 +207,8 @@ def django_test_expected_failures(self):
"model_fields.test_autofield.BigAutoFieldTests",
"model_fields.test_autofield.SmallAutoFieldTests",
"queries.tests.TestInvalidValuesRelation.test_invalid_values",
"schema.tests.SchemaTests.test_alter_autofield_pk_to_bigautofield_pk",
"schema.tests.SchemaTests.test_alter_autofield_pk_to_smallautofield_pk",
},
"Converters aren't run on returning fields from insert.": {
# Unsure this is needed for this backend. Can implement by request.
Expand All @@ -223,6 +227,7 @@ def django_test_expected_failures(self):
"queries.test_qs_combinators.QuerySetSetOperationTests.test_order_raises_on_non_selected_column",
"queries.tests.RelatedLookupTypeTests.test_values_queryset_lookup",
"queries.tests.ValuesSubqueryTests.test_values_in_subquery",
"sites_tests.tests.CreateDefaultSiteTests.test_no_site_id",
},
"Cannot use QuerySet.delete() when querying across multiple collections on MongoDB.": {
"admin_changelist.tests.ChangeListTests.test_distinct_for_many_to_many_at_second_level_in_search_fields",
Expand Down Expand Up @@ -562,7 +567,7 @@ def django_test_expected_failures(self):
"cache.tests.DBCacheWithTimeZoneTests",
},
"FilteredRelation not supported.": {
# https://github.com/mongodb-labs/django-mongodb-backend/issues/157
# https://github.com/mongodb/django-mongodb-backend/issues/157
"filtered_relation.tests.FilteredRelationAggregationTests",
"filtered_relation.tests.FilteredRelationAnalyticalAggregationTests",
"filtered_relation.tests.FilteredRelationTests",
Expand Down
34 changes: 3 additions & 31 deletions django_mongodb_backend/fields/auto.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from bson import ObjectId, errors
from django.core import exceptions
from django.db.models.fields import AutoField
from django.utils.functional import cached_property

Expand All @@ -22,39 +20,13 @@ def deconstruct(self):
return name, path, args, kwargs

def get_prep_value(self, value):
if value is None:
return None
# Accept int for compatibility with Django's test suite which has many
# instances of manually assigned integer IDs, as well as for things
# like settings.SITE_ID which has a system check requiring an integer.
if isinstance(value, (ObjectId | int)):
return value
try:
return ObjectId(value)
except errors.InvalidId as e:
# A manually assigned integer ID?
if isinstance(value, str) and value.isdigit():
return int(value)
raise ValueError(f"Field '{self.name}' expected an ObjectId but got {value!r}.") from e
# Override to omit super() which would call AutoField/IntegerField's
# implementation that requires value to be an integer.
return self.to_python(value)

def get_internal_type(self):
return "ObjectIdAutoField"

def to_python(self, value):
if value is None or isinstance(value, int):
return value
try:
return ObjectId(value)
except errors.InvalidId:
try:
return int(value)
except ValueError:
raise exceptions.ValidationError(
self.error_messages["invalid"],
code="invalid",
params={"value": value},
) from None

@cached_property
def validators(self):
# Avoid IntegerField validators inherited from AutoField.
Expand Down
21 changes: 20 additions & 1 deletion django_mongodb_backend/forms/fields/embedded_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@ def decompress(self, value):


class EmbeddedModelBoundField(forms.BoundField):
def __init__(self, form, field, name, prefix_override=None):
super().__init__(form, field, name)
# prefix_override overrides the prefix in self.field.form_kwargs so
# that nested embedded model form elements have the correct name.
self.prefix_override = prefix_override

def __str__(self):
"""Render the model form as the representation for this field."""
form = self.field.model_form_cls(instance=self.value(), **self.field.form_kwargs)
if self.prefix_override:
form.prefix = self.prefix_override
return mark_safe(f"{form.as_div()}") # noqa: S308


Expand Down Expand Up @@ -53,10 +61,21 @@ def compress(self, data_dict):
return self.model_form._meta.model(**values)

def get_bound_field(self, form, field_name):
return EmbeddedModelBoundField(form, self, field_name)
# Nested embedded model form fields need a double prefix.
prefix_override = f"{form.prefix}-{self.model_form.prefix}" if form.prefix else None
return EmbeddedModelBoundField(form, self, field_name, prefix_override)

def bound_data(self, data, initial):
if self.disabled:
return initial
# Transform the bound data into a model instance.
return self.compress(data)

def prepare_value(self, value):
# When rendering a form with errors, nested EmbeddedModelField data
# won't be compressed if MultiValueField.clean() raises ValidationError
# error before compress() is called. The data must be compressed here
# so that EmbeddedModelBoundField.value() returns a model instance
# (rather than a list) for initializing the form in
# EmbeddedModelBoundField.__str__().
return self.compress(value) if isinstance(value, list) else value
18 changes: 18 additions & 0 deletions django_mongodb_backend/routers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.apps import apps

from django_mongodb_backend.models import EmbeddedModel


class MongoRouter:
def allow_migrate(self, db, app_label, model_name=None, **hints):
"""
EmbeddedModels don't have their own collection and must be ignored by
dumpdata.
"""
if not model_name:
return None
try:
model = apps.get_model(app_label, model_name)
except LookupError:
return None
return False if issubclass(model, EmbeddedModel) else None
9 changes: 7 additions & 2 deletions django_mongodb_backend/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def check_django_compatability():
)


def parse_uri(uri, conn_max_age=0, test=None):
def parse_uri(uri, *, db_name=None, conn_max_age=0, test=None):
"""
Convert the given uri into a dictionary suitable for Django's DATABASES
setting.
Expand All @@ -45,16 +45,21 @@ def parse_uri(uri, conn_max_age=0, test=None):
host, port = nodelist[0]
elif len(nodelist) > 1:
host = ",".join([f"{host}:{port}" for host, port in nodelist])
db_name = db_name or uri["database"]
if not db_name:
raise ImproperlyConfigured("You must provide the db_name parameter.")
settings_dict = {
"ENGINE": "django_mongodb_backend",
"NAME": uri["database"],
"NAME": db_name,
"HOST": host,
"PORT": port,
"USER": uri.get("username"),
"PASSWORD": uri.get("password"),
"OPTIONS": uri.get("options"),
"CONN_MAX_AGE": conn_max_age,
}
if "authSource" not in settings_dict["OPTIONS"] and uri["database"]:
settings_dict["OPTIONS"]["authSource"] = uri["database"]
if test:
settings_dict["TEST"] = test
return settings_dict
Expand Down
9 changes: 6 additions & 3 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.append(str((Path(__file__).parent / "_ext").resolve()))

project = "django_mongodb_backend"
copyright = "2024, The MongoDB Python Team"
project = "Django MongoDB Backend"
copyright = "2025, The MongoDB Python Team"
author = "The MongoDB Python Team"
release = _version("django_mongodb_backend")

Expand All @@ -39,12 +39,15 @@
intersphinx_mapping = {
"django": (
"https://docs.djangoproject.com/en/5.0/",
"http://docs.djangoproject.com/en/5.0/_objects/",
"https://docs.djangoproject.com/en/5.0/_objects/",
),
"mongodb": ("https://www.mongodb.com/docs/languages/python/django-mongodb/v5.0/", None),
"pymongo": ("https://pymongo.readthedocs.io/en/stable/", None),
"python": ("https://docs.python.org/3/", None),
}

root_doc = "contents"

# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

Expand Down
25 changes: 25 additions & 0 deletions docs/source/contents.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
=================
Table of contents
=================

.. toctree::
:hidden:

index

.. toctree::
:maxdepth: 2

intro/index
topics/index
ref/index
howto/index
faq
releases/index
internals

Indices
=======

* :ref:`genindex`
* :ref:`modindex`
Loading
Loading