Chapter 23 · What changed

23.15 What changed in the other contrib apps

The changes from Django 5.0 to this commit in the contrib applications other than authentication, sessions, messages and the admin: the generic foreign key’s descriptor and GenericPrefetch, the static files finders and manifest, sitemaps and feeds, the aggregates and other parts of django.contrib.postgres, and GeoDjango’s functions, its GDAL and GEOS wrappers, GeoIP2 and the map widgets.

Two of these changes alter how a part works: the PostgreSQL aggregates handed their ordering and their string and bit aggregates to Django’s own, and GDAL’s geometries gained an M dimension and curves (Content types, static files and the other contrib apps).

Content types: GenericForeignKeyDescriptor and GenericPrefetch

Until 6.1 a GenericForeignKey was its own descriptor, as the field’s docstring still says. At this commit GenericForeignKey.contribute_to_class sets on the model class, under the field’s name, a GenericForeignKeyDescriptor. The attribute read on the class is no longer the field: in a run, a note model’s content_object was a GenericForeignKeyDescriptor, whose GenericForeignKeyDescriptor.field was the GenericForeignKey. Its GenericForeignKeyDescriptor.__get__ leaves a missing object to the instance’s fetch mode, which calls GenericForeignKeyDescriptor.fetch_one or GenericForeignKeyDescriptor.fetch_many, as for other relations since 6.1 (GenericForeignKeyDescriptor.__get__; What changed in querysets).

A Prefetch holds one queryset, and a generic key’s objects belong to several models. GenericPrefetch (5.0) holds a list, one queryset for each model; GenericForeignKeyDescriptor.get_prefetch_querysets files each under its model’s content type and uses it in place of the default query for that type (Generic foreign keys: GenericForeignKey).

Static files: find, the manifest and collectstatic’s summary

The keyword all of the function find, deprecated in 5.2 for find_all, was removed in 6.1. find and each finder’s BaseFinder.find take find_all=False, and find passes it by keyword to each finder it asks. In a run, a project’s own finder whose find still took all failed when find called it: “OldFinder.find() got an unexpected keyword argument ‘find_all’” (Finding static files: the finders).

Since 6.0 ManifestFilesMixin.save_manifest writes the manifest’s paths sorted by name, so that the same files give the same staticfiles.json, the release notes say (Static file storages: ManifestStaticFilesStorage and hashed names), and collectstatic at verbosity 1 only counts, in its summary, the files it skipped for a clash of paths and those --clear deleted, and names each at verbosity 2 (Collecting static files: the collectstatic command).

Sitemaps and feeds

5.0 removed the function and the command ping_google, because Google’s ping endpoint was to close in January 2024, the release notes say, and the exception SitemapNotFound with them. A sitemap built outside a request has written "https" URLs since 5.0, where it wrote "http": Sitemap.get_protocol falls back to it when neither the sitemap nor its caller names a protocol. SitemapIndexItem, the location and date the index view hands its template for each sitemap, has had no str of its own since 5.0 (Sitemaps: Sitemap and the sitemap views).

The feed generator takes stylesheets since 5.2, and SyndicationFeed.__init__ makes each string a Stylesheet. The release notes say every feed class writes them; at this commit only RssFeed.add_stylesheets does, and Atom1Feed.write never calls the hook, so in a run an Atom feed given a stylesheet wrote none (Feeds: django.contrib.syndication and the feed generator).

PostgreSQL: the aggregates become Django’s own

ArrayAgg, JSONBAgg and the PostgreSQL StringAgg once had an ordering of their own, the keyword ordering, which OrderableAggMixin gave them. 5.2 deprecated it for order_by, 6.0 gave order_by to Aggregate itself, allowed where Aggregate.allow_order_by is true, as on ArrayAgg and JSONBAgg, and 6.1 removed ordering. Since 5.0 ArrayAgg, JSONBAgg and the PostgreSQL StringAgg give None over no rows, where they gave an empty list or string, unless given a default.

What only PostgreSQL had moved into django.db.models: StringAgg in 6.0, and BitAnd, BitOr and BitXor in 6.1 (django/db/models/aggregates.py; What changed in expressions, lookups and the compiler). The PostgreSQL classes remain, subclasses of the general ones that warn when made, the old StringAgg with a second warning for a delimiter given as a string, which the general one would read as a field; OrderableAggMixin warns when a class is made from it. All go in Django 2028 (django/contrib/postgres/aggregates/general.py; The PostgreSQL application: PostgresConfig, operations and aggregates, What still warns: the deprecations and the releases that end them).

