From c76ace53d766bbd913c7dcf17289bc3803c23185 Mon Sep 17 00:00:00 2001 From: Robert Cronin Date: Sat, 31 Aug 2024 19:42:24 +1000 Subject: [PATCH] Add content set and report Signed-off-by: Robert Cronin --- modules/odr_core/odr_core/crud/content.py | 1 - .../odr_core/odr_core/crud/content_report.py | 40 +++ modules/odr_core/odr_core/crud/content_set.py | 69 +++++ modules/odr_core/odr_core/enums.py | 54 ++++ modules/odr_core/odr_core/models/content.py | 67 +++-- modules/odr_core/odr_core/models/user.py | 2 + .../odr_core/odr_core/schemas/annotation.py | 17 +- modules/odr_core/odr_core/schemas/content.py | 23 +- .../odr_core/schemas/content_report.py | 31 +++ .../odr_core/odr_core/schemas/content_set.py | 30 +++ .../odr_core/odr_core/schemas/embedding.py | 10 +- modules/odr_core/odr_core/schemas/user.py | 7 +- .../tests/unit/crud/test_content_crud.py | 250 ++++++++++++++++++ .../unit/crud/test_content_report_crud.py | 57 ++++ .../tests/unit/crud/test_content_set_crud.py | 129 +++++++++ .../tests/unit/models/test_content.py | 110 +++++++- .../unit/models/test_content_report_schema.py | 56 ++++ .../unit/models/test_content_set_schema.py | 70 +++++ .../tests/unit/schemas/test_content_schema.py | 4 +- 19 files changed, 944 insertions(+), 83 deletions(-) create mode 100644 modules/odr_core/odr_core/crud/content_report.py create mode 100644 modules/odr_core/odr_core/crud/content_set.py create mode 100644 modules/odr_core/odr_core/enums.py create mode 100644 modules/odr_core/odr_core/schemas/content_report.py create mode 100644 modules/odr_core/odr_core/schemas/content_set.py create mode 100644 modules/odr_core/tests/unit/crud/test_content_report_crud.py create mode 100644 modules/odr_core/tests/unit/crud/test_content_set_crud.py create mode 100644 modules/odr_core/tests/unit/models/test_content_report_schema.py create mode 100644 modules/odr_core/tests/unit/models/test_content_set_schema.py diff --git a/modules/odr_core/odr_core/crud/content.py b/modules/odr_core/odr_core/crud/content.py index f1f2129..6990e28 100644 --- a/modules/odr_core/odr_core/crud/content.py +++ b/modules/odr_core/odr_core/crud/content.py @@ -17,7 +17,6 @@ httpurl_to_str, ) from typing import List, Optional -from sqlalchemy import Enum as SQLAlchemyEnum from loguru import logger from datetime import datetime, timezone from sqlalchemy.exc import IntegrityError diff --git a/modules/odr_core/odr_core/crud/content_report.py b/modules/odr_core/odr_core/crud/content_report.py new file mode 100644 index 0000000..7b61b0d --- /dev/null +++ b/modules/odr_core/odr_core/crud/content_report.py @@ -0,0 +1,40 @@ +from sqlalchemy.orm import Session +from odr_core.models.content import ContentReport +from odr_core.schemas.content_report import ContentReportCreate, ContentReportUpdate +from typing import List, Optional + + +def create_content_report(db: Session, report: ContentReportCreate) -> ContentReport: + db_report = ContentReport(**report.model_dump()) + db.add(db_report) + db.commit() + db.refresh(db_report) + return db_report + + +def get_content_report(db: Session, report_id: int) -> Optional[ContentReport]: + return db.query(ContentReport).filter(ContentReport.id == report_id).first() + + +def get_content_reports(db: Session, skip: int = 0, limit: int = 100) -> List[ContentReport]: + return db.query(ContentReport).offset(skip).limit(limit).all() + + +def update_content_report(db: Session, report_id: int, report: ContentReportUpdate) -> Optional[ContentReport]: + db_report = db.query(ContentReport).filter(ContentReport.id == report_id).first() + if db_report: + update_data = report.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(db_report, key, value) + db.commit() + db.refresh(db_report) + return db_report + + +def delete_content_report(db: Session, report_id: int) -> bool: + db_report = db.query(ContentReport).filter(ContentReport.id == report_id).first() + if db_report: + db.delete(db_report) + db.commit() + return True + return False diff --git a/modules/odr_core/odr_core/crud/content_set.py b/modules/odr_core/odr_core/crud/content_set.py new file mode 100644 index 0000000..7c63173 --- /dev/null +++ b/modules/odr_core/odr_core/crud/content_set.py @@ -0,0 +1,69 @@ +from sqlalchemy.orm import Session +from odr_core.models.content import ContentSet, ContentSetItem, Content +from odr_core.schemas.content_set import ContentSetCreate, ContentSetUpdate +from typing import List, Optional + + +def create_content_set(db: Session, content_set: ContentSetCreate) -> ContentSet: + db_content_set = ContentSet(**content_set.model_dump()) + db.add(db_content_set) + db.commit() + db.refresh(db_content_set) + return db_content_set + + +def get_content_set(db: Session, content_set_id: int) -> Optional[ContentSet]: + return db.query(ContentSet).filter(ContentSet.id == content_set_id).first() + + +def get_content_sets(db: Session, skip: int = 0, limit: int = 100) -> List[ContentSet]: + return db.query(ContentSet).offset(skip).limit(limit).all() + + +def update_content_set(db: Session, content_set_id: int, content_set: ContentSetUpdate) -> Optional[ContentSet]: + db_content_set = db.query(ContentSet).filter(ContentSet.id == content_set_id).first() + if db_content_set: + update_data = content_set.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(db_content_set, key, value) + db.commit() + db.refresh(db_content_set) + return db_content_set + + +def delete_content_set(db: Session, content_set_id: int) -> bool: + db_content_set = db.query(ContentSet).filter(ContentSet.id == content_set_id).first() + if db_content_set: + db.delete(db_content_set) + db.commit() + return True + return False + + +def add_content_to_set(db: Session, content_set_id: int, content_id: int) -> bool: + db_content_set_item = ContentSetItem(content_set_id=content_set_id, content_id=content_id) + db.add(db_content_set_item) + try: + db.commit() + return True + except: # noqa + db.rollback() + return False + + +def remove_content_from_set(db: Session, content_set_id: int, content_id: int) -> bool: + db_content_set_item = db.query(ContentSetItem).filter( + ContentSetItem.content_set_id == content_set_id, + ContentSetItem.content_id == content_id + ).first() + if db_content_set_item: + db.delete(db_content_set_item) + db.commit() + return True + return False + + +def get_contents_in_set(db: Session, content_set_id: int, skip: int = 0, limit: int = 100) -> List[Content]: + return db.query(Content).join(ContentSetItem).filter( + ContentSetItem.content_set_id == content_set_id + ).offset(skip).limit(limit).all() diff --git a/modules/odr_core/odr_core/enums.py b/modules/odr_core/odr_core/enums.py new file mode 100644 index 0000000..1e2fc88 --- /dev/null +++ b/modules/odr_core/odr_core/enums.py @@ -0,0 +1,54 @@ +from enum import Enum + + +class ContentType(str, Enum): + IMAGE = "image" + VIDEO = "video" + VOICE = "voice" + MUSIC = "music" + TEXT = "text" + + +class ContentStatus(str, Enum): + PENDING = "PENDING" + AVAILABLE = "AVAILABLE" + UNAVAILABLE = "UNAVAILABLE" + DELISTED = "DELISTED" + + +class ContentSourceType(str, Enum): + URL = "url" + PATH = "path" + HUGGING_FACE = "hugging_face" + + +class ReportStatus(str, Enum): + PENDING = "pending" + REVIEWED = "reviewed" + RESOLVED = "resolved" + + +class AnnotationSourceType(str, Enum): + CONTENT_DESCRIPTION = "content_description" + SPATIAL_ANALYSIS = "spatial_analysis" + TAGS = "tags" + OTHER = "other" + + +class ReportType(str, Enum): + ILLEGAL_CONTENT = "illegal_content" + MALICIOUS_ANNOTATION = "malicious_annotation" + OTHER = "other" + + +class EmbeddingEngineType(str, Enum): + IMAGE = "image" + VIDEO = "video" + VOICE = "voice" + MUSIC = "music" + TEXT = "text" + + +class UserType(str, Enum): + user = "user" + bot = "bot" diff --git a/modules/odr_core/odr_core/models/content.py b/modules/odr_core/odr_core/models/content.py index a7ccc53..ffab23d 100644 --- a/modules/odr_core/odr_core/models/content.py +++ b/modules/odr_core/odr_core/models/content.py @@ -14,28 +14,7 @@ from sqlalchemy.sql import func from odr_core.models.base import Base from sqlalchemy import Enum as SQLAlchemyEnum -import enum - - -class ContentType(enum.Enum): - IMAGE = "IMAGE" - VIDEO = "VIDEO" - VOICE = "VOICE" - MUSIC = "MUSIC" - TEXT = "TEXT" - - -class ContentStatus(enum.Enum): - PENDING = "PENDING" - AVAILABLE = "AVAILABLE" - UNAVAILABLE = "UNAVAILABLE" - DELISTED = "DELISTED" - - -class ContentSourceType(enum.Enum): - URL = "url" - PATH = "path" - HUGGING_FACE = "hugging_face" +from odr_core.enums import ContentType, ContentStatus, ContentSourceType, ReportStatus class Content(Base): @@ -66,11 +45,13 @@ class Content(Base): from_user = relationship("User", back_populates="contents") from_team = relationship("Team", back_populates="contents") - content_authors = relationship("ContentAuthor", back_populates="content") annotations = relationship("Annotation", back_populates="content") embeddings = relationship("ContentEmbedding", back_populates="content") sources = relationship("ContentSource", back_populates="content", cascade="all,delete-orphan") events = relationship("ContentEvents", back_populates="content") + reports = relationship("ContentReport", back_populates="content") + content_authors = relationship("ContentAuthor", back_populates="content") + content_sets = relationship("ContentSet", secondary="content_set_items", back_populates="contents") class ContentAuthor(Base): @@ -91,7 +72,7 @@ class ContentSource(Base): id = Column(Integer, primary_key=True, index=True) content_id = Column(Integer, ForeignKey("contents.id")) - type = Column(Enum(ContentSourceType)) + type = Column(SQLAlchemyEnum(ContentSourceType)) value = Column(String, unique=True) source_metadata = Column(String, nullable=True) # JSON created_at = Column(DateTime(timezone=True), server_default=func.now()) @@ -113,3 +94,41 @@ class ContentEvents(Base): content = relationship("Content", back_populates="events") user = relationship("User", back_populates="content_events") + + +class ContentReport(Base): + __tablename__ = "content_reports" + + id = Column(Integer, primary_key=True, index=True) + content_id = Column(Integer, ForeignKey("contents.id"), nullable=False) + reporter_id = Column(Integer, ForeignKey("users.id"), nullable=False) + reason = Column(String, nullable=False) + description = Column(String) + status = Column(Enum(ReportStatus), default=ReportStatus.PENDING) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now(), nullable=False, server_default=func.now()) + + content = relationship("Content", back_populates="reports") + reporter = relationship("User", back_populates="content_reports") + + +class ContentSet(Base): + __tablename__ = "content_sets" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + description = Column(String) + created_by_id = Column(Integer, ForeignKey("users.id"), nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now(), nullable=False, server_default=func.now()) + + created_by = relationship("User", back_populates="content_sets") + contents = relationship("Content", secondary="content_set_items", back_populates="content_sets") + + +class ContentSetItem(Base): + __tablename__ = "content_set_items" + + content_set_id = Column(Integer, ForeignKey("content_sets.id"), primary_key=True) + content_id = Column(Integer, ForeignKey("contents.id"), primary_key=True) + added_at = Column(DateTime(timezone=True), server_default=func.now()) diff --git a/modules/odr_core/odr_core/models/user.py b/modules/odr_core/odr_core/models/user.py index 1bd0a7e..9baf926 100644 --- a/modules/odr_core/odr_core/models/user.py +++ b/modules/odr_core/odr_core/models/user.py @@ -43,6 +43,8 @@ class User(Base): user_type = Column(Enum(UserType), default=UserType.user) content_events = relationship("ContentEvents", back_populates="user") + content_reports = relationship("ContentReport", back_populates="reporter") + content_sets = relationship("ContentSet", back_populates="created_by") def __repr__(self): return f"" diff --git a/modules/odr_core/odr_core/schemas/annotation.py b/modules/odr_core/odr_core/schemas/annotation.py index adb9ca9..82cab20 100644 --- a/modules/odr_core/odr_core/schemas/annotation.py +++ b/modules/odr_core/odr_core/schemas/annotation.py @@ -1,7 +1,7 @@ -from pydantic import BaseModel, Field, field_serializer +from pydantic import BaseModel, Field from typing import List, Optional, Dict, Any from datetime import datetime -from enum import Enum +from odr_core.enums import AnnotationSourceType, ReportType class AnnotationBase(BaseModel): @@ -82,12 +82,6 @@ class Config: from_attributes = True -class ReportType(str, Enum): - ILLEGAL_CONTENT = "illegal_content" - MALICIOUS_ANNOTATION = "malicious_annotation" - OTHER = "other" - - class AnnotationReportBase(BaseModel): type: ReportType description: Optional[str] = None @@ -108,13 +102,6 @@ class Config: from_attributes = True -class AnnotationSourceType(str, Enum): - CONTENT_DESCRIPTION = "content_description" - SPATIAL_ANALYSIS = "spatial_analysis" - TAGS = "tags" - OTHER = "other" - - class AnnotationSourceBase(BaseModel): name: str ecosystem: Optional[str] = None diff --git a/modules/odr_core/odr_core/schemas/content.py b/modules/odr_core/odr_core/schemas/content.py index 64c46f6..cb330dc 100644 --- a/modules/odr_core/odr_core/schemas/content.py +++ b/modules/odr_core/odr_core/schemas/content.py @@ -1,28 +1,7 @@ from pydantic import BaseModel, HttpUrl from typing import List, Optional from datetime import datetime -from enum import Enum - - -class ContentType(str, Enum): - IMAGE = "IMAGE" - VIDEO = "VIDEO" - VOICE = "VOICE" - MUSIC = "MUSIC" - TEXT = "TEXT" - - -class ContentStatus(str, Enum): - PENDING = "PENDING" - AVAILABLE = "AVAILABLE" - UNAVAILABLE = "UNAVAILABLE" - DELISTED = "DELISTED" - - -class ContentSourceType(str, Enum): - URL = "URL" - PATH = "PATH" - HUGGING_FACE = "HUGGING_FACE" +from odr_core.enums import ContentStatus, ContentType, ContentSourceType class ContentAuthorBase(BaseModel): diff --git a/modules/odr_core/odr_core/schemas/content_report.py b/modules/odr_core/odr_core/schemas/content_report.py new file mode 100644 index 0000000..fe8bbcd --- /dev/null +++ b/modules/odr_core/odr_core/schemas/content_report.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime +from odr_core.enums import ReportStatus + + +class ContentReportBase(BaseModel): + content_id: int + reporter_id: int + reason: str + description: Optional[str] = None + + +class ContentReportCreate(ContentReportBase): + pass + + +class ContentReportUpdate(BaseModel): + reason: Optional[str] = None + description: Optional[str] = None + status: Optional[ReportStatus] = None + + +class ContentReport(ContentReportBase): + id: int + status: ReportStatus + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True diff --git a/modules/odr_core/odr_core/schemas/content_set.py b/modules/odr_core/odr_core/schemas/content_set.py new file mode 100644 index 0000000..0fe27ee --- /dev/null +++ b/modules/odr_core/odr_core/schemas/content_set.py @@ -0,0 +1,30 @@ +from pydantic import BaseModel +from typing import List, Optional +from datetime import datetime + + +class ContentSetBase(BaseModel): + name: str + description: Optional[str] = None + + +class ContentSetCreate(ContentSetBase): + created_by_id: int + + +class ContentSetUpdate(ContentSetBase): + pass + + +class ContentSet(ContentSetBase): + id: int + created_by_id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +class ContentSetWithContents(ContentSet): + contents: List[int] # List of content IDs diff --git a/modules/odr_core/odr_core/schemas/embedding.py b/modules/odr_core/odr_core/schemas/embedding.py index 87b8641..cbed4c1 100644 --- a/modules/odr_core/odr_core/schemas/embedding.py +++ b/modules/odr_core/odr_core/schemas/embedding.py @@ -1,15 +1,7 @@ from pydantic import BaseModel from typing import Optional, Any, List from datetime import datetime -from enum import Enum - - -class EmbeddingEngineType(str, Enum): - IMAGE = "image" - VIDEO = "video" - VOICE = "voice" - MUSIC = "music" - TEXT = "text" +from odr_core.enums import EmbeddingEngineType class EmbeddingEngineBase(BaseModel): diff --git a/modules/odr_core/odr_core/schemas/user.py b/modules/odr_core/odr_core/schemas/user.py index 945e264..2c9823a 100644 --- a/modules/odr_core/odr_core/schemas/user.py +++ b/modules/odr_core/odr_core/schemas/user.py @@ -1,15 +1,10 @@ from pydantic import BaseModel, EmailStr from datetime import datetime from typing import Optional -from enum import Enum +from odr_core.enums import UserType import uuid -class UserType(str, Enum): - user = "user" - bot = "bot" - - class UserBase(BaseModel): username: str email: EmailStr diff --git a/modules/odr_core/tests/unit/crud/test_content_crud.py b/modules/odr_core/tests/unit/crud/test_content_crud.py index e69de29..5355a82 100644 --- a/modules/odr_core/tests/unit/crud/test_content_crud.py +++ b/modules/odr_core/tests/unit/crud/test_content_crud.py @@ -0,0 +1,250 @@ +import pytest +from sqlalchemy.orm import Session +from odr_core.crud.content import ( + create_content, + get_content, + get_contents, + update_content, + delete_content, + get_content_by_hash, + get_contents_by_user, + create_content_source, + get_content_sources, + update_content_source, + delete_content_source +) +from odr_core.schemas.content import ( + ContentCreate, + ContentUpdate, + ContentSourceCreate, + ContentSourceUpdate, + ContentType, + ContentStatus, + ContentSourceType +) + + +def test_create_content(db: Session): + content_data = ContentCreate( + name="Test Content", + type=ContentType.IMAGE, + hash="test_hash", + phash="test_phash", + width=100, + height=100, + url=["http://example.com/image.jpg"], + format="jpg", + size=1024, + license="CC0", + sources=[ContentSourceCreate(type=ContentSourceType.URL, value="http://example.com/image.jpg")] + ) + content = create_content(db, content_data, from_user_id=1) + assert content.id is not None + assert content.name == "Test Content" + print("content.type", content.type) + assert content.type is ContentType.IMAGE + assert content.hash == "test_hash" + assert content.status is ContentStatus.PENDING + assert len(content.sources) == 1 + + +def test_get_content(db: Session): + content_data = ContentCreate( + name="Test Content", + type=ContentType.IMAGE, + hash="test_hash", + phash="test_phash", + format="jpg", + size=1024, + license="CC0", + sources=[ContentSourceCreate(type=ContentSourceType.URL, value="http://example.com/image.jpg")] + ) + created_content = create_content(db, content_data, from_user_id=1) + retrieved_content = get_content(db, content_id=created_content.id) + assert retrieved_content is not None + assert retrieved_content.id == created_content.id + assert retrieved_content.name == "Test Content" + + +def test_get_contents(db: Session): + for i in range(5): + content_data = ContentCreate( + name=f"Test Content {i}", + type=ContentType.IMAGE, + hash=f"test_hash_{i}", + phash=f"test_phash_{i}", + format="jpg", + size=1024, + license="CC0", + sources=[ContentSourceCreate(type=ContentSourceType.URL, value=f"http://example.com/image_{i}.jpg")] + ) + create_content(db, content_data, from_user_id=1) + + contents = get_contents(db) + assert len(contents) == 5 + + +def test_update_content(db: Session): + content_data = ContentCreate( + name="Test Content", + type=ContentType.IMAGE, + hash="test_hash", + phash="test_phash", + format="jpg", + size=1024, + license="CC0", + sources=[ContentSourceCreate(type=ContentSourceType.URL, value="http://example.com/image.jpg")] + ) + created_content = create_content(db, content_data, from_user_id=1) + + update_data = ContentUpdate( + name="Updated Content", + status=ContentStatus.AVAILABLE + ) + updated_content = update_content(db, created_content.id, update_data) + assert updated_content.name == "Updated Content" + assert updated_content.status == ContentStatus.AVAILABLE + + +def test_delete_content(db: Session): + content_data = ContentCreate( + name="Test Content", + type=ContentType.IMAGE, + hash="test_hash", + phash="test_phash", + format="jpg", + size=1024, + license="CC0", + sources=[ContentSourceCreate(type=ContentSourceType.URL, value="http://example.com/image.jpg")] + ) + created_content = create_content(db, content_data, from_user_id=1) + + delete_result = delete_content(db, created_content.id) + assert delete_result is True + + deleted_content = get_content(db, created_content.id) + assert deleted_content is None + + +def test_get_content_by_hash(db: Session): + content_data = ContentCreate( + name="Test Content", + type=ContentType.IMAGE, + hash="unique_hash", + phash="test_phash", + format="jpg", + size=1024, + license="CC0", + sources=[ContentSourceCreate(type=ContentSourceType.URL, value="http://example.com/image.jpg")] + ) + created_content = create_content(db, content_data, from_user_id=1) + + retrieved_content = get_content_by_hash(db, hash="unique_hash") + assert retrieved_content is not None + assert retrieved_content.id == created_content.id + + +def test_get_contents_by_user(db: Session): + for i in range(3): + content_data = ContentCreate( + name=f"User Content {i}", + type=ContentType.IMAGE, + hash=f"user_hash_{i}", + phash=f"user_phash_{i}", + format="jpg", + size=1024, + license="CC0", + sources=[ContentSourceCreate(type=ContentSourceType.URL, value=f"http://example.com/user_image_{i}.jpg")] + ) + create_content(db, content_data, from_user_id=1) + + user_contents = get_contents_by_user(db, user_id=1) + assert len(user_contents) == 3 + assert all(content.from_user_id == 1 for content in user_contents) + + +def test_create_content_source(db: Session): + content_data = ContentCreate( + name="Test Content", + type=ContentType.IMAGE, + hash="test_hash", + phash="test_phash", + format="jpg", + size=1024, + license="CC0", + sources=[] + ) + content = create_content(db, content_data, from_user_id=1) + + source_data = ContentSourceCreate( + type=ContentSourceType.URL, + value="http://example.com/new_image.jpg" + ) + source = create_content_source(db, content.id, source_data) + assert source.id is not None + assert source.content_id == content.id + assert source.type is ContentSourceType.URL + assert source.value == "http://example.com/new_image.jpg" + + +def test_get_content_sources(db: Session): + content_data = ContentCreate( + name="Test Content", + type=ContentType.IMAGE, + hash="test_hash", + phash="test_phash", + format="jpg", + size=1024, + license="CC0", + sources=[ + ContentSourceCreate(type=ContentSourceType.URL, value="http://example.com/image1.jpg"), + ContentSourceCreate(type=ContentSourceType.URL, value="http://example.com/image2.jpg") + ] + ) + content = create_content(db, content_data, from_user_id=1) + + sources = get_content_sources(db, content.id) + assert len(sources) == 2 + + +def test_update_content_source(db: Session): + content_data = ContentCreate( + name="Test Content", + type=ContentType.IMAGE, + hash="test_hash", + phash="test_phash", + format="jpg", + size=1024, + license="CC0", + sources=[ContentSourceCreate(type=ContentSourceType.URL, value="http://example.com/image.jpg")] + ) + content = create_content(db, content_data, from_user_id=1) + + source_update = ContentSourceUpdate( + id=content.sources[0].id, + type=ContentSourceType.PATH, + value="/path/to/image.jpg" + ) + updated_source = update_content_source(db, content.sources[0].id, source_update) + assert updated_source.type is ContentSourceType.PATH + assert updated_source.value == "/path/to/image.jpg" + + +def test_delete_content_source(db: Session): + content_data = ContentCreate( + name="Test Content", + type=ContentType.IMAGE, + hash="test_hash", + phash="test_phash", + format="jpg", + size=1024, + license="CC0", + sources=[ContentSourceCreate(type=ContentSourceType.URL, value="http://example.com/image.jpg")] + ) + content = create_content(db, content_data, from_user_id=1) + + delete_result = delete_content_source(db, content.sources[0].id) + assert delete_result is True + + sources = get_content_sources(db, content.id) + assert len(sources) == 0 diff --git a/modules/odr_core/tests/unit/crud/test_content_report_crud.py b/modules/odr_core/tests/unit/crud/test_content_report_crud.py new file mode 100644 index 0000000..c10a3f7 --- /dev/null +++ b/modules/odr_core/tests/unit/crud/test_content_report_crud.py @@ -0,0 +1,57 @@ +import pytest +from odr_core.crud.content_report import create_content_report, get_content_report, update_content_report, delete_content_report +from odr_core.schemas.content_report import ContentReportCreate, ContentReportUpdate, ReportStatus + + +def test_create_content_report(db): + report_data = ContentReportCreate( + content_id=1, + reporter_id=1, + reason="Inappropriate content", + description="This content violates community guidelines." + ) + report = create_content_report(db, report_data) + assert report.id is not None + assert report.content_id == 1 + assert report.reporter_id == 1 + assert report.reason == "Inappropriate content" + assert report.status == ReportStatus.PENDING + + +def test_get_content_report(db): + report_data = ContentReportCreate( + content_id=1, + reporter_id=1, + reason="Inappropriate content" + ) + created_report = create_content_report(db, report_data) + retrieved_report = get_content_report(db, report_id=created_report.id) + assert retrieved_report is not None + assert retrieved_report.id == created_report.id + assert retrieved_report.reason == "Inappropriate content" + + +def test_update_content_report(db): + report_data = ContentReportCreate( + content_id=1, + reporter_id=1, + reason="Inappropriate content" + ) + created_report = create_content_report(db, report_data) + update_data = ContentReportUpdate(reason="Updated reason", status=ReportStatus.REVIEWED) + updated_report = update_content_report(db, report_id=created_report.id, report=update_data) + assert updated_report.reason == "Updated reason" + assert updated_report.status == ReportStatus.REVIEWED + + +def test_delete_content_report(db): + report_data = ContentReportCreate( + content_id=1, + reporter_id=1, + reason="Inappropriate content" + ) + created_report = create_content_report(db, report_data) + delete_result = delete_content_report(db, report_id=created_report.id) + assert delete_result is True + deleted_report = get_content_report(db, report_id=created_report.id) + assert deleted_report is None diff --git a/modules/odr_core/tests/unit/crud/test_content_set_crud.py b/modules/odr_core/tests/unit/crud/test_content_set_crud.py new file mode 100644 index 0000000..510e572 --- /dev/null +++ b/modules/odr_core/tests/unit/crud/test_content_set_crud.py @@ -0,0 +1,129 @@ +import pytest +from odr_core.crud.content_set import ( + create_content_set, get_content_set, update_content_set, delete_content_set, + add_content_to_set, remove_content_from_set, get_contents_in_set +) +from odr_core.schemas.content_set import ContentSetCreate, ContentSetUpdate +from odr_core.models.content import Content, ContentSet + + +def test_create_content_set(db): + content_set_data = ContentSetCreate( + name="Test Content Set", + description="A test content set", + created_by_id=1 + ) + content_set = create_content_set(db, content_set_data) + assert content_set.id is not None + assert content_set.name == "Test Content Set" + assert content_set.created_by_id == 1 + + +def test_get_content_set(db): + content_set_data = ContentSetCreate( + name="Test Content Set", + description="A test content set", + created_by_id=1 + ) + created_set = create_content_set(db, content_set_data) + retrieved_set = get_content_set(db, content_set_id=created_set.id) + assert retrieved_set is not None + assert retrieved_set.id == created_set.id + assert retrieved_set.name == "Test Content Set" + + +def test_update_content_set(db): + content_set_data = ContentSetCreate( + name="Test Content Set", + description="A test content set", + created_by_id=1 + ) + created_set = create_content_set(db, content_set_data) + update_data = ContentSetUpdate(name="Updated Content Set", description="Updated description") + updated_set = update_content_set(db, content_set_id=created_set.id, content_set=update_data) + assert updated_set.name == "Updated Content Set" + assert updated_set.description == "Updated description" + + +def test_delete_content_set(db): + content_set_data = ContentSetCreate( + name="Test Content Set", + description="A test content set", + created_by_id=1 + ) + created_set = create_content_set(db, content_set_data) + delete_result = delete_content_set(db, content_set_id=created_set.id) + assert delete_result is True + deleted_set = get_content_set(db, content_set_id=created_set.id) + assert deleted_set is None + + +def test_add_content_to_set(db): + content_set = create_content_set(db, ContentSetCreate(name="Test Set", created_by_id=1)) + content = Content(name="Test Content", type="IMAGE", hash="test_hash", phash="test_phash", + format="jpg", size=1000, license="CC0", from_user_id=1) + db.add(content) + db.commit() + + result = add_content_to_set(db, content_set_id=content_set.id, content_id=content.id) + assert result is True + + contents = get_contents_in_set(db, content_set_id=content_set.id) + assert len(contents) == 1 + assert contents[0].id == content.id + + +def test_remove_content_from_set(db): + content_set = create_content_set(db, ContentSetCreate(name="Test Set", created_by_id=1)) + content = Content(name="Test Content", type="IMAGE", hash="test_hash", phash="test_phash", + format="jpg", size=1000, license="CC0", from_user_id=1) + db.add(content) + db.commit() + + add_content_to_set(db, content_set_id=content_set.id, content_id=content.id) + result = remove_content_from_set(db, content_set_id=content_set.id, content_id=content.id) + assert result is True + + contents = get_contents_in_set(db, content_set_id=content_set.id) + assert len(contents) == 0 + + +def test_get_contents_in_set(db): + content_set = create_content_set(db, ContentSetCreate(name="Test Set", created_by_id=1)) + contents = [ + Content(name=f"Test Content {i}", type="IMAGE", hash=f"test_hash_{i}", + phash=f"test_phash_{i}", format="jpg", size=1000, license="CC0", from_user_id=1) + for i in range(3) + ] + db.add_all(contents) + db.commit() + + for content in contents: + add_content_to_set(db, content_set_id=content_set.id, content_id=content.id) + + retrieved_contents = get_contents_in_set(db, content_set_id=content_set.id) + assert len(retrieved_contents) == 3 + assert all(c.id in [content.id for content in contents] for c in retrieved_contents) + + +def test_get_contents_in_set_with_pagination(db): + content_set = create_content_set(db, ContentSetCreate(name="Test Set", created_by_id=1)) + contents = [ + Content(name=f"Test Content {i}", type="IMAGE", hash=f"test_hash_{i}", + phash=f"test_phash_{i}", format="jpg", size=1000, license="CC0", from_user_id=1) + for i in range(10) + ] + db.add_all(contents) + db.commit() + + for content in contents: + add_content_to_set(db, content_set_id=content_set.id, content_id=content.id) + + # Test pagination + first_page = get_contents_in_set(db, content_set_id=content_set.id, skip=0, limit=5) + assert len(first_page) == 5 + + second_page = get_contents_in_set(db, content_set_id=content_set.id, skip=5, limit=5) + assert len(second_page) == 5 + + assert all(c.id not in [content.id for content in first_page] for c in second_page) diff --git a/modules/odr_core/tests/unit/models/test_content.py b/modules/odr_core/tests/unit/models/test_content.py index a7b5dfb..4071722 100644 --- a/modules/odr_core/tests/unit/models/test_content.py +++ b/modules/odr_core/tests/unit/models/test_content.py @@ -1,6 +1,7 @@ +from sqlalchemy.exc import IntegrityError +from odr_core.models.content import ContentReport, ContentSet, ContentSetItem, ReportStatus, Content, ContentType, ContentStatus, ContentAuthor +from odr_core.schemas.content import ContentCreate import pytest -from odr_core.models.content import Content, ContentType, ContentStatus, ContentAuthor -from odr_core.schemas.content import ContentCreate, Content as ContentSchema def test_create_content(db): @@ -32,9 +33,9 @@ def test_create_content(db): assert db_content.id is not None assert db_content.name == "Test Image" - assert db_content.type == ContentType.IMAGE + assert db_content.type is ContentType.IMAGE assert db_content.hash == "abcdef123456" - assert db_content.status == ContentStatus.PENDING + assert db_content.status is ContentStatus.PENDING assert len(db_content.url.split(' ')) == 2 assert "http://example.com/image1.jpg" in db_content.url @@ -62,5 +63,106 @@ def test_content_author_relationship(db): assert content.content_authors[0].name == "John Doe" +def test_content_report_model(db): + content_report = ContentReport( + content_id=1, + reporter_id=1, + reason="Inappropriate content", + description="This content violates community guidelines.", + status=ReportStatus.PENDING + ) + db.add(content_report) + db.commit() + + assert content_report.id is not None + assert content_report.content_id == 1 + assert content_report.reporter_id == 1 + assert content_report.reason == "Inappropriate content" + assert content_report.description == "This content violates community guidelines." + assert content_report.status == ReportStatus.PENDING + assert content_report.created_at is not None + assert content_report.updated_at is not None + + +def test_content_report_model_constraints(db): + with pytest.raises(IntegrityError): + invalid_report = ContentReport( + content_id=None, + reporter_id=1, + reason="Invalid report" + ) + db.add(invalid_report) + db.commit() + + db.rollback() + + with pytest.raises(IntegrityError): + invalid_report = ContentReport( + content_id=1, + reporter_id=None, + reason="Invalid report" + ) + db.add(invalid_report) + db.commit() + + db.rollback() + + +def test_content_set_model(db): + content_set = ContentSet( + name="Test Content Set", + description="A test content set", + created_by_id=1 + ) + db.add(content_set) + db.commit() + + assert content_set.id is not None + assert content_set.name == "Test Content Set" + assert content_set.description == "A test content set" + assert content_set.created_by_id == 1 + assert content_set.created_at is not None + assert content_set.updated_at is not None + + +def test_content_set_item_model(db): + content_set = ContentSet(name="Test Content Set", created_by_id=1) + content = Content(name="Test Content", type="IMAGE", hash="test_hash", phash="test_phash", + format="jpg", size=1000, license="CC0", from_user_id=1) + db.add(content_set) + db.add(content) + db.commit() + + content_set_item = ContentSetItem(content_set_id=content_set.id, content_id=content.id) + db.add(content_set_item) + db.commit() + + assert content_set_item.content_set_id == content_set.id + assert content_set_item.content_id == content.id + assert content_set_item.added_at is not None + + +def test_content_set_relationships(db): + content_set = ContentSet(name="Test Content Set", created_by_id=1) + content1 = Content(name="Test Content 1", type="IMAGE", hash="test_hash1", + phash="test_phash1", format="jpg", size=1000, license="CC0", from_user_id=1) + content2 = Content(name="Test Content 2", type="IMAGE", hash="test_hash2", + phash="test_phash2", format="jpg", size=1000, license="CC0", from_user_id=1) + + db.add(content_set) + db.add(content1) + db.add(content2) + db.commit() + + content_set.contents.extend([content1, content2]) + db.commit() + + assert len(content_set.contents) == 2 + assert content1 in content_set.contents + assert content2 in content_set.contents + assert content_set in content1.content_sets + assert content_set in content2.content_sets + + if __name__ == "__main__": pytest.main([__file__]) diff --git a/modules/odr_core/tests/unit/models/test_content_report_schema.py b/modules/odr_core/tests/unit/models/test_content_report_schema.py new file mode 100644 index 0000000..2fe4d3a --- /dev/null +++ b/modules/odr_core/tests/unit/models/test_content_report_schema.py @@ -0,0 +1,56 @@ +import pytest +from pydantic import ValidationError +from odr_core.schemas.content_report import ContentReportCreate, ContentReport, ReportStatus +from datetime import datetime + + +def test_content_report_create_schema(): + valid_data = { + "content_id": 1, + "reporter_id": 1, + "reason": "Inappropriate content", + "description": "This content violates community guidelines." + } + report = ContentReportCreate(**valid_data) + assert report.content_id == 1 + assert report.reporter_id == 1 + assert report.reason == "Inappropriate content" + assert report.description == "This content violates community guidelines." + + # Test invalid data + with pytest.raises(ValidationError): + ContentReportCreate(content_id="invalid", reporter_id=1, reason="Invalid report") + + with pytest.raises(ValidationError): + ContentReportCreate(content_id=1, reporter_id=1, reason=None) + + +def test_content_report_schema(): + valid_data = { + "id": 1, + "content_id": 1, + "reporter_id": 1, + "reason": "Inappropriate content", + "description": "This content violates community guidelines.", + "status": ReportStatus.PENDING, + "created_at": datetime.now(), + "updated_at": datetime.now() + } + report = ContentReport(**valid_data) + assert report.id == 1 + assert report.content_id == 1 + assert report.reporter_id == 1 + assert report.reason == "Inappropriate content" + assert report.description == "This content violates community guidelines." + assert report.status == ReportStatus.PENDING + assert isinstance(report.created_at, datetime) + assert isinstance(report.updated_at, datetime) + + # Test invalid data + with pytest.raises(ValidationError): + ContentReport(id="invalid", content_id=1, reporter_id=1, reason="Invalid report", + status=ReportStatus.PENDING, created_at=datetime.now(), updated_at=datetime.now()) + + with pytest.raises(ValidationError): + ContentReport(id=1, content_id=1, reporter_id=1, reason="Invalid report", + status="invalid_status", created_at=datetime.now(), updated_at=datetime.now()) diff --git a/modules/odr_core/tests/unit/models/test_content_set_schema.py b/modules/odr_core/tests/unit/models/test_content_set_schema.py new file mode 100644 index 0000000..b4cea3c --- /dev/null +++ b/modules/odr_core/tests/unit/models/test_content_set_schema.py @@ -0,0 +1,70 @@ +import pytest +from pydantic import ValidationError +from odr_core.schemas.content_set import ContentSetCreate, ContentSet, ContentSetWithContents +from datetime import datetime + + +def test_content_set_create_schema(): + valid_data = { + "name": "Test Content Set", + "description": "A test content set", + "created_by_id": 1 + } + content_set = ContentSetCreate(**valid_data) + assert content_set.name == "Test Content Set" + assert content_set.description == "A test content set" + assert content_set.created_by_id == 1 + + # Test invalid data + with pytest.raises(ValidationError): + ContentSetCreate(name=None, created_by_id=1) # name is required + + with pytest.raises(ValidationError): + ContentSetCreate(name="Test Set", created_by_id="invalid") # created_by_id should be an integer + + +def test_content_set_schema(): + valid_data = { + "id": 1, + "name": "Test Content Set", + "description": "A test content set", + "created_by_id": 1, + "created_at": datetime.now(), + "updated_at": datetime.now() + } + content_set = ContentSet(**valid_data) + assert content_set.id == 1 + assert content_set.name == "Test Content Set" + assert content_set.description == "A test content set" + assert content_set.created_by_id == 1 + assert isinstance(content_set.created_at, datetime) + assert isinstance(content_set.updated_at, datetime) + + # Test invalid data + with pytest.raises(ValidationError): + ContentSet(id="invalid", name="Test Set", created_by_id=1, created_at=datetime.now(), updated_at=datetime.now()) + + +def test_content_set_with_contents_schema(): + valid_data = { + "id": 1, + "name": "Test Content Set", + "description": "A test content set", + "created_by_id": 1, + "created_at": datetime.now(), + "updated_at": datetime.now(), + "contents": [1, 2, 3] + } + content_set = ContentSetWithContents(**valid_data) + assert content_set.id == 1 + assert content_set.name == "Test Content Set" + assert content_set.description == "A test content set" + assert content_set.created_by_id == 1 + assert isinstance(content_set.created_at, datetime) + assert isinstance(content_set.updated_at, datetime) + assert content_set.contents == [1, 2, 3] + + # Test invalid data + with pytest.raises(ValidationError): + ContentSetWithContents(id=1, name="Test Set", created_by_id=1, created_at=datetime.now(), + updated_at=datetime.now(), contents="invalid") diff --git a/modules/odr_core/tests/unit/schemas/test_content_schema.py b/modules/odr_core/tests/unit/schemas/test_content_schema.py index 4be5656..63689d9 100644 --- a/modules/odr_core/tests/unit/schemas/test_content_schema.py +++ b/modules/odr_core/tests/unit/schemas/test_content_schema.py @@ -22,9 +22,9 @@ def test_content_schema(): content = ContentSchema(**content_data) assert content.id == 1 assert content.name == "Test Image" - assert content.type == ContentType.IMAGE.value + assert content.type is ContentType.IMAGE assert content.hash == "abcdef123456" - assert content.status == ContentStatus.PENDING.value + assert content.status is ContentStatus.PENDING assert len(content.url) == 1 assert content.url[0].unicode_string() == "http://example.com/image3.jpg"