Repository: activist-org/activist
Pull request #1211: Doc: Added documentation in content
Author: login: rohityadav2604
Created: 2025-04-08T19:26:20Z | Merged: 2025-04-12T09:06:57Z

<!---
Thank you for your pull request! 🚀
-->

### Contributor checklist

<!-- Please replace the empty checkboxes [] below with checked ones [x] accordingly. -->

- [x] This pull request is on a [separate branch](https://docs.github.com/en/get-started/quickstart/github-flow) and not the main branch
- [x] I have run the tests for the backend and frontend depending on what's needed for my changes as described in the [testing section of the contributing guide](CONTRIBUTING.md#testing)

---

### Description


This pull request contains numpy documentation for the backend content folder. I have tried to keep the documentation as similar as events folder [Doc:events](https://github.com/activist-org/activist/pull/1204). If there are any changes I will be happy to incorporate it.

### Changes

- Backend/content folder

### Testing
I have done the backend testing by following this [doc](https://github.com/activist-org/activist/blob/main/CONTRIBUTING.md#testing-). Attached are the screenshots
<img width="963" alt="Screenshot 2025-04-09 at 12 15 34 AM" src="https://github.com/user-attachments/assets/b6bd7c17-9fd0-4627-9acd-2b7ecb944472" />
<img width="975" alt="Screenshot 2025-04-09 at 12 17 14 AM" src="https://github.com/user-attachments/assets/61dcbaeb-4b35-45c9-9fc5-9820097cbc90" />
<img width="980" alt="Screenshot 2025-04-09 at 12 18 31 AM" src="https://github.com/user-attachments/assets/103a3c42-b24f-4292-b5ee-82963c91050a" />


### Related issue


- [Issue 1198](https://github.com/activist-org/activist/issues/1198)



Diff:
diff --git a/backend/content/factories.py b/backend/content/factories.py
index 5206771e5..0c8cb5274 100644
--- a/backend/content/factories.py
+++ b/backend/content/factories.py
@@ -1,4 +1,8 @@
 # SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+Factory classes for creating content test instances.
+"""
+
 import datetime
 import random
 from uuid import uuid4
@@ -11,6 +15,10 @@
 
 
 class EntityLocationFactory(factory.django.DjangoModelFactory):
+    """
+    Factory for creating Location model instances for entities.
+    """
+
     class Meta:
         model = Location
 
@@ -58,6 +66,10 @@ def location(self, create, extracted, **kwargs):
 
 
 class EventLocationFactory(factory.django.DjangoModelFactory):
+    """
+    Factory for creating Location model instances for events.
+    """
+
     class Meta:
         model = Location
 
@@ -105,6 +117,10 @@ def location(self, create, extracted, **kwargs):
 
 
 class FaqFactory(factory.django.DjangoModelFactory):
+    """
+    Factory for creating Faq model instances.
+    """
+
     class Meta:
         model = Faq
 
@@ -113,6 +129,10 @@ class Meta:
 
 
 class ImageFactory(factory.django.DjangoModelFactory):
+    """
+    Factory for creating Image model instances.
+    """
+
     class Meta:
         model = Image
 
@@ -127,6 +147,10 @@ class Meta:
 
 
 class ResourceFactory(factory.django.DjangoModelFactory):
+    """
+    Factory for creating Resource model instances.
+    """
+
     class Meta:
         model = Resource
 
@@ -146,6 +170,10 @@ class Meta:
 
 
 class TaskFactory(factory.django.DjangoModelFactory):
+    """
+    Factory for creating Task model instances.
+    """
+
     class Meta:
         model = Task
 
@@ -157,6 +185,10 @@ class Meta:
 
 
 class TopicFactory(factory.django.DjangoModelFactory):
+    """
+    Factory for creating Topic model instances.
+    """
+
     class Meta:
         model = Topic
 
diff --git a/backend/content/models.py b/backend/content/models.py
index e87d9a82d..9d3170be4 100644
--- a/backend/content/models.py
+++ b/backend/content/models.py
@@ -19,6 +19,10 @@
 
 
 class Discussion(models.Model):
+    """
+    Discussion model for community conversations.
+    """
+
     id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
     created_by = models.ForeignKey("authentication.UserModel", on_delete=models.CASCADE)
     title = models.CharField(max_length=255)
@@ -32,6 +36,10 @@ def __str__(self) -> str:
 
 
 class Faq(models.Model):
+    """
+    Frequently Asked Questions model.
+    """
+
     id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
     iso = models.CharField(max_length=3, choices=ISO_CHOICES)
     primary = models.BooleanField(default=False)
@@ -44,9 +52,28 @@ def __str__(self) -> str:
         return self.question
 
 
-# This is used to set the filename to the UUID of the model, in the Image model.
 def set_filename_to_uuid(instance: Any, filename: str) -> str:
-    """Generate a new filename using the model's UUID and keep the original extension."""
+    """
+    Generate a new filename using the model's UUID and keep the original extension.
+
+    Parameters
+    ----------
+    instance : Any
+        The model instance that the file is being attached to.
+
+    filename : str
+        The original filename of the uploaded file.
+
+    Returns
+    -------
+    str
+        The new file path with UUID-based filename.
+
+    Notes
+    -----
+    This function is used to maintain unique filenames and prevent collisions.
+    File extensions are forced to lowercase for consistency.
+    """
     ext = os.path.splitext(filename)[1]  # extract file extension
     # Note: Force extension to lowercase.
     new_filename = f"{instance.id}{ext.lower()}"  # use model UUID as filename
@@ -55,6 +82,10 @@ def set_filename_to_uuid(instance: Any, filename: str) -> str:
 
 
 class Image(models.Model):
+    """
+    Image model for storing uploaded images.
+    """
+
     id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
     file_object = models.ImageField(
         upload_to=set_filename_to_uuid,
@@ -70,12 +101,32 @@ def __str__(self) -> str:
 def delete_image_file(sender: Type[Image], instance: Image, **kwargs: Any) -> None:
     """
     Delete the file from the filesystem when the Image instance is deleted.
+
+    Parameters
+    ----------
+    sender : Type[Image]
+        The model class that sent the signal.
+
+    instance : Image
+        The actual instance being deleted.
+
+    **kwargs : Any
+        Additional keyword arguments passed to the receiver.
+
+    Notes
+    -----
+    This signal handler prevents orphaned files in the filesystem
+    when Image model instances are deleted.
     """
     if instance.file_object:
         instance.file_object.delete(save=False)
 
 
 class Location(models.Model):
+    """
+    Location model for geographic data.
+    """
+
     id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
     lat = models.CharField(max_length=24)
     lon = models.CharField(max_length=24)
@@ -89,6 +140,10 @@ def __str__(self) -> str:
 
 
 class Resource(models.Model):
+    """
+    Resource model for sharing useful links and information.
+    """
+
     id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
     created_by = models.ForeignKey("authentication.UserModel", on_delete=models.CASCADE)
     name = models.CharField(max_length=255)
@@ -111,6 +166,10 @@ def __str__(self) -> str:
 
 
 class Role(models.Model):
+    """
+    Role model for defining user roles.
+    """
+
     id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
     name = models.CharField(max_length=255)
     is_custom = models.BooleanField(default=False)
@@ -123,6 +182,10 @@ def __str__(self) -> str:
 
 
 class SocialLink(models.Model):
+    """
+    SocialLink model for storing links to social media profiles.
+    """
+
     id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
     link = models.CharField(max_length=255)
     label = models.CharField(max_length=255)
@@ -135,6 +198,10 @@ def __str__(self) -> str:
 
 
 class Tag(models.Model):
+    """
+    Tag model for content categorization.
+    """
+
     id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
     text = models.CharField(max_length=255)
     description = models.CharField(max_length=255)
@@ -145,6 +212,10 @@ def __str__(self) -> str:
 
 
 class Task(models.Model):
+    """
+    Task model for tracking actionable items.
+    """
+
     id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
     name = models.CharField(max_length=255)
     description = models.TextField(max_length=500)
@@ -158,6 +229,10 @@ def __str__(self) -> str:
 
 
 class Topic(models.Model):
+    """
+    Topic model for categorizing content by subject matter.
+    """
+
     id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
     name = models.CharField(max_length=255)
     active = models.BooleanField(default=True)
@@ -176,6 +251,10 @@ def __str__(self) -> str:
 
 
 class DiscussionEntry(models.Model):
+    """
+    DiscussionEntry model for individual posts within a discussion.
+    """
+
     id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
     discussion = models.ForeignKey(
         "content.Discussion", on_delete=models.CASCADE, related_name="discussion_entry"
diff --git a/backend/content/serializers.py b/backend/content/serializers.py
index a55d13668..0558ae736 100644
--- a/backend/content/serializers.py
+++ b/backend/content/serializers.py
@@ -28,12 +28,20 @@
 
 
 class DiscussionSerializer(serializers.ModelSerializer[Discussion]):
+    """
+    Serializer for Discussion model data.
+    """
+
     class Meta:
         model = Discussion
         fields = "__all__"
 
 
 class FaqSerializer(serializers.ModelSerializer[Faq]):
+    """
+    Serializer for Faq model data.
+    """
+
     class Meta:
         model = Faq
         fields = "__all__"
@@ -45,6 +53,25 @@ class Meta:
 def scrub_exif(image_file: InMemoryUploadedFile) -> InMemoryUploadedFile:
     """
     Remove EXIF metadata from JPEGs and text metadata from PNGs.
+
+    This function helps protect user privacy by removing potentially
+    sensitive metadata from uploaded image files.
+
+    Parameters
+    ----------
+    image_file : InMemoryUploadedFile
+        The uploaded image file to be processed.
+
+    Returns
+    -------
+    InMemoryUploadedFile
+        Processed image file with metadata removed.
+
+    Notes
+    -----
+    For JPEG files, the function converts to RGB and removes EXIF data.
+    For PNG files, it clears the metadata info dictionary.
+    Other file types are returned unchanged.
     """
     try:
         img: PILImage.Image = PILImage.open(image_file)
@@ -86,12 +113,34 @@ def scrub_exif(image_file: InMemoryUploadedFile) -> InMemoryUploadedFile:
 
 
 class ImageSerializer(serializers.ModelSerializer[Image]):
+    """
+    Serializer for Image model data.
+    """
+
     class Meta:
         model = Image
         fields = ["id", "file_object", "creation_date"]
         read_only_fields = ["id", "creation_date"]
 
     def validate(self, data: Dict[str, UploadedFile]) -> Dict[str, UploadedFile]:
+        """
+        Validate uploaded image files.
+
+        Parameters
+        ----------
+        data : Dict[str, UploadedFile]
+            Dictionary containing the file_object.
+
+        Returns
+        -------
+        Dict[str, UploadedFile]
+            Validated data dictionary.
+
+        Raises
+        ------
+        ValidationError
+            If no file was submitted or if the file size exceeds the maximum limit.
+        """
         if "file_object" not in data:
             raise serializers.ValidationError("No file was submitted.")
 
@@ -107,8 +156,27 @@ def validate(self, data: Dict[str, UploadedFile]) -> Dict[str, UploadedFile]:
 
         return data
 
-    # Using 'Any' type until a more correct type is determined.
     def create(self, validated_data: Dict[str, Any]) -> Image:
+        """
+        Create an Image instance with privacy-enhanced processing.
+
+        Parameters
+        ----------
+        validated_data : Dict[str, Any]
+            Dictionary containing validated data for creating the image.
+
+        Returns
+        -------
+        Image
+            Created Image instance.
+
+        Notes
+        -----
+        This method:
+        1. Processes the uploaded file to remove metadata
+        2. Creates the image record
+        3. Links the image to an organization if specified
+        """
         request = self.context["request"]
 
         if file_obj := request.FILES.get("file_object"):
@@ -130,23 +198,54 @@ def create(self, validated_data: Dict[str, Any]) -> Image:
 
 
 class LocationSerializer(serializers.ModelSerializer[Location]):
+    """
+    Serializer for Location model data.
+    """
+
     class Meta:
         model = Location
         fields = "__all__"
 
 
 class ResourceSerializer(serializers.ModelSerializer[Resource]):
+    """
+    Serializer for Resource model data.
+    """
+
     class Meta:
         model = Resource
         fields = "__all__"
 
 
 class TopicSerializer(serializers.ModelSerializer[Topic]):
+    """
+    Serializer for Topic model data.
+    """
+
     class Meta:
         model = Topic
         fields = "__all__"
 
     def validate(self, data: Dict[str, Union[str, int]]) -> Dict[str, Union[str, int]]:
+        """
+        Validate topic data including active status and deprecation date.
+
+        Parameters
+        ----------
+        data : Dict[str, Union[str, int]]
+            Topic data dictionary to validate.
+
+        Returns
+        -------
+        Dict[str, Union[str, int]]
+            Validated data dictionary.
+
+        Raises
+        ------
+        ValidationError
+            If an active topic has a deprecation date or
+            an inactive topic doesn't have a deprecation date.
+        """
         if data["active"] is True and data.get("deprecation_date") is not None:
             raise serializers.ValidationError(
                 _("Active topics cannot have a deprecation date."),
@@ -168,6 +267,10 @@ def validate(self, data: Dict[str, Union[str, int]]) -> Dict[str, Union[str, int
 
 
 class DiscussionEntrySerializer(serializers.ModelSerializer[DiscussionEntry]):
+    """
+    Serializer for DiscussionEntry model data.
+    """
+
     class Meta:
         model = DiscussionEntry
         fields = "__all__"
diff --git a/backend/content/urls.py b/backend/content/urls.py
index 14e16f220..2bf2dd07e 100644
--- a/backend/content/urls.py
+++ b/backend/content/urls.py
@@ -1,4 +1,8 @@
 # SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+URL routing for the content app.
+"""
+
 from django.urls import include, path
 from rest_framework.routers import DefaultRouter
 
diff --git a/backend/content/views.py b/backend/content/views.py
index c78cbbbf4..73a651506 100644
--- a/backend/content/views.py
+++ b/backend/content/views.py
@@ -1,5 +1,9 @@
 # SPDX-License-Identifier: AGPL-3.0-or-later
 # mypy: disable-error-code="override"
+"""
+API views for content management.
+"""
+
 from typing import Any
 
 from django.db.models import Q
@@ -22,12 +26,29 @@
 
 
 class DiscussionViewSet(viewsets.ModelViewSet[Discussion]):
+    """
+    Viewset for CRUD operations on Discussion objects.
+    """
+
     queryset = Discussion.objects.all()
     serializer_class = DiscussionSerializer
     pagination_class = CustomPagination
     permission_classes = [IsAuthenticatedOrReadOnly]
 
     def create(self, request: Request) -> Response:
+        """
+        Create a new discussion with the authenticated user as creator.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request containing discussion data.
+
+        Returns
+        -------
+        Response
+            Response with created discussion data or error message.
+        """
         if request.user.is_authenticated:
             request.data["created_by"] = request.user
             serializer = self.get_serializer(data=request.data)
@@ -42,6 +63,22 @@ def create(self, request: Request) -> Response:
         )
 
     def retrieve(self, request: Request, pk: str | None = None) -> Response:
+        """
+        Retrieve a specific discussion by ID.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request object.
+
+        pk : str or None
+            Primary key of the discussion to retrieve.
+
+        Returns
+        -------
+        Response
+            Response with discussion data or error message.
+        """
         queryset = self.get_queryset()
         if pk is not None:
             item = queryset.filter(id=pk).first()
@@ -56,6 +93,20 @@ def retrieve(self, request: Request, pk: str | None = None) -> Response:
         return Response(serializer.data, status=status.HTTP_200_OK)
 
     def list(self, request: Request) -> Response:
+        """
+        List all discussions with privacy filtering based on authentication.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request object.
+
+        Returns
+        -------
+        Response
+            Paginated response with filtered discussions.
+        """
+
         if request.user.is_authenticated:
             query = self.queryset.filter(
                 Q(is_private=False) | Q(is_private=True, created_by=request.user)
@@ -70,7 +121,24 @@ def list(self, request: Request) -> Response:
 
     def update(self, request: Request, pk: str | None = None) -> Response:
         """
-        Just the created_by user can update the discussion.
+        Update a discussion if the user is the creator.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request with updated discussion data.
+
+        pk : str or None
+            Primary key of the discussion to update.
+
+        Returns
+        -------
+        Response
+            Response with updated data or error message.
+
+        Notes
+        -----
+        Only the user who created the discussion can update it.
         """
         item = self.get_object()
         if item.created_by != request.user:
@@ -85,6 +153,26 @@ def update(self, request: Request, pk: str | None = None) -> Response:
         return Response(serializer.data, status=status.HTTP_200_OK)
 
     def partial_update(self, request: Request, pk: str | None = None) -> Response:
+        """
+        Partially update a discussion if the user is the creator.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request with partial discussion data.
+
+        pk : str or None
+            Primary key of the discussion to update.
+
+        Returns
+        -------
+        Response
+            Response with updated data or error message.
+
+        Notes
+        -----
+        Only the user who created the discussion can update it.
+        """
         item = self.get_object()
         if item.created_by != request.user:
             return Response(
@@ -100,7 +188,24 @@ def partial_update(self, request: Request, pk: str | None = None) -> Response:
 
     def destroy(self, request: Request, pk: str | None = None) -> Response:
         """
-        Deleted the whole discussion - requires created_by user.
+        Delete a discussion if the user is the creator.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request object.
+
+        pk : str or None
+            Primary key of the discussion to delete.
+
+        Returns
+        -------
+        Response
+            Empty response on success or error message.
+
+        Notes
+        -----
+        Only the user who created the discussion can delete it.
         """
         item = self.get_object()
         if item.created_by != request.user:
@@ -115,11 +220,28 @@ def destroy(self, request: Request, pk: str | None = None) -> Response:
 
 
 class ResourceViewSet(viewsets.ModelViewSet[Resource]):
+    """
+    Viewset for CRUD operations on Resource objects.
+    """
+
     queryset = Resource.objects.all()
     serializer_class = ResourceSerializer
     pagination_class = CustomPagination
 
     def create(self, request: Request) -> Response:
+        """
+        Create a new resource.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request containing resource data.
+
+        Returns
+        -------
+        Response
+            Response with created resource data or error message.
+        """
         if request.user.is_authenticated:
             serializer = self.get_serializer(data=request.data)
             serializer.is_valid(raise_exception=True)
@@ -133,6 +255,28 @@ def create(self, request: Request) -> Response:
         )
 
     def retrieve(self, request: Request, pk: str | None = None) -> Response:
+        """
+        Retrieve a specific resource by ID.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request object.
+
+        pk : str or None
+            Primary key of the resource to retrieve.
+
+        Returns
+        -------
+        Response
+            Response with resource data or error message.
+
+        Notes
+        -----
+        Privacy settings are enforced - authenticated users can see their own
+        private resources and all public resources, while unauthenticated users
+        can only see public resources.
+        """
         if request.user.is_authenticated:
             if pk is not None:
                 query = self.queryset.filter(
@@ -159,6 +303,24 @@ def retrieve(self, request: Request, pk: str | None = None) -> Response:
         return Response(serializer.data)
 
     def list(self, request: Request) -> Response:
+        """
+        List all resources.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request object.
+
+        Returns
+        -------
+        Response
+            Paginated response with filtered resources.
+
+        Notes
+        -----
+        Authenticated users can see public resources and their own private ones.
+        Unauthenticated users can only see public resources.
+        """
         if request.user.is_authenticated:
             query = self.queryset.filter(
                 Q(is_private=False) | Q(is_private=True, created_by=request.user)
@@ -171,6 +333,26 @@ def list(self, request: Request) -> Response:
         return self.get_paginated_response(self.paginate_queryset(serializer.data))
 
     def update(self, request: Request, pk: str | None = None) -> Response:
+        """
+        Update a resource if the user is the creator.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request with updated resource data.
+
+        pk : str or None
+            Primary key of the resource to update.
+
+        Returns
+        -------
+        Response
+            Response with updated data or error message.
+
+        Notes
+        -----
+        Only the user who created the resource can update it.
+        """
         item = self.get_object()
         if item.created_by != request.user:
             return Response(
@@ -185,6 +367,26 @@ def update(self, request: Request, pk: str | None = None) -> Response:
         return Response(serializer.data, status=status.HTTP_200_OK)
 
     def partial_update(self, request: Request, pk: str | None = None) -> Response:
+        """
+        Partially update a resource if the user is the creator.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request with partial resource data.
+
+        pk : str or None
+            Primary key of the resource to update.
+
+        Returns
+        -------
+        Response
+            Response with updated data or error message.
+
+        Notes
+        -----
+        Only the user who created the resource can update it.
+        """
         item = self.get_object()
         if item.created_by != request.user:
             return Response(
@@ -215,12 +417,29 @@ def destroy(self, request: Request, pk: str | None = None) -> Response:
 
 
 class DiscussionEntryViewSet(viewsets.ModelViewSet[DiscussionEntry]):
+    """
+    Viewset for operations on DiscussionEntry objects.
+    """
+
     queryset = DiscussionEntry.objects.all()
     serializer_class = DiscussionEntrySerializer
     pagination_class = CustomPagination
     permission_classes = [IsAuthenticatedOrReadOnly]
 
     def create(self, request: Request) -> Response:
+        """
+        Create a new discussion entry with the authenticated user as creator.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request containing discussion entry data.
+
+        Returns
+        -------
+        Response
+            Response with created discussion entry data or error message.
+        """
         if request.user.is_authenticated:
             request.data["created_by"] = request.user
             serializer = self.get_serializer(data=request.data)
@@ -235,6 +454,22 @@ def create(self, request: Request) -> Response:
         )
 
     def retrieve(self, request: Request, pk: str | None = None) -> Response:
+        """
+        Retrieve a specific discussion entry by ID.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request object.
+
+        pk : str or None
+            Primary key of the discussion entry to retrieve.
+
+        Returns
+        -------
+        Response
+            Response with discussion entry data or error message.
+        """
         queryset = self.get_queryset()
         if pk is not None:
             item = queryset.filter(id=pk).first()
@@ -249,19 +484,45 @@ def retrieve(self, request: Request, pk: str | None = None) -> Response:
         return Response(serializer.data, status=status.HTTP_200_OK)
 
     def list(self, request: Request) -> Response:
-        if request.user.is_authenticated:
-            query = self.queryset.filter(
-                Q(is_private=False) | Q(is_private=True, created_by=request.user)
-            )
+        """
+        List all discussion entries.
 
-        else:
-            query = self.queryset.filter()
+        Parameters
+        ----------
+        request : Request
+            HTTP request object.
 
+        Returns
+        -------
+        Response
+            Paginated response with discussion entries.
+        """
+        query = self.queryset.filter()
         serializer = self.get_serializer(query, many=True)
 
         return self.get_paginated_response(self.paginate_queryset(serializer.data))
 
     def update(self, request: Request, pk: str | None = None) -> Response:
+        """
+        Update a discussion entry if the user is the creator.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request with updated discussion entry data.
+
+        pk : str or None
+            Primary key of the discussion entry to update.
+
+        Returns
+        -------
+        Response
+            Response with updated data or error message.
+
+        Notes
+        -----
+        Only the user who created the discussion entry can update it.
+        """
         item = self.get_object()
         if item.created_by != request.user:
             return Response(
@@ -276,6 +537,26 @@ def update(self, request: Request, pk: str | None = None) -> Response:
         return Response(serializer.data, status=status.HTTP_200_OK)
 
     def partial_update(self, request: Request, pk: str | None = None) -> Response:
+        """
+        Partially update a discussion entry if the user is the creator.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request with partial discussion entry data.
+
+        pk : str or None
+            Primary key of the discussion entry to update.
+
+        Returns
+        -------
+        Response
+            Response with updated data or error message.
+
+        Notes
+        -----
+        Only the user who created the discussion entry can update it.
+        """
         item = self.get_object()
         if item.created_by != request.user:
             return Response(
@@ -290,8 +571,27 @@ def partial_update(self, request: Request, pk: str | None = None) -> Response:
         return Response(serializer.data, status=status.HTTP_200_OK)
 
     def destroy(self, request: Request, pk: str | None = None) -> Response:
-        item = self.get_object()
+        """
+        Delete a discussion entry if the user is the creator.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request object.
 
+        pk : str or None
+            Primary key of the discussion entry to delete.
+
+        Returns
+        -------
+        Response
+            Empty response on success or error message.
+
+        Notes
+        -----
+        Only the user who created the discussion entry can delete it.
+        """
+        item = self.get_object()
         if item.created_by != request.user:
             return Response(
                 {"error": "You are not allowed to delete this discussion entry."},
@@ -304,12 +604,39 @@ def destroy(self, request: Request, pk: str | None = None) -> Response:
 
 
 class ImageViewSet(viewsets.ModelViewSet[Image]):
+    """
+    Viewset for operations on Image objects.
+    """
+
     queryset = Image.objects.all()
     serializer_class = ImageSerializer
     parser_classes = (MultiPartParser, FormParser)
 
-    # Using 'Any' type until a more correct type is determined.
     def create(self, request: Request, *args: Any, **kwargs: Any) -> Response:
+        """
+        Upload and create a new image.
+
+        Parameters
+        ----------
+        request : Request
+            HTTP request containing image file data.
+
+        *args : Any
+            Variable positional arguments.
+
+        **kwargs : Any
+            Variable keyword arguments.
+
+        Returns
+        -------
+        Response
+            Response with created image data or error message.
+
+        Notes
+        -----
+        Handles file upload through multipart form data.
+        Passes request context to serializer for proper file handling.
+        """
         serializer = self.get_serializer(
             data=request.data,
             context={"request": request},  # pass request to serializer


Reviews:
user: login: cquinn540
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T14:07:18Z
user: login: cquinn540
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T14:09:12Z
user: login: rohityadav2604
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T18:38:29Z
user: login: cquinn540
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T19:51:09Z
user: login: cquinn540
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-10T19:56:49Z
user: login: andrewtavis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T08:53:01Z
user: login: andrewtavis
body: Really a great PR with loads of useful docstrings, @rohityadav2604! Thanks so much for your support here 😊 See my commit for very minor changes that would be good to mind in future contributions, but also no stress and let's keep going! :)
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-12T08:59:51Z

Review comments:
diff_hunk: @@ -44,9 +52,27 @@ def __str__(self) -> str:
         return self.question
 
 
-# This is used to set the filename to the UUID of the model, in the Image model.
 def set_filename_to_uuid(instance: Any, filename: str) -> str:
-    """Generate a new filename using the model's UUID and keep the original extension."""
+    """
+    Generate a new filename using the model's UUID and keep the original extension.
path: backend/content/models.py
user: login: cquinn540
body: Nice!  I think these longer comments will be very helpful
created_at: 2025-04-10T14:07:18Z
author_association: COLLABORATOR
diff_hunk: @@ -1,4 +1,8 @@
 # SPDX-License-Identifier: AGPL-3.0-or-later
+"""
path: backend/content/factories.py
user: login: cquinn540
body: Were these shorter comments auto-generated or required by a linter?

They don't hurt, but I don't think these shorter comments add much useful information.
created_at: 2025-04-10T14:09:11Z
author_association: COLLABORATOR
diff_hunk: @@ -1,4 +1,8 @@
 # SPDX-License-Identifier: AGPL-3.0-or-later
+"""
path: backend/content/factories.py
user: login: rohityadav2604
body: @cquinn540, these comments were already present in a lot of files. That was the reason I did not change it.
created_at: 2025-04-10T18:38:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,4 +1,8 @@
 # SPDX-License-Identifier: AGPL-3.0-or-later
+"""
path: backend/content/factories.py
user: login: cquinn540
body: If it's consistent with the rest of the files then it's not a problem for me.  👍 
created_at: 2025-04-10T19:51:09Z
author_association: COLLABORATOR
diff_hunk: @@ -44,9 +52,27 @@ def __str__(self) -> str:
         return self.question
 
 
-# This is used to set the filename to the UUID of the model, in the Image model.
 def set_filename_to_uuid(instance: Any, filename: str) -> str:
-    """Generate a new filename using the model's UUID and keep the original extension."""
+    """
+    Generate a new filename using the model's UUID and keep the original extension.
+
+    Parameters
+    ----------
+    instance : Any
+        The model instance that the file is being attached to.
+    filename : str
path: backend/content/models.py
user: login: andrewtavis
body: No stress here, @rohityadav2604, but could we get a blank line between parameter entries? I find it a bit more readable :) I'll send the change along, so just for future PRs, and thanks for this great docstring!
created_at: 2025-04-12T08:53:01Z
author_association: MEMBER

PR comments:
user: login: netlify[bot]
created_at: 2025-04-08T19:26:38Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *activist-org* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit |  |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/activist-org/deploys/67fa2be7fc8949dd2691e0da |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1211--activist-org.netlify.app](https://deploy-preview-1211--activist-org.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTEyMTEtLWFjdGl2aXN0LW9yZy5uZXRsaWZ5LmFwcCJ9.cRKzVO-F0snLQoEQ1irllesCUwqY-RjPNBYAgZYSJiU)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [activist-org Preview](https://deploy-preview-1211--activist-org.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/activist-org/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-08T19:26:43Z
body: ## Thank you for the pull request! ❤️

The activist team will do our best to address your contribution as soon as we can. If you're not already a member of our [public Matrix community](https://matrix.to/#/#activist_community:matrix.org), please consider joining! We'd suggest using [Element](https://element.io/) as your Matrix client, and definitely join the `General` and `Development` rooms once you're in. Also consider attending our [bi-weekly Saturday developer syncs](https://etherpad.wikimedia.org/p/activist-dev-sync)! It'd be great to meet you 😊
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-08T19:26:43Z
body: ## Maintainer Checklist

The following is a checklist for maintainers to make sure this process goes as well as possible. Feel free to address the points below yourself in further commits if you realize that actions are needed :)

- [x] The TypeScript, pytest and formatting workflows within the [PR checks](https://github.com/activist-org/activist/pull/1211/checks) do not indicate new errors in the files changed

- [x] The Playwright end to end and Zap penetration tests have been ran and are passing (if necessary)

- [x] The [changelog](CHANGELOG.md) has been updated with a description of the changes for the upcoming release and the corresponding issue (if necessary)
author_association: CONTRIBUTOR
user: login: andrewtavis
created_at: 2025-04-11T17:17:30Z
body: Hey @rohityadav2604 👋 I'll hopefully be able to check this later this evening. Would you be able to open a PR for the change you mentioned in your comment [here](https://github.com/activist-org/activist/pull/1204#issuecomment-2787468534) so we don't need to do documentation for `__str__` methods?
author_association: MEMBER
user: login: andrewtavis
created_at: 2025-04-12T09:02:22Z
body: Likely just a random Netlify error for the failures. Giving it a look :)
author_association: MEMBER
user: login: andrewtavis
created_at: 2025-04-12T09:06:46Z
body: As expected the deploy worked once we restarted :) Merging this in. Thanks @rohityadav2604 and also thanks @cquinn540 for the review! 😊
author_association: MEMBER