Table 23.4 The other changes to django.contrib.postgres.
what change release
CICharField, CIEmailField, CITextField change kept for old migrations only, failing the checks "fields.E905" to "fields.E907"; the mixin CIText gone (citext.py django/contrib/postgres/fields/) release 5.1
ArrayField of django.db.models.JSONField change an item None saved as SQL null, not JSON null (PostgreSQL fields: ArrayField, the range fields and hstore) release 6.1
ExclusionConstraint change opclasses removed, OpClass instead, and violation_error_code (5.0); the index type "hash", for one pair with RangeOperators.EQUAL and no include (6.1) (PostgreSQL indexes and ExclusionConstraint) release 5.0, 6.1
BTreeIndex change deduplicate_items (BTreeIndex.get_with_params django/contrib/postgres/indexes.py) release 5.1
Lexeme change a search term escaped into one parameter (Full-text search: SearchVector, SearchQuery and SearchRank) release 6.0
"postgres.E005" change the fields, indexes and constraint fail their checks without the application (CheckPostgresInstalledMixin django/contrib/postgres/utils.py) release 6.0
CreateExtension and its subclasses change take hints for the router (CreateExtension django/contrib/postgres/operations.py) release 6.0
inspectdb change finds hstore by the type’s name, with psycopg 3.2 or later and the application installed (DatabaseIntrospection.get_table_description django/db/backends/postgresql/introspection.py) release 6.1

GeoDjango: GDAL’s dimensions and curves

OGRGeometry once knew a geometry’s coordinate dimension only as OGRGeometry.coord_dim, which could be set. Since 5.1 it reads a Z and an M dimension apart, OGRGeometry.is_3d and OGRGeometry.is_measured, changed by OGRGeometry.set_3d and OGRGeometry.set_measured, and the M coordinate as Point.m and LineString.m; the setter of coord_dim, deprecated then, went in 6.0. Since 5.2 it reads curved geometries, CircularString, CompoundCurve, CurvePolygon, MultiCurve and MultiSurface, with OGRGeometry.has_curve, OGRGeometry.get_linear_geometry and OGRGeometry.get_curve_geometry. These classes set OGRGeometry.geos_support false, and their OGRGeometry.geos raises GEOSException, saying that GEOS does not support them. OGRGeometry.centroid is every geometry’s since 5.1.

On the GEOS side, GEOSGeometryBase.equals_identical (5.0) compares geometries point by point and GEOSGeometryBase.hasm (6.0) asks for an M dimension; both raise GEOSException on a GEOS older than 3.12 (Geometries in Python: GEOS and GDAL).

A data source’s time and date-time fields keep their milliseconds since 6.2. GDAL’s Field.as_datetime asks for the seconds as a ctypes.c_float where it asked for a ctypes.c_int, and OFTDateTime.value and OFTTime.value split the fraction off.

GeoDjango: functions, lookups, GeoIP2 and the widgets

The functions, aggregates, lookups and units added, or given a backend, since 5.0:

Table 23.5 Where each runs, from the backends’ operations at this commit (django/contrib/gis/db/backends/). GeometryType, IsEmpty, IsValid and NumDimensions are also transforms of the spatial fields (Spatial lookups, functions and distances).
what release where
ClosestPoint release 5.0 where PostGIS and SpatiaLite
the spatial aggregates’ filter release 5.0 where as on any Aggregate
BoundingCircle release 5.1 where SpatiaLite 5.1 or later too
Collect release 5.1 where MySQL too; MariaDB from 12.0.1 (6.0)
FromWKB, FromWKT release 5.1 where take an SRID, 0 by default, which Oracle ignores
"ha" release 5.1 where a unit of django.contrib.gis.measure.Area
"coveredby", "covers" release 5.2 where MySQL too; "coveredby" on MariaDB from 12.0.1 (6.0)
GeoHash, IsValid, "isvalid" release 6.0 where MariaDB from 12.0.1 too
Rotate release 6.0 where PostGIS
GeometryType, "geom_type" release 6.0 where every backend; Oracle from 23
IsEmpty, "isempty" release 6.1 where SpatiaLite too
NumDimensions, "num_dimensions" release 6.1 where PostGIS and SpatiaLite

GeoIP2 opens one database since 5.1, where from a directory it opened both: the path given if it is a file, else the city database in that directory, else the country one; GeoIP2.country reads whichever is open. It takes ipaddress.IPv4Address and ipaddress.IPv6Address as well as strings, and the answers of GeoIP2.country gained "continent_code", "continent_name" and "is_in_european_union", and those of GeoIP2.city "accuracy_radius", "region_name", "metro_code" and "region_code", beside the old "dma_code" and "region". Its coords and open went in 6.0, for GeoIP2.lon_lat and the constructor (GeoDjango’s forms, admin, feeds and data tools).

The admin’s own GeoModelAdmin, OSMGeoAdmin and OpenLayersWidget went in 5.0, leaving GISModelAdmin and the form widgets, and BaseGeometryWidget lost map_width and map_height in 5.1. Since 6.0 the widgets render no inline JavaScript: their options go into the page as JSON for the widget’s script, and BaseGeometryWidget.base_layer names the map’s tiles. OpenLayersWidget loads OpenLayers 10.9.0 since 6.1, 7.2.2 before (GeoDjango’s forms, admin, feeds and data tools).