Skip to content

Feat/forum voting#505

Merged
A1L13N merged 13 commits into
alphaonelabs:mainfrom
Inshamhaque:feat/forum-voting
Apr 9, 2025
Merged

Feat/forum voting#505
A1L13N merged 13 commits into
alphaonelabs:mainfrom
Inshamhaque:feat/forum-voting

Conversation

@Inshamhaque
Copy link
Copy Markdown
Contributor

@Inshamhaque Inshamhaque commented Apr 8, 2025

added forum voting feature
closes #464

supported screenshots :-

Screenshot from 2025-04-08 16-12-38
Screenshot from 2025-04-08 16-12-17

comprehensive version of #489

Summary by CodeRabbit

  • New Features
    • Introduced voting on forum topics and replies, allowing users to upvote or downvote.
    • Vote counts, including upvotes, downvotes, and overall scores, are now clearly displayed.
    • Enhanced forum pages with interactive vote buttons and accessible feedback.
    • Improved visual experience with conditional styling for authenticated users.
    • New voting routes added for topics and replies.
  • Bug Fixes
    • Added error handling for voting functionality to manage cases where topics or replies do not exist.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 8, 2025

Walkthrough

The pull request introduces a new voting functionality for forum topics and replies. It adds a migration that creates a database model (ForumVote) with fields for vote type (up/down), timestamps, and foreign key relationships to topics, replies, and users. Corresponding model changes in the code add utility methods (upvote_count, downvote_count, vote_score, and user_vote) to ForumTopic and ForumReply. Forum templates are updated to display vote counts and interactive voting buttons, with a supporting JavaScript block. Additionally, new URL routes and view functions are introduced to process vote submissions.

Changes

File(s) Change Summary
web/migrations/0056_forumvote.py, web/models.py Introduces the ForumVote model with fields for id, vote_type, timestamps, and foreign keys (ForumReply, ForumTopic, user), enforces constraints, and adds vote utility methods (upvote_count, downvote_count, vote_score, user_vote) to ForumTopic and ForumReply.
web/templates/base.html Removes a comment line within the <body> section.
web/templates/.../forum/category.html, web/templates/.../forum/topic.html Updates forum templates to display vote counts and adds upvote/downvote buttons with JavaScript for handling vote submissions.
web/urls.py Adds new URL patterns for voting on forum topics (/forum/topic/<int:pk>/vote/) and replies (/forum/reply/<int:pk>/vote/).
web/views.py Introduces new view functions (topic_vote, reply_vote) for processing vote actions and updates context in topic and reply detail views with user voting information.

Assessment against linked issues

