Skip to content

Commit 182f646

Browse files
authored
Merge branch 'main' into claude/resolve-issue-1332-OxMNg
Signed-off-by: JSIV <5049984+JSv4@users.noreply.github.com>
2 parents b7e4315 + b7b4810 commit 182f646

8 files changed

Lines changed: 49 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- **Var-annotated additions**: `id_to_children: dict[Any, list[Any]]` in `base_types.py`, `read_only_fields: list[str]` in `serializers.py`, `this_model_permission_id_map: dict[int, str]` etc. in middleware.
2020
- **Five modules graduated from the mypy baseline** (`mypy.ini` → no longer `ignore_errors = True`): `config.graphql.base_types`, `config.graphql.conversation_types`, `config.graphql.permissioning.permission_annotator.middleware`, `config.graphql.permissioning.permission_annotator.utils`, `config.graphql.serializers`. Their entries in `docs/typing/mypy_baseline.txt` (11 lines) were also pruned. Future PRs can graduate the remaining baselined files as the structural issues they expose (custom `visible_to_user` manager method not seen by `django-stubs`, `set_permissions_for_obj_to_user` signature mismatch, mixin `_meta` access) are addressed.
2121
- **Tooling**: zero new `# type: ignore` markers; black & isort applied; `flake8 config/graphql/` clean. `mypy --config-file mypy.ini opencontractserver config` passes with the updated baseline.
22+
- **Mypy: graduated `opencontractserver/users/tasks.py` out of the baseline** (Issue #1333 follow-up): `tasks.py` was the last `opencontractserver.users` module still suppressed in `mypy.ini`. PR #1370 left it untyped because the file is only loaded when `settings.USE_AUTH0=True`, so it never failed at runtime under the test settings; the typing gap kept the package short of the issue's "all four packages at ≥80% return-annotation coverage" Done-When criterion. Added return + parameter annotations to all five Auth0 sync tasks (`get_new_auth0_token`, `apply_data_to_user`, `sync_remote_user`, `ensure_valid_auth0_token`, `get_user_details_async`), introduced a module-level docstring documenting the `USE_AUTH0` gating, and removed the `[mypy-opencontractserver.users.tasks] ignore_errors = True` section. Local `data` rebound from request body (`dict[str, str]`) to response payload (`dict[str, Any]`) was split into two distinctly-named variables (`request_data` / `payload`) so the types are unambiguous; behavior is unchanged. No callers needed updating — `config/graphql_auth0_auth/utils.py` still consumes `sync_remote_user.delay(...)` exactly as before.
2223

2324
### Fixed
2425

26+
- **`test_superuser_sees_all_queryset` miscounts personal corpuses by 1** (Issue #1394, `opencontractserver/tests/test_visibility_managers.py`, `opencontractserver/tests/test_resolvers.py`): Two `VisibleToUserTests.test_superuser_sees_all_queryset` cases asserted that `Corpus.objects.visible_to_user(superuser).count() == 4` (public + private + 2 personal), but the actual count is 5 because the test DB starts with a pre-existing personal corpus owned by django-guardian's `AnonymousUser` (created during fixture setup before/around the username-based skip in `opencontractserver/users/signals.py::user_created_signal`). The assertion is now scoped to corpuses created by the test's two users (`creator__in=[self.user, self.superuser]`), making it resilient to any fixture-level corpuses that exist at test DB init time. Production code is unchanged.
2527
- **Merged `frontend` Codecov flag drops to ~33% on every commit where Frontend CI's CT job fails** (`frontend/package.json` `test:coverage:ct`): the script chained `playwright test ... && mkdir -p ... && nyc report ...`, so a failing CT run short-circuited before `nyc report` could turn the per-test JSON files in `.nyc_output` into an `lcov.info`. The downstream `Upload CT Coverage to Codecov` step (`if: success() || failure()`) then errored with "No coverage reports found" and `frontend-component` did not upload for that SHA. Codecov's server-side aggregation of the `frontend` flag was left with only `frontend-unit` (~23%) and `frontend-e2e` (~24%), pulling the merged number down to ~33% even though the previous commit was at ~67% — observed on six consecutive main commits 2026-04-26T01:02..02:58Z (`2d7033f8`..`be5bcfc8`) before recovering on `30298391`. Mirrored the existing `test:e2e:coverage` pattern (`; CT_EXIT=$?; nyc report ... || echo "No coverage data to report"; exit $CT_EXIT`) so `nyc report` runs regardless of test outcome and the lcov ships even on red CT runs. `frontend-component` will still report a slightly lower number when tests fail (failed tests register fewer hits), but it will report — keeping the merged `frontend` flag's denominator stable.
2628
- **`User.__init__` shared-state mutation re-introduced by branch merge** (`opencontractserver/users/models.py:172-180` removed): PR #1374 (commit `50ed6740`) deleted the `User.__init__` override that mutated `Field.validators[0]` on every instantiation, but a subsequent merge (`b68c1cb4 → 6d2cddbf`) resurrected the override along with its mypy-narrowing changes. The current main on commit `6d2cddbf` therefore reproduced the original `#1358` bug: `User(...)` rebound `username_field.validators[0]` and clobbered any third-party validator prepended to the list. Removed the `__init__` override entirely; the class-body declaration `validators=[UserUnicodeUsernameValidator()]` on the `username` field (still present from PR #1374) is the canonical and only declaration. Also dropped the now-unused `Field` import. Regression coverage from PR #1374 (`opencontractserver/tests/test_user_username_validator.py`) was already on main and is what surfaced the regression in CI.
2729

mypy.ini

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,10 +1080,7 @@ ignore_errors = True
10801080
[mypy-opencontractserver.tests.websocket.test_unified_agent_consumer]
10811081
ignore_errors = True
10821082

1083-
# --- opencontractserver.users (1 file) ---
1084-
1085-
[mypy-opencontractserver.users.tasks]
1086-
ignore_errors = True
1083+
# --- opencontractserver.users ---
10871084

10881085
# ``opencontractserver.users.models.User`` defines reverse relations to
10891086
# ``opencontractserver.corpuses.models.Corpus``, ``.CorpusFolder``, and

opencontractserver/tests/test_resolvers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,14 @@ def test_superuser_sees_all_queryset(self):
5959
"""Superusers should see all objects ordered by creation."""
6060
result = Corpus.objects.visible_to_user(self.superuser)
6161

62+
# Filter to corpuses created by this test's users to make the assertion
63+
# resilient to fixture-level personal corpuses (e.g. the one auto-created
64+
# for guardian's AnonymousUser during DB setup). See issue #1394.
65+
scoped = result.filter(creator__in=[self.user, self.superuser])
66+
6267
# Should see both test corpora + 2 personal corpuses (one per user)
6368
# Each user (user, superuser) gets a personal corpus auto-created
64-
self.assertEqual(result.count(), 4) # public + private + 2 personal
69+
self.assertEqual(scoped.count(), 4) # public + private + 2 personal
6570
# Should be ordered by created
6671
self.assertEqual(result.query.order_by, ("created",))
6772

opencontractserver/tests/test_visibility_managers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,14 @@ def test_superuser_sees_all_queryset(self):
7171
"""Superusers should see all objects ordered by creation."""
7272
result = Corpus.objects.visible_to_user(self.superuser)
7373

74+
# Filter to corpuses created by this test's users to make the assertion
75+
# resilient to fixture-level personal corpuses (e.g. the one auto-created
76+
# for guardian's AnonymousUser during DB setup). See issue #1394.
77+
scoped = result.filter(creator__in=[self.user, self.superuser])
78+
7479
# Should see both test corpora + 2 personal corpuses (one per user)
7580
# Each user (user, superuser) gets a personal corpus auto-created
76-
self.assertEqual(result.count(), 4) # public + private + 2 personal
81+
self.assertEqual(scoped.count(), 4) # public + private + 2 personal
7782
# Should be ordered by created
7883
self.assertEqual(result.query.order_by, ("created",))
7984

opencontractserver/users/tasks.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
"""Celery tasks for synchronising user data with Auth0.
2+
3+
These tasks are defined only when ``settings.USE_AUTH0`` is enabled — they
4+
fetch a Machine-to-Machine token, look up the remote profile via the Auth0
5+
Management API, and copy the result onto the local :class:`User` row.
6+
"""
7+
18
import datetime
29
import json
310
import logging
11+
from typing import Any, Optional
412

513
import requests
614
from celery import chain
15+
from celery.result import AsyncResult
716
from django.conf import settings
817
from django.contrib.auth import get_user_model
918

@@ -21,34 +30,34 @@
2130
if settings.USE_AUTH0:
2231

2332
@celery_app.task()
24-
def get_new_auth0_token():
33+
def get_new_auth0_token() -> Optional[str]:
2534

2635
# print("get_new_auth0_token")
2736
url = f"https://{auth0_settings.AUTH0_DOMAIN}/oauth/token"
2837
# print(url)
2938

30-
headers = {"content-type": "application/json"}
39+
headers: dict[str, str] = {"content-type": "application/json"}
3140
# print(headers)
3241

33-
data = {
42+
request_data: dict[str, str] = {
3443
"grant_type": auth0_settings.AUTH0_M2M_MANAGEMENT_GRANT_TYPE,
3544
"client_id": auth0_settings.AUTH0_M2M_MANAGEMENT_API_ID,
3645
"client_secret": auth0_settings.AUTH0_M2M_MANAGEMENT_API_SECRET,
3746
"audience": f"https://{auth0_settings.AUTH0_DOMAIN}/api/v2/",
3847
}
39-
# print(f"Machine-2-Machine Request data: {data}")
48+
# print(f"Machine-2-Machine Request data: {request_data}")
4049

41-
response = requests.post(url, headers=headers, json=data)
50+
response = requests.post(url, headers=headers, json=request_data)
4251

4352
# print("Auth0 Response:")
4453
# print(response.status_code)
4554
# print(response.text)
4655

4756
if response.status_code == 200:
48-
data = json.loads(response.text)
49-
# print(data)
50-
access_token = data["access_token"]
51-
expires_in = data["expires_in"]
57+
payload: dict[str, Any] = json.loads(response.text)
58+
# print(payload)
59+
access_token: str = payload["access_token"]
60+
expires_in: int = payload["expires_in"]
5261

5362
newToken = Auth0APIToken()
5463
newToken.token = access_token
@@ -60,11 +69,11 @@ def get_new_auth0_token():
6069

6170
return newToken.token
6271

63-
else:
64-
logger.error("Error retrieving access token to Auth0.")
72+
logger.error("Error retrieving access token to Auth0.")
73+
return None
6574

6675
@celery_app.task()
67-
def apply_data_to_user(data, userPk):
76+
def apply_data_to_user(data: dict[str, Any], userPk: str) -> None:
6877

6978
# print(f"apply_data_to_user() - userPk is: {userPk}\nData: {data}")
7079

@@ -103,7 +112,7 @@ def apply_data_to_user(data, userPk):
103112
)
104113

105114
@celery_app.task()
106-
def sync_remote_user(user_pk):
115+
def sync_remote_user(user_pk: str) -> AsyncResult:
107116

108117
refresh = False
109118
tokens = Auth0APIToken.objects.all()
@@ -133,7 +142,7 @@ def sync_remote_user(user_pk):
133142
return data.apply_async()
134143

135144
@celery_app.task()
136-
def ensure_valid_auth0_token():
145+
def ensure_valid_auth0_token() -> Optional[str]:
137146

138147
tokens = Auth0APIToken.objects.all()
139148

@@ -147,6 +156,7 @@ def ensure_valid_auth0_token():
147156
for tok in tokens:
148157
tok.delete()
149158
return get_new_auth0_token.delay().get()
159+
return None
150160
else:
151161
if tokens[0].expiration_Date < datetime.datetime.now(datetime.timezone.utc):
152162
# print("Token has expired. Refetching from Auth0")
@@ -157,8 +167,8 @@ def ensure_valid_auth0_token():
157167
return tokens[0].token
158168

159169
@celery_app.task
160-
def get_user_details_async(token, auth0_Id):
161-
headers = {
170+
def get_user_details_async(token: str, auth0_Id: str) -> dict[str, Any]:
171+
headers: dict[str, str] = {
162172
"Authorization": f"Bearer {token}",
163173
}
164174
url = f"https://{auth0_settings.AUTH0_DOMAIN}/api/v2/users/{auth0_Id}"

requirements/base.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ celery==5.6.3 # pyup: < 6.0 # https://github.com/celery/celery
77
flower==2.0.1 # https://github.com/mher/flower
88
django-celery-beat==2.9.0 # https://github.com/celery/django-celery-beat
99
pyjwt==2.12.1 # https://github.com/jpadilla/pyjwt
10-
cryptography==46.0.7 # https://github.com/pyca/cryptography
10+
cryptography==47.0.0 # https://github.com/pyca/cryptography
1111
pydantic==2.*
1212
typing-extensions==4.* # https://github.com/python/typing_extensions
1313
requests==2.* # https://requests.readthedocs.io/en/latest/
@@ -31,7 +31,7 @@ pgvector>=0.4.0 # Python binding for pgvector; PostgreSQL extension v0.8.2+ req
3131
channels==4.3.2 # https://github.com/django/channels
3232
daphne==4.2.1 # https://github.com/django/daphne
3333
channels-redis==4.3.0
34-
Twisted[tls,http2]
34+
Twisted[tls,http2]>=25.5.0
3535

3636
# Django REST Framework
3737
# ------------------------------------------------------------------------------
@@ -86,7 +86,7 @@ ipython>=9.13.0 # not directly required, pinned by Snyk to avoid a vulnerability
8686
requests>=2.32.2 # not directly required, pinned by Snyk to avoid a vulnerability
8787
setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability
8888
sqlparse>=0.5.0 # not directly required, pinned by Snyk to avoid a vulnerability
89-
tornado>=6.4.2 # not directly required, pinned by Snyk to avoid a vulnerability
89+
tornado>=6.5.5 # not directly required, pinned by Snyk to avoid a vulnerability
9090
urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability
9191
zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability
9292
pillow>=10.3.0 # not directly required, pinned by Snyk to avoid a vulnerability

requirements/local.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ipdb==0.13.13 # https://github.com/gotcha/ipdb
88
# Testing
99
# ------------------------------------------------------------------------------
1010

11-
mypy==1.20.1 # https://github.com/python/mypy
11+
mypy==1.20.2 # https://github.com/python/mypy
1212
django-stubs==6.0.3 # https://github.com/typeddjango/django-stubs
1313
pytest==9.0.3 # https://github.com/pytest-dev/pytest
1414
pytest-cov==7.1.0 # https://github.com/pytest-dev/pytest-cov
@@ -52,12 +52,12 @@ python-pptx
5252
# Not directly required, pinned by Snyk to avoid a vulnerability
5353
# ------------------------------------------------------------------------------
5454
django==5.2.13 # https://www.djangoproject.com/
55-
twisted>=24.7.0rc1 # not directly required, pinned by Snyk to avoid a vulnerability
55+
twisted>=25.5.0 # not directly required, pinned by Snyk to avoid a vulnerability
5656
ipython>=9.13.0 # not directly required, pinned by Snyk to avoid a vulnerability
5757
requests>=2.32.2 # not directly required, pinned by Snyk to avoid a vulnerability
5858
setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability
5959
sqlparse>=0.5.0 # not directly required, pinned by Snyk to avoid a vulnerability
60-
tornado>=6.4.2 # not directly required, pinned by Snyk to avoid a vulnerability
60+
tornado>=6.5.5 # not directly required, pinned by Snyk to avoid a vulnerability
6161
urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability
6262
zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability
6363
pillow>=12.2.0 # not directly required, pinned by Snyk to avoid a vulnerability

requirements/production.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ google-auth==2.49.2
1818
# Duplicated accross requirements files due to Snyk not understanding requirements inheritance
1919
# ------------------------------------------------------------------------------
2020
django==5.2.13 # https://www.djangoproject.com/
21-
twisted>=24.7.0rc1 # not directly required, pinned by Snyk to avoid a vulnerability
21+
twisted>=25.5.0 # not directly required, pinned by Snyk to avoid a vulnerability
2222
ipython>=9.13.0 # not directly required, pinned by Snyk to avoid a vulnerability
2323
requests>=2.32.2 # not directly required, pinned by Snyk to avoid a vulnerability
2424
setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability
2525
sqlparse>=0.5.0 # not directly required, pinned by Snyk to avoid a vulnerability
26-
tornado>=6.4.2 # not directly required, pinned by Snyk to avoid a vulnerability
26+
tornado>=6.5.5 # not directly required, pinned by Snyk to avoid a vulnerability
2727
urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability
2828
zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability
2929
pillow>=12.2.0 # not directly required, pinned by Snyk to avoid a vulnerability

0 commit comments

Comments
 (0)