Skip to content

Commit f7476b9

Browse files
committed
Add multi-signal authority model
1 parent 7d59526 commit f7476b9

13 files changed

Lines changed: 1856 additions & 121 deletions

File tree

‎core/tests/test_permissions.py‎

Lines changed: 77 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -263,88 +263,95 @@ def test_reader_is_denied_contributor_and_admin_endpoints(self):
263263
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
264264

265265
def test_member_can_use_contributor_endpoints_but_not_admin_only_ones(self):
266-
_typed_client(self.client).force_authenticate(self.member_user)
266+
with patch("entities.extraction.queue_entity_identity_enrichment"):
267+
_typed_client(self.client).force_authenticate(self.member_user)
267268

268-
review_queue_response = self.client.get(
269-
reverse(
270-
"v1:project-review-queue-list",
271-
kwargs={"project_id": _require_pk(self.project)},
269+
review_queue_response = self.client.get(
270+
reverse(
271+
"v1:project-review-queue-list",
272+
kwargs={"project_id": _require_pk(self.project)},
273+
)
272274
)
273-
)
274-
self.assertEqual(review_queue_response.status_code, status.HTTP_200_OK)
275+
self.assertEqual(review_queue_response.status_code, status.HTTP_200_OK)
275276

276-
source_config_response = self.client.post(
277-
reverse(
278-
"v1:project-source-config-list",
279-
kwargs={"project_id": _require_pk(self.project)},
280-
),
281-
{
282-
"plugin_name": SourcePluginName.RSS,
283-
"config": {"feed_url": "https://example.com/feed.xml"},
284-
"is_active": True,
285-
},
286-
format="json",
287-
)
288-
self.assertEqual(source_config_response.status_code, status.HTTP_201_CREATED)
277+
source_config_response = self.client.post(
278+
reverse(
279+
"v1:project-source-config-list",
280+
kwargs={"project_id": _require_pk(self.project)},
281+
),
282+
{
283+
"plugin_name": SourcePluginName.RSS,
284+
"config": {"feed_url": "https://example.com/feed.xml"},
285+
"is_active": True,
286+
},
287+
format="json",
288+
)
289+
self.assertEqual(
290+
source_config_response.status_code, status.HTTP_201_CREATED
291+
)
289292

290-
topic_summary_response = self.client.get(
291-
reverse(
292-
"v1:project-topic-centroid-snapshot-summary",
293-
kwargs={"project_id": _require_pk(self.project)},
293+
topic_summary_response = self.client.get(
294+
reverse(
295+
"v1:project-topic-centroid-snapshot-summary",
296+
kwargs={"project_id": _require_pk(self.project)},
297+
)
294298
)
295-
)
296-
self.assertEqual(topic_summary_response.status_code, status.HTTP_200_OK)
299+
self.assertEqual(topic_summary_response.status_code, status.HTTP_200_OK)
297300

298-
accept_candidate_response = self.client.post(
299-
reverse(
300-
"v1:project-entity-candidate-accept",
301-
kwargs={
302-
"project_id": _require_pk(self.project),
303-
"pk": _require_pk(self.entity_candidate),
304-
},
305-
),
306-
format="json",
307-
)
308-
self.assertEqual(accept_candidate_response.status_code, status.HTTP_200_OK)
301+
accept_candidate_response = self.client.post(
302+
reverse(
303+
"v1:project-entity-candidate-accept",
304+
kwargs={
305+
"project_id": _require_pk(self.project),
306+
"pk": _require_pk(self.entity_candidate),
307+
},
308+
),
309+
format="json",
310+
)
311+
self.assertEqual(accept_candidate_response.status_code, status.HTTP_200_OK)
309312

310-
delete_own_feedback_response = self.client.delete(
311-
reverse(
312-
"v1:project-feedback-detail",
313-
kwargs={
314-
"project_id": _require_pk(self.project),
315-
"pk": _require_pk(self.member_feedback),
316-
},
313+
delete_own_feedback_response = self.client.delete(
314+
reverse(
315+
"v1:project-feedback-detail",
316+
kwargs={
317+
"project_id": _require_pk(self.project),
318+
"pk": _require_pk(self.member_feedback),
319+
},
320+
)
321+
)
322+
self.assertEqual(
323+
delete_own_feedback_response.status_code, status.HTTP_204_NO_CONTENT
317324
)
318-
)
319-
self.assertEqual(
320-
delete_own_feedback_response.status_code, status.HTTP_204_NO_CONTENT
321-
)
322325

323-
update_project_response = self.client.patch(
324-
reverse("v1:project-detail", kwargs={"id": _require_pk(self.project)}),
325-
{"name": "Member Update"},
326-
format="json",
327-
)
328-
self.assertEqual(update_project_response.status_code, status.HTTP_403_FORBIDDEN)
326+
update_project_response = self.client.patch(
327+
reverse("v1:project-detail", kwargs={"id": _require_pk(self.project)}),
328+
{"name": "Member Update"},
329+
format="json",
330+
)
331+
self.assertEqual(
332+
update_project_response.status_code, status.HTTP_403_FORBIDDEN
333+
)
329334

330-
list_credentials_response = self.client.get(
331-
reverse(
332-
"v1:project-bluesky-credentials-list",
333-
kwargs={"project_id": _require_pk(self.project)},
335+
list_credentials_response = self.client.get(
336+
reverse(
337+
"v1:project-bluesky-credentials-list",
338+
kwargs={"project_id": _require_pk(self.project)},
339+
)
340+
)
341+
self.assertEqual(
342+
list_credentials_response.status_code, status.HTTP_403_FORBIDDEN
334343
)
335-
)
336-
self.assertEqual(
337-
list_credentials_response.status_code, status.HTTP_403_FORBIDDEN
338-
)
339344

340-
rotate_token_response = self.client.post(
341-
reverse(
342-
"v1:project-rotate-intake-token",
343-
kwargs={"id": _require_pk(self.project)},
344-
),
345-
format="json",
346-
)
347-
self.assertEqual(rotate_token_response.status_code, status.HTTP_403_FORBIDDEN)
345+
rotate_token_response = self.client.post(
346+
reverse(
347+
"v1:project-rotate-intake-token",
348+
kwargs={"id": _require_pk(self.project)},
349+
),
350+
format="json",
351+
)
352+
self.assertEqual(
353+
rotate_token_response.status_code, status.HTTP_403_FORBIDDEN
354+
)
348355

349356
def test_member_cannot_delete_other_users_feedback(self):
350357
_typed_client(self.client).force_authenticate(self.member_user)

‎entities/admin.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,9 @@ class EntityCandidateAdmin(admin.ModelAdmin):
174174
"project",
175175
"suggested_type",
176176
"occurrence_count",
177+
"cluster_key",
177178
"status",
179+
"auto_promotion_blocked_reason",
178180
"merged_into",
179181
"first_seen_in",
180182
"created_at",
@@ -196,7 +198,7 @@ def accept_selected_candidates(self, request, queryset):
196198
for candidate in queryset.select_related("project"):
197199
if candidate.status == EntityCandidateStatus.ACCEPTED:
198200
continue
199-
accept_entity_candidate(candidate)
201+
accept_entity_candidate(candidate, schedule_enrichment=True)
200202
accepted_count += 1
201203
self.message_user(
202204
request,
@@ -238,7 +240,11 @@ def merge_into_existing_entities(
238240
if matching_entities.count() != 1:
239241
unresolved_names.append(candidate.name)
240242
continue
241-
merge_entity_candidate(candidate, matching_entities.get())
243+
merge_entity_candidate(
244+
candidate,
245+
matching_entities.get(),
246+
schedule_enrichment=True,
247+
)
242248
merged_count += 1
243249

244250
if merged_count:

‎entities/api.py‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@
1313
build_crud_action_overrides,
1414
document_project_owned_viewset,
1515
)
16-
from entities.extraction import (
17-
accept_entity_candidate,
18-
merge_entity_candidate,
19-
reject_entity_candidate,
20-
)
2116
from core.permissions import (
2217
IsProjectAdmin,
2318
IsProjectContributor,
2419
IsProjectMember,
2520
IsProjectMemberWritable,
2621
)
22+
from entities.extraction import (
23+
accept_entity_candidate,
24+
merge_entity_candidate,
25+
reject_entity_candidate,
26+
)
2727
from entities.models import Entity, EntityCandidate, EntityMention
2828
from entities.serializers import (
2929
EntityAuthoritySnapshotSerializer,
@@ -55,6 +55,7 @@ class EntityViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet):
5555
queryset = (
5656
Entity.objects.select_related("project")
5757
.annotate(mention_count=Count("mentions", distinct=True))
58+
.prefetch_related("identity_claims")
5859
.prefetch_related(
5960
Prefetch(
6061
"mentions",
@@ -180,7 +181,7 @@ def accept(self, request, *args, **kwargs):
180181
"""Accept an entity candidate and return its updated representation."""
181182

182183
candidate = self.get_object()
183-
accept_entity_candidate(candidate)
184+
accept_entity_candidate(candidate, schedule_enrichment=True)
184185
candidate.refresh_from_db()
185186
serializer = self.get_serializer(candidate)
186187
return Response(serializer.data)
@@ -226,7 +227,11 @@ def merge(self, request, *args, **kwargs):
226227
context=self.get_serializer_context(),
227228
)
228229
serializer.is_valid(raise_exception=True)
229-
merge_entity_candidate(candidate, serializer.validated_data["merged_into"])
230+
merge_entity_candidate(
231+
candidate,
232+
serializer.validated_data["merged_into"],
233+
schedule_enrichment=True,
234+
)
230235
candidate.refresh_from_db()
231236
response_serializer = self.get_serializer(candidate)
232237
return Response(response_serializer.data)

0 commit comments

Comments
 (0)