Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
75ea651
update to django5
matthewelwell Dec 31, 2025
29c5815
Update django-debug-toolbar
matthewelwell Dec 31, 2025
392dd99
Update django-softdelete
matthewelwell Dec 31, 2025
048bc69
remove uses of index_together
matthewelwell Dec 31, 2025
8d185aa
move django-debug-toolbar to dev dependency and loosen dependency spe…
matthewelwell Dec 31, 2025
98dda95
add rename index migrations
matthewelwell Dec 31, 2025
4223e54
Add no-op migration needed for through model
matthewelwell Dec 31, 2025
384497d
Typing fixes / ignore additions following django 5 upgrade
matthewelwell Dec 31, 2025
65b0e0d
Replace uses of timezone.utc
matthewelwell Dec 31, 2025
bf2ccbb
Update djoser to 2.3.3
matthewelwell Dec 31, 2025
79cf1ab
Use `.value` for enum following django upgrade
matthewelwell Dec 31, 2025
0622b3f
Remove DEBUG from tests
matthewelwell Dec 31, 2025
8a2e936
Replace other uses of timezone.utc
matthewelwell Dec 31, 2025
93e20f3
Replace assertQuerysetEqual with assertQuerySetEqual
matthewelwell Dec 31, 2025
18a3e5d
Add pragma no cover
matthewelwell Dec 31, 2025
1054669
Remove unnecessary DEBUG value from test settings.
matthewelwell Jan 2, 2026
07b1f64
Only rename index for postgres as per previous migration
matthewelwell Jan 2, 2026
f6c1c4c
Update flagsmith-common
matthewelwell Jan 6, 2026
e68f085
Poetry lock after rebase
matthewelwell Jan 21, 2026
80de6a1
update django-debug-toolbar
matthewelwell Jan 21, 2026
67f7303
Revert change to flagsmith-common
matthewelwell Jan 21, 2026
5e485fa
Bump djoser again...
matthewelwell Jan 21, 2026
211fc8f
temp: use auth-controller branch
matthewelwell Jan 21, 2026
b8c7230
Merge branch 'main' into deps/django-5-upgrade
matthewelwell Jan 21, 2026
fefa9f2
temp: update auth-controller to latest commit
matthewelwell Jan 21, 2026
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
2 changes: 0 additions & 2 deletions api/app/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,3 @@
"""

ENABLE_POSTPONE_DECORATOR = False

DEBUG = True
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the upgrade of django-debug-toolbar, this was causing issues because we only include the debug toolbar if DEBUG is True (see here). As I understand it, it's because we don't also add it to installed_apps here which is now required (but seemingly wasn't in older versions of the package).

I decided the better solution here was simply to remove the DEBUG = True as I'm not sure why it existed here for tests in the first place.

2 changes: 1 addition & 1 deletion api/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
),
]

if settings.DEBUG:
if settings.DEBUG: # pragma: no cover
urlpatterns = [
re_path(r"^__debug__/", include("debug_toolbar.urls")),
] + urlpatterns
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.9 on 2025-12-31 15:34

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("app_analytics", "0006_add_labels"),
]

operations = [
migrations.RenameIndex(
model_name="apiusageraw",
new_name="app_analyti_environ_b61cad_idx",
old_fields=("environment_id", "created_at"),
),
Comment on lines +13 to +17
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caused by deprecation of Model.Meta.index_together. I've researched and, as I understand it, it requires a lock on the metadata, but no the table or index itself.

Since we don't reference the name of the index directly (although we might in specific oracle migrations 🤔) we shouldn't have any schema mismatch issues.

]
2 changes: 1 addition & 1 deletion api/app_analytics/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class APIUsageRaw(models.Model):
labels = HStoreField(default=dict)

class Meta:
index_together = (("environment_id", "created_at"),)
indexes = [models.Index(fields=["environment_id", "created_at"])]


class AbstractBucket(LifecycleModelMixin, models.Model): # type: ignore[misc]
Expand Down
2 changes: 1 addition & 1 deletion api/audit/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def _get_organisation(self) -> Organisation | None:

Since we're applying the base filters to the query set
"""
return (
return ( # type: ignore[no-any-return]
self.request.user.organisations.filter( # type: ignore[union-attr]
userorganisation__role=OrganisationRole.ADMIN
)
Expand Down
2 changes: 2 additions & 0 deletions api/custom_auth/mfa/trench/command/remove_backup_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ def remove_backup_code_command(user_id: Any, method_name: str, code: str) -> Non
.values_list("_backup_codes", flat=True)
.first()
)
if serialized_codes is None: # pragma: no cover
return
Comment on lines +14 to +15
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added for typing reasons.

codes = MFAMethod._BACKUP_CODES_DELIMITER.join(
_remove_code_from_set( # type: ignore[arg-type]
backup_codes=set(serialized_codes.split(MFAMethod._BACKUP_CODES_DELIMITER)),
Expand Down
7 changes: 4 additions & 3 deletions api/custom_auth/mfa/trench/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def post(request: Request, method: str) -> Response:
try:
user = request.user
mfa = create_mfa_method_command(
user_id=user.id,
user_id=user.id, # type: ignore[arg-type]
name=method,
)
except MFAValidationError as cause:
Expand All @@ -57,7 +57,7 @@ def post(request: Request, method: str) -> Response:
if not serializer.is_valid():
return Response(status=HTTP_400_BAD_REQUEST, data=serializer.errors)
backup_codes = activate_mfa_method_command(
user_id=request.user.id,
user_id=request.user.id, # type: ignore[arg-type]
name=method,
code=serializer.validated_data["code"],
)
Expand All @@ -71,7 +71,8 @@ class MFAMethodDeactivationView(APIView):
def post(request: Request, method: str) -> Response:
try:
deactivate_mfa_method_command(
mfa_method_name=method, user_id=request.user.id
mfa_method_name=method,
user_id=request.user.id, # type: ignore[arg-type]
)
return Response(status=HTTP_204_NO_CONTENT)
except MFAValidationError as cause:
Expand Down
2 changes: 1 addition & 1 deletion api/environments/identities/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def with_context(
self,
integrations: "Iterable[IntegrationConfig] | None" = None,
extra_select_related: "Iterable[str] | None" = None,
extra_prefetch_related: "Iterable[str | Prefetch] | None" = None,
extra_prefetch_related: "Iterable[str | Prefetch] | None" = None, # type: ignore[type-arg]
) -> "QuerySet[Identity]":
from integrations.integration import IDENTITY_INTEGRATIONS

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.2.9 on 2025-12-31 15:29
from common.migrations.helpers import PostgresOnlyRunSQL
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("identities", "0005_revert_sanitized_identifiers"),
]

operations = [
migrations.SeparateDatabaseAndState(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we only added this index for postgres dbs (see here), we only want to rename it for those databases too.

state_operations=[
migrations.RenameIndex(
model_name="identity",
new_name="environment_environ_341dc9_idx",
old_fields=("environment", "created_date"),
),
],
database_operations=[
PostgresOnlyRunSQL(
'ALTER INDEX "environments_identity_environment_id_created_date_idx" RENAME TO "environment_environ_341dc9_idx"',
reverse_sql='ALTER INDEX "environment_environ_341dc9_idx" RENAME TO "environments_identity_environment_id_created_date_idx"'
)
],
)
]
2 changes: 1 addition & 1 deletion api/environments/identities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class Meta:
# Note that the environment / created_date index is added only to postgres, so we can add it concurrently to
# avoid any downtime. If people using MySQL / Oracle have issues with poor performance on the identities table,
# we can provide them the SQL to add it manually in a small window of downtime.
index_together = (("environment", "created_date"),)
indexes = [models.Index(fields=["environment", "created_date"])]

def natural_key(self): # type: ignore[no-untyped-def]
return self.identifier, self.environment.api_key
Expand Down
2 changes: 1 addition & 1 deletion api/environments/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def filter_for_document_builder( # type: ignore[no-untyped-def]
self,
*args,
extra_select_related: list[str] | None = None,
extra_prefetch_related: list[Prefetch | str] | None = None,
extra_prefetch_related: list[Prefetch | str] | None = None, # type: ignore[type-arg]
**kwargs,
):
return (
Expand Down
8 changes: 4 additions & 4 deletions api/features/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ class FeatureAdmin(SimpleHistoryAdmin): # type: ignore[misc]
class FeatureSegmentAdmin(admin.ModelAdmin): # type: ignore[type-arg]
model = FeatureSegment

def add_view(self, *args, **kwargs): # type: ignore[no-untyped-def]
self.exclude = ("priority",)
def add_view(self, *args, **kwargs): # type: ignore[no-untyped-def] # pragma: no cover
self.exclude = ("priority",) # type: ignore[misc]
return super(FeatureSegmentAdmin, self).add_view(*args, **kwargs)

def change_view(self, *args, **kwargs): # type: ignore[no-untyped-def]
self.exclude = ()
def change_view(self, *args, **kwargs): # type: ignore[no-untyped-def] # pragma: no cover
self.exclude = () # type: ignore[misc]
return super(FeatureSegmentAdmin, self).change_view(*args, **kwargs)


Expand Down
2 changes: 1 addition & 1 deletion api/features/feature_health/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def dismiss_feature_health_event(
FeatureHealthEvent.objects.create(
feature=feature_health_event.feature,
environment=feature_health_event.environment,
type=FeatureHealthEventType.HEALTHY,
type=FeatureHealthEventType.HEALTHY.value,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one was somewhat odd. I can't see any indication of any changes to this behaviour in the notes here, but looking at this test I do think it was wrong previously anyway.

The error that I was seeing was that it couldn't create the event with the type as the literal string "FeatureHealthEventType.HEALTHY" which makes sense - perhaps Django 5 is more strict on enforcing the choices behaviour. Either way, I think this change is a good step.

reason=json.dumps(reason),
provider_name=feature_health_event.provider_name,
external_id=feature_health_event.external_id,
Expand Down
2 changes: 1 addition & 1 deletion api/features/feature_states/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ def has_permission(self, request: Request, view: APIView) -> bool:
except Environment.DoesNotExist:
return False

return request.user.has_environment_permission( # type: ignore[union-attr]
return request.user.has_environment_permission( # type: ignore[union-attr,no-any-return]
UPDATE_FEATURE_STATE, environment
)
8 changes: 4 additions & 4 deletions api/features/import_export/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def has_permission(self, request: Request, view: APIView) -> bool:
return False

environment = Environment.objects.get(id=request.data["environment_id"])
return request.user.is_environment_admin(environment) # type: ignore[union-attr]
return request.user.is_environment_admin(environment) # type: ignore[union-attr,no-any-return]


class DownloadFeatureExportPermissions(IsAuthenticated):
Expand All @@ -39,7 +39,7 @@ def has_permission(self, request: Request, view: APIView) -> bool:

feature_export = FeatureExport.objects.get(id=view.kwargs["feature_export_id"])

return request.user.is_environment_admin(feature_export.environment) # type: ignore[union-attr]
return request.user.is_environment_admin(feature_export.environment) # type: ignore[union-attr,no-any-return]


class FeatureExportListPermissions(IsAuthenticated):
Expand All @@ -50,7 +50,7 @@ def has_permission(self, request: Request, view: ListAPIView) -> bool: # type:
project = Project.objects.get(id=view.kwargs["project_pk"])
# The user will only see environment feature exports
# that the user is an environment admin.
return request.user.has_project_permission(VIEW_PROJECT, project) # type: ignore[union-attr]
return request.user.has_project_permission(VIEW_PROJECT, project) # type: ignore[union-attr,no-any-return]


class FeatureImportListPermissions(IsAuthenticated):
Expand All @@ -61,4 +61,4 @@ def has_permission(self, request: Request, view: ListAPIView) -> bool: # type:
project = Project.objects.get(id=view.kwargs["project_pk"])
# The user will only see environment feature imports
# that the user is an environment admin.
return request.user.has_project_permission(VIEW_PROJECT, project) # type: ignore[union-attr]
return request.user.has_project_permission(VIEW_PROJECT, project) # type: ignore[union-attr,no-any-return]
10 changes: 5 additions & 5 deletions api/features/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@ def has_permission(self, request: Request, view: GenericViewSet) -> bool: # typ

tag_ids = list(feature.tags.values_list("id", flat=True))

return request.user.has_environment_permission( # type: ignore[union-attr]
return request.user.has_environment_permission( # type: ignore[union-attr,no-any-return]
required_permission,
environment,
tag_ids=tag_ids, # type: ignore[arg-type]
tag_ids=tag_ids,
)
return False

Expand All @@ -144,10 +144,10 @@ def has_object_permission(
if permission in TAG_SUPPORTED_ENVIRONMENT_PERMISSIONS:
tag_ids = list(obj.feature.tags.values_list("id", flat=True))

return request.user.has_environment_permission( # type: ignore[union-attr]
return request.user.has_environment_permission( # type: ignore[union-attr,no-any-return]
permission,
environment=obj.environment, # type: ignore[arg-type]
tag_ids=tag_ids, # type: ignore[arg-type]
environment=obj.environment,
tag_ids=tag_ids,
)


Expand Down
22 changes: 11 additions & 11 deletions api/features/versioning/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ def has_permission(self, request: Request, view: GenericViewSet) -> bool: # typ
feature = Feature.objects.get(id=feature_id, project=environment.project)
tag_ids = list(feature.tags.values_list("id", flat=True))

return request.user.has_environment_permission( # type: ignore[union-attr]
return request.user.has_environment_permission( # type: ignore[union-attr,no-any-return]
permission=required_permission,
environment=environment,
tag_ids=tag_ids, # type: ignore[arg-type]
tag_ids=tag_ids,
)

def has_object_permission(
Expand All @@ -53,10 +53,10 @@ def has_object_permission(
if required_permission in TAG_SUPPORTED_ENVIRONMENT_PERMISSIONS:
tag_ids = list(obj.feature.tags.values_list("id", flat=True))

return request.user.has_environment_permission( # type: ignore[union-attr]
return request.user.has_environment_permission( # type: ignore[union-attr,no-any-return]
permission=required_permission,
environment=obj.environment,
tag_ids=tag_ids, # type: ignore[arg-type]
tag_ids=tag_ids,
)


Expand All @@ -73,11 +73,11 @@ def has_permission(self, request: Request, view: GenericViewSet) -> bool: # typ
environment = Environment.objects.get(id=environment_pk)

if view.action == "list":
return request.user.has_environment_permission( # type: ignore[union-attr]
return request.user.has_environment_permission( # type: ignore[union-attr,no-any-return]
permission=VIEW_ENVIRONMENT, environment=environment
)

return request.user.has_environment_permission( # type: ignore[union-attr]
return request.user.has_environment_permission( # type: ignore[union-attr,no-any-return]
permission=UPDATE_FEATURE_STATE, environment=environment
)

Expand All @@ -87,13 +87,13 @@ def has_object_permission(
view: GenericViewSet, # type: ignore[override,type-arg]
obj: FeatureState,
) -> bool:
if view.action == "retrieve":
return request.user.has_environment_permission( # type: ignore[union-attr]
if view.action == "retrieve": # pragma: no cover
return request.user.has_environment_permission( # type: ignore[union-attr,no-any-return]
permission=VIEW_ENVIRONMENT,
environment=obj.environment, # type: ignore[arg-type]
environment=obj.environment,
)

return request.user.has_environment_permission( # type: ignore[union-attr]
return request.user.has_environment_permission( # type: ignore[union-attr,no-any-return]
permission=UPDATE_FEATURE_STATE,
environment=obj.environment, # type: ignore[arg-type]
environment=obj.environment,
)
6 changes: 3 additions & 3 deletions api/features/versioning/versioning_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def get_environment_flags_list(
additional_filters: Q = None, # type: ignore[assignment]
additional_select_related_args: typing.Iterable[str] = None, # type: ignore[assignment]
additional_prefetch_related_args: typing.Iterable[
typing.Union[str, Prefetch]
typing.Union[str, Prefetch[typing.Any]]
] = None, # type: ignore[assignment]
from_replica: bool = False,
) -> list[FeatureState]:
Expand Down Expand Up @@ -64,7 +64,7 @@ def get_environment_flags_dict(
additional_filters: Q = None, # type: ignore[assignment]
additional_select_related_args: typing.Iterable[str] = None, # type: ignore[assignment]
additional_prefetch_related_args: typing.Iterable[
typing.Union[str, Prefetch]
typing.Union[str, Prefetch[typing.Any]]
] = None, # type: ignore[assignment]
key_function: typing.Callable[[FeatureState], tuple] = None, # type: ignore[type-arg,assignment]
from_replica: bool = False,
Expand Down Expand Up @@ -507,7 +507,7 @@ def _get_feature_states_queryset(
additional_filters: Q = None, # type: ignore[assignment]
additional_select_related_args: typing.Iterable[str] = None, # type: ignore[assignment]
additional_prefetch_related_args: typing.Iterable[
typing.Union[str, Prefetch]
typing.Union[str, Prefetch[typing.Any]]
] = None, # type: ignore[assignment]
from_replica: bool = False,
) -> QuerySet[FeatureState]:
Expand Down
11 changes: 4 additions & 7 deletions api/organisations/chargebee/webhook_handlers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from datetime import datetime
from datetime import timezone as dttz

from django.utils import timezone
from rest_framework import status
Expand Down Expand Up @@ -129,7 +130,7 @@ def process_subscription(request: Request) -> Response: # noqa: C901
cancellation_date = subscription.get("current_term_end")
if cancellation_date is not None:
cancellation_date = datetime.fromtimestamp(cancellation_date).replace(
tzinfo=timezone.utc # type: ignore[attr-defined]
tzinfo=dttz.utc
)
else:
cancellation_date = timezone.now()
Expand Down Expand Up @@ -168,9 +169,7 @@ def process_subscription(request: Request) -> Response: # noqa: C901
else:
osic_defaults["current_billing_term_ends_at"] = datetime.fromtimestamp(
current_term_end
).replace(
tzinfo=timezone.utc # type: ignore[attr-defined]
)
).replace(tzinfo=dttz.utc)

if "current_term_start" in subscription:
current_term_start = subscription["current_term_start"]
Expand All @@ -179,9 +178,7 @@ def process_subscription(request: Request) -> Response: # noqa: C901
else:
osic_defaults["current_billing_term_starts_at"] = datetime.fromtimestamp(
current_term_start
).replace(
tzinfo=timezone.utc # type: ignore[attr-defined]
)
).replace(tzinfo=dttz.utc)

OrganisationSubscriptionInformationCache.objects.update_or_create(
organisation_id=existing_subscription.organisation_id,
Expand Down
2 changes: 1 addition & 1 deletion api/organisations/permissions/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,4 @@ def has_permission(self, request: Request, view: View) -> bool:
return False

# All organisation users can see api usage notifications.
return request.user.belongs_to(view.kwargs.get("organisation_pk")) # type: ignore[union-attr]
return request.user.belongs_to(view.kwargs.get("organisation_pk")) # type: ignore[union-attr,no-any-return]
Loading
Loading