Objective Addressed Explanation
Add up/down voting on forum posts and show counts in list and detail pages (#464)

Possibly related PRs

  • added feature-page #293: Implements a similar voting mechanism through a FeatureVote model, closely related to the ForumVote functionality.
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d8af726 and c86aba7.

📒 Files selected for processing (7)
  • web/migrations/0056_forumvote.py (1 hunks)
  • web/models.py (3 hunks)
  • web/templates/base.html (1 hunks)
  • web/templates/web/forum/category.html (1 hunks)
  • web/templates/web/forum/topic.html (7 hunks)
  • web/urls.py (1 hunks)
  • web/views.py (4 hunks)
🧰 Additional context used
🧬 Code Definitions (2)
web/urls.py (1)
web/views.py (2)
  • topic_vote (7134-7169)
  • reply_vote (7173-7209)
web/views.py (1)
web/models.py (6)
  • ForumVote (2867-2899)
  • ForumReply (832-870)
  • delete (467-473)
  • user_vote (815-823)
  • user_vote (855-863)
  • ForumTopic (789-829)
🪛 Ruff (0.8.2)
web/models.py

803-803: Missing return type annotation for public function upvote_count

(ANN201)


807-807: Missing return type annotation for public function downvote_count

(ANN201)


811-811: Missing return type annotation for public function vote_score

(ANN201)


815-815: Missing return type annotation for public function user_vote

(ANN201)


815-815: Missing type annotation for function argument user

(ANN001)


821-821: Consider moving this statement to an else block

(TRY300)


843-843: Missing return type annotation for public function upvote_count

(ANN201)


847-847: Missing return type annotation for public function downvote_count

(ANN201)


851-851: Missing return type annotation for public function vote_score

(ANN201)


855-855: Missing return type annotation for public function user_vote

(ANN201)


855-855: Missing type annotation for function argument user

(ANN001)


861-861: Consider moving this statement to an else block

(TRY300)


2870-2873: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


2883-2883: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


2884-2892: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


2891-2891: Trailing comma missing

Add trailing comma

(COM812)


2894-2894: Missing return type annotation for special method __str__

Add return type annotation: str

(ANN204)


2897-2897: Unnecessary elif after return statement

Remove unnecessary elif

(RET505)

web/migrations/0056_forumvote.py

10-13: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


15-66: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


61-61: Trailing comma missing

Add trailing comma

(COM812)

web/views.py

2899-2899: Unnecessary else after return statement

Remove unnecessary else

(RET505)


7134-7134: Missing return type annotation for public function topic_vote

(ANN201)


7134-7134: Missing type annotation for function argument request

(ANN001)


7134-7134: Missing type annotation for function argument pk

(ANN001)


7150-7150: Trailing comma missing

Add trailing comma

(COM812)


7173-7173: Missing return type annotation for public function reply_vote

(ANN201)


7173-7173: Missing type annotation for function argument request

(ANN001)


7173-7173: Missing type annotation for function argument pk

(ANN001)


7188-7188: Trailing comma missing

Add trailing comma

(COM812)


7208-7208: try-except block with duplicate exception ForumReply.DoesNotExist

(B025)


7212-7212: Missing return type annotation for public function topic_detail

(ANN201)


7212-7212: Missing type annotation for function argument request

(ANN001)


7212-7212: Missing type annotation for function argument pk

(ANN001)


7228-7228: Trailing comma missing

Add trailing comma

(COM812)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Run Tests
  • GitHub Check: build-and-test
🔇 Additional comments (7)
web/templates/web/forum/category.html (1)

138-148: Good implementation of voting metrics display!

The voting metrics display is well-integrated into the topic information section using proper semantic HTML and consistent styling with the rest of the UI.

web/urls.py (1)

218-219: Well-designed URL patterns for voting functionality.

The new URL patterns for forum voting follow consistent naming conventions and properly use RESTful design principles for the voting actions.

web/templates/base.html (1)

154-154: Good practice for exposing authentication state to client-side code.

Adding the user-authenticated class to the body element is a clean way to make authentication state available to JavaScript without exposing sensitive information.

web/templates/web/forum/topic.html (3)

6-7: Appropriate template imports for the new functionality.

The addition of the dict_filters library and static files import properly supports the voting feature implementation.


134-210: Excellent voting UI implementation with strong accessibility.

The topic voting UI is well-structured with proper accessibility attributes (aria-label, aria-pressed), clear visual feedback, and separation of concerns. The SVG icons and color-coded vote score provide excellent visual cues.


299-321: Well-designed vote statistics for replies.

The vote statistics display for replies is appropriately sized and styled, maintaining visual consistency with the topic statistics while being properly scaled for reply elements.

web/migrations/0056_forumvote.py (1)

10-66: False positive about mutable class attributes.

The static analysis tool flags the class attributes (e.g., dependencies or operations) as mutable and suggests using typing.ClassVar. In Django migrations, these attributes are conventionally defined as is and pose no real issue. You may safely ignore this warning unless your project guidelines specifically require type annotations for migrations.

🧰 Tools
🪛 Ruff (0.8.2)

10-13: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


15-66: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


61-61: Trailing comma missing

Add trailing comma

(COM812)

Comment thread web/templates/web/forum/topic.html
Comment thread web/templates/web/forum/topic.html
Comment thread web/migrations/0056_forumvote.py
Comment thread web/models.py
Comment thread web/models.py
Comment thread web/views.py
Comment thread web/views.py
Comment thread web/views.py
Comment thread web/views.py
Comment thread web/views.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🔭 Outside diff range comments (1)
web/views.py (1)

7209-7237: 🧹 Nitpick (assertive)

topic_detail changes: add missing type hints and handle trailing comma.

  1. Missing annotations:
    def topic_detail(request: HttpRequest, pk: int) -> HttpResponse:
        ...
  2. Trailing comma: The dict at line 7228 could include a trailing comma after the last key-value pair if following project style.
  3. These changes correctly retrieve user_topic_vote and user_reply_votes with minimal overhead. Good job ensuring the code checks request.user.is_authenticated first.
🧰 Tools
🪛 Ruff (0.8.2)

7212-7212: Missing return type annotation for public function topic_detail

(ANN201)


7212-7212: Missing type annotation for function argument request

(ANN001)


7212-7212: Missing type annotation for function argument pk

(ANN001)


7228-7228: Trailing comma missing

Add trailing comma

(COM812)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c86aba7 and 78750c4.

📒 Files selected for processing (2)
  • web/templates/web/forum/topic.html (7 hunks)
  • web/views.py (4 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
web/views.py (1)
web/models.py (6)
  • ForumVote (2867-2899)
  • ForumReply (832-870)
  • delete (467-473)
  • user_vote (815-823)
  • user_vote (855-863)
  • ForumTopic (789-829)
🪛 Ruff (0.8.2)
web/views.py

2899-2899: Unnecessary else after return statement

Remove unnecessary else

(RET505)


7134-7134: Missing return type annotation for public function topic_vote

(ANN201)


7134-7134: Missing type annotation for function argument request

(ANN001)


7134-7134: Missing type annotation for function argument pk

(ANN001)


7150-7150: Trailing comma missing

Add trailing comma

(COM812)


7173-7173: Missing return type annotation for public function reply_vote

(ANN201)


7173-7173: Missing type annotation for function argument request

(ANN001)


7173-7173: Missing type annotation for function argument pk

(ANN001)


7188-7188: Trailing comma missing

Add trailing comma

(COM812)


7208-7208: try-except block with duplicate exception ForumReply.DoesNotExist

(B025)


7212-7212: Missing return type annotation for public function topic_detail

(ANN201)


7212-7212: Missing type annotation for function argument request

(ANN001)


7212-7212: Missing type annotation for function argument pk

(ANN001)


7228-7228: Trailing comma missing

Add trailing comma

(COM812)

🔇 Additional comments (12)
web/templates/web/forum/topic.html (10)

6-8: Load Template Filters and Static Resources

The addition of {% load dict_filters %} and {% load static %} ensures that custom template filters (e.g., for dictionary lookups) and static file handling are available. This is essential for the subsequent use of filters like get_item in the template.


134-152: Accessible Topic Upvote Button

The upvote button for topics now includes descriptive attributes:

  • aria-label="Upvote this topic" clarifies the purpose for screen readers.
  • aria-pressed="{% if user_topic_vote == 'up' %}true{% else %}false{% endif %}" dynamically reflects the vote state.

The SVG icon styling conditionally applies the text-teal-500 class when active. This approach improves both accessibility and visual feedback.


153-171: Consistent Vote Score Display and Downvote Button

The vote score is presented with conditional styling based on its value, and the downvote button mirrors the accessibility features of the upvote button. Using clear, conditional classes (e.g., text-red-500 when the user has downvoted) ensures a cohesive user experience.


176-210: Clear Voting Statistics and Conditional Delete Option

The block displaying vote statistics (upvotes and downvotes) and the conditional delete form for the topic is well-organized. The separation of concerns—displaying vote counts and offering deletion only to the author—improves both clarity and security.


223-257: Enhanced Accessibility for Reply Voting

The reply voting buttons are updated to include aria-label and aria-pressed attributes. This, together with the use of the custom get_item filter to determine the vote state, significantly improves accessibility and consistency with the topic voting buttons.


303-325: Clear Presentation of Reply Vote Statistics

The section that displays reply vote counts uses concise SVG icons and a clear layout. Conditional styling on vote counts (using classes like text-teal-500 for positive scores) ensures that users receive immediate visual feedback on the reply’s popularity.


375-383: Efficient CSRF Token Extraction

The getCsrfToken function is succinct and reliably retrieves the CSRF token from the document. This encapsulation improves maintainability for secure AJAX operations.


385-424: Robust AJAX Vote Handling

The handleVote function efficiently manages vote submissions by:

  • Preventing the default event action.
  • Checking if the user is authenticated (and redirecting if not).
  • Determining the correct URL dynamically based on whether the vote is for a topic or a reply.
  • Initiating a fetch POST request with proper headers and body data.

The promise chain handles the response by updating the UI via updateVoteUI. This refactoring removes redundant code and enhances the overall user experience.


430-458: Dynamic UI Update After Voting

The updateVoteUI function effectively updates the vote score and adjusts button highlights based on the user's vote. The targeted DOM queries (e.g., selecting the vote score element and the associated SVGs) ensure that only the necessary parts of the UI are updated. This approach minimizes layout thrashing and maintains a responsive interface.


460-463: Efficient Event Binding for Vote Buttons

Attaching the handleVote event listener to all elements with the .vote-btn class is a clean and efficient approach. This ensures consistent behavior across both topic and reply voting without duplicating code.

web/views.py (2)

131-131: Added import for ForumVote: Looks good.

The addition of ForumVote in the import list appears correct and needed for the voting features below. No issues here.


1843-1887: POST handling for replies, voting, and deletion: Implementation approved.

All conditionals and permissions checks look consistent with the existing forum logic. The checks for reply.author == request.user and topic.author == request.user properly control deletion actions. The approach of retrieving user votes later in the code (user_topic_vote, user_reply_votes) is cleanly separated from POST actions. Overall, these changes align well with existing patterns.

Comment thread web/templates/web/forum/topic.html
Comment thread web/views.py
Comment thread web/views.py
Comment thread web/views.py
coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 8, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
web/migrations/0056_forumvote.py (1)

53-62: Consider adding a trailing comma for consistency.

According to the static analysis hint (COM812), adding a trailing comma after the name="vote_topic_xor_reply" argument in the CheckConstraint can improve consistency. This change is purely stylistic and does not affect functionality.

 models.CheckConstraint(
     condition=models.Q(
         models.Q(("reply__isnull", True), ("topic__isnull", False)),
         models.Q(("reply__isnull", False), ("topic__isnull", True)),
         _connector="OR",
     ),
-    name="vote_topic_xor_reply"
+    name="vote_topic_xor_reply",
 )
🧰 Tools
🪛 Ruff (0.8.2)

61-61: Trailing comma missing

Add trailing comma

(COM812)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 78750c4 and a8c8627.

📒 Files selected for processing (1)
  • web/migrations/0056_forumvote.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
web/migrations/0056_forumvote.py

10-13: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


15-66: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


61-61: Trailing comma missing

Add trailing comma

(COM812)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Run Tests
🔇 Additional comments (1)
web/migrations/0056_forumvote.py (1)

16-65: The ForumVote model design looks good.

The ForumVote model implementation correctly:

  • Uses appropriate field types and relationships
  • Enforces constraints to ensure a vote belongs to either a topic OR a reply (not both)
  • Prevents duplicate votes from the same user with unique_together constraints
  • Tracks creation and update timestamps automatically

This provides a solid foundation for the forum voting feature.

🧰 Tools
🪛 Ruff (0.8.2)

61-61: Trailing comma missing

Add trailing comma

(COM812)

Comment thread web/migrations/0056_forumvote.py
coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 9, 2025
@Inshamhaque
Copy link
Copy Markdown
Contributor Author

@coderabbitai can u assess why the docker build and run is failing and how to fix it ?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find Docker-related files in the repository
echo "=== Docker Files ==="
find . -name "Dockerfile" -o -name "docker-compose.yml" -o -name "*.dockerfile" | sort

# If Dockerfile exists, look at its content
echo -e "\n=== Dockerfile Content ==="
if [ -f Dockerfile ]; then
    cat Dockerfile
elif [ -f docker/Dockerfile ]; then
    cat docker/Dockerfile
else
    echo "No Dockerfile found at root or in docker/ directory"
    # Try to find any Dockerfile in the repo
    DOCKERFILE=$(find . -name "Dockerfile" | head -1)
    if [ ! -z "$DOCKERFILE" ]; then
        cat "$DOCKERFILE"
    fi
fi

# If docker-compose.yml exists, look at its content
echo -e "\n=== docker-compose.yml Content ==="
if [ -f docker-compose.yml ]; then
    cat docker-compose.yml
elif [ -f docker/docker-compose.yml ]; then
    cat docker/docker-compose.yml
else
    echo "No docker-compose.yml found at root or in docker/ directory"
    # Try to find any docker-compose.yml in the repo
    COMPOSE=$(find . -name "docker-compose.yml" | head -1)
    if [ ! -z "$COMPOSE" ]; then
        cat "$COMPOSE"
    fi
fi

# Check for Docker-related scripts
echo -e "\n=== Docker-related Scripts ==="
find . -name "*.sh" -exec grep -l "docker" {} \; | sort

# Check recent changes to Docker files
echo -e "\n=== Recent Changes to Docker Files ==="
git diff HEAD~10..HEAD -- $(find . -name "Dockerfile" -o -name "docker-compose.yml" -o -name "*.dockerfile")

# Check for potential issues in requirements.txt or package.json that might cause build failures
echo -e "\n=== Python Requirements ==="
if [ -f requirements.txt ]; then
    cat requirements.txt
fi

echo -e "\n=== Node Package Dependencies ==="
if [ -f package.json ]; then
    cat package.json | grep -A 50 '"dependencies"' | grep -B 50 '"devDependencies"'
fi

Length of output: 7458


@Inshamhaque To diagnose why your Docker build is failing, I need to examine your .env.sample file and any error logs you're receiving. However, based on the Dockerfile analysis, I can identify several likely issues:

  1. Database Connection Problems: Your Dockerfile runs migrations and creates test data during the build process, but there's no database configuration. Django needs a database to run migrations.

  2. Environment Variable Issues: The Dockerfile simply copies .env.sample to .env without customization, which may not have proper settings for a containerized environment.

  3. Build-time vs. Runtime Operations: You're running migrations, creating test data, and creating a superuser during build time rather than at container startup, which is problematic if these operations require external services.

To fix these issues, I recommend:

  1. Move database migrations and data creation to a startup script instead of the build process
  2. Create a docker-compose.yml file to manage the database and application services together
  3. Share the specific error message you're receiving for more targeted assistance

Would you like me to help create a proper docker-compose.yml file and startup script to address these issues?

@Inshamhaque
Copy link
Copy Markdown
Contributor Author

Inshamhaque commented Apr 9, 2025

@coderabbitai examine the .env.sample and here are the logs from docker build and run tests
Error response from daemon: No such container: education-app
Error: Process completed with exit code 1.

kindly give the possible fixes and errors

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if .env.sample exists and examine its content
echo "=== .env.sample Content ==="
if [ -f .env.sample ]; then
    cat .env.sample
else
    echo "No .env.sample file found at root directory"
    ENV_SAMPLE=$(find . -name ".env.sample" | head -1)
    if [ ! -z "$ENV_SAMPLE" ]; then
        cat "$ENV_SAMPLE"
    fi
fi

# Check Docker-related files
echo -e "\n=== Dockerfile Content ==="
if [ -f Dockerfile ]; then
    cat Dockerfile
else
    DOCKERFILE=$(find . -name "Dockerfile" | head -1)
    if [ ! -z "$DOCKERFILE" ]; then
        echo "Found Dockerfile at $DOCKERFILE:"
        cat "$DOCKERFILE"
    fi
fi

# Check if there are any CI/CD workflows referencing the container
echo -e "\n=== GitHub Workflow Files with Docker References ==="
if [ -d ".github/workflows" ]; then
    grep -r "education-app" --include="*.yml" --include="*.yaml" .github/workflows/ || echo "No references to education-app found in workflows"
fi

# Check for any docker-compose files
echo -e "\n=== docker-compose files ==="
find . -name "docker-compose*.yml" -o -name "docker-compose*.yaml" | xargs -I{} sh -c 'echo "File: {}"; cat {}'

Length of output: 3651


@Inshamhaque I've examined your Docker configuration and identified several issues that are likely causing the "No such container: education-app" error. Here's what's happening and how to fix it:

Root Issues

  1. Conflicting Operations: Your Dockerfile runs migrations, creates test data, and a superuser during build time, but your GitHub workflow tries to do the same at runtime.

  2. Container Startup Failure: The container is likely failing to start properly due to environment or database issues, which is why subsequent commands can't find it.

  3. SQLite in Docker: Using SQLite (as configured in .env.sample) in containerized environments can cause permission issues.

Solution Steps

  1. Separate Build vs. Runtime Operations:
    • Remove these lines from your Dockerfile:
      # Run migrations and create test data
      RUN python manage.py migrate
      

&&
python manage.py create_test_data

 # Create superuser
 ENV DJANGO_SUPERUSER_USERNAME=admin
 ENV DJANGO_SUPERUSER_EMAIL=admin@example.com
 ENV DJANGO_SUPERUSER_PASSWORD=adminpassword
 RUN python manage.py createsuperuser --noinput
 ```
  • Keep these operations only in your workflow or startup script
  1. Create a Startup Script:

    # Create startup script
    RUN echo '#!/bin/bash\npython manage.py migrate\npython manage.py create_test_data\nexec "$@"' > /app/docker-entrypoint.sh && \
        chmod +x /app/docker-entrypoint.sh
    
    # Change the CMD to use the startup script
    ENTRYPOINT ["/app/docker-entrypoint.sh"]
    CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
  2. Modify GitHub Workflow:

    • Add container health check before running commands:
    - name: Wait for container to be healthy
      run: |
        timeout=60
        while [ $timeout -gt 0 ]; do
          if docker inspect education-app | grep -q '"Status": "running"'; then
            echo "Container is running"
            break
          fi
          sleep 1
          timeout=$((timeout-1))
        done
        if [ $timeout -eq 0 ]; then
          echo "Container failed to start properly"
          docker logs education-app
          exit 1
        fi
  3. Optional: Create docker-compose.yml for better development:

    version: '3'
    services:
      web:
        build: .
        container_name: education-app
        ports:
          - "8000:8000"
        volumes:
          - ./:/app
          - db_data:/app/db.sqlite3
        environment:
          - DEBUG=True
    
    volumes:
      db_data:

Let me know if you'd like me to elaborate on any of these solutions or if you need help implementing them!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find Docker-related files in the repository
echo "=== Docker Files ==="
find . -name "Dockerfile" -o -name "docker-compose.yml" -o -name "*.dockerfile" | sort

# If Dockerfile exists, look at its content
echo -e "\n=== Dockerfile Content ==="
if [ -f Dockerfile ]; then
    cat Dockerfile
elif [ -f docker/Dockerfile ]; then
    cat docker/Dockerfile
else
    echo "No Dockerfile found at root or in docker/ directory"
    # Try to find any Dockerfile in the repo
    DOCKERFILE=$(find . -name "Dockerfile" | head -1)
    if [ ! -z "$DOCKERFILE" ]; then
        cat "$DOCKERFILE"
    fi
fi

# Look at .env.sample content
echo -e "\n=== .env.sample Content ==="
if [ -f .env.sample ]; then
    cat .env.sample
else
    echo "No .env.sample found at root"
    # Try to find any .env.sample in the repo
    ENV_SAMPLE=$(find . -name ".env.sample" | head -1)
    if [ ! -z "$ENV_SAMPLE" ]; then
        cat "$ENV_SAMPLE"
    fi
fi

# Check docker-compose.yml content if it exists
echo -e "\n=== docker-compose.yml Content ==="
if [ -f docker-compose.yml ]; then
    cat docker-compose.yml
elif [ -f docker/docker-compose.yml ]; then
    cat docker/docker-compose.yml
else
    COMPOSE=$(find . -name "docker-compose.yml" | head -1)
    if [ ! -z "$COMPOSE" ]; then
        cat "$COMPOSE"
    else
        echo "No docker-compose.yml found"
    fi
fi

# Check for Docker-related scripts
echo -e "\n=== Docker-related Scripts ==="
find . -name "*.sh" | xargs grep -l "docker" 2>/dev/null || echo "No Docker scripts found"

# Check for CI/CD workflows that might be running Docker
echo -e "\n=== CI/CD Workflows ==="
find .github/workflows -type f -name "*.yml" -o -name "*.yaml" 2>/dev/null | xargs cat 2>/dev/null || echo "No GitHub workflow files found"

Length of output: 23819


@Inshamhaque Based on the files I've examined, I can identify why you're getting the "No such container: education-app" error. Here are the problems and solutions:

Issues Identified

  1. Database Operations During Build: Your Dockerfile runs migrations and creates test data during the build phase, but databases aren't accessible during image building.

  2. Missing Container Name: You're trying to interact with a container named "education-app" but haven't properly named your container when running it.

  3. No Docker Compose: There's no docker-compose.yml to manage related services like databases.

Solutions

1. Create a docker-compose.yml file:

version: '3.8'

services:
  web:
    build: .
    container_name: education-app
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=sqlite:///db.sqlite3
      - DEBUG=True
    volumes:
      - ./:/app
    command:

>
      bash -c "python manage.py migrate &&
               python manage.py create_test_data &&
               python manage.py createsuperuser --noinput &&
               python manage.py runserver 0.0.0.0:8000"

2. Modify your Dockerfile:

# Python base image
FROM python:3.10-slim

# Set working directory
WORKDIR /app

# Install dependencies
RUN apt-get update && apt-get install -y \
    curl \
    && rm -rf /var/lib/apt/lists/*

# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN python -m pip install --upgrade pip && \
    python -m pip install -r requirements.txt

# Copy project files
COPY . .

# Create necessary directories for static files
RUN mkdir -p /app/static /app/staticfiles

# Create and configure environment variables
COPY .env.sample .env

# Collect static files
RUN python manage.py collectstatic --noinput

# Set superuser credentials as environment variables
ENV DJANGO_SUPERUSER_USERNAME=admin
ENV DJANGO_SUPERUSER_EMAIL=admin@example.com
ENV DJANGO_SUPERUSER_PASSWORD=adminpassword

# Expose port
EXPOSE 8000

# Start the server (migrations will be run by docker-compose)
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

3. Build and run commands:

# Build and start containers
docker-compose up --build

# If you want to run without docker-compose:
docker build -t education-website:latest .
docker run -p 8000:8000 --name education-app education-website:latest

4. For running commands inside the container:

# Make sure to use the correct container name
docker exec -it education-app python manage.py shell

This approach fixes the issue by:

  1. Moving database operations to runtime (not build time)
  2. Ensuring consistent container naming
  3. Providing proper container orchestration

Would you like me to explain any part of this solution in more detail?

Comment thread web/templates/base.html Outdated
@A1L13N A1L13N enabled auto-merge April 9, 2025 20:24
@A1L13N
Copy link
Copy Markdown
Contributor

A1L13N commented Apr 9, 2025

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2025

✅ Actions performed

Full review triggered.

@A1L13N A1L13N added this pull request to the merge queue Apr 9, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (13)
web/migrations/0056_forumvote.py (2)

1-1: 🧹 Nitpick (assertive)

Remove “test” suffix from the migration comment.
This appears to be a debugging artifact. For consistency, clean up the auto-generated heading to avoid confusion in production migrations.

-# Generated by Django 5.1.6 on 2025-04-08 09:41 test
+# Generated by Django 5.1.6 on 2025-04-08 09:41

53-62: 🧹 Nitpick (assertive)

Add a trailing comma to comply with style checks.
The static analysis hint (COM812) recommends adding a trailing comma in the check constraint for cleaner diffs and style consistency.

 name="vote_topic_xor_reply"
+name="vote_topic_xor_reply",
🧰 Tools
🪛 Ruff (0.8.2)

61-61: Trailing comma missing

Add trailing comma

(COM812)

web/models.py (4)

802-824: Add type annotations to ForumTopic methods for clarity.

The voting methods look functionally correct, but would benefit from type annotations to improve code readability and maintainability. Also, consider using an else block in the exception handling for better structure.

-def upvote_count(self):
+def upvote_count(self) -> int:
     """Return the number of upvotes for this topic."""
     return self.votes.filter(vote_type="up").count()

-def downvote_count(self):
+def downvote_count(self) -> int:
     """Return the number of downvotes for this topic."""
     return self.votes.filter(vote_type="down").count()

-def vote_score(self):
+def vote_score(self) -> int:
     """Return the total vote score (upvotes - downvotes)."""
     return self.upvote_count() - self.downvote_count()

-def user_vote(self, user):
+def user_vote(self, user: User) -> Optional[str]:
     """Return the user's vote type for this topic, or None if not voted."""
     if not user.is_authenticated:
         return None
     try:
         vote = self.votes.get(user=user)
         return vote.vote_type
     except ForumVote.DoesNotExist:
-        return None
+        pass
+    return None
🧰 Tools
🪛 Ruff (0.8.2)

803-803: Missing return type annotation for public function upvote_count

(ANN201)


807-807: Missing return type annotation for public function downvote_count

(ANN201)


811-811: Missing return type annotation for public function vote_score

(ANN201)


815-815: Missing return type annotation for public function user_vote

(ANN201)


815-815: Missing type annotation for function argument user

(ANN001)


821-821: Consider moving this statement to an else block

(TRY300)


842-864: Add type annotations to ForumReply methods for consistency.

Similarly, adding type hints (e.g., return types for upvote_count, downvote_count, vote_score, etc.) can help maintain consistency and clarity across both ForumTopic and ForumReply classes.

-def upvote_count(self):
+def upvote_count(self) -> int:
     """Return the number of upvotes for this reply."""
     return self.votes.filter(vote_type="up").count()

-def downvote_count(self):
+def downvote_count(self) -> int:
     """Return the number of downvotes for this reply."""
     return self.votes.filter(vote_type="down").count()

-def vote_score(self):
+def vote_score(self) -> int:
     """Return the total vote score (upvotes - downvotes)."""
     return self.upvote_count() - self.downvote_count()

-def user_vote(self, user):
+def user_vote(self, user: User) -> Optional[str]:
     """Return the user's vote type for this reply, or None if not voted."""
     if not user.is_authenticated:
         return None
     try:
         vote = self.votes.get(user=user)
         return vote.vote_type
     except ForumVote.DoesNotExist:
-        return None
+        pass
+    return None
🧰 Tools
🪛 Ruff (0.8.2)

843-843: Missing return type annotation for public function upvote_count

(ANN201)


847-847: Missing return type annotation for public function downvote_count

(ANN201)


851-851: Missing return type annotation for public function vote_score

(ANN201)


855-855: Missing return type annotation for public function user_vote

(ANN201)


855-855: Missing type annotation for function argument user

(ANN001)


861-861: Consider moving this statement to an else block

(TRY300)


2884-2892: Add a trailing comma to match Django style conventions.

As in your migration file, Django often prefers a trailing comma for multi-line lists or constraints. This is purely stylistic and won't affect behavior, but it keeps your code consistent with Django's recommended style.

constraints = [
    models.CheckConstraint(
        check=(
            models.Q(topic__isnull=False, reply__isnull=True)
            | models.Q(topic__isnull=True, reply__isnull=False)
        ),
-        name="vote_topic_xor_reply"
+        name="vote_topic_xor_reply",
    )
]
🧰 Tools
🪛 Ruff (0.8.2)

2884-2892: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


2891-2891: Trailing comma missing

Add trailing comma

(COM812)


2894-2901: Refine __str__ method structure for simplicity.

The static analysis flags an "Unnecessary elif after return statement" (RET505). Functionally, this code is fine. However, you could simplify flow by using separate if checks instead of elif. If you prefer to keep the if/elif, you can safely ignore this minor style nitpick. Additionally, consider adding a return type annotation.

-def __str__(self):
+def __str__(self) -> str:
    if self.topic:
        return f"{self.user.username} {self.vote_type}voted topic #{self.topic.id}"
-    elif self.reply:
+    if self.reply:
        return f"{self.user.username} {self.vote_type}voted reply #{self.reply.id}"
    return f"{self.user.username} cast a vote"
🧰 Tools
🪛 Ruff (0.8.2)

2894-2894: Missing return type annotation for special method __str__

Add return type annotation: str

(ANN204)


2897-2897: Unnecessary elif after return statement

Remove unnecessary elif

(RET505)

web/views.py (7)

7209-7237: 🧹 Nitpick (assertive)

Add type annotations to topic_detail.

The topic_detail function is missing type annotations, similar to the other new functions.

-def topic_detail(request, pk):
+from django.http import HttpRequest, HttpResponse
+
+def topic_detail(request: HttpRequest, pk: int) -> HttpResponse:
     topic = get_object_or_404(ForumTopic, pk=pk)

Also, add a trailing comma to the values_list call:

     reply_votes = ForumVote.objects.filter(reply__topic=topic, user=request.user).values_list(
-        "reply_id", "vote_type"
+        "reply_id", 
+        "vote_type",
     )
🧰 Tools
🪛 Ruff (0.8.2)

7209-7209: Missing return type annotation for public function topic_detail

(ANN201)


7209-7209: Missing type annotation for function argument request

(ANN001)


7209-7209: Missing type annotation for function argument pk

(ANN001)


7225-7225: Trailing comma missing

Add trailing comma

(COM812)


7133-7170: 🧹 Nitpick (assertive)

Add type annotations and trailing commas to the topic_vote function.

The function implementation looks good, but could benefit from type annotations and style improvements.

-def topic_vote(request, pk):
+from django.http import HttpRequest, HttpResponse
+from typing import Union
+
+def topic_vote(request: HttpRequest, pk: int) -> Union[HttpResponse, JsonResponse]:
     """Handle voting on a topic."""
     if request.method != "POST":
         return JsonResponse({"error": "Only POST method allowed"}, status=405)

     try:
         topic = ForumTopic.objects.get(pk=pk)
         vote_type = request.POST.get("vote_type")

         if vote_type not in ["up", "down"]:
             # For form submissions, redirect back with an error message if needed
             messages.error(request, "Invalid vote type")
             return redirect("topic_vote", pk=topic.id)

         # Check if user already voted on this topic
         vote, created = ForumVote.objects.get_or_create(
-            user=request.user, topic=topic, defaults={"vote_type": vote_type}
+            user=request.user,
+            topic=topic,
+            defaults={"vote_type": vote_type},
         )
🧰 Tools
🪛 Ruff (0.8.2)

7134-7134: Missing return type annotation for public function topic_vote

(ANN201)


7134-7134: Missing type annotation for function argument request

(ANN001)


7134-7134: Missing type annotation for function argument pk

(ANN001)


7150-7150: Trailing comma missing

Add trailing comma

(COM812)


7140-7142: 🧹 Nitpick (assertive)

Consider preventing authors from voting on their own topic.

Currently, any authenticated user (including topic authors) can vote. If you want to exclude self-voting or impose other constraints (like one vote per user), add checks here.

 try:
     topic = ForumTopic.objects.get(pk=pk)
     vote_type = request.POST.get("vote_type")
+    
+    # Prevent authors from voting on their own topics
+    if topic.author == request.user:
+        messages.error(request, "You cannot vote on your own topic")
+        return redirect("forum_topic", category_slug=topic.category.slug, topic_id=topic.id)

7166-7166: ⚠️ Potential issue

Potential issue with "view_topic" route.

You're redirecting to "topic_vote" with just a pk parameter, but this doesn't match the URL patterns seen in the error handling below, which use "forum_topic" with both category_slug and topic_id. This may cause redirect failures.

Consider fixing the redirect target:

- return redirect("topic_vote", pk=topic.id)
+ return redirect("forum_topic", category_slug=topic.category.slug, topic_id=topic.id)

7173-7207: 🧹 Nitpick (assertive)

Apply the same refinements as topic_vote to reply_vote.

The reply_vote function needs the same improvements as the topic_vote function.

-def reply_vote(request, pk):
+from django.http import HttpRequest, HttpResponse
+from typing import Union
+
+def reply_vote(request: HttpRequest, pk: int) -> Union[HttpResponse, JsonResponse]:
     """Handle voting on a reply."""
     if request.method != "POST":
         return JsonResponse({"error": "Only POST method allowed"}, status=405)

     try:
         reply = ForumReply.objects.get(pk=pk)
         vote_type = request.POST.get("vote_type")

         if vote_type not in ["up", "down"]:
             messages.error(request, "Invalid vote type")
             return redirect("forum_topic", category_slug=reply.topic.category.slug, topic_id=reply.topic.id)

         # Check if user already voted on this reply
         vote, created = ForumVote.objects.get_or_create(
-            user=request.user, reply=reply, defaults={"vote_type": vote_type}
+            user=request.user,
+            reply=reply, 
+            defaults={"vote_type": vote_type},
         )

Also consider preventing authors from voting on their own replies, similar to the suggestion for topics.

🧰 Tools
🪛 Ruff (0.8.2)

7173-7173: Missing return type annotation for public function reply_vote

(ANN201)


7173-7173: Missing type annotation for function argument request

(ANN001)


7173-7173: Missing type annotation for function argument pk

(ANN001)


7188-7188: Trailing comma missing

Add trailing comma

(COM812)


7208-7208: ⚠️ Potential issue

Duplicate exception clause may hide errors.

This is a duplicate except block for ForumReply.DoesNotExist. The second occurrence (at line 7208) is never reached because the first one (at line 7205) always catches the exception first.

Remove the second exception block or merge them:

    except ForumReply.DoesNotExist:
        messages.error(request, "Reply not found")
        return redirect("forum_categories")

-    except ForumReply.DoesNotExist:
-        return JsonResponse({"error": "Reply not found"}, status=404)

2893-2900: 🧹 Nitpick (assertive)

Replace or remove debugging print statement.

Line 2900 uses a plain print(form.errors) for debugging. Consider removing this line or using a logger call; relying on print in production code can clutter logs or cause confusion.

     if form.is_valid():
         category = form.save(commit=False)
         if not category.slug:
             category.slug = slugify(category.name)
         category.save()
         messages.success(request, f"Forum category '{category.name}' created successfully!")
         return redirect("forum_category", slug=category.slug)
-    else:
-        print(form.errors)
+    # Error details are automatically passed to the template
🧰 Tools
🪛 Ruff (0.8.2)

2899-2899: Unnecessary else after return statement

Remove unnecessary else

(RET505)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b143d9c and f04ea83.

📒 Files selected for processing (7)
  • web/migrations/0056_forumvote.py (1 hunks)
  • web/models.py (3 hunks)
  • web/templates/base.html (0 hunks)
  • web/templates/web/forum/category.html (1 hunks)
  • web/templates/web/forum/topic.html (7 hunks)
  • web/urls.py (1 hunks)
  • web/views.py (4 hunks)
💤 Files with no reviewable changes (1)
  • web/templates/base.html
🧰 Additional context used
🧬 Code Graph Analysis (2)
web/urls.py (1)
web/views.py (2)
  • topic_vote (7134-7169)
  • reply_vote (7173-7206)
web/views.py (1)
web/models.py (6)
  • ForumVote (2867-2899)
  • ForumReply (832-870)
  • delete (467-473)
  • user_vote (815-823)
  • user_vote (855-863)
  • ForumTopic (789-829)
🪛 Ruff (0.8.2)
web/migrations/0056_forumvote.py

10-13: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


15-66: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


61-61: Trailing comma missing

Add trailing comma

(COM812)

web/models.py

803-803: Missing return type annotation for public function upvote_count

(ANN201)


807-807: Missing return type annotation for public function downvote_count

(ANN201)


811-811: Missing return type annotation for public function vote_score

(ANN201)


815-815: Missing return type annotation for public function user_vote

(ANN201)


815-815: Missing type annotation for function argument user

(ANN001)


821-821: Consider moving this statement to an else block

(TRY300)


843-843: Missing return type annotation for public function upvote_count

(ANN201)


847-847: Missing return type annotation for public function downvote_count

(ANN201)


851-851: Missing return type annotation for public function vote_score

(ANN201)


855-855: Missing return type annotation for public function user_vote

(ANN201)


855-855: Missing type annotation for function argument user

(ANN001)


861-861: Consider moving this statement to an else block

(TRY300)


2870-2873: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


2883-2883: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


2884-2892: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


2891-2891: Trailing comma missing

Add trailing comma

(COM812)


2894-2894: Missing return type annotation for special method __str__

Add return type annotation: str

(ANN204)


2897-2897: Unnecessary elif after return statement

Remove unnecessary elif

(RET505)

web/views.py

2899-2899: Unnecessary else after return statement

Remove unnecessary else

(RET505)


7134-7134: Missing return type annotation for public function topic_vote

(ANN201)


7134-7134: Missing type annotation for function argument request

(ANN001)


7134-7134: Missing type annotation for function argument pk

(ANN001)


7150-7150: Trailing comma missing

Add trailing comma

(COM812)


7173-7173: Missing return type annotation for public function reply_vote

(ANN201)


7173-7173: Missing type annotation for function argument request

(ANN001)


7173-7173: Missing type annotation for function argument pk

(ANN001)


7188-7188: Trailing comma missing

Add trailing comma

(COM812)


7209-7209: Missing return type annotation for public function topic_detail

(ANN201)


7209-7209: Missing type annotation for function argument request

(ANN001)


7209-7209: Missing type annotation for function argument pk

(ANN001)


7225-7225: Trailing comma missing

Add trailing comma

(COM812)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build-and-test
🔇 Additional comments (9)
web/templates/web/forum/category.html (1)

138-148: Highlight the voting metrics for clarity.
By displaying both the upvote and downvote counts, you're providing immediate visibility into topic engagement. The implementation is straightforward and aids quick scanning. Nice addition!

web/urls.py (1)

218-219: Confirm URL patterns remain consistent.
Adding topic_vote and reply_vote routes is straightforward. Make sure these new endpoints conform to existing forum URL naming conventions (e.g., forum_topic vs. topic_vote). If consistent, this looks good.

web/templates/web/forum/topic.html (4)

6-8: Imports for additional template filters.
Loading dict_filters here is a nice way to Condition-check user votes ({% if user_reply_votes|get_item:reply.id ... %}). This keeps your templates more readable.


134-176: Topic voting layout provides clarity.
The side-by-side arrangement of upvote, score, and downvote is intuitive. The usage of aria-pressed for accessibility is commendable. Great job ensuring screen readers can identify the state of each button!


177-210: Stats block complements the voting UI.
Displaying upvote and downvote counts directly under the topic fosters transparency. Keeping the textual “upvotes” & “downvotes” clarifies the numbers for all audiences.


303-325: Clear separation of reply vote statistics.
Well done separating direct numeric counts (upvote/downvote) from the reply’s main content. This improves scannability and keeps the interaction area distinct.

web/models.py (1)

2867-2883: The ForumVote model looks well-structured.

The model correctly implements the voting functionality with appropriate fields and constraints. The unique_together constraint ensures a user can only have one vote per topic or reply, and the check constraint ensures a vote is associated with either a topic or a reply (but not both).

For completeness, you could add type annotations to the class attributes:

-    VOTE_TYPES = [
+    VOTE_TYPES: typing.ClassVar[list[tuple[str, str]]] = [
        ("up", "Upvote"),
        ("down", "Downvote"),
    ]
🧰 Tools
🪛 Ruff (0.8.2)

2870-2873: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


2883-2883: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

web/views.py (2)

131-131: Appropriate model import added.

The ForumVote model is correctly imported from the models module to support the new voting functionality.


1843-1887: Good enhancement to the forum topic view with user voting data.

The forum topic view now properly includes voting information by fetching the user's votes on the topic and its replies. This contextual data enables the template to display voting state appropriately.

Comment on lines 220 to +257
{% for reply in replies %}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4 lg:p-5">
<div class="flex">
<!-- Voting for Replies -->
<div class="flex flex-col items-center mr-3 bg-gray-100 dark:bg-gray-700 rounded-md p-1 pb-0">
<!-- Upvote button -->
<button class="vote-btn p-1 hover:text-teal-500 transition-colors duration-200 focus:outline-none"
data-reply="{{ reply.id }}"
data-vote-type="up"
aria-label="Upvote this reply"
aria-pressed="{% if user_reply_votes|get_item:reply.id == 'up' %}true{% else %}false{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5 {% if user_reply_votes|get_item:reply.id == 'up' %}text-teal-500{% endif %}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
</svg>
</button>
<!-- Vote score -->
<span class="vote-score text-xs font-bold my-1 {% if reply.vote_score > 0 %}text-teal-500{% elif reply.vote_score < 0 %}text-red-500{% else %}text-gray-500 dark:text-gray-400{% endif %}">
{{ reply.vote_score }}
</span>
<!-- Downvote button -->
<button class="vote-btn p-1 hover:text-red-500 transition-colors duration-200 focus:outline-none"
data-reply="{{ reply.id }}"
data-vote-type="down"
aria-label="Downvote this reply"
aria-pressed="{% if user_reply_votes|get_item:reply.id == 'down' %}true{% else %}false{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5 {% if user_reply_votes|get_item:reply.id == 'down' %}text-red-500{% endif %}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Reply voting duplicates the topic approach effectively.
Reusing the same pattern for replies is consistent. It may be worthwhile to consider a shared partial template if the voting UI continues to grow or if you plan to add more engagement features.

Comment on lines +375 to +465
<script>
document.addEventListener("DOMContentLoaded", function() {
// CSRF token extractor
function getCsrfToken() {
const csrfInput = document.querySelector("[name=csrfmiddlewaretoken]");
return csrfInput ? csrfInput.value : '';
}

const loginUrl = "{% url 'account_signup' %}";

// Event handler for vote buttons
function handleVote(e) {
e.preventDefault();

// Redirect to login if user is not authenticated
if (!document.body.classList.contains("user-authenticated")) {
window.location.href = loginUrl;
return;
}

const button = e.currentTarget;
const voteType = button.dataset.voteType;
const csrftoken = getCsrfToken();
const isTopicVote = button.hasAttribute('data-topic');

let url;
if (isTopicVote) {
const topicId = button.dataset.topic;
url = "{% url 'topic_vote' topic.id %}";
} else {
const replyId = button.dataset.reply;
url = "{% url 'reply_vote' 0 %}".replace('0', replyId);
}

fetch(url, {
method: "POST",
headers: {
"X-CSRFToken": csrftoken,
"Content-Type": "application/x-www-form-urlencoded"
},
body: `vote_type=${voteType}`
})
.then(response => {
if (response.ok) return response.json();
throw new Error("Vote failed");
})
.then(data => {
updateVoteUI(button, data);
})
.catch(error => {
console.error(error);
location.reload(); // Fallback
});
}

// Update the UI after voting
function updateVoteUI(button, data) {
const container = button.closest('.flex-col'); // Find vote container
const scoreEl = container.querySelector('.vote-score');
const upBtn = container.querySelector('button[data-vote-type="up"]');
const downBtn = container.querySelector('button[data-vote-type="down"]');

// Update score and color
scoreEl.textContent = data.vote_score;
scoreEl.className = "vote-score text-sm font-bold my-1";
if (data.vote_score > 0) {
scoreEl.classList.add("text-teal-500");
} else if (data.vote_score < 0) {
scoreEl.classList.add("text-red-500");
} else {
scoreEl.classList.add("text-gray-500", "dark:text-gray-400");
}

// Reset icon colors
upBtn.querySelector("svg").classList.remove("text-teal-500");
downBtn.querySelector("svg").classList.remove("text-red-500");

// Toggle button highlight
if (data.user_vote === "up") {
upBtn.querySelector("svg").classList.add("text-teal-500");
} else if (data.user_vote === "down") {
downBtn.querySelector("svg").classList.add("text-red-500");
}
}

// Attach to all vote buttons
document.querySelectorAll(".vote-btn").forEach(btn => {
btn.addEventListener("click", handleVote);
});
});
</script>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

AJAX vote logic improves user experience.

  1. Using fetch to post vote_type and updating the UI without reload is excellent.
  2. Consider a more user-friendly error handling strategy than location.reload() for smoother UX.
  3. Overall, the handleVote and updateVoteUI functions are well-structured and easy to maintain.

Comment thread web/views.py
Comment on lines +7133 to +7207
@login_required
def topic_vote(request, pk):
"""Handle voting on a topic."""
if request.method != "POST":
return JsonResponse({"error": "Only POST method allowed"}, status=405)

try:
topic = ForumTopic.objects.get(pk=pk)
vote_type = request.POST.get("vote_type")

if vote_type not in ["up", "down"]:
# For form submissions, redirect back with an error message if needed
messages.error(request, "Invalid vote type")
return redirect("topic_vote", pk=topic.id)

# Check if user already voted on this topic
vote, created = ForumVote.objects.get_or_create(
user=request.user, topic=topic, defaults={"vote_type": vote_type}
)

if not created:
# User already voted, check if they're changing their vote
if vote.vote_type == vote_type:
# Same vote type, so remove the vote
vote.delete()
else:
# Different vote type, so update the vote
vote.vote_type = vote_type
vote.save()

# After processing the vote, redirect back to the topic page
return redirect("forum_topic", category_slug=topic.category.slug, topic_id=topic.id)

except ForumTopic.DoesNotExist:
# Handle case when topic doesn't exist
messages.error(request, "Topic not found")
return redirect("forum_categories")


@login_required
def reply_vote(request, pk):
"""Handle voting on a reply."""
if request.method != "POST":
return JsonResponse({"error": "Only POST method allowed"}, status=405)

try:
reply = ForumReply.objects.get(pk=pk)
vote_type = request.POST.get("vote_type")

if vote_type not in ["up", "down"]:
messages.error(request, "Invalid vote type")
return redirect("forum_topic", category_slug=reply.topic.category.slug, topic_id=reply.topic.id)

# Check if user already voted on this reply
vote, created = ForumVote.objects.get_or_create(
user=request.user, reply=reply, defaults={"vote_type": vote_type}
)

if not created:
# User already voted, check if they're changing their vote
if vote.vote_type == vote_type:
# Same vote type, so remove the vote
vote.delete()
else:
# Different vote type, so update the vote
vote.vote_type = vote_type
vote.save()

# After processing the vote, redirect back to the topic page
return redirect("forum_topic", category_slug=reply.topic.category.slug, topic_id=reply.topic.id)

except ForumReply.DoesNotExist:
messages.error(request, "Reply not found")
return redirect("forum_categories")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider refactoring the vote handling functions to reduce duplication.

The topic_vote and reply_vote functions contain nearly identical logic. Consider refactoring them into a single, more generic function to handle both cases.

You could create a helper function that takes the vote target (topic or reply) and performs the common vote handling logic:

def handle_vote(request, vote_target, target_type):
    """
    Handle voting on a forum entity (topic or reply).
    
    Args:
        request: The HTTP request
        vote_target: The target object (topic or reply)
        target_type: String 'topic' or 'reply'
    
    Returns:
        HttpResponse: Redirect to the appropriate page
    """
    vote_type = request.POST.get("vote_type")
    
    if vote_type not in ["up", "down"]:
        messages.error(request, "Invalid vote type")
        return get_redirect_response(vote_target)
    
    # Prevent self-voting
    if vote_target.author == request.user:
        messages.error(request, f"You cannot vote on your own {target_type}")
        return get_redirect_response(vote_target)
    
    # Create vote kwargs based on target type
    vote_kwargs = {
        "user": request.user,
        target_type: vote_target,
        "defaults": {"vote_type": vote_type}
    }
    
    # Process vote
    vote, created = ForumVote.objects.get_or_create(**vote_kwargs)
    
    if not created:
        if vote.vote_type == vote_type:
            vote.delete()
        else:
            vote.vote_type = vote_type
            vote.save()
    
    return get_redirect_response(vote_target)

def get_redirect_response(vote_target):
    """Generate the appropriate redirect response based on the vote target."""
    if hasattr(vote_target, 'topic'):  # It's a reply
        return redirect("forum_topic", 
                      category_slug=vote_target.topic.category.slug, 
                      topic_id=vote_target.topic.id)
    else:  # It's a topic
        return redirect("forum_topic", 
                      category_slug=vote_target.category.slug, 
                      topic_id=vote_target.id)

Then your view functions would be much simpler:

@login_required
def topic_vote(request: HttpRequest, pk: int) -> HttpResponse:
    """Handle voting on a topic."""
    if request.method != "POST":
        return JsonResponse({"error": "Only POST method allowed"}, status=405)
    
    try:
        topic = ForumTopic.objects.get(pk=pk)
        return handle_vote(request, topic, "topic")
    except ForumTopic.DoesNotExist:
        messages.error(request, "Topic not found")
        return redirect("forum_categories")

@login_required
def reply_vote(request: HttpRequest, pk: int) -> HttpResponse:
    """Handle voting on a reply."""
    if request.method != "POST":
        return JsonResponse({"error": "Only POST method allowed"}, status=405)
    
    try:
        reply = ForumReply.objects.get(pk=pk)
        return handle_vote(request, reply, "reply")
    except ForumReply.DoesNotExist:
        messages.error(request, "Reply not found")
        return redirect("forum_categories")
🧰 Tools
🪛 Ruff (0.8.2)

7134-7134: Missing return type annotation for public function topic_vote

(ANN201)


7134-7134: Missing type annotation for function argument request

(ANN001)


7134-7134: Missing type annotation for function argument pk

(ANN001)


7150-7150: Trailing comma missing

Add trailing comma

(COM812)


7173-7173: Missing return type annotation for public function reply_vote

(ANN201)


7173-7173: Missing type annotation for function argument request

(ANN001)


7173-7173: Missing type annotation for function argument pk

(ANN001)


7188-7188: Trailing comma missing

Add trailing comma

(COM812)

Comment thread web/views.py
Comment on lines +7222 to +7228
user_reply_votes = {}
if request.user.is_authenticated:
reply_votes = ForumVote.objects.filter(reply__topic=topic, user=request.user).values_list(
"reply_id", "vote_type"
)
user_reply_votes = dict(reply_votes)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Optimize database query with prefetch_related.

The current implementation makes a separate database query for vote information. Consider using prefetch_related to optimize this and reduce the number of database queries.

     # Get user votes on replies
     user_reply_votes = {}
     if request.user.is_authenticated:
-        reply_votes = ForumVote.objects.filter(reply__topic=topic, user=request.user).values_list(
-            "reply_id", "vote_type"
-        )
-        user_reply_votes = dict(reply_votes)
+        # More efficient query using prefetch_related
+        votes = ForumVote.objects.filter(
+            user=request.user,
+        ).filter(
+            Q(topic=topic) | Q(reply__topic=topic)
+        ).select_related('reply')
+        
+        # Process votes
+        user_topic_vote = next((v.vote_type for v in votes if v.topic_id == topic.id), None)
+        user_reply_votes = {v.reply_id: v.vote_type for v in votes if v.reply_id is not None}

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.8.2)

7225-7225: Trailing comma missing

Add trailing comma

(COM812)

Merged via the queue into alphaonelabs:main with commit fac6cf4 Apr 9, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Apr 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add up / down voting on forum posts show counts in list and detail pages

2 participants