diff --git a/alembic/versions/2025_12_05_1721-dfb64594049f_create_anonymous_annotation_name.py b/alembic/versions/2025_12_05_1721-dfb64594049f_create_anonymous_annotation_name.py new file mode 100644 index 00000000..848b9e98 --- /dev/null +++ b/alembic/versions/2025_12_05_1721-dfb64594049f_create_anonymous_annotation_name.py @@ -0,0 +1,47 @@ +"""Create anonymous_annotation_name + +Revision ID: dfb64594049f +Revises: 1d3398f9cd8a +Create Date: 2025-12-05 17:21:35.134935 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID + +from src.util.alembic_helpers import created_at_column + +# revision identifiers, used by Alembic. +revision: str = 'dfb64594049f' +down_revision: Union[str, None] = '1d3398f9cd8a' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "link__anonymous_sessions__name_suggestions", + sa.Column( + "session_id", + UUID, + sa.ForeignKey("anonymous_sessions.id"), + nullable=False + ), + sa.Column( + "suggestion_id", + sa.Integer(), + sa.ForeignKey("url_name_suggestions.id"), + nullable=False, + ), + created_at_column(), + sa.PrimaryKeyConstraint( + "session_id", + "suggestion_id" + ) + ) + + +def downgrade() -> None: + pass diff --git a/src/api/endpoints/annotate/anonymous/post/query.py b/src/api/endpoints/annotate/anonymous/post/query.py index 593d79d9..29670c80 100644 --- a/src/api/endpoints/annotate/anonymous/post/query.py +++ b/src/api/endpoints/annotate/anonymous/post/query.py @@ -3,10 +3,13 @@ from sqlalchemy.ext.asyncio import AsyncSession from src.api.endpoints.annotate.all.post.models.request import AllAnnotationPostInfo +from src.db.models.impl.link.anonymous_sessions__name_suggestion import LinkAnonymousSessionNameSuggestion from src.db.models.impl.url.suggestion.anonymous.agency.sqlalchemy import AnonymousAnnotationAgency from src.db.models.impl.url.suggestion.anonymous.location.sqlalchemy import AnonymousAnnotationLocation from src.db.models.impl.url.suggestion.anonymous.record_type.sqlalchemy import AnonymousAnnotationRecordType from src.db.models.impl.url.suggestion.anonymous.url_type.sqlalchemy import AnonymousAnnotationURLType +from src.db.models.impl.url.suggestion.name.enums import NameSuggestionSource +from src.db.models.impl.url.suggestion.name.sqlalchemy import URLNameSuggestion from src.db.queries.base.builder import QueryBuilderBase @@ -31,6 +34,28 @@ async def run(self, session: AsyncSession) -> None: ) session.add(url_type_suggestion) + name_id: int | None + if self.post_info.name_info.new_name is not None: + name_suggestion = URLNameSuggestion( + url_id=self.url_id, + suggestion=self.post_info.name_info.new_name, + source=NameSuggestionSource.USER + ) + session.add(name_suggestion) + await session.flush() + name_id = name_suggestion.id + elif self.post_info.name_info.existing_name_id is not None: + name_id = self.post_info.name_info.existing_name_id + else: + name_id = None + + if name_id is not None: + name_suggestion = LinkAnonymousSessionNameSuggestion( + suggestion_id=name_id, + session_id=self.session_id + ) + session.add(name_suggestion) + if self.post_info.record_type is not None: record_type_suggestion = AnonymousAnnotationRecordType( url_id=self.url_id, diff --git a/src/core/tasks/url/operators/validate/queries/ctes/counts/impl/name.py b/src/core/tasks/url/operators/validate/queries/ctes/counts/impl/name.py index 5cb014f1..4000e6e2 100644 --- a/src/core/tasks/url/operators/validate/queries/ctes/counts/impl/name.py +++ b/src/core/tasks/url/operators/validate/queries/ctes/counts/impl/name.py @@ -1,28 +1,76 @@ from sqlalchemy import select, func from src.core.tasks.url.operators.validate.queries.ctes.counts.core import ValidatedCountsCTEContainer +from src.db.models.impl.link.anonymous_sessions__name_suggestion import LinkAnonymousSessionNameSuggestion from src.db.models.impl.link.user_name_suggestion.sqlalchemy import LinkUserNameSuggestion from src.db.models.impl.url.suggestion.name.sqlalchemy import URLNameSuggestion from src.db.models.views.unvalidated_url import UnvalidatedURL +_user_counts = ( + select( + URLNameSuggestion.url_id, + URLNameSuggestion.suggestion.label("entity"), + func.count().label("votes") + ) + .join( + LinkUserNameSuggestion, + LinkUserNameSuggestion.suggestion_id == URLNameSuggestion.id + ) + .group_by( + URLNameSuggestion.url_id, + URLNameSuggestion.suggestion + ) + .cte("user_counts") +) + +_anon_counts = ( + select( + URLNameSuggestion.url_id, + URLNameSuggestion.suggestion.label("entity"), + func.count().label("votes") + ) + .join( + LinkAnonymousSessionNameSuggestion, + LinkAnonymousSessionNameSuggestion.suggestion_id == URLNameSuggestion.id + ) + .group_by( + URLNameSuggestion.url_id, + URLNameSuggestion.suggestion + ) + .cte("anon_counts") +) + +_union_counts = ( + select( + _user_counts.c.url_id, + _user_counts.c.entity, + _user_counts.c.votes + ) + .union_all( + select( + _anon_counts.c.url_id, + _anon_counts.c.entity, + _anon_counts.c.votes + ) + ) + .cte("counts_name_union") +) + + NAME_VALIDATION_COUNTS_CTE = ValidatedCountsCTEContainer( ( select( - URLNameSuggestion.url_id, - URLNameSuggestion.suggestion.label("entity"), - func.count().label("votes") + _union_counts.c.url_id, + _union_counts.c.entity, + func.sum(_union_counts.c.votes).label("votes") ) .join( UnvalidatedURL, - URLNameSuggestion.url_id == UnvalidatedURL.url_id - ) - .join( - LinkUserNameSuggestion, - LinkUserNameSuggestion.suggestion_id == URLNameSuggestion.id + _union_counts.c.url_id == UnvalidatedURL.url_id ) .group_by( - URLNameSuggestion.url_id, - URLNameSuggestion.suggestion + _union_counts.c.url_id, + _union_counts.c.entity, ) ).cte("counts_name") ) \ No newline at end of file diff --git a/src/db/models/impl/link/anonymous_sessions__name_suggestion.py b/src/db/models/impl/link/anonymous_sessions__name_suggestion.py new file mode 100644 index 00000000..a5773bd7 --- /dev/null +++ b/src/db/models/impl/link/anonymous_sessions__name_suggestion.py @@ -0,0 +1,24 @@ +from sqlalchemy import PrimaryKeyConstraint, ForeignKey, Integer, Column + +from src.db.models.mixins import CreatedAtMixin, AnonymousSessionMixin +from src.db.models.templates_.base import Base + + +class LinkAnonymousSessionNameSuggestion( + Base, + AnonymousSessionMixin, + CreatedAtMixin +): + __tablename__ = "link__anonymous_sessions__name_suggestions" + suggestion_id = Column( + Integer, + ForeignKey("url_name_suggestions.id"), + primary_key=True, + nullable=False, + ) + __table_args__ = ( + PrimaryKeyConstraint( + "session_id", + "suggestion_id" + ), + ) \ No newline at end of file diff --git a/tests/automated/integration/api/annotate/anonymous/test_core.py b/tests/automated/integration/api/annotate/anonymous/test_core.py index b6fb93fa..9e3b9af9 100644 --- a/tests/automated/integration/api/annotate/anonymous/test_core.py +++ b/tests/automated/integration/api/annotate/anonymous/test_core.py @@ -3,7 +3,6 @@ import pytest from src.api.endpoints.annotate.all.get.models.name import NameAnnotationSuggestion -from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse from src.api.endpoints.annotate.all.post.models.agency import AnnotationPostAgencyInfo from src.api.endpoints.annotate.all.post.models.location import AnnotationPostLocationInfo from src.api.endpoints.annotate.all.post.models.name import AnnotationPostNameInfo @@ -12,10 +11,12 @@ from src.core.enums import RecordType from src.db.dtos.url.mapping_.simple import SimpleURLMapping from src.db.models.impl.flag.url_validated.enums import URLType +from src.db.models.impl.link.anonymous_sessions__name_suggestion import LinkAnonymousSessionNameSuggestion from src.db.models.impl.url.suggestion.anonymous.agency.sqlalchemy import AnonymousAnnotationAgency from src.db.models.impl.url.suggestion.anonymous.location.sqlalchemy import AnonymousAnnotationLocation from src.db.models.impl.url.suggestion.anonymous.record_type.sqlalchemy import AnonymousAnnotationRecordType from src.db.models.impl.url.suggestion.anonymous.url_type.sqlalchemy import AnonymousAnnotationURLType +from src.db.models.impl.url.suggestion.name.sqlalchemy import URLNameSuggestion from src.db.models.mixins import URLDependentMixin from tests.automated.integration.api.annotate.anonymous.helper import get_next_url_for_anonymous_annotation, \ post_and_get_next_url_for_anonymous_annotation @@ -90,6 +91,16 @@ async def test_annotate_anonymous( instance: model = instances[0] assert instance.url_id == get_response_1.next_annotation.url_info.url_id + # Check for existence of name suggestion (2 were added by setup) + name_suggestions: list[URLNameSuggestion] = await ddc.adb_client.get_all(URLNameSuggestion) + assert len(name_suggestions) == 3 + + # Check for existence of link + link_instances: list[LinkAnonymousSessionNameSuggestion] = await ddc.adb_client.get_all(LinkAnonymousSessionNameSuggestion) + assert len(link_instances) == 1 + link_instance: LinkAnonymousSessionNameSuggestion = link_instances[0] + assert link_instance.session_id == session_id + # Run again without giving session ID, confirm original URL returned get_response_2: GetNextURLForAnonymousAnnotationResponse = await get_next_url_for_anonymous_annotation(rv) assert get_response_2.session_id != session_id diff --git a/tests/automated/integration/tasks/url/impl/validate/helper.py b/tests/automated/integration/tasks/url/impl/validate/helper.py index 879fbc66..091fe5fa 100644 --- a/tests/automated/integration/tasks/url/impl/validate/helper.py +++ b/tests/automated/integration/tasks/url/impl/validate/helper.py @@ -132,7 +132,7 @@ async def add_record_type_suggestions( async def add_name_suggestion( self, count: int = 1, - ) -> str: + ) -> int: name = f"Test Validate Task Name" suggestion_id: int = await self.db_data_creator.name_suggestion( url_id=self.url_id, @@ -144,7 +144,7 @@ async def add_name_suggestion( suggestion_id=suggestion_id, user_id=next_int(), ) - return name + return suggestion_id async def check_name(self) -> None: urls: list[URL] = await self.adb_client.get_all(URL) diff --git a/tests/automated/integration/tasks/url/impl/validate/test_data_source.py b/tests/automated/integration/tasks/url/impl/validate/test_data_source.py index 4fe0d444..434e8f06 100644 --- a/tests/automated/integration/tasks/url/impl/validate/test_data_source.py +++ b/tests/automated/integration/tasks/url/impl/validate/test_data_source.py @@ -13,6 +13,7 @@ from src.core.enums import RecordType from src.core.tasks.url.operators.validate.core import AutoValidateURLTaskOperator from src.db.models.impl.flag.url_validated.enums import URLType +from src.db.models.impl.link.anonymous_sessions__name_suggestion import LinkAnonymousSessionNameSuggestion from src.db.models.impl.url.suggestion.anonymous.agency.sqlalchemy import AnonymousAnnotationAgency from src.db.models.impl.url.suggestion.anonymous.location.sqlalchemy import AnonymousAnnotationLocation from src.db.models.impl.url.suggestion.anonymous.record_type.sqlalchemy import AnonymousAnnotationRecordType @@ -45,7 +46,7 @@ async def test_data_source( assert not await operator.meets_task_prerequisites() - await helper.add_name_suggestion(count=2) + suggestion_id: int = await helper.add_name_suggestion(count=1) assert not await operator.meets_task_prerequisites() @@ -74,11 +75,16 @@ async def test_data_source( session_id=session_id, url_id=helper.url_id ) + anon_name_link = LinkAnonymousSessionNameSuggestion( + suggestion_id=suggestion_id, + session_id=session_id + ) for model in [ anon_url_type, anon_record_type, anon_location, - anon_agency + anon_agency, + anon_name_link ]: await helper.adb_client.add(model)