Skip to content

Classroom Crowdfunding Feature #346

Closed
debadityaroy99 wants to merge 7 commits into
alphaonelabs:mainfrom
debadityaroy99:debaditya/crowdfunding
Closed

Classroom Crowdfunding Feature #346
debadityaroy99 wants to merge 7 commits into
alphaonelabs:mainfrom
debadityaroy99:debaditya/crowdfunding

Conversation

@debadityaroy99
Copy link
Copy Markdown
Contributor

@debadityaroy99 debadityaroy99 commented Mar 28, 2025

snap 1
snap 2
snap 3
snap 5
snap 4

Fixes #306
feat(crowdfunding): add Classroom Crowdfunding feature
Teachers Create a Campaign

1)Title, description, images, funding goal

-Itemized budget and purpose

-Optional video pitch

2)Campaign Goes Live

-Featured on Alpha One Labs’ Crowdfund page

Summary by CodeRabbit

  • New Features
    • Launched a new crowdfunding feature set enabling educators to create, view, and manage campaigns.
    • Introduced a dedicated page for campaign creation with an intuitive input form.
    • Added detailed campaign pages that display comprehensive campaign information and include donation options.
    • Updated the teacher dashboard and homepage with easy-to-access buttons linking to crowdfunding functionalities.
    • Added a list page to view all active crowdfunding campaigns.
    • Enhanced forms with user-friendly widgets and validation for campaign creation.
    • Implemented a structured layout for displaying campaign details and donation options.
    • Integrated Stripe payment processing for donations to campaigns.
    • Introduced a new model for managing campaigns and tracking donations.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 28, 2025

Walkthrough

This pull request introduces a classroom crowdfunding feature by adding new models, forms, views, and templates. It includes the Campaign and CampaignDonation models for managing crowdfunding campaigns and donations, respectively. A new form, CampaignForm, facilitates campaign creation. The implementation includes new views for listing, creating, and detailing campaigns, alongside corresponding URL patterns and HTML templates. Additionally, updates to the teacher dashboard and the main index provide links to access these new functionalities. The changes enhance the application without altering existing features.

Changes

File(s) Change Summary
web/forms.py Added CampaignForm, a subclass of forms.ModelForm for handling campaign input with customized widgets.
web/models.py Added new classes Campaign and CampaignDonation with fields and methods to represent crowdfunding campaigns and their associated donations.
web/templates/crowdfunding_create.html, web/templates/crowdfunding_detail.html, web/templates/crowdfunding_list.html Introduced new HTML templates for creating, detailing, and listing crowdfunding campaigns.
web/templates/dashboard/teacher.html, web/templates/index.html Added new buttons/sections linking to crowdfunding creation and listing pages.
web/urls.py Added four new URL patterns mapping to crowdfunding_list, crowdfunding_create, crowdfunding_detail, and create_donation_intent views.
web/views.py Added functions: create_donation_intent, handle_successful_donation, handle_failed_donation, crowdfunding_detail, crowdfunding_list, and crowdfunding_create to process crowdfunding requests and render corresponding templates.

Assessment against linked issues

Objective Addressed Explanation
Teachers create a campaign [#306]
Display and list crowdfunding campaigns [#306]
Donation tracking and integration [#306] Donation model and payment integration are implemented.

Possibly related PRs

Suggested labels

CR:!, d:23

✨ 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: 5

🧹 Nitpick comments (7)
web/templates/crowdfunding_create.html (1)

1-31: Template Structure & djLint Compliance
The template is well structured, clearly extending the base and defining distinct blocks for title and content. It correctly includes a CSRF token and iterates over the form fields to render them with associated error messages.

However, note that the pipeline indicates a djLint linting failure. Please run djLint’s auto-fix (or review its suggestions) to ensure the template formatting conforms to the project’s style guidelines.

🧰 Tools
🪛 GitHub Actions: .github/workflows/test.yml

[error] 1-1: djLint linting for Django failed. Files were modified by this hook.

web/templates/crowdfunding_detail.html (1)

1-53: Campaign Detail Display & Donation Form
This new template effectively presents detailed information for a crowdfunding campaign. It conditionally displays the campaign image and video pitch (if available) and shows key details such as the funding goal and amount raised. The donation form is set up correctly with CSRF protection and an appropriate input for the donation amount.

A couple of points for consideration:

  • The use of the json_script filter within a <pre> tag for displaying the itemized budget is unconventional. Verify that this is the intended output format; typically, json_script is used to safely pass JSON data to JavaScript rather than for direct display.
  • Ensure that any external video URLs used in the iframe are sanitized and validated on the backend to prevent potential security issues.

Overall, the template is concise and well-organized.

🧰 Tools
🪛 GitHub Actions: .github/workflows/test.yml

[error] 1-1: djLint linting for Django failed. Files were modified by this hook.

web/forms.py (2)

59-97: Add CampaignForm to the all list.

The newly added CampaignForm class should be included in the all list to make it importable from this module.

 __all__ = [
     "UserRegistrationForm",
     "ProfileForm",
     "ChallengeSubmissionForm",
     "CourseCreationForm",
     "CourseForm",
     "PeerChallengeForm",
     "PeerChallengeInvitationForm",
     "SessionForm",
     "ReviewForm",
     "CourseMaterialForm",
     "TeacherSignupForm",
     "ProfileUpdateForm",
     "CustomLoginForm",
     "LearnForm",
     "TeachForm",
     "InviteStudentForm",
     "ForumCategoryForm",
     "ForumTopicForm",
     "BlogPostForm",
     "MessageTeacherForm",
     "FeedbackForm",
     "GoodsForm",
     "StorefrontForm",
     "EducationalVideoForm",
     "ProgressTrackerForm",
     "SuccessStoryForm",
     "TeamGoalForm",
     "TeamInviteForm",
     "MemeForm",
     "QuizForm",
     "QuizQuestionForm",
     "QuizOptionFormSet",
     "TakeQuizForm",
     "AvatarForm",
     "GradeableLinkForm",
     "LinkGradeForm",
     "AwardAchievementForm",
+    "CampaignForm",
 ]

289-293: Add URL validation for video_url field.

The video_url field should be validated to ensure it's a proper URL format and potentially check if it's from supported video platforms (YouTube, Vimeo, etc.).

def clean_video_url(self):
    video_url = self.cleaned_data.get("video_url")
    if video_url:
        # Similar to the validation in EducationalVideoForm
        youtube_pattern = r"^(https?://)?(www\.)?(youtube\.com/watch\?v=|youtu\.be/)[a-zA-Z0-9_-]{11}.*$"
        vimeo_pattern = r"^(https?://)?(www\.)?vimeo\.com/[0-9]{8,}.*$"
        if not (re.match(youtube_pattern, video_url) or re.match(vimeo_pattern, video_url)):
            raise forms.ValidationError("Please enter a valid YouTube or Vimeo URL")
    return video_url
web/models.py (2)

28-73: Consider validation and concurrency improvements for crowdfunding.

The Campaign model lacks validation to prevent invalid or negative funding_goal and might experience race conditions when multiple donations occur concurrently. Adding a minimum value validator to funding_goal helps ensure non-negative campaign targets. Also, wrapping update_amount_raised logic in a database transaction or using an F-expression can mitigate race conditions.

Example diff to require a positive funding goal (applied to line 38):

-from django.core.validators import MaxValueValidator, MinValueValidator
+from django.core.validators import MaxValueValidator, MinValueValidator

 funding_goal = models.DecimalField(
-    max_digits=10, decimal_places=2, help_text="Target funding amount in dollars"
+    max_digits=10,
+    decimal_places=2,
+    validators=[MinValueValidator(1.00)],
+    help_text="Target funding amount in dollars"
 )

74-88: Enforce a minimum donation amount.

Consider preventing zero or negative donations by using a MinValueValidator on the amount field.

Example diff (applied to line 84):

-from django.core.validators import MaxValueValidator, MinValueValidator
+from django.core.validators import MaxValueValidator, MinValueValidator

 amount = models.DecimalField(
     max_digits=10,
     decimal_places=2,
+    validators=[MinValueValidator(0.01)],
     help_text="Donation amount in dollars"
 )
web/urls.py (1)

41-43: Strengthen access control on the create route.

Although the view checks request.user.profile.is_teacher, applying a decorator (e.g., login_required) or a custom teacher-only decorator at the URL level can promptly prevent unauthorized/unauthenticated access before the view logic runs.

Example approach:

from django.contrib.auth.decorators import login_required, user_passes_test

def teacher_required(user):
    return user.is_authenticated and user.profile.is_teacher

urlpatterns += i18n_patterns(
    # ...
    path("crowdfunding/create/",
         login_required(user_passes_test(teacher_required)(views.crowdfunding_create)),
         name="crowdfunding_create"),
    # ...
)
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between c7ad28c and eb88d92.

📒 Files selected for processing (9)
  • web/forms.py (2 hunks)
  • web/models.py (1 hunks)
  • web/templates/crowdfunding_create.html (1 hunks)
  • web/templates/crowdfunding_detail.html (1 hunks)
  • web/templates/crowdfunding_list.html (1 hunks)
  • web/templates/dashboard/teacher.html (1 hunks)
  • web/templates/index.html (1 hunks)
  • web/urls.py (1 hunks)
  • web/views.py (3 hunks)
🧰 Additional context used
🧬 Code Definitions (2)
web/models.py (1)
web/forms.py (16)
  • Meta (260-299)
  • Meta (351-357)
  • Meta (361-390)
  • Meta (436-474)
  • Meta (485-522)
  • Meta (550-556)
  • Meta (560-584)
  • Meta (678-680)
  • Meta (783-803)
  • Meta (826-838)
  • Meta (844-860)
  • Meta (944-981)
  • Meta (987-998)
  • Meta (1015-1020)
  • Meta (1086-1117)
  • Meta (1123-1144)
web/urls.py (1)
web/views.py (3)
  • crowdfunding_list (1232-1238)
  • crowdfunding_create (1241-1262)
  • crowdfunding_detail (1223-1229)
🪛 GitHub Actions: .github/workflows/test.yml
web/templates/crowdfunding_create.html

[error] 1-1: djLint linting for Django failed. Files were modified by this hook.

web/templates/crowdfunding_list.html

[error] 1-1: djLint linting for Django failed. Files were modified by this hook.

web/templates/crowdfunding_detail.html

[error] 1-1: djLint linting for Django failed. Files were modified by this hook.

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

240-246: New Crowdfunding Button Integration
The newly added section (lines 240–246) adds a clear, well-styled "Create Crowdfunding Campaigns" button. It is placed appropriately within the “else” block when no storefront exists, maintaining UI consistency.

Ensure that the URL tag {% url 'crowdfunding_create' %} correctly matches the URL configuration in your web/urls.py.

web/templates/crowdfunding_list.html (1)

1-30: Active Campaigns Listing Template
This new template cleanly displays active crowdfunding campaigns. The use of conditional logic to check for available campaigns and the grid layout for iterating over campaign objects is effective. The alt attributes for images (using the campaign title) enhance accessibility.

Overall, the markup is clear and consistent with typical Django template standards.

🧰 Tools
🪛 GitHub Actions: .github/workflows/test.yml

[error] 1-1: djLint linting for Django failed. Files were modified by this hook.

web/templates/index.html (1)

677-684: Call-to-Action Button for Crowdfunding Campaigns
The addition of the "View Crowdfunding Campaigns" button (lines 677–684) integrates smoothly into the index page. The styling mirrors that of other call-to-action buttons, and it uses the Django URL tag to link to the campaigns list.

Please verify that the URL name 'crowdfunding_list' is correctly configured in your URLs module.

web/forms.py (1)

17-17: Correct import of new Campaign model.

The Campaign model is correctly imported from the models module for use in the new CampaignForm.

web/views.py (3)

63-63: LGTM: Added import for CampaignForm

This import is correctly placed within the forms import group and is needed for the new crowdfunding functionality.


104-104: LGTM: Added import for Campaign model

This import is correctly placed within the models import group and is required for the new crowdfunding features.


1232-1239: LGTM: Crowdfunding list implementation

The list function correctly filters for approved and live campaigns, ordered by most recent first. This implementation is appropriate and follows the pattern used in other list views throughout the codebase.

Comment thread web/forms.py Outdated
Comment on lines +283 to +286
attrs={
"placeholder": "Optional JSON field for itemized budget breakdown",
"class": "appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none",
}
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.

🛠️ Refactor suggestion

Consider adding JSON validation for itemized_budget field.

Since itemized_budget is described as a JSON field, it would be helpful to add validation to ensure it contains valid JSON when provided. This can prevent errors when processing the data later.

def clean_itemized_budget(self):
    itemized_budget = self.cleaned_data.get("itemized_budget")
    if itemized_budget:
        try:
            import json
            json.loads(itemized_budget)
        except json.JSONDecodeError:
            raise forms.ValidationError("Please enter valid JSON for the itemized budget")
    return itemized_budget

Comment thread web/forms.py
Comment thread web/models.py Outdated
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: 1

🧹 Nitpick comments (5)
web/models.py (3)

38-41: Consider adding validation for monetary fields.

While the model structure is sound, monetary fields should have validation to ensure positive values.

-    funding_goal = models.DecimalField(max_digits=10, decimal_places=2, help_text="Target funding amount in dollars")
-    amount_raised = models.DecimalField(
-        max_digits=10, decimal_places=2, default=0, help_text="Total funds raised so far"
-    )
+    funding_goal = models.DecimalField(
+        max_digits=10, 
+        decimal_places=2, 
+        validators=[MinValueValidator(0.01)],
+        help_text="Target funding amount in dollars"
+    )
+    amount_raised = models.DecimalField(
+        max_digits=10, 
+        decimal_places=2, 
+        default=0, 
+        validators=[MinValueValidator(0)],
+        help_text="Total funds raised so far"
+    )

83-84: Consider adding notifications when donations are received.

Teachers would likely want to be notified when they receive donations for their campaigns.

You could implement this by modifying the CampaignDonation model's save method:

def save(self, *args, **kwargs):
    is_new = self.pk is None
    super().save(*args, **kwargs)
    
    if is_new:
        # Update campaign amount
        self.campaign.update_amount_raised()
        
        # Create notification for the teacher
        Notification.objects.create(
            user=self.campaign.teacher,
            title="New Donation Received",
            message=f"{self.donor_name} donated ${self.amount} to your campaign: {self.campaign.title}",
            notification_type="success"
        )

28-89: Ensure the models are registered with Django admin.

The Campaign and CampaignDonation models should be registered with the Django admin site for easy management.

Would you like me to generate the admin registration code for these models?

web/views.py (2)

1223-1233: Consider returning a 404 or 403 instead of redirecting.

When the campaign is not publicly accessible, you might prefer returning a 403 Forbidden or a 404 Not Found for better REST semantics rather than redirecting users to the list page. This approach clarifies to the user (or API client) that the campaign is unavailable rather than simply sending them elsewhere.

 if not (campaign.approved and campaign.live) and not (
     request.user.is_authenticated
     and (campaign.teacher == request.user or request.user.is_staff)
 ):
-    messages.error(request, "This campaign is not available.")
-    return redirect("crowdfunding_list")
+    return HttpResponseForbidden("You do not have permission to access this campaign.")

1235-1242: Add pagination support for listing campaigns.

If the list of active campaigns grows large, you may wish to implement pagination or infinite scroll to improve performance and user experience.

 def crowdfunding_list(request):
     campaigns = Campaign.objects.filter(approved=True, live=True).order_by("-created_at")
+    # Example pagination snippet
+    from django.core.paginator import Paginator
+    paginator = Paginator(campaigns, 10)
+    page_number = request.GET.get("page")
+    page_obj = paginator.get_page(page_number)
+    return render(request, "crowdfunding_list.html", {"campaigns": page_obj, "is_paginated": True})
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between eb88d92 and 3a747a4.

📒 Files selected for processing (3)
  • web/forms.py (2 hunks)
  • web/models.py (1 hunks)
  • web/views.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/forms.py
🧰 Additional context used
🧬 Code Definitions (1)
web/models.py (1)
web/forms.py (21)
  • Meta (260-295)
  • Meta (357-363)
  • Meta (367-396)
  • Meta (442-480)
  • Meta (491-528)
  • Meta (556-562)
  • Meta (566-590)
  • Meta (684-686)
  • Meta (789-809)
  • Meta (832-844)
  • Meta (850-866)
  • Meta (950-987)
  • Meta (993-1004)
  • Meta (1021-1026)
  • Meta (1092-1123)
  • Meta (1129-1150)
  • save (218-256)
  • save (614-639)
  • save (706-720)
  • save (1343-1356)
  • save (1517-1532)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build-and-test
  • GitHub Check: Run Tests
🔇 Additional comments (7)
web/models.py (3)

28-72: Well-structured Campaign model with comprehensive attributes.

The Campaign model effectively captures all essential aspects of a crowdfunding campaign: basic information (title, description), financial details (funding_goal, amount_raised), supporting media (video_url, image), and administrative controls (approved, live).

I especially like the update_amount_raised method for recalculating the total raised amount from related donations.


47-48: Consider default approval flow for campaigns.

Both approved and live default to True, which means campaigns go live immediately without review. Depending on your moderation requirements, you might want to set approved=False as the default and implement an approval workflow.

Is this the intended behavior? If moderation is required before campaigns go public, consider changing the defaults.


74-89: Well-designed CampaignDonation model for tracking contributions.

The CampaignDonation model captures all essential donation information and establishes the correct relationship with the Campaign model. The string representation method provides a clear description of each donation record.

web/views.py (4)

63-63: Importing CampaignForm looks correct.

No issues spotted with this addition.


104-104: Importing Campaign is valid.

This aligns with the crowdfunding feature and is consistent with the usage in subsequent functions.


2170-2171: Confirm the intent of commenting out these decorators.

By commenting out @login_required and @teacher_required, the teacher_dashboard view can be accessed by any user, potentially exposing teacher-only functionality to unauthorized users. Ensure this is intentional or revert if access should remain restricted.

Do you want to restore these decorators to maintain teacher-only access?


1243-1265: Ensure the approval workflow is correctly handled.

Here, the code auto-approves and activates the campaign (campaign.approved = True and campaign.live = True), bypassing a potential moderation or admin-approval workflow. Confirm that this is desired, as it could allow unreviewed campaigns to become public.

Would you like to introduce an extra check or a review step before making campaigns live?

Comment thread web/models.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: 0

🧹 Nitpick comments (6)
web/models.py (6)

28-71: Campaign model is well-structured but lacks validation.

The Campaign model is well-designed with appropriate fields for a crowdfunding campaign. However, it doesn't include validation for financial fields.

Consider adding validation to ensure funding_goal is positive:

from django.core.validators import MinValueValidator

class Campaign(models.Model):
    # ...
-    funding_goal = models.DecimalField(max_digits=10, decimal_places=2, help_text="Target funding amount in dollars")
+    funding_goal = models.DecimalField(
+        max_digits=10, 
+        decimal_places=2, 
+        validators=[MinValueValidator(0.01)],
+        help_text="Target funding amount in dollars"
+    )

28-71: Consider adding indexing for better performance.

The model doesn't define any custom indexes which could impact performance as the number of campaigns grows.

Add appropriate indexes for common query patterns:

class Campaign(models.Model):
    # ...
    class Meta:
        ordering = ["-created_at"]
        verbose_name = "Crowdfunding Campaign"
        verbose_name_plural = "Crowdfunding Campaigns"
+        indexes = [
+            models.Index(fields=['teacher']),
+            models.Index(fields=['approved', 'live']),
+            models.Index(fields=['-created_at']),
+        ]

74-88: CampaignDonation model needs validation for donation amount.

The CampaignDonation model is missing validation to ensure the donation amount is positive.

Add validation to ensure amount is positive:

class CampaignDonation(models.Model):
    # ...
-    amount = models.DecimalField(max_digits=10, decimal_places=2, help_text="Donation amount in dollars")
+    amount = models.DecimalField(
+        max_digits=10, 
+        decimal_places=2, 
+        validators=[MinValueValidator(0.01)],
+        help_text="Donation amount in dollars"
+    )

74-88: Consider adding a donor privacy option.

For donor privacy, it might be good to add an option for anonymous donations.

class CampaignDonation(models.Model):
    # ...
    donor_name = models.CharField(max_length=255, help_text="Name of the donor")
    donor_email = models.EmailField(help_text="Email address of the donor")
+    is_anonymous = models.BooleanField(default=False, help_text="Whether the donation should be displayed as anonymous")
    amount = models.DecimalField(max_digits=10, decimal_places=2, help_text="Donation amount in dollars")
    donated_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
+        donor_display = "Anonymous" if self.is_anonymous else self.donor_name
-        return f"{self.donor_name} donated ${self.amount} to {self.campaign.title}"
+        return f"{donor_display} donated ${self.amount} to {self.campaign.title}"

28-71: Consider adding progress tracking functionality.

The model tracks funding_goal and amount_raised but doesn't provide a method to calculate progress percentage.

Add a property method to calculate the funding progress percentage:

class Campaign(models.Model):
    # ...existing code...
    
+    @property
+    def progress_percentage(self):
+        """
+        Returns the percentage of the funding goal that has been raised.
+        """
+        if self.funding_goal == 0:
+            return 0
+        return min(100, int((self.amount_raised / self.funding_goal) * 100))

28-71: Missing campaign end date functionality.

The model doesn't track when a campaign should end, which is a common feature for crowdfunding campaigns.

Add an end date field and a property to check if the campaign is active:

class Campaign(models.Model):
    # ...
    live = models.BooleanField(default=True, help_text="Indicates whether the campaign is live and accepting donations")
    created_at = models.DateTimeField(auto_now_add=True)
+    end_date = models.DateTimeField(null=True, blank=True, help_text="Date when the campaign ends")
    
    # ...

+    @property
+    def is_active(self):
+        """
+        Returns whether the campaign is currently active.
+        """
+        now = timezone.now()
+        return (
+            self.approved and
+            self.live and
+            (not self.end_date or self.end_date > now)
+        )
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3a747a4 and cc661f4.

📒 Files selected for processing (1)
  • web/models.py (3 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
web/models.py (1)
web/forms.py (16)
  • Meta (260-295)
  • Meta (357-363)
  • Meta (367-396)
  • Meta (442-480)
  • Meta (491-528)
  • Meta (556-562)
  • Meta (566-590)
  • Meta (684-686)
  • Meta (789-809)
  • Meta (832-844)
  • Meta (850-866)
  • Meta (950-987)
  • Meta (993-1004)
  • Meta (1021-1026)
  • Meta (1092-1123)
  • Meta (1129-1150)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Run Tests
  • GitHub Check: build-and-test
🔇 Additional comments (3)
web/models.py (3)

65-71: Excellent implementation of the update_amount_raised method.

The method efficiently uses aggregation to calculate the total amount raised from donations.


2790-2798: Good use of Django signals to keep donation totals in sync.

The signal handlers ensure that donation totals are automatically updated whenever donations are created, modified, or deleted.


33-35:

Details

❓ Verification inconclusive

Potential performance concern with related_name choice.

The related_name="campaigns" could conflict with other models if they also use the same related name for their relations to User.

Run the following to check for any other models with the same related name:


🏁 Script executed:

#!/bin/bash
# Check for other models that might have a related_name="campaigns" on a ForeignKey to User
rg "models\.ForeignKey.*User.*related_name=\"campaigns\"" --type py

Length of output: 67


Attention: Verify related_name Uniqueness

After running a search for occurrences of related_name="campaigns" across the codebase, no instances beyond the one in web/models.py (Lines 33–35) were found. However, because the initial script output was low-quality (i.e. no output despite expecting to find potential conflicts), please manually verify that no other models are inadvertently using the same reverse relation name on the User model. Consistent uniqueness of reverse relation names is essential to avoid potential performance or lookup conflicts as the codebase grows.

  • Confirm that no additional models are using a conflicting related_name="campaigns".
  • Ensure that any future additions linking to User also follow a unique naming convention.

Copy link
Copy Markdown
Contributor

@A1L13N A1L13N left a comment

Choose a reason for hiding this comment

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

please add the stripe payment info so payments can be sent

@A1L13N
Copy link
Copy Markdown
Contributor

A1L13N commented Mar 28, 2025

feedback given and ready for you @debadityaroy99

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: 3

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between cc661f4 and 07f9613.

📒 Files selected for processing (1)
  • web/views.py (4 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
web/views.py (1)
web/forms.py (6)
  • CampaignForm (259-306)
  • save (218-256)
  • save (614-639)
  • save (706-720)
  • save (1343-1356)
  • save (1517-1532)
🪛 Ruff (0.8.2)
web/views.py

1247-1247: Missing return type annotation for public function crowdfunding_detail

(ANN201)


1247-1247: Missing type annotation for function argument request

(ANN001)


1247-1247: Missing type annotation for function argument campaign_id

(ANN001)


1259-1259: Missing return type annotation for public function crowdfunding_list

(ANN201)


1259-1259: Missing type annotation for function argument request

(ANN001)


1269-1269: Missing return type annotation for public function crowdfunding_create

(ANN201)


1269-1269: Missing type annotation for function argument request

(ANN001)


1284-1284: Unnecessary else after return statement

Remove unnecessary else

(RET505)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build-and-test
  • GitHub Check: Run Tests
🔇 Additional comments (4)
web/views.py (4)

1247-1257: Proper access control implementation - looks good!

The detail view correctly implements access control to ensure that only approved and live campaigns are visible to regular users, while campaign creators or admins can see their own unapproved campaigns. The error message and redirect path are appropriate.

🧰 Tools
🪛 Ruff (0.8.2)

1247-1247: Missing return type annotation for public function crowdfunding_detail

(ANN201)


1247-1247: Missing type annotation for function argument request

(ANN001)


1247-1247: Missing type annotation for function argument campaign_id

(ANN001)


1259-1266: Well-structured campaign listing function.

The function effectively retrieves and displays all active campaigns that are both approved and live. The ordering by creation date (newest first) provides a good default sorting.

🧰 Tools
🪛 Ruff (0.8.2)

1259-1259: Missing return type annotation for public function crowdfunding_list

(ANN201)


1259-1259: Missing type annotation for function argument request

(ANN001)


63-63: Correct form import added.

Adding the import for CampaignForm maintains the alphabetical ordering of imports.


104-104: Correct model import added.

Adding the import for Campaign maintains the alphabetical ordering of imports.

Comment thread web/views.py
Comment on lines +2194 to 2196
# @login_required
# @teacher_required
def teacher_dashboard(request):
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.

⚠️ Potential issue

Security issue: Teacher dashboard protection removed.

The @login_required and @teacher_required decorators have been commented out from the teacher_dashboard function. This removes authentication and authorization protection, potentially allowing any user (even unauthenticated ones) to access the teacher dashboard.

Apply this diff to restore the security measures:

-# @login_required
-# @teacher_required
+@login_required
+@teacher_required
def teacher_dashboard(request):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# @login_required
# @teacher_required
def teacher_dashboard(request):
@login_required
@teacher_required
def teacher_dashboard(request):
🧰 Tools
🪛 Ruff (0.8.2)

2196-2196: Missing return type annotation for public function teacher_dashboard

(ANN201)


2196-2196: Missing type annotation for function argument request

(ANN001)

Comment thread web/views.py
Comment thread web/views.py
@debadityaroy99
Copy link
Copy Markdown
Contributor Author

@coderabbitai code the stripe payment for the crowdfunding feature

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 29, 2025

Tip

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

Code Analysis Tools

Tool Analysis Results

CodeRabbit ran static analysis tools on your files and found the following:

Details

🪛 Ruff (0.8.2)
web/forms.py

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

(RUF012)


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

(RUF012)


266-266: Trailing comma missing

Add trailing comma

(COM812)


267-267: Trailing comma missing

Add trailing comma

(COM812)


272-272: Trailing comma missing

Add trailing comma

(COM812)


277-277: Trailing comma missing

Add trailing comma

(COM812)


281-281: Trailing comma missing

Add trailing comma

(COM812)


282-282: Trailing comma missing

Add trailing comma

(COM812)


288-288: Trailing comma missing

Add trailing comma

(COM812)


293-293: Trailing comma missing

Add trailing comma

(COM812)


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

(RUF012)


302-302: Missing return type annotation for private function clean_funding_goal

(ANN202)


305-305: Avoid specifying long messages outside the exception class

(TRY003)

web/models.py

34-34: Trailing comma missing

Add trailing comma

(COM812)


40-40: Trailing comma missing

Add trailing comma

(COM812)


43-43: Trailing comma missing

Add trailing comma

(COM812)


45-45: Avoid using null=True on string-based fields such as URLField

(DJ001)


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

(RUF012)


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

Add return type annotation: str

(ANN204)


59-59: Missing return type annotation for public function get_absolute_url

(ANN201)


65-65: Missing return type annotation for public function update_amount_raised

Add return type annotation: None

(ANN201)


80-80: Trailing comma missing

Add trailing comma

(COM812)


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

Add return type annotation: str

(ANN204)


2791-2791: Missing return type annotation for public function update_campaign_amount_on_donation

Add return type annotation: None

(ANN201)


2791-2791: Missing type annotation for function argument sender

(ANN001)


2791-2791: Unused function argument: sender

(ARG001)


2791-2791: Missing type annotation for function argument instance

(ANN001)


2791-2791: Missing type annotation for function argument created

(ANN001)


2791-2791: Unused function argument: created

(ARG001)


2791-2791: Missing type annotation for **kwargs

(ANN003)


2791-2791: Unused function argument: kwargs

(ARG001)


2796-2796: Missing return type annotation for public function update_campaign_amount_on_donation_delete

Add return type annotation: None

(ANN201)


2796-2796: Missing type annotation for function argument sender

(ANN001)


2796-2796: Unused function argument: sender

(ARG001)


2796-2796: Missing type annotation for function argument instance

(ANN001)


2796-2796: Missing type annotation for **kwargs

(ANN003)


2796-2796: Unused function argument: kwargs

(ARG001)

web/views.py

1247-1247: Missing return type annotation for public function crowdfunding_detail

(ANN201)


1247-1247: Missing type annotation for function argument request

(ANN001)


1247-1247: Missing type annotation for function argument campaign_id

(ANN001)


1259-1259: Missing return type annotation for public function crowdfunding_list

(ANN201)


1259-1259: Missing type annotation for function argument request

(ANN001)


1269-1269: Missing return type annotation for public function crowdfunding_create

(ANN201)


1269-1269: Missing type annotation for function argument request

(ANN001)


1284-1284: Unnecessary else after return statement

Remove unnecessary else

(RET505)

Files with Issues

web/forms.py
🪛 Ruff (0.8.2)

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

(RUF012)


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

(RUF012)


266-266: Trailing comma missing

Add trailing comma

(COM812)


267-267: Trailing comma missing

Add trailing comma

(COM812)


272-272: Trailing comma missing

Add trailing comma

(COM812)


277-277: Trailing comma missing

Add trailing comma

(COM812)


281-281: Trailing comma missing

Add trailing comma

(COM812)


282-282: Trailing comma missing

Add trailing comma

(COM812)


288-288: Trailing comma missing

Add trailing comma

(COM812)


293-293: Trailing comma missing

Add trailing comma

(COM812)


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

(RUF012)


302-302: Missing return type annotation for private function clean_funding_goal

(ANN202)


305-305: Avoid specifying long messages outside the exception class

(TRY003)

web/models.py
🪛 Ruff (0.8.2)

34-34: Trailing comma missing

Add trailing comma

(COM812)


40-40: Trailing comma missing

Add trailing comma

(COM812)


43-43: Trailing comma missing

Add trailing comma

(COM812)


45-45: Avoid using null=True on string-based fields such as URLField

(DJ001)


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

(RUF012)


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

Add return type annotation: str

(ANN204)


59-59: Missing return type annotation for public function get_absolute_url

(ANN201)


65-65: Missing return type annotation for public function update_amount_raised

Add return type annotation: None

(ANN201)


80-80: Trailing comma missing

Add trailing comma

(COM812)


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

Add return type annotation: str

(ANN204)


2791-2791: Missing return type annotation for public function update_campaign_amount_on_donation

Add return type annotation: None

(ANN201)


2791-2791: Missing type annotation for function argument sender

(ANN001)


2791-2791: Unused function argument: sender

(ARG001)


2791-2791: Missing type annotation for function argument instance

(ANN001)


2791-2791: Missing type annotation for function argument created

(ANN001)


2791-2791: Unused function argument: created

(ARG001)


2791-2791: Missing type annotation for **kwargs

(ANN003)


2791-2791: Unused function argument: kwargs

(ARG001)


2796-2796: Missing return type annotation for public function update_campaign_amount_on_donation_delete

Add return type annotation: None

(ANN201)


2796-2796: Missing type annotation for function argument sender

(ANN001)


2796-2796: Unused function argument: sender

(ARG001)


2796-2796: Missing type annotation for function argument instance

(ANN001)


2796-2796: Missing type annotation for **kwargs

(ANN003)


2796-2796: Unused function argument: kwargs

(ARG001)

web/views.py
🪛 Ruff (0.8.2)

1247-1247: Missing return type annotation for public function crowdfunding_detail

(ANN201)


1247-1247: Missing type annotation for function argument request

(ANN001)


1247-1247: Missing type annotation for function argument campaign_id

(ANN001)


1259-1259: Missing return type annotation for public function crowdfunding_list

(ANN201)


1259-1259: Missing type annotation for function argument request

(ANN001)


1269-1269: Missing return type annotation for public function crowdfunding_create

(ANN201)


1269-1269: Missing type annotation for function argument request

(ANN001)


1284-1284: Unnecessary else after return statement

Remove unnecessary else

(RET505)

Next Steps

Based on these results, you can:

  • Fix the issues identified by the tools
  • Ask me to suggest specific fixes for these issues
  • Proceed with planning or implementing changes while accounting for these issues

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 29, 2025

A summary of the edits CodeRabbit can apply:

  • We will add the Stripe package dependency to requirements.txt so that the project installs the correct version.

  • We will update website/settings.py to include Stripe API keys from environment variables.

  • We will update the CampaignDonation model in web/models.py to include new fields for tracking Stripe payment intent ID and payment status.

  • We will update the Stripe webhook handler in web/views.py to update campaign donation statuses based on payment_intent events.

  • We will update the crowdfunding_detail view in web/views.py so that the template context includes the Stripe publishable key.

  • We will update web/urls.py to include new URL endpoints for Stripe payment processing.

  • We will modify the web/templates/crowdfunding_detail.html template to replace the old donation form with a Stripe Elements–enabled payment form.

  • We will update the payment success template to display donation details and a thank-you message.

  • We will update the payment cancellation template to inform the user that the donation was cancelled and provide navigation options.

  • We will add the Stripe package (version 2.76.0) to requirements.txt for payment processing.

  • We will add the Stripe API key settings to website/settings.py.

  • We will update the CampaignDonation model in web/models.py to include payment tracking fields.

  • We will add new URL patterns for Stripe payment endpoints in web/urls.py.

  • We will update the crowdfunding_detail view in web/views.py to pass the stripe_publishable_key into the template context.

  • We will update the stripe_webhook handler in web/views.py to process campaign donation events.

  • We will update the crowdfunding_detail.html template in web/templates to include a Stripe Elements–based donation form.

  • We will install the Stripe package by adding "stripe==2.76.0" to requirements.txt.

  • We will update website/settings.py to include the Stripe API keys.

  • We will update the CampaignDonation model in web/models.py to include Stripe payment tracking fields.

  • We will update web/templates/crowdfunding_detail.html to integrate a Stripe payment form using Stripe Elements.

  • We will update web/urls.py to add new URL patterns for Stripe payment processing endpoints.

  • We will update web/views.py to (1) include the Stripe publishable key in the crowdfunding_detail view’s context and (2) update the stripe_webhook endpoint with helper functions to process campaign donation payment events.

  • We will install the Stripe package (stripe==2.76.0) by adding it to requirements.txt.

  • We will update website/settings.py to include the Stripe API keys.

  • We will update the CampaignDonation model in web/models.py to add Stripe payment fields for tracking donations.

  • We will update web/urls.py by adding new URL endpoints to handle Stripe payment processing, including creating payment intents, handling success/cancellation, and processing Stripe webhooks.

  • We will update the crowdfunding_detail view in web/views.py to pass the Stripe publishable key to the template.

  • We will update the Stripe webhook handler in web/views.py to process campaign donation payment events and add helper functions to update donation status.

  • We will update the crowdfunding_detail template in web/templates/crowdfunding_detail.html to replace the placeholder donation form with a Stripe Elements–based donate form.

  • Will add the Stripe dependency "stripe==2.76.0" into requirements.txt.

  • Will update website/settings.py to include Stripe API key settings.

  • Will update the CampaignDonation model in web/models.py to include Stripe payment tracking fields.

  • Will update the stripe_webhook view in web/views.py to process campaign donation payments and add helper functions for successful and failed donations.

  • Will update the crowdfunding_detail view in web/views.py to include the Stripe publishable key in the template context.

  • Will update web/urls.py to add Stripe-related URL patterns for creating payment intents, handling donation success/cancel, and receiving Stripe webhooks.

  • Will update web/templates/crowdfunding_detail.html to replace the donation form with a Stripe Elements–based form.

  • Will update web/templates/payment_success.html to display donation success information including donation details and navigation options.

  • Will update web/templates/payment_cancel.html to display donation cancellation information and offer the user links to try again or browse other campaigns.

  • We will add the Stripe dependency (stripe==2.76.0) to the requirements.txt file.

  • We will update website/settings.py to include the Stripe API keys as environment-based settings.

  • We will update the CampaignDonation model in web/models.py to include fields for tracking Stripe payment details.

  • We will update the crowdfunding_detail template (web/templates/crowdfunding_detail.html) to integrate Stripe Elements for processing donations.

  • We will update the payment_success template (web/templates/payment_success.html) to display successful donation details.

  • We will update the payment_cancel template (web/templates/payment_cancel.html) to notify users that their donation was cancelled.

  • We will update web/urls.py to add new URL patterns for the Stripe payment processing endpoints.

  • We will update web/views.py to integrate Stripe into the crowdfunding pages and webhook processing.

  • This will install the Stripe package by adding “stripe==2.76.0” to requirements.txt.

  • This will update website/settings.py by appending the necessary Stripe API key settings.

  • This will update the CampaignDonation model in web/models.py to include payment tracking fields (Stripe PaymentIntent ID and payment status).

  • This will update web/templates/crowdfunding_detail.html to replace the plain donation form with a fully integrated Stripe Elements form.

  • This will update web/templates/payment_success.html to provide a success page for Stripe payments.

  • This will update web/templates/payment_cancel.html so that if a payment is cancelled, a dedicated cancellation page is shown.

  • This will update web/urls.py to add the new Stripe payment endpoints and the webhook endpoint.

  • This will update the crowdfunding_detail view in web/views.py to include the Stripe publishable key in its context.

  • This will update the Stripe webhook handler in web/views.py to process campaign donation events by checking payment_intent metadata and calling new helper functions for success or failure.

A summary of the context that CodeRabbit is considering across the codebase:

  • Before any changes, we verified that the Stripe package was not already mentioned in requirements.txt using a grep search. The shell command returned no results.

  • To verify that using environment variables (via os.environ.get) is a best practice for securely storing Stripe API keys in Django settings, a web query was executed. The results confirm that keys should not be hardcoded and must be fetched securely from the environment.

  • Before coding the webhook and payment-confirmation flows, a web query was executed to verify how to implement a Stripe webhook endpoint in Django. The results provided detailed steps on verifying signatures, handling events, and updating donation statuses based on metadata.

  • This change ensures that the Stripe package is installed for making API calls to Stripe.

  • Stripe requires API keys (publishable, secret, and webhook secret) to be available at runtime. This change adds these keys to the Django settings by reading them from environment variables.

  • The CampaignDonation model must now store the Stripe payment intent ID and the current payment status. This is essential later to reconcile payment events (success, failure, cancellation) received from Stripe’s webhooks.

  • New endpoints are needed for creating payment intents, handling payment success/cancellation, and receiving Stripe webhook events.

  • The crowdfunding_detail view must supply the Stripe publishable key to the template context so that the Stripe Elements code in the front-end can initialize correctly.

  • The webhook handler must detect Stripe payment events (succeeded and failed) specific to campaign donations. When such an event is received, the handler must update the corresponding CampaignDonation’s payment_status accordingly.

  • The donation form in the crowdfunding_detail template should now use Stripe Elements to securely collect credit card details. The updated code includes error handling, a processing indicator, and logic to create a payment intent on the server.

  • First, let's check if there's an existing Campaign model that we need to work with for the crowdfunding feature.

  • Let's check if there's an existing CampaignDonation model that we need to modify for Stripe integration.

  • Let's try to find the models.py file in the correct location.

  • Now let's check if there's a CampaignDonation model in the models.py file.

  • Let's check the existing crowdfunding views to understand how they currently work.

  • Let's check the rest of the crowdfunding_create view.

  • Let's check the existing Stripe integration in the project to understand how it's currently implemented.

  • Let's check the existing Stripe webhook handler to understand how it's currently implemented.

  • Let's check the existing payment handling functions to understand how they're currently implemented.

  • Let's check the existing templates for the crowdfunding feature to understand how they're currently structured.

  • Let's look at the crowdfunding_detail.html template to understand how it's currently structured.

  • Let's check the settings.py file to see if Stripe settings are already configured.

  • Let's check the URL patterns for the crowdfunding feature to understand how they're currently structured.

  • Let's find the urls.py file.

  • Let's check the URL patterns for the crowdfunding feature in the web/urls.py file.

  • Let's check if there's an existing create_donation_payment_intent view that's referenced in the crowdfunding_detail.html template.

  • Let's check if there's any URL pattern for create_donation_payment_intent.

  • Let's check if there are any existing payment templates we can reference.

  • Let's look at the payment_success.html template to understand how it's structured.

  • Let's look at the payment_cancel.html template to understand how it's structured.

  • We want to be sure that the Stripe package is installed. A quick grep command (e.g. “grep stripe== requirements.txt -n”) will later verify that this dependency exists.

  • These settings make the Stripe API keys configurable via environment variables so that they can be updated securely in production.

  • We add the Stripe payment intent ID (to tie our donation to a payment in Stripe) and a payment status indicator for tracking the success or failure of donations.

  • The goal is to send donation data from this form to a new endpoint that creates a Stripe payment intent. Once the payment is confirmed via Stripe Elements, the user is redirected to a success page. This aligns with modern Stripe integration practices.

  • These endpoints allow the frontend to trigger creation of a payment intent and handle post-payment redirects (for both success and cancellation) as well as exposing a webhook for asynchronous events from Stripe.

  • Passing the publishable key to the template ensures that the JavaScript running on the client can initialize Stripe. Meanwhile, updating the webhook handler allows our backend to react to payment events and update each CampaignDonation record accordingly.

  • We want to ensure that the latest stable release of Stripe for Python is used and properly recorded.

  • We need to install the Stripe package (version 2.76.0) so that all Stripe API calls work as expected. A web query confirmed that while this version is extremely outdated compared to the current stable version (≥11.6.0), the requirement specifically asks for stripe==2.76.0.

  • We must update the project settings to include the Stripe API keys. This will allow the application to securely initialize the Stripe client.

  • The CampaignDonation model must be augmented so that each donation records its associated Stripe payment intent ID and current payment status. A shell search for "class CampaignDonation" in web/models.py confirms the existing model definition.

  • The Stripe webhook endpoint should process payment events for campaign donations. A shell search for “def stripe_webhook” confirmed the portion of web/views.py where the webhook view is defined.

  • The crowdfunding_detail view must pass Stripe’s publishable key to the template so that the donation form can initialize Stripe Elements correctly.

  • Before adding new endpoints for payment processing, we need to update the URL patterns. A shell search for "urlpatterns" in web/urls.py confirmed the file’s structure.

  • The donation form on the crowdfunding detail page must use Stripe Elements for secure card data entry. A shell search for "form" in web/templates/crowdfunding_detail.html confirms the donation form region.

  • Payment feedback pages are necessary to inform users about the status of their donation. The provided templates for successful and cancelled payments need to be updated.

  • To ensure there are no conflicting or duplicate Stripe integrations, we ran a repository-wide search for any existing "import stripe" references.

  • We verified that the crowdfunding_detail view is now passing the Stripe publishable key by searching for its usage in the project.

  • This change is derived from the requirement [stripe_integration_1/6 install_requirements]. No additional dependency search was necessary.

  • This update comes from [stripe_integration_2/6 update_settings] and ensures secure API key configuration via environment variables.

  • This change is based on combining requirements [stripe_integration_3/6 update_models] and [stripe_integration_1/5 update_campaign_donation_model]. A grep search over models confirmed the appropriate insertion point.

  • This update is derived from [stripe_integration_4/5 update_template] and [stripe_integration_5/6 update_templates]. It replaces a legacy form with a modern Stripe Elements–based integration.

  • These template updates come from [stripe_integration_5/6 update_templates] and ensure users see clear feedback when a payment succeeds or is cancelled.

  • The URL additions derive from [stripe_integration_6/6 update_urls] and [stripe_integration_5/5 add_urls] to support the new payment processing routes and webhook handling.

  • This update is adapted from [stripe_integration_bonus update_view] and ensures that the Frontend has the key required to initialize Stripe.js.

  • This update originates from [stripe_integration_3/5 update_webhook_handler]. A search in web/views.py for the "payment_intent.succeeded" branch confirmed the appropriate insertion point for the new helper functions.

  • ✅ Create PR with these edits

  • 📋 Get copyable edits

  • 📌 Create commit in current branch

  • 🔄 Try a different approach

Code Analysis Tools

Tool Analysis Results

CodeRabbit ran static analysis tools on your files and found the following:

Details

🪛 Ruff (0.8.2)
web/forms.py

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

(RUF012)


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

(RUF012)


266-266: Trailing comma missing

Add trailing comma

(COM812)


267-267: Trailing comma missing

Add trailing comma

(COM812)


272-272: Trailing comma missing

Add trailing comma

(COM812)


277-277: Trailing comma missing

Add trailing comma

(COM812)


281-281: Trailing comma missing

Add trailing comma

(COM812)


282-282: Trailing comma missing

Add trailing comma

(COM812)


288-288: Trailing comma missing

Add trailing comma

(COM812)


293-293: Trailing comma missing

Add trailing comma

(COM812)


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

(RUF012)


302-302: Missing return type annotation for private function clean_funding_goal

(ANN202)


305-305: Avoid specifying long messages outside the exception class

(TRY003)

web/models.py

34-34: Trailing comma missing

Add trailing comma

(COM812)


40-40: Trailing comma missing

Add trailing comma

(COM812)


43-43: Trailing comma missing

Add trailing comma

(COM812)


45-45: Avoid using null=True on string-based fields such as URLField

(DJ001)


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

(RUF012)


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

Add return type annotation: str

(ANN204)


59-59: Missing return type annotation for public function get_absolute_url

(ANN201)


65-65: Missing return type annotation for public function update_amount_raised

Add return type annotation: None

(ANN201)


80-80: Trailing comma missing

Add trailing comma

(COM812)


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

Add return type annotation: str

(ANN204)


2791-2791: Missing return type annotation for public function update_campaign_amount_on_donation

Add return type annotation: None

(ANN201)


2791-2791: Missing type annotation for function argument sender

(ANN001)


2791-2791: Unused function argument: sender

(ARG001)


2791-2791: Missing type annotation for function argument instance

(ANN001)


2791-2791: Missing type annotation for function argument created

(ANN001)


2791-2791: Unused function argument: created

(ARG001)


2791-2791: Missing type annotation for **kwargs

(ANN003)


2791-2791: Unused function argument: kwargs

(ARG001)


2796-2796: Missing return type annotation for public function update_campaign_amount_on_donation_delete

Add return type annotation: None

(ANN201)


2796-2796: Missing type annotation for function argument sender

(ANN001)


2796-2796: Unused function argument: sender

(ARG001)


2796-2796: Missing type annotation for function argument instance

(ANN001)


2796-2796: Missing type annotation for **kwargs

(ANN003)


2796-2796: Unused function argument: kwargs

(ARG001)

web/views.py

1247-1247: Missing return type annotation for public function crowdfunding_detail

(ANN201)


1247-1247: Missing type annotation for function argument request

(ANN001)


1247-1247: Missing type annotation for function argument campaign_id

(ANN001)


1259-1259: Missing return type annotation for public function crowdfunding_list

(ANN201)


1259-1259: Missing type annotation for function argument request

(ANN001)


1269-1269: Missing return type annotation for public function crowdfunding_create

(ANN201)


1269-1269: Missing type annotation for function argument request

(ANN001)


1284-1284: Unnecessary else after return statement

Remove unnecessary else

(RET505)

Files with Issues

web/forms.py
🪛 Ruff (0.8.2)

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

(RUF012)


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

(RUF012)


266-266: Trailing comma missing

Add trailing comma

(COM812)


267-267: Trailing comma missing

Add trailing comma

(COM812)


272-272: Trailing comma missing

Add trailing comma

(COM812)


277-277: Trailing comma missing

Add trailing comma

(COM812)


281-281: Trailing comma missing

Add trailing comma

(COM812)


282-282: Trailing comma missing

Add trailing comma

(COM812)


288-288: Trailing comma missing

Add trailing comma

(COM812)


293-293: Trailing comma missing

Add trailing comma

(COM812)


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

(RUF012)


302-302: Missing return type annotation for private function clean_funding_goal

(ANN202)


305-305: Avoid specifying long messages outside the exception class

(TRY003)

web/models.py
🪛 Ruff (0.8.2)

34-34: Trailing comma missing

Add trailing comma

(COM812)


40-40: Trailing comma missing

Add trailing comma

(COM812)


43-43: Trailing comma missing

Add trailing comma

(COM812)


45-45: Avoid using null=True on string-based fields such as URLField

(DJ001)


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

(RUF012)


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

Add return type annotation: str

(ANN204)


59-59: Missing return type annotation for public function get_absolute_url

(ANN201)


65-65: Missing return type annotation for public function update_amount_raised

Add return type annotation: None

(ANN201)


80-80: Trailing comma missing

Add trailing comma

(COM812)


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

Add return type annotation: str

(ANN204)


2791-2791: Missing return type annotation for public function update_campaign_amount_on_donation

Add return type annotation: None

(ANN201)


2791-2791: Missing type annotation for function argument sender

(ANN001)


2791-2791: Unused function argument: sender

(ARG001)


2791-2791: Missing type annotation for function argument instance

(ANN001)


2791-2791: Missing type annotation for function argument created

(ANN001)


2791-2791: Unused function argument: created

(ARG001)


2791-2791: Missing type annotation for **kwargs

(ANN003)


2791-2791: Unused function argument: kwargs

(ARG001)


2796-2796: Missing return type annotation for public function update_campaign_amount_on_donation_delete

Add return type annotation: None

(ANN201)


2796-2796: Missing type annotation for function argument sender

(ANN001)


2796-2796: Unused function argument: sender

(ARG001)


2796-2796: Missing type annotation for function argument instance

(ANN001)


2796-2796: Missing type annotation for **kwargs

(ANN003)


2796-2796: Unused function argument: kwargs

(ARG001)

web/views.py
🪛 Ruff (0.8.2)

1247-1247: Missing return type annotation for public function crowdfunding_detail

(ANN201)


1247-1247: Missing type annotation for function argument request

(ANN001)


1247-1247: Missing type annotation for function argument campaign_id

(ANN001)


1259-1259: Missing return type annotation for public function crowdfunding_list

(ANN201)


1259-1259: Missing type annotation for function argument request

(ANN001)


1269-1269: Missing return type annotation for public function crowdfunding_create

(ANN201)


1269-1269: Missing type annotation for function argument request

(ANN001)


1284-1284: Unnecessary else after return statement

Remove unnecessary else

(RET505)

Next Steps

Based on these results, you can:

  • Fix the issues identified by the tools
  • Ask me to suggest specific fixes for these issues
  • Proceed with planning or implementing changes while accounting for these issues

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: 11

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

41-61: 💡 Verification agent

🧩 Analysis chain

Consider adding URL patterns for donation success and cancellation.

The crowdfunding feature includes a redirect to "donation_success" in the template, but there doesn't appear to be a specific URL pattern for handling successful crowdfunding donations.

Let's check if separate success/cancel routes are needed for crowdfunding donations:


🏁 Script executed:

#!/bin/bash
# Check if donation_success view has handling for campaign donations

# Look for donation_success view implementation
rg -A 10 "def donation_success" web/views.py

# Check for references to crowdfunding in donation success handling
rg -A 3 "donation_success.*crowdfund" web/views.py

Length of output: 480


Action Required: Separate Endpoints for Crowdfunding Donation Flows

The current URL patterns provide generic donation endpoints:

  • /donate/success/ → handled by donation_success
  • /donate/cancel/ → handled by donation_cancel

However, the crowdfunding flow (via templates and the crowdfunding donation intent at /crowdfunding/<int:campaign_id>/donate/) redirects to the generic donation success endpoint. The donation_success view implementation confirms it handles only generic donation logic (no crowdfunding-specific logic is present).

Recommendations:

  • Dedicated Crowdfunding Endpoints: Consider adding specific URL patterns (e.g., /crowdfunding/<int:campaign_id>/donate/success/ and /crowdfunding/<int:campaign_id>/donate/cancel/) with accompanying view logic that can handle any nuances required for crowdfunding donations.
  • Alternative Approach: If you intend to use the generic donation endpoints for both donation types, update the donation_success (and potentially donation_cancel) view to correctly differentiate and handle crowdfunding donations versus regular donations.
♻️ Duplicate comments (3)
web/views.py (3)

2294-2296: ⚠️ Potential issue

Security issue: Teacher dashboard protection removed

The @login_required and @teacher_required decorators have been commented out from the teacher_dashboard function. This removes authentication and authorization protection, potentially allowing any user (even unauthenticated ones) to access the teacher dashboard.

Apply this diff to restore the security measures:

-# @login_required
-# @teacher_required
+@login_required
+@teacher_required
def teacher_dashboard(request):
🧰 Tools
🪛 Ruff (0.8.2)

2296-2296: Missing return type annotation for public function teacher_dashboard

(ANN201)


2296-2296: Missing type annotation for function argument request

(ANN001)


1359-1366: 🧹 Nitpick (assertive)

Add type annotations to crowdfunding_list

The function implementation is straightforward and correct, but requires type annotations.

-def crowdfunding_list(request):
+def crowdfunding_list(request: HttpRequest) -> HttpResponse:
    """
    Display all active crowdfunding campaigns.
    Active campaigns are those that are approved and live.
    """
    campaigns = Campaign.objects.filter(approved=True, live=True).order_by("-created_at")
    return render(request, "crowdfunding_list.html", {"campaigns": campaigns})
🧰 Tools
🪛 Ruff (0.8.2)

1359-1359: Missing return type annotation for public function crowdfunding_list

(ANN201)


1359-1359: Missing type annotation for function argument request

(ANN001)


1367-1389: 🧹 Nitpick (assertive)

Add type annotations and remove unnecessary else after return in crowdfunding_create

The implementation of the campaign creation function is good, with proper authentication, authorization, and message handling. However, it needs type annotations and has an unnecessary else block after a return statement.

Here's how to improve the function:

@login_required
@teacher_required
-def crowdfunding_create(request):
+def crowdfunding_create(request: HttpRequest) -> HttpResponse:
    """
    View to allow teachers to create a new crowdfunding campaign.
    """
    if request.method == "POST":
        form = CampaignForm(request.POST, request.FILES)
        if form.is_valid():
            campaign = form.save(commit=False)
            campaign.teacher = request.user
            # Set initial approval status (set to False if admin approval is required)
            campaign.approved = True  # or False if approval workflow is needed
            campaign.live = True      # or False if campaigns shouldn't be live immediately
            campaign.save()
            messages.success(request, "Your crowdfunding campaign has been created successfully!")
            return redirect("crowdfunding_detail", campaign_id=campaign.id)
-        else:
-            messages.error(request, "Please correct the errors below.")
+        messages.error(request, "Please correct the errors below.")
    else:
        form = CampaignForm()
    return render(request, "crowdfunding_create.html", {"form": form})

You might also want to consider adding validation checks for the form and a more informative error message that points out specific fields that have issues.

🧰 Tools
🪛 Ruff (0.8.2)

1369-1369: Missing return type annotation for public function crowdfunding_create

(ANN201)


1369-1369: Missing type annotation for function argument request

(ANN001)


1384-1384: 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 07f9613 and 0c3dbb7.

📒 Files selected for processing (3)
  • web/templates/crowdfunding_detail.html (1 hunks)
  • web/urls.py (2 hunks)
  • web/views.py (6 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
web/urls.py (1)
web/views.py (4)
  • crowdfunding_list (1359-1365)
  • crowdfunding_create (1369-1388)
  • crowdfunding_detail (1346-1356)
  • create_donation_intent (1174-1206)
🪛 GitHub Actions: .github/workflows/test.yml
web/templates/crowdfunding_detail.html

[error] 1-1: Trailing whitespace found and removed by the trailing-whitespace hook.

web/views.py

[error] 1-1: Trailing whitespace found and removed by the trailing-whitespace hook.

🪛 Ruff (0.8.2)
web/views.py

1174-1174: Missing return type annotation for public function create_donation_intent

(ANN201)


1174-1174: Missing type annotation for function argument request

(ANN001)


1174-1174: Missing type annotation for function argument campaign_id

(ANN001)


1202-1202: Do not catch blind exception: Exception

(BLE001)


1255-1255: Missing return type annotation for public function handle_successful_donation

Add return type annotation: None

(ANN201)


1255-1255: Missing type annotation for function argument payment_intent

(ANN001)


1275-1275: Local variable donation is assigned to but never used

Remove assignment to unused variable donation

(F841)


1291-1291: Do not catch blind exception: Exception

(BLE001)


1294-1294: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


1294-1294: Logging statement uses f-string

(G004)


1297-1297: Missing return type annotation for public function handle_failed_donation

Add return type annotation: None

(ANN201)


1297-1297: Missing type annotation for function argument payment_intent

(ANN001)


1307-1307: Logging statement uses f-string

(G004)


1308-1308: Do not catch blind exception: Exception

(BLE001)


1310-1310: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


1310-1310: Logging statement uses f-string

(G004)


1346-1346: Missing return type annotation for public function crowdfunding_detail

(ANN201)


1346-1346: Missing type annotation for function argument request

(ANN001)


1346-1346: Missing type annotation for function argument campaign_id

(ANN001)


1354-1354: Trailing comma missing

Add trailing comma

(COM812)


1359-1359: Missing return type annotation for public function crowdfunding_list

(ANN201)


1359-1359: Missing type annotation for function argument request

(ANN001)


1369-1369: Missing return type annotation for public function crowdfunding_create

(ANN201)


1369-1369: Missing type annotation for function argument request

(ANN001)


1384-1384: Unnecessary else after return statement

Remove unnecessary else

(RET505)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build-and-test
🔇 Additional comments (2)
web/views.py (1)

1330-1340: Good implementation of payment intent handling in webhook

The integration of donation-specific handling in the webhook is well implemented. The code correctly checks if the payment intent is for a course or a donation campaign.

web/urls.py (1)

41-43: The crowdfunding URL structure looks good.

The URL patterns for the crowdfunding list, creation, and detail views are well-organized and follow the application's URL naming conventions.

Comment thread web/views.py
Comment on lines +1297 to +1311
def handle_failed_donation(payment_intent):
"""
Handle a failed donation payment.
You can log the failure or notify the donor that the payment did not go through.
"""
try:
campaign_id = payment_intent.metadata.get("campaign_id")
user_id = payment_intent.metadata.get("user_id")

logger = logging.getLogger(__name__)
logger.warning(f"Donation payment failed for campaign {campaign_id} and user {user_id}.")
except Exception as e:
logger = logging.getLogger(__name__)
logger.error(f"Error handling failed donation: {e}")

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)

Add type annotations and improve error handling in handle_failed_donation

Similar to the previous function, this handler needs some improvements:

  1. Add type annotations
  2. Improve exception handling
  3. Use logging.exception instead of logging.error

Here's the improved implementation:

-def handle_failed_donation(payment_intent):
+def handle_failed_donation(payment_intent) -> None:
    """
    Handle a failed donation payment.
    You can log the failure or notify the donor that the payment did not go through.
    """
    try:
        campaign_id = payment_intent.metadata.get("campaign_id")
        user_id = payment_intent.metadata.get("user_id")

        logger = logging.getLogger(__name__)
-        logger.warning(f"Donation payment failed for campaign {campaign_id} and user {user_id}.")
+        logger.warning("Donation payment failed for campaign %s and user %s", campaign_id, user_id)
    except Exception as e:
        logger = logging.getLogger(__name__)
-        logger.error(f"Error handling failed donation: {e}")
+        logger.exception("Error handling failed donation")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def handle_failed_donation(payment_intent):
"""
Handle a failed donation payment.
You can log the failure or notify the donor that the payment did not go through.
"""
try:
campaign_id = payment_intent.metadata.get("campaign_id")
user_id = payment_intent.metadata.get("user_id")
logger = logging.getLogger(__name__)
logger.warning(f"Donation payment failed for campaign {campaign_id} and user {user_id}.")
except Exception as e:
logger = logging.getLogger(__name__)
logger.error(f"Error handling failed donation: {e}")
def handle_failed_donation(payment_intent) -> None:
"""
Handle a failed donation payment.
You can log the failure or notify the donor that the payment did not go through.
"""
try:
campaign_id = payment_intent.metadata.get("campaign_id")
user_id = payment_intent.metadata.get("user_id")
logger = logging.getLogger(__name__)
logger.warning("Donation payment failed for campaign %s and user %s", campaign_id, user_id)
except Exception as e:
logger = logging.getLogger(__name__)
logger.exception("Error handling failed donation")
🧰 Tools
🪛 Ruff (0.8.2)

1297-1297: Missing return type annotation for public function handle_failed_donation

Add return type annotation: None

(ANN201)


1297-1297: Missing type annotation for function argument payment_intent

(ANN001)


1307-1307: Logging statement uses f-string

(G004)


1308-1308: Do not catch blind exception: Exception

(BLE001)


1310-1310: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


1310-1310: Logging statement uses f-string

(G004)

Comment thread web/views.py
Comment thread web/views.py
Comment on lines +1346 to +1356
def crowdfunding_detail(request, campaign_id):
campaign = get_object_or_404(Campaign, id=campaign_id)
# Check if campaign is approved and live or if user has special access
if not (campaign.approved and campaign.live) and not (request.user.is_authenticated and (campaign.teacher == request.user or request.user.is_staff)):
messages.error(request, "This campaign is not available.")
return redirect("crowdfunding_list")
context = {
"campaign": campaign,
"STRIPE_PUBLISHABLE_KEY": settings.STRIPE_PUBLISHABLE_KEY
}
return render(request, "crowdfunding_detail.html", context)
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)

Add type annotations and fix missing trailing comma in crowdfunding_detail

This function is well-structured and includes proper permission checks. It needs type annotations and has a missing trailing comma in the context dictionary.

Here's how to improve it:

-def crowdfunding_detail(request, campaign_id):
+def crowdfunding_detail(request: HttpRequest, campaign_id: int) -> HttpResponse:
    campaign = get_object_or_404(Campaign, id=campaign_id)
    # Check if campaign is approved and live or if user has special access
    if not (campaign.approved and campaign.live) and not (request.user.is_authenticated and (campaign.teacher == request.user or request.user.is_staff)):
        messages.error(request, "This campaign is not available.")
        return redirect("crowdfunding_list")
    context = {
        "campaign": campaign,
-        "STRIPE_PUBLISHABLE_KEY": settings.STRIPE_PUBLISHABLE_KEY
+        "STRIPE_PUBLISHABLE_KEY": settings.STRIPE_PUBLISHABLE_KEY,
        }
    return render(request, "crowdfunding_detail.html", context)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def crowdfunding_detail(request, campaign_id):
campaign = get_object_or_404(Campaign, id=campaign_id)
# Check if campaign is approved and live or if user has special access
if not (campaign.approved and campaign.live) and not (request.user.is_authenticated and (campaign.teacher == request.user or request.user.is_staff)):
messages.error(request, "This campaign is not available.")
return redirect("crowdfunding_list")
context = {
"campaign": campaign,
"STRIPE_PUBLISHABLE_KEY": settings.STRIPE_PUBLISHABLE_KEY
}
return render(request, "crowdfunding_detail.html", context)
def crowdfunding_detail(request: HttpRequest, campaign_id: int) -> HttpResponse:
campaign = get_object_or_404(Campaign, id=campaign_id)
# Check if campaign is approved and live or if user has special access
if not (campaign.approved and campaign.live) and not (request.user.is_authenticated and (campaign.teacher == request.user or request.user.is_staff)):
messages.error(request, "This campaign is not available.")
return redirect("crowdfunding_list")
context = {
"campaign": campaign,
"STRIPE_PUBLISHABLE_KEY": settings.STRIPE_PUBLISHABLE_KEY,
}
return render(request, "crowdfunding_detail.html", context)
🧰 Tools
🪛 Ruff (0.8.2)

1346-1346: Missing return type annotation for public function crowdfunding_detail

(ANN201)


1346-1346: Missing type annotation for function argument request

(ANN001)


1346-1346: Missing type annotation for function argument campaign_id

(ANN001)


1354-1354: Trailing comma missing

Add trailing comma

(COM812)

Comment thread web/views.py
Comment on lines +1255 to +1295
def handle_successful_donation(payment_intent):
"""
Handle a successful donation payment.
Create a Donation record and update the campaign's amount raised.
"""
try:
campaign_id = payment_intent.metadata.get("campaign_id")
user_id = payment_intent.metadata.get("user_id")
if not campaign_id or not user_id:
# Not a donation payment; you might log this or ignore.
return

# Convert amount from cents to dollars.
donation_amount = Decimal(payment_intent.amount_received) / 100

# Retrieve campaign and user.
campaign = Campaign.objects.get(id=campaign_id)
user = User.objects.get(id=user_id)

# Create the donation record.
donation = Donation.objects.create(
campaign=campaign,
donor_name=user.get_full_name() or user.username,
donor_email=user.email,
amount=donation_amount,
status="completed", # or set appropriate status if your Donation model uses one
stripe_payment_intent_id=payment_intent.id,
)

# Update the campaign's amount raised.
campaign.update_amount_raised()

# Optionally, you can send a notification email to the donor or campaign teacher here.
# For example:
# send_donation_confirmation_email(user, campaign, donation)

except Exception as e:
# Log error if needed.
logger = logging.getLogger(__name__)
logger.error(f"Error handling successful donation: {e}")

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)

Add type annotations and improve error handling in handle_successful_donation

The function contains good functionality for handling successful donations but needs some refinements:

  1. Add type annotations
  2. Use more specific exception handling
  3. The created donation variable is assigned but never used
  4. Use logging.exception instead of logging.error for better stack traces

Here's an improved implementation:

-def handle_successful_donation(payment_intent):
+def handle_successful_donation(payment_intent) -> None:
    """
    Handle a successful donation payment.
    Create a Donation record and update the campaign's amount raised.
    """
    try:
        campaign_id = payment_intent.metadata.get("campaign_id")
        user_id = payment_intent.metadata.get("user_id")
        if not campaign_id or not user_id:
            # Not a donation payment; you might log this or ignore.
            return

        # Convert amount from cents to dollars.
        donation_amount = Decimal(payment_intent.amount_received) / 100

        # Retrieve campaign and user.
        campaign = Campaign.objects.get(id=campaign_id)
        user = User.objects.get(id=user_id)

        # Create the donation record.
-        donation = Donation.objects.create(
+        Donation.objects.create(
            campaign=campaign,
            donor_name=user.get_full_name() or user.username,
            donor_email=user.email,
            amount=donation_amount,
            status="completed",  # or set appropriate status if your Donation model uses one
            stripe_payment_intent_id=payment_intent.id,
        )

        # Update the campaign's amount raised.
        campaign.update_amount_raised()

        # Optionally, you can send a notification email to the donor or campaign teacher here.
        # For example:
        # send_donation_confirmation_email(user, campaign, donation)

-    except Exception as e:
+    except (Campaign.DoesNotExist, User.DoesNotExist) as e:
        # Log error if needed.
        logger = logging.getLogger(__name__)
-        logger.error(f"Error handling successful donation: {e}")
+        logger.error(f"Campaign or User not found: {e}")
+    except Exception as e:
+        # Log error if needed.
+        logger = logging.getLogger(__name__)
+        logger.exception("Error handling successful donation")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def handle_successful_donation(payment_intent):
"""
Handle a successful donation payment.
Create a Donation record and update the campaign's amount raised.
"""
try:
campaign_id = payment_intent.metadata.get("campaign_id")
user_id = payment_intent.metadata.get("user_id")
if not campaign_id or not user_id:
# Not a donation payment; you might log this or ignore.
return
# Convert amount from cents to dollars.
donation_amount = Decimal(payment_intent.amount_received) / 100
# Retrieve campaign and user.
campaign = Campaign.objects.get(id=campaign_id)
user = User.objects.get(id=user_id)
# Create the donation record.
donation = Donation.objects.create(
campaign=campaign,
donor_name=user.get_full_name() or user.username,
donor_email=user.email,
amount=donation_amount,
status="completed", # or set appropriate status if your Donation model uses one
stripe_payment_intent_id=payment_intent.id,
)
# Update the campaign's amount raised.
campaign.update_amount_raised()
# Optionally, you can send a notification email to the donor or campaign teacher here.
# For example:
# send_donation_confirmation_email(user, campaign, donation)
except Exception as e:
# Log error if needed.
logger = logging.getLogger(__name__)
logger.error(f"Error handling successful donation: {e}")
def handle_successful_donation(payment_intent) -> None:
"""
Handle a successful donation payment.
Create a Donation record and update the campaign's amount raised.
"""
try:
campaign_id = payment_intent.metadata.get("campaign_id")
user_id = payment_intent.metadata.get("user_id")
if not campaign_id or not user_id:
# Not a donation payment; you might log this or ignore.
return
# Convert amount from cents to dollars.
donation_amount = Decimal(payment_intent.amount_received) / 100
# Retrieve campaign and user.
campaign = Campaign.objects.get(id=campaign_id)
user = User.objects.get(id=user_id)
# Create the donation record.
Donation.objects.create(
campaign=campaign,
donor_name=user.get_full_name() or user.username,
donor_email=user.email,
amount=donation_amount,
status="completed", # or set appropriate status if your Donation model uses one
stripe_payment_intent_id=payment_intent.id,
)
# Update the campaign's amount raised.
campaign.update_amount_raised()
# Optionally, you can send a notification email to the donor or campaign teacher here.
# For example:
# send_donation_confirmation_email(user, campaign, donation)
except (Campaign.DoesNotExist, User.DoesNotExist) as e:
# Log error if needed.
logger = logging.getLogger(__name__)
logger.error(f"Campaign or User not found: {e}")
except Exception as e:
# Log error if needed.
logger = logging.getLogger(__name__)
logger.exception("Error handling successful donation")
🧰 Tools
🪛 Ruff (0.8.2)

1255-1255: Missing return type annotation for public function handle_successful_donation

Add return type annotation: None

(ANN201)


1255-1255: Missing type annotation for function argument payment_intent

(ANN001)


1275-1275: Local variable donation is assigned to but never used

Remove assignment to unused variable donation

(F841)


1291-1291: Do not catch blind exception: Exception

(BLE001)


1294-1294: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


1294-1294: Logging statement uses f-string

(G004)

Comment thread web/templates/crowdfunding_detail.html Outdated
Comment on lines +46 to +47
<input type="number" id="amount" name="amount" step="0.01" min="1" required
class="border rounded p-2 w-full mb-4">
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.

⚠️ Potential issue

Fix trailing whitespace issue.

There's a trailing whitespace at the end of line 46 that needs to be removed, as indicated by the pipeline failure.

Apply this diff to fix the whitespace issue:

-        <input type="number" id="amount" name="amount" step="0.01" min="1" required 
+        <input type="number" id="amount" name="amount" step="0.01" min="1" required
               class="border rounded p-2 w-full mb-4">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<input type="number" id="amount" name="amount" step="0.01" min="1" required
class="border rounded p-2 w-full mb-4">
<input type="number" id="amount" name="amount" step="0.01" min="1" required
class="border rounded p-2 w-full mb-4">

Comment thread web/templates/crowdfunding_detail.html Outdated
Comment on lines +81 to +83
// Initialize Stripe with your publishable key.
var stripe = Stripe("{{ STRIPE_PUBLISHABLE_KEY }}");
var elements = stripe.elements();
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)

Replace hardcoded Stripe initialization with environment variable check.

The Stripe initialization is using the publishable key directly from template context, but there's no visible verification that the key exists before initialization.

Add a check before initializing Stripe:

  // Initialize Stripe with your publishable key.
-  var stripe = Stripe("{{ STRIPE_PUBLISHABLE_KEY }}");
+  {% if STRIPE_PUBLISHABLE_KEY %}
+  var stripe = Stripe("{{ STRIPE_PUBLISHABLE_KEY }}");
+  {% else %}
+  console.error("Missing Stripe publishable key");
+  alert("Payment system configuration error. Please contact support.");
+  {% endif %}
  var elements = stripe.elements();

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

Comment thread web/templates/crowdfunding_detail.html Outdated
Comment on lines +26 to +28
<div class="aspect-w-16 aspect-h-9">
<iframe src="{{ campaign.video_url }}" frameborder="0" allowfullscreen></iframe>
</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.

💡 Verification agent

🧩 Analysis chain

Secure the iframe source to prevent potential XSS attacks.

The video URL is being inserted directly into the iframe src attribute without validation, which could potentially be a security risk.

It's important to validate that the video URL comes from trusted sources like YouTube or Vimeo:


🏁 Script executed:

#!/bin/bash
# Check if any sanitization for video URLs is implemented

# Search for video URL sanitization in the views
rg -A 3 "video_url" web/views.py

# Check the Campaign model for any validators on video_url field
rg -A 3 "video_url.*Field" web/models.py

Length of output: 899


Action Required: Validate video URL sources to prevent XSS risks.

  • The current implementation relies on Django’s built-in URLField validator, which checks the URL format but does not restrict the source to trusted domains.
  • There is no additional sanitization or domain verification (e.g., ensuring the URL is from YouTube or Vimeo) either in the views or in the Campaign model.
  • Please implement a whitelist check (or similar mechanism) to explicitly allow only URLs from trusted video providers before rendering the iframe.

Comment thread web/templates/crowdfunding_detail.html
Comment thread web/templates/crowdfunding_detail.html Outdated
Comment thread web/urls.py
path("blog/create/", views.create_blog_post, name="create_blog_post"),
path("blog/tag/<str:tag>/", views.blog_tag, name="blog_tag"),
path("blog/<slug:slug>/", views.blog_detail, name="blog_detail"),
path("crowdfunding/<int:campaign_id>/donate/", views.create_donation_intent, name="create_donation_intent"),
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 relocating the donation intent URL to group with other crowdfunding URLs.

The URL for creating a donation intent is separated from the other crowdfunding URLs, which might make maintenance more difficult.

Consider moving this URL to be grouped with the other crowdfunding URLs for better organization:

-    path("crowdfunding/<int:campaign_id>/donate/", views.create_donation_intent, name="create_donation_intent"),

Move it to line 44 after the other crowdfunding URLs:

+    path("crowdfunding/<int:campaign_id>/donate/", views.create_donation_intent, name="create_donation_intent"),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
path("crowdfunding/<int:campaign_id>/donate/", views.create_donation_intent, name="create_donation_intent"),
# Example snippet from web/urls.py
urlpatterns = [
# ... other URL patterns ...
# Crowdfunding URLs grouped together for better organization
path("crowdfunding/<int:campaign_id>/details/", views.campaign_details, name="campaign_details"),
path("crowdfunding/<int:campaign_id>/stats/", views.campaign_stats, name="campaign_stats"),
+ path("crowdfunding/<int:campaign_id>/donate/", views.create_donation_intent, name="create_donation_intent"),
# ... other URLs that were originally placed later in the file ...
]

@debadityaroy99 debadityaroy99 requested a review from A1L13N March 29, 2025 05:33
@debadityaroy99
Copy link
Copy Markdown
Contributor Author

stripe pic 1
stripe pic 2

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: 10

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

59-96: 🛠️ Refactor suggestion

Update __all__ to include the new CampaignForm.

The CampaignForm class is not included in the __all__ list, which means it won't be exported when importing * from this module.

 __all__ = [
     "UserRegistrationForm",
     "ProfileForm",
     "ChallengeSubmissionForm",
     "CourseCreationForm",
     "CourseForm",
     "PeerChallengeForm",
     "PeerChallengeInvitationForm",
     "SessionForm",
     "ReviewForm",
     "CourseMaterialForm",
     "TeacherSignupForm",
     "ProfileUpdateForm",
     "CustomLoginForm",
     "LearnForm",
     "TeachForm",
     "InviteStudentForm",
     "ForumCategoryForm",
     "ForumTopicForm",
     "BlogPostForm",
     "MessageTeacherForm",
     "FeedbackForm",
     "GoodsForm",
     "StorefrontForm",
     "EducationalVideoForm",
     "ProgressTrackerForm",
     "SuccessStoryForm",
     "TeamGoalForm",
     "TeamInviteForm",
     "MemeForm",
     "QuizForm",
     "QuizQuestionForm",
     "QuizOptionFormSet",
     "TakeQuizForm",
     "AvatarForm",
     "GradeableLinkForm",
     "LinkGradeForm",
     "AwardAchievementForm",
+    "CampaignForm",
 ]
🧰 Tools
🪛 Ruff (0.8.2)

59-97: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

♻️ Duplicate comments (12)
web/templates/crowdfunding_detail.html (5)

26-26: ⚠️ Potential issue

Security vulnerability: Validate video URL source.

The iframe source is directly using the campaign's video URL without validation, which could lead to XSS attacks. Implement proper URL validation to ensure only trusted video providers are allowed.

Replace the current implementation with a solution that validates the video source:

-            <iframe src="{{ campaign.video_url }}" frameborder="0" allowfullscreen></iframe>
+            {% if "youtube.com" in campaign.video_url or "youtu.be" in campaign.video_url or "vimeo.com" in campaign.video_url %}
+              <iframe src="{{ campaign.video_url }}" frameborder="0" allowfullscreen class="w-full aspect-video"></iframe>
+            {% else %}
+              <p class="text-red-500">Invalid video source. Only YouTube and Vimeo are supported.</p>
+            {% endif %}

32-34: ⚠️ Potential issue

Fix itemized budget display.

The current implementation uses json_script but doesn't properly display the formatted budget. This will cause the raw JSON to be shown instead of a formatted display.

Apply the suggested fix for properly parsing and displaying the JSON budget:

-          <pre class="bg-gray-100 p-2 rounded">{{ campaign.itemized_budget|json_script:"itemizedBudget" }}</pre>
+          <pre class="bg-gray-100 p-2 rounded" id="budget-display">Loading budget...</pre>
+          {{ campaign.itemized_budget|json_script:"itemizedBudget" }}
+          
+          <script>
+          document.addEventListener('DOMContentLoaded', function() {
+            try {
+              const budgetJson = JSON.parse(document.getElementById('itemizedBudget').textContent);
+              let formattedBudget = '';
+              for (const [item, amount] of Object.entries(budgetJson)) {
+                formattedBudget += `${item}: $${amount}\n`;
+              }
+              document.getElementById('budget-display').textContent = formattedBudget || 'No budget items available';
+            } catch (e) {
+              console.error('Error parsing budget:', e);
+              document.getElementById('budget-display').textContent = 'Error displaying budget';
+            }
+          });
+          </script>

83-85: 🛠️ Refactor suggestion

Add error handling for missing Stripe key.

The Stripe initialization doesn't check if the publishable key exists before initializing.

Add a check before initializing Stripe:

-      var stripe = Stripe("{{ STRIPE_PUBLISHABLE_KEY }}");
-      var elements = stripe.elements();
+      {% if STRIPE_PUBLISHABLE_KEY %}
+      var stripe = Stripe("{{ STRIPE_PUBLISHABLE_KEY }}");
+      var elements = stripe.elements();
+      {% else %}
+      console.error("Missing Stripe publishable key");
+      document.getElementById('card-element').innerHTML = '<div class="p-4 bg-red-100 text-red-700 rounded">Payment system configuration error. Please contact support.</div>';
+      document.getElementById('donate-button').disabled = true;
+      {% endif %}

112-116: 🛠️ Refactor suggestion

Enhance donation amount validation.

The current validation only checks if the amount is positive, but it would be better to also validate against unreasonably large amounts and add additional user feedback.

Improve the validation:

-          if (!donationAmount || donationAmount <= 0) {
-              alert("Please enter a valid donation amount.");
-              return;
-          }
+          if (!donationAmount || donationAmount <= 0) {
+              alert("Please enter a positive donation amount.");
+              return;
+          }
+          if (donationAmount > 10000) {
+              if (!confirm("Are you sure you want to donate $" + donationAmount + "? For donations over $10,000, please contact us directly.")) {
+                  return;
+              }
+          }

143-149: 🧹 Nitpick (assertive)

Add loading indicator during payment processing.

The payment confirmation process doesn't show any loading state, which could lead to users thinking the payment failed if processing takes time.

Add a loading state:

                  }).then(function(result) {
+                      // Show loading state
+                      const donateButton = document.getElementById('donate-button');
+                      donateButton.disabled = true;
+                      donateButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Processing...';
+                      
                      if (result.error) {
                          document.getElementById('card-errors').textContent = result.error.message;
+                         // Reset button state
+                         donateButton.disabled = false;
+                         donateButton.innerHTML = 'Donate';
                      } else if (result.paymentIntent && result.paymentIntent.status === 'succeeded') {
                          window.location.href = "{% url 'donation_success' %}";
                      }
web/views.py (7)

2297-2299: ⚠️ Potential issue

Security issue: Teacher dashboard protection removed.

The @login_required and @teacher_required decorators have been commented out from the teacher_dashboard function. This removes authentication and authorization protection, potentially allowing any user (even unauthenticated ones) to access the teacher dashboard.

Apply this diff to restore the security measures:

-# @login_required
-# @teacher_required
+@login_required
+@teacher_required
def teacher_dashboard(request):
🧰 Tools
🪛 Ruff (0.8.2)

2299-2299: Missing return type annotation for public function teacher_dashboard

(ANN201)


2299-2299: Missing type annotation for function argument request

(ANN001)


1348-1358: 🧹 Nitpick (assertive)

Add type annotations and fix missing trailing comma in crowdfunding_detail.

This function needs type annotations and a minor fix for the trailing comma in the context dictionary.

-def crowdfunding_detail(request, campaign_id):
+def crowdfunding_detail(request: HttpRequest, campaign_id: int) -> HttpResponse:
    campaign = get_object_or_404(Campaign, id=campaign_id)
    # Check if campaign is approved and live or if user has special access
    if not (campaign.approved and campaign.live) and not (
        request.user.is_authenticated and (campaign.teacher == request.user or request.user.is_staff)
    ):
        messages.error(request, "This campaign is not available.")
        return redirect("crowdfunding_list")
-    context = {"campaign": campaign, "STRIPE_PUBLISHABLE_KEY": settings.STRIPE_PUBLISHABLE_KEY}
+    context = {"campaign": campaign, "STRIPE_PUBLISHABLE_KEY": settings.STRIPE_PUBLISHABLE_KEY,}
    return render(request, "crowdfunding_detail.html", context)
🧰 Tools
🪛 Ruff (0.8.2)

1348-1348: Missing return type annotation for public function crowdfunding_detail

(ANN201)


1348-1348: Missing type annotation for function argument request

(ANN001)


1348-1348: Missing type annotation for function argument campaign_id

(ANN001)


1360-1367: 🧹 Nitpick (assertive)

Add type annotations to crowdfunding_list.

This function implementation is good but needs type annotations.

-def crowdfunding_list(request):
+def crowdfunding_list(request: HttpRequest) -> HttpResponse:
    """
    Display all active crowdfunding campaigns.
    Active campaigns are those that are approved and live.
    """
    campaigns = Campaign.objects.filter(approved=True, live=True).order_by("-created_at")
    return render(request, "crowdfunding_list.html", {"campaigns": campaigns})
🧰 Tools
🪛 Ruff (0.8.2)

1360-1360: Missing return type annotation for public function crowdfunding_list

(ANN201)


1360-1360: Missing type annotation for function argument request

(ANN001)


1300-1314: 🧹 Nitpick (assertive)

Improve error handling and logging in handle_failed_donation.

The current implementation uses f-strings in logging (which is not recommended) and catches all exceptions broadly.

-def handle_failed_donation(payment_intent):
+def handle_failed_donation(payment_intent) -> None:
    """
    Handle a failed donation payment.
    You can log the failure or notify the donor that the payment did not go through.
    """
    try:
        campaign_id = payment_intent.metadata.get("campaign_id")
        user_id = payment_intent.metadata.get("user_id")

        logger = logging.getLogger(__name__)
-        logger.warning(f"Donation payment failed for campaign {campaign_id} and user {user_id}.")
+        logger.warning("Donation payment failed for campaign %s and user %s", campaign_id, user_id)
    except Exception as e:
        logger = logging.getLogger(__name__)
-        logger.error(f"Error handling failed donation: {e}")
+        logger.exception("Error handling failed donation")
🧰 Tools
🪛 Ruff (0.8.2)

1300-1300: Missing return type annotation for public function handle_failed_donation

Add return type annotation: None

(ANN201)


1300-1300: Missing type annotation for function argument payment_intent

(ANN001)


1310-1310: Logging statement uses f-string

(G004)


1311-1311: Do not catch blind exception: Exception

(BLE001)


1313-1313: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


1313-1313: Logging statement uses f-string

(G004)


1174-1208: 🛠️ Refactor suggestion

Add type annotations and improve error handling in create_donation_intent.

The function implementation is good overall, but it should include proper type annotations and more specific error handling for better maintainability and robustness.

Apply these improvements:

@csrf_exempt  # For simplicity; in production, configure CSRF properly.
-def create_donation_intent(request, campaign_id):
+def create_donation_intent(request: HttpRequest, campaign_id: int) -> JsonResponse:
    """
    Create a Stripe PaymentIntent for donating to a crowdfunding campaign.
    """
    # Get the campaign
    campaign = get_object_or_404(Campaign, id=campaign_id)
    
    # For simplicity, we assume the donation amount is sent via POST data as a float (in dollars)
    if request.method == "POST":
        try:
            data = json.loads(request.body)
            donation_amount = float(data.get("amount", 0))
            if donation_amount <= 0:
                return JsonResponse({"error": "Invalid donation amount."}, status=400)

            # Convert dollars to cents for Stripe
            amount_cents = int(donation_amount * 100)

            # Create a PaymentIntent
            intent = stripe.PaymentIntent.create(
                amount=amount_cents,
                currency="usd",
                metadata={
                    "campaign_id": campaign.id,
                    "user_id": request.user.id,
                },
            )
            return JsonResponse({"clientSecret": intent.client_secret})
-        except Exception as e:
+        except (ValueError, json.JSONDecodeError) as e:
+            return JsonResponse({"error": f"Invalid input: {str(e)}"}, status=400)
+        except stripe.error.StripeError as e:
            return JsonResponse({"error": str(e)}, status=400)
    else:
        # For non-POST requests, redirect back or render a page.
        return JsonResponse({"error": "POST method required"}, status=405)
🧰 Tools
🪛 Ruff (0.8.2)

1175-1175: Missing return type annotation for public function create_donation_intent

(ANN201)


1175-1175: Missing type annotation for function argument request

(ANN001)


1175-1175: Missing type annotation for function argument campaign_id

(ANN001)


1203-1203: Do not catch blind exception: Exception

(BLE001)


1369-1391: 🧹 Nitpick (assertive)

Add type annotations and remove unnecessary else in crowdfunding_create.

This function has good access control and form handling but needs type annotations and has an unnecessary else clause after a return statement.

@login_required
@teacher_required
-def crowdfunding_create(request):
+def crowdfunding_create(request: HttpRequest) -> HttpResponse:
    """
    View to allow teachers to create a new crowdfunding campaign.
    """
    if request.method == "POST":
        form = CampaignForm(request.POST, request.FILES)
        if form.is_valid():
            campaign = form.save(commit=False)
            campaign.teacher = request.user
            # Set initial approval status (set to False if admin approval is required)
            campaign.approved = True  # or False if approval workflow is needed
            campaign.live = True  # or False if campaigns shouldn't be live immediately
            campaign.save()
            messages.success(request, "Your crowdfunding campaign has been created successfully!")
            return redirect("crowdfunding_detail", campaign_id=campaign.id)
-        else:
-            messages.error(request, "Please correct the errors below.")
+        messages.error(request, "Please correct the errors below.")
    else:
        form = CampaignForm()
    return render(request, "crowdfunding_create.html", {"form": form})
🧰 Tools
🪛 Ruff (0.8.2)

1371-1371: Missing return type annotation for public function crowdfunding_create

(ANN201)


1371-1371: Missing type annotation for function argument request

(ANN001)


1386-1386: Unnecessary else after return statement

Remove unnecessary else

(RET505)


1258-1298: ⚠️ Potential issue

Fix the incomplete implementation of handle_successful_donation.

This function has significant parts of the implementation commented out, which will prevent successful processing of donations. Additionally, the error handling can be improved.

Uncomment and fix the donation creation code, and improve error handling:

-def handle_successful_donation(payment_intent):
+def handle_successful_donation(payment_intent) -> None:
    """
    Handle a successful donation payment.
    Create a Donation record and update the campaign's amount raised.
    """
    try:
        campaign_id = payment_intent.metadata.get("campaign_id")
        user_id = payment_intent.metadata.get("user_id")
        if not campaign_id or not user_id:
            # Not a donation payment; you might log this or ignore.
            return

        # Convert amount from cents to dollars.
-        # donation_amount = Decimal(payment_intent.amount_received) / 100
+        donation_amount = Decimal(payment_intent.amount_received) / 100

        # Retrieve campaign and user.
        campaign = Campaign.objects.get(id=campaign_id)
-        # user = User.objects.get(id=user_id)
+        user = User.objects.get(id=user_id)

-        # # Create the donation record.
-        # donation = Donation.objects.create(
-        #     campaign=campaign,
-        #     donor_name=user.get_full_name() or user.username,
-        #     donor_email=user.email,
-        #     amount=donation_amount,
-        #     status="completed",  # or set appropriate status if your Donation model uses one
-        #     stripe_payment_intent_id=payment_intent.id,
-        # )
+        # Create the donation record.
+        donation = Donation.objects.create(
+            campaign=campaign,
+            donor_name=user.get_full_name() or user.username,
+            donor_email=user.email,
+            amount=donation_amount,
+            status="completed",  # or set appropriate status if your Donation model uses one
+            stripe_payment_intent_id=payment_intent.id,
+        )

        # Update the campaign's amount raised.
        campaign.update_amount_raised()

        # Optionally, you can send a notification email to the donor or campaign teacher here.
        # For example:
        # send_donation_confirmation_email(user, campaign, donation)

-    except Exception as e:
+    except (Campaign.DoesNotExist, User.DoesNotExist) as e:
        # Log error if needed.
        logger = logging.getLogger(__name__)
-        logger.error(f"Error handling successful donation: {e}")
+        logger.error("Campaign or User not found: %s", e)
+    except Exception as e:
+        # Log error if needed.
+        logger = logging.getLogger(__name__)
+        logger.exception("Error handling successful donation")
🧰 Tools
🪛 Ruff (0.8.2)

1258-1258: Missing return type annotation for public function handle_successful_donation

Add return type annotation: None

(ANN201)


1258-1258: Missing type annotation for function argument payment_intent

(ANN001)


1294-1294: Do not catch blind exception: Exception

(BLE001)


1297-1297: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


1297-1297: Logging statement uses f-string

(G004)

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0c3dbb7 and ad0e246.

📒 Files selected for processing (6)
  • web/forms.py (2 hunks)
  • web/models.py (3 hunks)
  • web/templates/crowdfunding_create.html (1 hunks)
  • web/templates/crowdfunding_detail.html (1 hunks)
  • web/templates/crowdfunding_list.html (1 hunks)
  • web/views.py (6 hunks)
🧰 Additional context used
🧬 Code Definitions (2)
web/forms.py (2)
web/models.py (17)
  • Campaign (28-69)
  • Meta (49-52)
  • Meta (105-106)
  • Meta (263-264)
  • Meta (427-428)
  • Meta (538-539)
  • Meta (594-595)
  • Meta (616-617)
  • Meta (663-666)
  • Meta (717-718)
  • Meta (770-772)
  • Meta (796-797)
  • Meta (813-815)
  • Meta (837-838)
  • Meta (853-854)
  • Meta (901-902)
  • Meta (968-969)
web/widgets.py (4)
  • TailwindInput (5-16)
  • TailwindTextarea (19-31)
  • TailwindNumberInput (48-59)
  • TailwindFileInput (89-101)
web/views.py (2)
web/forms.py (6)
  • CampaignForm (259-301)
  • save (218-256)
  • save (610-635)
  • save (702-716)
  • save (1339-1352)
  • save (1513-1528)
web/models.py (13)
  • Campaign (28-69)
  • update_amount_raised (63-69)
  • save (157-161)
  • save (215-251)
  • save (269-272)
  • save (336-370)
  • save (433-460)
  • save (550-552)
  • save (777-780)
  • save (974-979)
  • save (1032-1037)
  • save (1112-1115)
  • save (1198-1209)
🪛 Ruff (0.8.2)
web/forms.py

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

(RUF012)


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

(RUF012)


268-268: Trailing comma missing

Add trailing comma

(COM812)


273-273: Trailing comma missing

Add trailing comma

(COM812)


276-276: Trailing comma missing

Add trailing comma

(COM812)


282-282: Trailing comma missing

Add trailing comma

(COM812)


287-287: Trailing comma missing

Add trailing comma

(COM812)


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

(RUF012)


297-297: Missing return type annotation for private function clean_funding_goal

(ANN202)


300-300: Avoid specifying long messages outside the exception class

(TRY003)

web/models.py

34-34: Trailing comma missing

Add trailing comma

(COM812)


40-40: Trailing comma missing

Add trailing comma

(COM812)


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

(RUF012)


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

Add return type annotation: str

(ANN204)


57-57: Missing return type annotation for public function get_absolute_url

(ANN201)


63-63: Missing return type annotation for public function update_amount_raised

Add return type annotation: None

(ANN201)


78-78: Trailing comma missing

Add trailing comma

(COM812)


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

Add return type annotation: str

(ANN204)


2790-2790: Missing return type annotation for public function update_campaign_amount_on_donation

Add return type annotation: None

(ANN201)


2790-2790: Missing type annotation for function argument sender

(ANN001)


2790-2790: Unused function argument: sender

(ARG001)


2790-2790: Missing type annotation for function argument instance

(ANN001)


2790-2790: Missing type annotation for function argument created

(ANN001)


2790-2790: Unused function argument: created

(ARG001)


2790-2790: Missing type annotation for **kwargs

(ANN003)


2790-2790: Unused function argument: kwargs

(ARG001)


2796-2796: Missing return type annotation for public function update_campaign_amount_on_donation_delete

Add return type annotation: None

(ANN201)


2796-2796: Missing type annotation for function argument sender

(ANN001)


2796-2796: Unused function argument: sender

(ARG001)


2796-2796: Missing type annotation for function argument instance

(ANN001)


2796-2796: Missing type annotation for **kwargs

(ANN003)


2796-2796: Unused function argument: kwargs

(ARG001)

web/views.py

1175-1175: Missing return type annotation for public function create_donation_intent

(ANN201)


1175-1175: Missing type annotation for function argument request

(ANN001)


1175-1175: Missing type annotation for function argument campaign_id

(ANN001)


1203-1203: Do not catch blind exception: Exception

(BLE001)


1258-1258: Missing return type annotation for public function handle_successful_donation

Add return type annotation: None

(ANN201)


1258-1258: Missing type annotation for function argument payment_intent

(ANN001)


1294-1294: Do not catch blind exception: Exception

(BLE001)


1297-1297: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


1297-1297: Logging statement uses f-string

(G004)


1300-1300: Missing return type annotation for public function handle_failed_donation

Add return type annotation: None

(ANN201)


1300-1300: Missing type annotation for function argument payment_intent

(ANN001)


1310-1310: Logging statement uses f-string

(G004)


1311-1311: Do not catch blind exception: Exception

(BLE001)


1313-1313: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


1313-1313: Logging statement uses f-string

(G004)


1348-1348: Missing return type annotation for public function crowdfunding_detail

(ANN201)


1348-1348: Missing type annotation for function argument request

(ANN001)


1348-1348: Missing type annotation for function argument campaign_id

(ANN001)


1360-1360: Missing return type annotation for public function crowdfunding_list

(ANN201)


1360-1360: Missing type annotation for function argument request

(ANN001)


1371-1371: Missing return type annotation for public function crowdfunding_create

(ANN201)


1371-1371: Missing type annotation for function argument request

(ANN001)


1386-1386: Unnecessary else after return statement

Remove unnecessary else

(RET505)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build-and-test
🔇 Additional comments (4)
web/models.py (3)

16-16: Looks good for signal imports.
No issues found with importing post_delete and post_save to facilitate signals.


72-86: Verify PII considerations for storing donor data.
Storing donor_email and donor_name fields may require privacy and data-retention policies. Please ensure compliance with relevant regulations and your application’s privacy statements.

🧰 Tools
🪛 Ruff (0.8.2)

78-78: Trailing comma missing

Add trailing comma

(COM812)


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

Add return type annotation: str

(ANN204)


2787-2789: Decorator usage for signal registration looks fine.
The @receiver(post_save, sender=CampaignDonation) pattern is correct and aligns well with Django’s recommended approach.

web/views.py (1)

1333-1343: Integration with webhook handler looks good.

The modifications to the webhook handler to check for donation-related events and route them to the appropriate handlers are well implemented.

Comment on lines +10 to +17
<form method="post" enctype="multipart/form-data" class="space-y-6">
{% csrf_token %}
<div class="space-y-4">
{% for field in form %}
{{ field }}
{% for error in field.errors %}<p class="mt-1 text-xs text-red-500">{{ error }}</p>{% endfor %}
{% endfor %}
</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)

Update form structure to improve accessibility.

The form lacks accessibility features such as proper labeling and field descriptions. Consider adding labels and help text to provide better guidance to users, especially for complex fields like the itemized budget.

  <form method="post" enctype="multipart/form-data" class="space-y-6">
    {% csrf_token %}
    <div class="space-y-4">
      {% for field in form %}
-       {{ field }}
+       <div class="mb-4">
+         <label for="{{ field.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
+           {{ field.label }}
+         </label>
+         {{ field }}
+         {% if field.help_text %}<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ field.help_text }}</p>{% endif %}
+       </div>
        {% for error in field.errors %}<p class="mt-1 text-xs text-red-500">{{ error }}</p>{% endfor %}
      {% endfor %}
    </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<form method="post" enctype="multipart/form-data" class="space-y-6">
{% csrf_token %}
<div class="space-y-4">
{% for field in form %}
{{ field }}
{% for error in field.errors %}<p class="mt-1 text-xs text-red-500">{{ error }}</p>{% endfor %}
{% endfor %}
</div>
<form method="post" enctype="multipart/form-data" class="space-y-6">
{% csrf_token %}
<div class="space-y-4">
{% for field in form %}
<div class="mb-4">
<label for="{{ field.id_for_label }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{{ field.label }}
</label>
{{ field }}
{% if field.help_text %}<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ field.help_text }}</p>{% endif %}
</div>
{% for error in field.errors %}<p class="mt-1 text-xs text-red-500">{{ error }}</p>{% endfor %}
{% endfor %}
</div>

Comment on lines +7 to +26
<div class="container mx-auto p-9">
<div class="max-w-3xl mx-auto bg-white dark:bg-gray-800 shadow-md rounded-lg p-8">
<h1 class="text-3xl font-bold mb-8 text-gray-800 dark:text-gray-100 text-center">Create Crowdfunding Campaign</h1>
<form method="post" enctype="multipart/form-data" class="space-y-6">
{% csrf_token %}
<div class="space-y-4">
{% for field in form %}
{{ field }}
{% for error in field.errors %}<p class="mt-1 text-xs text-red-500">{{ error }}</p>{% endfor %}
{% endfor %}
</div>
<div>
<button type="submit"
class="w-full bg-green-500 hover:bg-green-600 text-white font-semibold px-4 py-3 rounded-lg transition duration-200">
Create Campaign
</button>
</div>
</form>
</div>
</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)

Add campaign preview functionality.

Consider adding a preview functionality to let teachers see how their campaign will appear before submitting it. This would improve the user experience and help ensure campaigns look as intended.

  <div class="container mx-auto p-9">
    <div class="max-w-3xl mx-auto bg-white dark:bg-gray-800 shadow-md rounded-lg p-8">
      <h1 class="text-3xl font-bold mb-8 text-gray-800 dark:text-gray-100 text-center">Create Crowdfunding Campaign</h1>
+     <div class="flex justify-end mb-4">
+       <button type="button" id="preview-button" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition duration-200">
+         Preview Campaign
+       </button>
+     </div>
      <form method="post" enctype="multipart/form-data" class="space-y-6">
        {% csrf_token %}
        <div class="space-y-4">
          {% for field in form %}
            {{ field }}
            {% for error in field.errors %}<p class="mt-1 text-xs text-red-500">{{ error }}</p>{% endfor %}
          {% endfor %}
        </div>
        <div>
          <button type="submit"
                  class="w-full bg-green-500 hover:bg-green-600 text-white font-semibold px-4 py-3 rounded-lg transition duration-200">
            Create Campaign
          </button>
        </div>
      </form>
    </div>
+   <!-- Preview Modal -->
+   <div id="preview-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
+     <div class="bg-white dark:bg-gray-800 rounded-lg max-w-3xl w-full max-h-[90vh] overflow-y-auto p-6">
+       <div class="flex justify-between items-center mb-4">
+         <h2 class="text-2xl font-bold">Campaign Preview</h2>
+         <button id="close-preview" class="text-gray-500 hover:text-gray-700">&times;</button>
+       </div>
+       <div id="preview-content"></div>
+     </div>
+   </div>
  </div>
+
+ <script>
+   document.getElementById('preview-button').addEventListener('click', function() {
+     const form = document.querySelector('form');
+     const formData = new FormData(form);
+     const previewContent = document.getElementById('preview-content');
+     
+     // Basic preview of campaign details
+     let title = formData.get('title') || 'Campaign Title';
+     let description = formData.get('description') || 'Campaign Description';
+     let fundingGoal = formData.get('funding_goal') || '0';
+     
+     // Create preview HTML
+     previewContent.innerHTML = `
+       <div class="space-y-4">
+         <h3 class="text-xl font-bold">${title}</h3>
+         <p>${description}</p>
+         <div class="mt-2">
+           <strong>Goal:</strong> $${fundingGoal}
+           <br />
+           <strong>Raised:</strong> $0
+         </div>
+       </div>
+     `;
+     
+     // Show the modal
+     document.getElementById('preview-modal').classList.remove('hidden');
+   });
+   
+   document.getElementById('close-preview').addEventListener('click', function() {
+     document.getElementById('preview-modal').classList.add('hidden');
+   });
+ </script>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="container mx-auto p-9">
<div class="max-w-3xl mx-auto bg-white dark:bg-gray-800 shadow-md rounded-lg p-8">
<h1 class="text-3xl font-bold mb-8 text-gray-800 dark:text-gray-100 text-center">Create Crowdfunding Campaign</h1>
<form method="post" enctype="multipart/form-data" class="space-y-6">
{% csrf_token %}
<div class="space-y-4">
{% for field in form %}
{{ field }}
{% for error in field.errors %}<p class="mt-1 text-xs text-red-500">{{ error }}</p>{% endfor %}
{% endfor %}
</div>
<div>
<button type="submit"
class="w-full bg-green-500 hover:bg-green-600 text-white font-semibold px-4 py-3 rounded-lg transition duration-200">
Create Campaign
</button>
</div>
</form>
</div>
</div>
<div class="container mx-auto p-9">
<div class="max-w-3xl mx-auto bg-white dark:bg-gray-800 shadow-md rounded-lg p-8">
<h1 class="text-3xl font-bold mb-8 text-gray-800 dark:text-gray-100 text-center">Create Crowdfunding Campaign</h1>
<div class="flex justify-end mb-4">
<button type="button" id="preview-button" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition duration-200">
Preview Campaign
</button>
</div>
<form method="post" enctype="multipart/form-data" class="space-y-6">
{% csrf_token %}
<div class="space-y-4">
{% for field in form %}
{{ field }}
{% for error in field.errors %}<p class="mt-1 text-xs text-red-500">{{ error }}</p>{% endfor %}
{% endfor %}
</div>
<div>
<button type="submit"
class="w-full bg-green-500 hover:bg-green-600 text-white font-semibold px-4 py-3 rounded-lg transition duration-200">
Create Campaign
</button>
</div>
</form>
</div>
<!-- Preview Modal -->
<div id="preview-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
<div class="bg-white dark:bg-gray-800 rounded-lg max-w-3xl w-full max-h-[90vh] overflow-y-auto p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold">Campaign Preview</h2>
<button id="close-preview" class="text-gray-500 hover:text-gray-700">&times;</button>
</div>
<div id="preview-content"></div>
</div>
</div>
</div>
<script>
document.getElementById('preview-button').addEventListener('click', function() {
const form = document.querySelector('form');
const formData = new FormData(form);
const previewContent = document.getElementById('preview-content');
// Basic preview of campaign details
let title = formData.get('title') || 'Campaign Title';
let description = formData.get('description') || 'Campaign Description';
let fundingGoal = formData.get('funding_goal') || '0';
// Create preview HTML
previewContent.innerHTML = `
<div class="space-y-4">
<h3 class="text-xl font-bold">${title}</h3>
<p>${description}</p>
<div class="mt-2">
<strong>Goal:</strong> $${fundingGoal}
<br />
<strong>Raised:</strong> $0
</div>
</div>
`;
// Show the modal
document.getElementById('preview-modal').classList.remove('hidden');
});
document.getElementById('close-preview').addEventListener('click', function() {
document.getElementById('preview-modal').classList.add('hidden');
});
</script>

{% endfor %}
</div>
{% else %}
<p>No active crowdfunding campaigns at this time.</p>
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)

Enhance empty state experience.

The current empty state message is very plain. Consider enhancing it with a more engaging message, possibly an illustration, and a clear call-to-action for teachers to create a campaign.

-      <p>No active crowdfunding campaigns at this time.</p>
+      <div class="text-center py-10">
+        <div class="mx-auto w-24 h-24 bg-gray-200 rounded-full flex items-center justify-center mb-4">
+          <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
+          </svg>
+        </div>
+        <h2 class="text-xl font-semibold mb-2">No active campaigns yet</h2>
+        <p class="text-gray-600 mb-6">Be the first to create a crowdfunding campaign for your classroom project!</p>
+        {% if request.user.profile.is_teacher %}
+          <a href="{% url 'crowdfunding_create' %}" class="inline-block bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg font-semibold">
+            Create a Campaign
+          </a>
+        {% endif %}
+      </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p>No active crowdfunding campaigns at this time.</p>
<div class="text-center py-10">
<div class="mx-auto w-24 h-24 bg-gray-200 rounded-full flex items-center justify-center mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h2 class="text-xl font-semibold mb-2">No active campaigns yet</h2>
<p class="text-gray-600 mb-6">Be the first to create a crowdfunding campaign for your classroom project!</p>
{% if request.user.profile.is_teacher %}
<a href="{% url 'crowdfunding_create' %}" class="inline-block bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg font-semibold">
Create a Campaign
</a>
{% endif %}
</div>

Comment on lines +17 to +21
<div class="mt-2">
<strong>Goal:</strong> ${{ campaign.funding_goal }}
<br />
<strong>Raised:</strong> ${{ campaign.amount_raised }}
</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)

Add progress bar for funding visualization.

The current funding display only shows the goal and amount raised as text. Adding a visual progress bar would make it easier for users to quickly understand campaign progress.

            <div class="mt-2">
              <strong>Goal:</strong> ${{ campaign.funding_goal }}
              <br />
              <strong>Raised:</strong> ${{ campaign.amount_raised }}
+             <div class="mt-2 w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
+               <div class="bg-blue-600 h-2.5 rounded-full" style="width: {% widthratio campaign.amount_raised campaign.funding_goal 100 %}%"></div>
+             </div>
+             <p class="text-xs text-gray-500 mt-1">{% widthratio campaign.amount_raised campaign.funding_goal 100 %}% funded</p>
            </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="mt-2">
<strong>Goal:</strong> ${{ campaign.funding_goal }}
<br />
<strong>Raised:</strong> ${{ campaign.amount_raised }}
</div>
<div class="mt-2">
<strong>Goal:</strong> ${{ campaign.funding_goal }}
<br />
<strong>Raised:</strong> ${{ campaign.amount_raised }}
<div class="mt-2 w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
<div class="bg-blue-600 h-2.5 rounded-full" style="width: {% widthratio campaign.amount_raised campaign.funding_goal 100 %}%"></div>
</div>
<p class="text-xs text-gray-500 mt-1">{% widthratio campaign.amount_raised campaign.funding_goal 100 %}% funded</p>
</div>

Comment on lines +7 to +26
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for campaign in campaigns %}
<div class="border rounded-lg shadow-lg p-4">
{% if campaign.image %}
<img src="{{ campaign.image.url }}"
alt="{{ campaign.title }}"
class="w-full h-48 object-cover rounded" />
{% endif %}
<h2 class="mt-3 text-xl font-semibold">{{ campaign.title }}</h2>
<p class="mt-1 text-gray-700">{{ campaign.description|truncatewords:20 }}</p>
<div class="mt-2">
<strong>Goal:</strong> ${{ campaign.funding_goal }}
<br />
<strong>Raised:</strong> ${{ campaign.amount_raised }}
</div>
<a href="{% url 'crowdfunding_detail' campaign.id %}"
class="mt-4 inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">View Details</a>
</div>
{% endfor %}
</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)

Add filtering and sorting functionality for campaigns.

Currently, the list view displays all campaigns without any filtering or sorting options. Consider adding controls to filter by category, sort by various criteria (newest, most funded, etc.), and search by title or description.

  <div class="container mx-auto p-6">
    <h1 class="text-3xl font-bold mb-6">Active Crowdfunding Campaigns</h1>
+   <div class="mb-6 flex flex-col md:flex-row gap-4 justify-between">
+     <div class="flex gap-2 items-center">
+       <input type="text" id="search-input" placeholder="Search campaigns..." 
+              class="border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
+       <button id="search-button" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
+         Search
+       </button>
+     </div>
+     <div class="flex gap-4">
+       <select id="sort-campaigns" class="border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
+         <option value="newest">Newest</option>
+         <option value="most-funded">Most Funded</option>
+         <option value="closest-to-goal">Closest to Goal</option>
+       </select>
+     </div>
+   </div>
    {% if campaigns %}
      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {% for campaign in campaigns %}
          <div class="border rounded-lg shadow-lg p-4">
            {% if campaign.image %}
              <img src="{{ campaign.image.url }}"
                   alt="{{ campaign.title }}"
                   class="w-full h-48 object-cover rounded" />
            {% endif %}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for campaign in campaigns %}
<div class="border rounded-lg shadow-lg p-4">
{% if campaign.image %}
<img src="{{ campaign.image.url }}"
alt="{{ campaign.title }}"
class="w-full h-48 object-cover rounded" />
{% endif %}
<h2 class="mt-3 text-xl font-semibold">{{ campaign.title }}</h2>
<p class="mt-1 text-gray-700">{{ campaign.description|truncatewords:20 }}</p>
<div class="mt-2">
<strong>Goal:</strong> ${{ campaign.funding_goal }}
<br />
<strong>Raised:</strong> ${{ campaign.amount_raised }}
</div>
<a href="{% url 'crowdfunding_detail' campaign.id %}"
class="mt-4 inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">View Details</a>
</div>
{% endfor %}
</div>
<div class="container mx-auto p-6">
<h1 class="text-3xl font-bold mb-6">Active Crowdfunding Campaigns</h1>
<div class="mb-6 flex flex-col md:flex-row gap-4 justify-between">
<div class="flex gap-2 items-center">
<input type="text" id="search-input" placeholder="Search campaigns..."
class="border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
<button id="search-button" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
Search
</button>
</div>
<div class="flex gap-4">
<select id="sort-campaigns" class="border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="newest">Newest</option>
<option value="most-funded">Most Funded</option>
<option value="closest-to-goal">Closest to Goal</option>
</select>
</div>
</div>
{% if campaigns %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for campaign in campaigns %}
<div class="border rounded-lg shadow-lg p-4">
{% if campaign.image %}
<img src="{{ campaign.image.url }}"
alt="{{ campaign.title }}"
class="w-full h-48 object-cover rounded" />
{% endif %}
<h2 class="mt-3 text-xl font-semibold">{{ campaign.title }}</h2>
<p class="mt-1 text-gray-700">{{ campaign.description|truncatewords:20 }}</p>
<div class="mt-2">
<strong>Goal:</strong> ${{ campaign.funding_goal }}
<br />
<strong>Raised:</strong> ${{ campaign.amount_raised }}
</div>
<a href="{% url 'crowdfunding_detail' campaign.id %}"
class="mt-4 inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">
View Details
</a>
</div>
{% endfor %}
</div>
{% endif %}
</div>

Comment on lines +37 to +60
<div class="mt-6">
<h2 class="text-xl font-semibold mb-2">Donate to this Campaign</h2>
<form id="donation-form">
{% csrf_token %}
<input type="hidden" id="campaign_id" value="{{ campaign.id }}" />
<label for="amount" class="block mb-1 text-gray-700">Donation Amount ($):</label>
<input type="number"
id="amount"
name="amount"
step="0.01"
min="1"
required
class="border rounded p-2 w-full mb-4" />
<!-- Stripe Card Element Container -->
<div id="card-element" class="mb-4"></div>
<!-- Display errors from Stripe Element -->
<div id="card-errors" class="text-red-500 text-sm mb-4"></div>
<button type="button"
id="donate-button"
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold px-4 py-2 rounded">
Donate
</button>
</form>
</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)

Add donation tiers for better user experience.

Instead of having users enter a free-form amount, consider adding donation tiers with predefined amounts to simplify the donation process. This can increase conversion rates by reducing decision fatigue.

      <!-- Donation Section -->
      <div class="mt-6">
        <h2 class="text-xl font-semibold mb-2">Donate to this Campaign</h2>
        <form id="donation-form">
          {% csrf_token %}
          <input type="hidden" id="campaign_id" value="{{ campaign.id }}" />
-         <label for="amount" class="block mb-1 text-gray-700">Donation Amount ($):</label>
-         <input type="number"
-                id="amount"
-                name="amount"
-                step="0.01"
-                min="1"
-                required
-                class="border rounded p-2 w-full mb-4" />
+         <div class="mb-4">
+           <label class="block mb-1 text-gray-700">Select donation amount:</label>
+           <div class="grid grid-cols-2 md:grid-cols-4 gap-2 mb-2">
+             <button type="button" class="donation-tier p-2 border rounded hover:bg-blue-50 focus:ring-2 focus:ring-blue-500" data-amount="5">$5</button>
+             <button type="button" class="donation-tier p-2 border rounded hover:bg-blue-50 focus:ring-2 focus:ring-blue-500" data-amount="10">$10</button>
+             <button type="button" class="donation-tier p-2 border rounded hover:bg-blue-50 focus:ring-2 focus:ring-blue-500" data-amount="25">$25</button>
+             <button type="button" class="donation-tier p-2 border rounded hover:bg-blue-50 focus:ring-2 focus:ring-blue-500" data-amount="50">$50</button>
+           </div>
+           <div class="flex items-center">
+             <label for="amount" class="inline-block mr-2 text-gray-700">Custom amount:</label>
+             <input type="number"
+                    id="amount"
+                    name="amount"
+                    step="0.01"
+                    min="1"
+                    required
+                    class="border rounded p-2 w-full" />
+           </div>
+         </div>
          <!-- Stripe Card Element Container -->
          <div id="card-element" class="mb-4"></div>
          <!-- Display errors from Stripe Element -->
          <div id="card-errors" class="text-red-500 text-sm mb-4"></div>
          <button type="button"
                  id="donate-button"
                  class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold px-4 py-2 rounded">
            Donate
          </button>
        </form>
      </div>

Add the necessary JavaScript to handle the donation tiers:

// Add this to your script section
document.addEventListener('DOMContentLoaded', function() {
  const amountInput = document.getElementById('amount');
  const tiers = document.querySelectorAll('.donation-tier');
  
  tiers.forEach(button => {
    button.addEventListener('click', function() {
      // Remove active class from all buttons
      tiers.forEach(btn => btn.classList.remove('bg-blue-100', 'font-semibold'));
      
      // Add active class to clicked button
      this.classList.add('bg-blue-100', 'font-semibold');
      
      // Set amount value
      amountInput.value = this.getAttribute('data-amount');
    });
  });
});

Comment thread web/models.py
Comment on lines +2790 to +2793
def update_campaign_amount_on_donation(sender, instance, created, **kwargs):
"""Update campaign amount_raised when a donation is saved."""
instance.campaign.update_amount_raised()

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)

Rename unused parameters or remove them to avoid confusion.
The sender, created, and **kwargs parameters are unused in update_campaign_amount_on_donation. Consider renaming them with a leading underscore or removing them if unneeded. Also add a return type annotation for clarity.

 @receiver(post_save, sender=CampaignDonation)
-def update_campaign_amount_on_donation(sender, instance, created, **kwargs):
+def update_campaign_amount_on_donation(_sender, instance, _created, **_kwargs) -> None:
     """Update campaign amount_raised when a donation is saved."""
     instance.campaign.update_amount_raised()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def update_campaign_amount_on_donation(sender, instance, created, **kwargs):
"""Update campaign amount_raised when a donation is saved."""
instance.campaign.update_amount_raised()
@receiver(post_save, sender=CampaignDonation)
def update_campaign_amount_on_donation(_sender, instance, _created, **_kwargs) -> None:
"""Update campaign amount_raised when a donation is saved."""
instance.campaign.update_amount_raised()
🧰 Tools
🪛 Ruff (0.8.2)

2790-2790: Missing return type annotation for public function update_campaign_amount_on_donation

Add return type annotation: None

(ANN201)


2790-2790: Missing type annotation for function argument sender

(ANN001)


2790-2790: Unused function argument: sender

(ARG001)


2790-2790: Missing type annotation for function argument instance

(ANN001)


2790-2790: Missing type annotation for function argument created

(ANN001)


2790-2790: Unused function argument: created

(ARG001)


2790-2790: Missing type annotation for **kwargs

(ANN003)


2790-2790: Unused function argument: kwargs

(ARG001)

Comment thread web/models.py
Comment on lines +2795 to +2798
@receiver(post_delete, sender=CampaignDonation)
def update_campaign_amount_on_donation_delete(sender, instance, **kwargs):
"""Update campaign amount_raised when a donation is deleted."""
instance.campaign.update_amount_raised()
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)

Apply the same cleanup for the delete signal handler.
Similar to the previous function, you can add a return type annotation and rename unused parameters.

 @receiver(post_delete, sender=CampaignDonation)
-def update_campaign_amount_on_donation_delete(sender, instance, **kwargs):
+def update_campaign_amount_on_donation_delete(_sender, instance, **_kwargs) -> None:
     """Update campaign amount_raised when a donation is deleted."""
     instance.campaign.update_amount_raised()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@receiver(post_delete, sender=CampaignDonation)
def update_campaign_amount_on_donation_delete(sender, instance, **kwargs):
"""Update campaign amount_raised when a donation is deleted."""
instance.campaign.update_amount_raised()
@receiver(post_delete, sender=CampaignDonation)
def update_campaign_amount_on_donation_delete(_sender, instance, **_kwargs) -> None:
"""Update campaign amount_raised when a donation is deleted."""
instance.campaign.update_amount_raised()
🧰 Tools
🪛 Ruff (0.8.2)

2796-2796: Missing return type annotation for public function update_campaign_amount_on_donation_delete

Add return type annotation: None

(ANN201)


2796-2796: Missing type annotation for function argument sender

(ANN001)


2796-2796: Unused function argument: sender

(ARG001)


2796-2796: Missing type annotation for function argument instance

(ANN001)


2796-2796: Missing type annotation for **kwargs

(ANN003)


2796-2796: Unused function argument: kwargs

(ARG001)

Comment thread web/models.py
Comment on lines +28 to +71
class Campaign(models.Model):
"""
Model representing a crowdfunding campaign created by a teacher.
"""

teacher = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="campaigns", help_text="Teacher who created the campaign."
)
title = models.CharField(max_length=255, help_text="Campaign title")
description = models.TextField(help_text="Detailed campaign description")
funding_goal = models.DecimalField(max_digits=10, decimal_places=2, help_text="Target funding amount in dollars")
amount_raised = models.DecimalField(
max_digits=10, decimal_places=2, default=0, help_text="Total funds raised so far"
)
itemized_budget = models.JSONField(blank=True, help_text="Optional JSON field for itemized budget breakdown")
video_url = models.URLField(blank=True, help_text="Optional video pitch URL for the campaign")
image = models.ImageField(upload_to="campaign_images/", blank=True, help_text="Optional campaign image")
approved = models.BooleanField(default=True, help_text="Indicates whether the campaign is approved by moderators")
live = models.BooleanField(default=True, help_text="Indicates whether the campaign is live and accepting donations")
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
ordering = ["-created_at"]
verbose_name = "Crowdfunding Campaign"
verbose_name_plural = "Crowdfunding Campaigns"

def __str__(self):
return self.title

def get_absolute_url(self):
"""
Returns the URL to access a particular campaign's detail view.
"""
return reverse("crowdfunding_detail", kwargs={"campaign_id": self.id})

def update_amount_raised(self):
"""
Recalculate and update the total amount raised from related donations.
"""
total = self.donations.aggregate(total=models.Sum("amount"))["total"] or 0
self.amount_raised = total
self.save()


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 minor enhancements for concurrency and type annotations.

  1. In update_amount_raised(), regularly recalculating might introduce overhead for high-volume donations. You could use an atomic approach (e.g., F()) to increment amount_raised directly in the donation signals, which is often more performant and concurrency-safe.
  2. According to static analysis, adding return type annotations (e.g., -> None) to methods like __str__, get_absolute_url, and update_amount_raised can improve code clarity.
 class Campaign(models.Model):
     ...
-    def __str__(self):
+    def __str__(self) -> str:
         return self.title

-    def get_absolute_url(self):
+    def get_absolute_url(self) -> str:
         return reverse("crowdfunding_detail", kwargs={"campaign_id": self.id})

-    def update_amount_raised(self):
+    def update_amount_raised(self) -> None:
         total = self.donations.aggregate(total=models.Sum("amount"))["total"] or 0
         self.amount_raised = total
         self.save()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class Campaign(models.Model):
"""
Model representing a crowdfunding campaign created by a teacher.
"""
teacher = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="campaigns", help_text="Teacher who created the campaign."
)
title = models.CharField(max_length=255, help_text="Campaign title")
description = models.TextField(help_text="Detailed campaign description")
funding_goal = models.DecimalField(max_digits=10, decimal_places=2, help_text="Target funding amount in dollars")
amount_raised = models.DecimalField(
max_digits=10, decimal_places=2, default=0, help_text="Total funds raised so far"
)
itemized_budget = models.JSONField(blank=True, help_text="Optional JSON field for itemized budget breakdown")
video_url = models.URLField(blank=True, help_text="Optional video pitch URL for the campaign")
image = models.ImageField(upload_to="campaign_images/", blank=True, help_text="Optional campaign image")
approved = models.BooleanField(default=True, help_text="Indicates whether the campaign is approved by moderators")
live = models.BooleanField(default=True, help_text="Indicates whether the campaign is live and accepting donations")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
verbose_name = "Crowdfunding Campaign"
verbose_name_plural = "Crowdfunding Campaigns"
def __str__(self):
return self.title
def get_absolute_url(self):
"""
Returns the URL to access a particular campaign's detail view.
"""
return reverse("crowdfunding_detail", kwargs={"campaign_id": self.id})
def update_amount_raised(self):
"""
Recalculate and update the total amount raised from related donations.
"""
total = self.donations.aggregate(total=models.Sum("amount"))["total"] or 0
self.amount_raised = total
self.save()
class Campaign(models.Model):
"""
Model representing a crowdfunding campaign created by a teacher.
"""
teacher = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="campaigns", help_text="Teacher who created the campaign."
)
title = models.CharField(max_length=255, help_text="Campaign title")
description = models.TextField(help_text="Detailed campaign description")
funding_goal = models.DecimalField(max_digits=10, decimal_places=2, help_text="Target funding amount in dollars")
amount_raised = models.DecimalField(
max_digits=10, decimal_places=2, default=0, help_text="Total funds raised so far"
)
itemized_budget = models.JSONField(blank=True, help_text="Optional JSON field for itemized budget breakdown")
video_url = models.URLField(blank=True, help_text="Optional video pitch URL for the campaign")
image = models.ImageField(upload_to="campaign_images/", blank=True, help_text="Optional campaign image")
approved = models.BooleanField(default=True, help_text="Indicates whether the campaign is approved by moderators")
live = models.BooleanField(default=True, help_text="Indicates whether the campaign is live and accepting donations")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
verbose_name = "Crowdfunding Campaign"
verbose_name_plural = "Crowdfunding Campaigns"
def __str__(self) -> str:
return self.title
def get_absolute_url(self) -> str:
"""
Returns the URL to access a particular campaign's detail view.
"""
return reverse("crowdfunding_detail", kwargs={"campaign_id": self.id})
def update_amount_raised(self) -> None:
"""
Recalculate and update the total amount raised from related donations.
"""
total = self.donations.aggregate(total=models.Sum("amount"))["total"] or 0
self.amount_raised = total
self.save()
🧰 Tools
🪛 Ruff (0.8.2)

34-34: Trailing comma missing

Add trailing comma

(COM812)


40-40: Trailing comma missing

Add trailing comma

(COM812)


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

(RUF012)


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

Add return type annotation: str

(ANN204)


57-57: Missing return type annotation for public function get_absolute_url

(ANN201)


63-63: Missing return type annotation for public function update_amount_raised

Add return type annotation: None

(ANN201)

Copy link
Copy Markdown
Contributor

@A1L13N A1L13N left a comment

Choose a reason for hiding this comment

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

Please see feedback and make sure tests are passing

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: 7

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

61-99: 🛠️ Refactor suggestion

The CampaignForm needs to be added to the __all__ list.

The new CampaignForm class should be added to the __all__ list at the top of the file to make it available for importing from other modules.

 __all__ = [
     "UserRegistrationForm",
     "ProfileForm",
     "ChallengeSubmissionForm",
     "CourseCreationForm",
     "CourseForm",
     "PeerChallengeForm",
     "PeerChallengeInvitationForm",
     "SessionForm",
     "ReviewForm",
     "CourseMaterialForm",
     "TeacherSignupForm",
     "ProfileUpdateForm",
     "CustomLoginForm",
     "LearnForm",
     "TeachForm",
     "InviteStudentForm",
     "ForumCategoryForm",
     "ForumTopicForm",
     "BlogPostForm",
     "MessageTeacherForm",
     "FeedbackForm",
     "GoodsForm",
     "StorefrontForm",
     "EducationalVideoForm",
     "ProgressTrackerForm",
     "SuccessStoryForm",
     "TeamGoalForm",
     "TeamInviteForm",
     "MemeForm",
     "QuizForm",
     "QuizQuestionForm",
     "QuizOptionFormSet",
     "TakeQuizForm",
     "AvatarForm",
     "GradeableLinkForm",
     "LinkGradeForm",
     "AwardAchievementForm",
+    "CampaignForm",
 ]
🧰 Tools
🪛 Ruff (0.8.2)

61-99: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

♻️ Duplicate comments (7)
web/forms.py (3)

323-323: 🛠️ Refactor suggestion

Add JSON validation for itemized_budget field.

Since itemized_budget is described as a JSON field, validation should be added to ensure it contains valid JSON when provided.

def clean_itemized_budget(self) -> dict:
    """Validate that itemized_budget contains valid JSON."""
    itemized_budget = self.cleaned_data.get("itemized_budget")
    if itemized_budget:
        try:
            import json
            json_data = json.loads(itemized_budget)
            if not isinstance(json_data, dict):
                raise forms.ValidationError("Budget must be a JSON object with key-value pairs.")
        except json.JSONDecodeError:
            raise forms.ValidationError("Please enter valid JSON for the itemized budget.")
    return itemized_budget

318-322: 🧹 Nitpick (assertive)

Add return type annotation and improve error message.

The clean_funding_goal method is missing a return type annotation, and the error message could be improved by adding a period at the end for consistency.

-    def clean_funding_goal(self):
+    def clean_funding_goal(self) -> 'decimal.Decimal':
         funding_goal = self.cleaned_data.get("funding_goal")
         if funding_goal <= 0:
-            raise forms.ValidationError("Funding goal must be greater than zero")
+            raise forms.ValidationError("Funding goal must be greater than zero.")
         return funding_goal
🧰 Tools
🪛 Ruff (0.8.2)

318-318: Missing return type annotation for private function clean_funding_goal

(ANN202)


321-321: Avoid specifying long messages outside the exception class

(TRY003)


280-310: 🛠️ Refactor suggestion

Fix mutable class attributes and add trailing commas.

The form's widgets dictionary is a mutable class attribute that should be annotated with typing.ClassVar. Also, trailing commas are missing at the end of multiple dictionary entries.

+from typing import ClassVar, Dict, Any

 class CampaignForm(forms.ModelForm):
     class Meta:
         model = Campaign
         fields = ["title", "description", "funding_goal", "itemized_budget", "video_url", "image"]
-        widgets = {
+        widgets: ClassVar[Dict[str, Any]] = {
             "title": TailwindInput(attrs={"placeholder": "Campaign Title"}),
             "description": TailwindTextarea(
                 attrs={
-                    "placeholder": "Describe your campaign in detail",
+                    "placeholder": "Describe your campaign in detail",
                 }
             ),
             "funding_goal": TailwindNumberInput(
                 attrs={
-                    "placeholder": "Target funding amount in dollars",
+                    "placeholder": "Target funding amount in dollars",
                 }
             ),
             "itemized_budget": TailwindTextarea(
-                attrs={"placeholder": "Optional JSON field for itemized budget breakdown"}
+                attrs={"placeholder": "Optional JSON field for itemized budget breakdown"},
             ),
             "video_url": TailwindInput(
                 attrs={
                     "type": "url",
-                    "placeholder": "Optional video pitch URL",
+                    "placeholder": "Optional video pitch URL",
                 }
             ),
             "image": TailwindFileInput(
                 attrs={
-                    "accept": "image/*",
+                    "accept": "image/*",
                 }
             ),
         }
🧰 Tools
🪛 Ruff (0.8.2)

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

(RUF012)


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

(RUF012)


289-289: Trailing comma missing

Add trailing comma

(COM812)


294-294: Trailing comma missing

Add trailing comma

(COM812)


297-297: Trailing comma missing

Add trailing comma

(COM812)


303-303: Trailing comma missing

Add trailing comma

(COM812)


308-308: Trailing comma missing

Add trailing comma

(COM812)

web/views.py (4)

2344-2346: ⚠️ Potential issue

Security issue: Teacher dashboard protection removed.

The @login_required and @teacher_required decorators are commented out, removing authentication and authorization requirements. This is a significant security risk that could allow any unauthenticated user to access the teacher dashboard.

Restore the security decorators to protect this view:

-# @login_required
-# @teacher_required
+@login_required
+@teacher_required
def teacher_dashboard(request):
🧰 Tools
🪛 Ruff (0.8.2)

2346-2346: Missing return type annotation for public function teacher_dashboard

(ANN201)


2346-2346: Missing type annotation for function argument request

(ANN001)


1395-1404: 🧹 Nitpick (assertive)

Add type annotations and fix missing trailing comma in crowdfunding_detail.

The function contains good access control checks but needs a few minor improvements:

-def crowdfunding_detail(request, campaign_id):
+def crowdfunding_detail(request: HttpRequest, campaign_id: int) -> HttpResponse:
    campaign = get_object_or_404(Campaign, id=campaign_id)
    # Check if campaign is approved and live or if user has special access
    if not (campaign.approved and campaign.live) and not (
        request.user.is_authenticated and (campaign.teacher == request.user or request.user.is_staff)
    ):
        messages.error(request, "This campaign is not available.")
        return redirect("crowdfunding_list")
-    context = {"campaign": campaign, "STRIPE_PUBLISHABLE_KEY": settings.STRIPE_PUBLISHABLE_KEY}
+    context = {"campaign": campaign, "STRIPE_PUBLISHABLE_KEY": settings.STRIPE_PUBLISHABLE_KEY,}
    return render(request, "crowdfunding_detail.html", context)
🧰 Tools
🪛 Ruff (0.8.2)

1395-1395: Missing return type annotation for public function crowdfunding_detail

(ANN201)


1395-1395: Missing type annotation for function argument request

(ANN001)


1395-1395: Missing type annotation for function argument campaign_id

(ANN001)


1347-1361: 🧹 Nitpick (assertive)

Improve error handling and add type annotations in handle_failed_donation.

The function needs type annotations and better logging practices:

-def handle_failed_donation(payment_intent):
+def handle_failed_donation(payment_intent) -> None:
    """
    Handle a failed donation payment.
    You can log the failure or notify the donor that the payment did not go through.
    """
    try:
        campaign_id = payment_intent.metadata.get("campaign_id")
        user_id = payment_intent.metadata.get("user_id")

        logger = logging.getLogger(__name__)
-        logger.warning(f"Donation payment failed for campaign {campaign_id} and user {user_id}.")
+        logger.warning("Donation payment failed for campaign %s and user %s", campaign_id, user_id)
    except Exception as e:
        logger = logging.getLogger(__name__)
-        logger.error(f"Error handling failed donation: {e}")
+        logger.exception("Error handling failed donation")
🧰 Tools
🪛 Ruff (0.8.2)

1347-1347: Missing return type annotation for public function handle_failed_donation

Add return type annotation: None

(ANN201)


1347-1347: Missing type annotation for function argument payment_intent

(ANN001)


1357-1357: Logging statement uses f-string

(G004)


1358-1358: Do not catch blind exception: Exception

(BLE001)


1360-1360: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


1360-1360: Logging statement uses f-string

(G004)


1221-1254: 🛠️ Refactor suggestion

Add type annotations and improve error handling in create_donation_intent.

This function is well-structured for creating Stripe payment intents, but could use several improvements:

  1. Add type annotations for better code maintainability
  2. Use more specific exception handling instead of catching all exceptions
  3. Verify user authentication before accessing request.user.id

Here's a safer implementation with appropriate type annotations:

@csrf_exempt  # For simplicity; in production, configure CSRF properly.
-def create_donation_intent(request, campaign_id):
+def create_donation_intent(request: HttpRequest, campaign_id: int) -> JsonResponse:
    """
    Create a Stripe PaymentIntent for donating to a crowdfunding campaign.
    """
    # Get the campaign
    campaign = get_object_or_404(Campaign, id=campaign_id)

    # For simplicity, we assume the donation amount is sent via POST data as a float (in dollars)
    if request.method == "POST":
        try:
            data = json.loads(request.body)
            donation_amount = float(data.get("amount", 0))
            if donation_amount <= 0:
                return JsonResponse({"error": "Invalid donation amount."}, status=400)

            # Convert dollars to cents for Stripe
            amount_cents = int(donation_amount * 100)

            # Create a PaymentIntent
            intent = stripe.PaymentIntent.create(
                amount=amount_cents,
                currency="usd",
                metadata={
                    "campaign_id": campaign.id,
-                   "user_id": request.user.id,
+                   "user_id": request.user.id if request.user.is_authenticated else None,
                },
            )
            return JsonResponse({"clientSecret": intent.client_secret})
-        except Exception as e:
+        except json.JSONDecodeError:
+            return JsonResponse({"error": "Invalid JSON in request body"}, status=400)
+        except ValueError:
+            return JsonResponse({"error": "Invalid donation amount format"}, status=400)
+        except stripe.error.StripeError as e:
            return JsonResponse({"error": str(e)}, status=400)
+        except Exception as e:
+            logger.exception("Unexpected error in create_donation_intent")
+            return JsonResponse({"error": "An unexpected error occurred"}, status=500)
    else:
        # For non-POST requests, redirect back or render a page.
        return JsonResponse({"error": "POST method required"}, status=405)
🧰 Tools
🪛 Ruff (0.8.2)

1222-1222: Missing return type annotation for public function create_donation_intent

(ANN201)


1222-1222: Missing type annotation for function argument request

(ANN001)


1222-1222: Missing type annotation for function argument campaign_id

(ANN001)


1250-1250: Do not catch blind exception: Exception

(BLE001)

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between ad0e246 and bebdf5a.

📒 Files selected for processing (4)
  • web/forms.py (2 hunks)
  • web/models.py (3 hunks)
  • web/urls.py (2 hunks)
  • web/views.py (6 hunks)
🧰 Additional context used
🧬 Code Definitions (3)
web/forms.py (2)
web/models.py (17)
  • Campaign (28-69)
  • Meta (49-52)
  • Meta (105-106)
  • Meta (263-264)
  • Meta (455-456)
  • Meta (601-602)
  • Meta (657-658)
  • Meta (679-680)
  • Meta (726-729)
  • Meta (780-781)
  • Meta (833-835)
  • Meta (859-860)
  • Meta (876-878)
  • Meta (900-901)
  • Meta (916-917)
  • Meta (964-965)
  • Meta (1031-1032)
web/widgets.py (4)
  • TailwindInput (5-16)
  • TailwindTextarea (19-31)
  • TailwindNumberInput (48-59)
  • TailwindFileInput (89-101)
web/urls.py (1)
web/views.py (4)
  • crowdfunding_list (1407-1413)
  • crowdfunding_create (1418-1437)
  • crowdfunding_detail (1395-1404)
  • create_donation_intent (1222-1254)
web/models.py (1)
web/forms.py (21)
  • Meta (281-310)
  • Meta (374-380)
  • Meta (384-413)
  • Meta (459-497)
  • Meta (508-547)
  • Meta (575-581)
  • Meta (585-609)
  • Meta (703-705)
  • Meta (808-828)
  • Meta (851-863)
  • Meta (869-885)
  • Meta (969-1006)
  • Meta (1012-1023)
  • Meta (1040-1045)
  • Meta (1111-1142)
  • Meta (1148-1169)
  • save (239-277)
  • save (633-658)
  • save (725-739)
  • save (1362-1375)
  • save (1536-1551)
🪛 Ruff (0.8.2)
web/forms.py

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

(RUF012)


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

(RUF012)


289-289: Trailing comma missing

Add trailing comma

(COM812)


294-294: Trailing comma missing

Add trailing comma

(COM812)


297-297: Trailing comma missing

Add trailing comma

(COM812)


303-303: Trailing comma missing

Add trailing comma

(COM812)


308-308: Trailing comma missing

Add trailing comma

(COM812)


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

(RUF012)


318-318: Missing return type annotation for private function clean_funding_goal

(ANN202)


321-321: Avoid specifying long messages outside the exception class

(TRY003)

web/models.py

34-34: Trailing comma missing

Add trailing comma

(COM812)


40-40: Trailing comma missing

Add trailing comma

(COM812)


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

(RUF012)


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

Add return type annotation: str

(ANN204)


57-57: Missing return type annotation for public function get_absolute_url

(ANN201)


63-63: Missing return type annotation for public function update_amount_raised

Add return type annotation: None

(ANN201)


78-78: Trailing comma missing

Add trailing comma

(COM812)


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

Add return type annotation: str

(ANN204)


2854-2854: Missing return type annotation for public function update_campaign_amount_on_donation

Add return type annotation: None

(ANN201)


2854-2854: Missing type annotation for function argument sender

(ANN001)


2854-2854: Unused function argument: sender

(ARG001)


2854-2854: Missing type annotation for function argument instance

(ANN001)


2854-2854: Missing type annotation for function argument created

(ANN001)


2854-2854: Unused function argument: created

(ARG001)


2854-2854: Missing type annotation for **kwargs

(ANN003)


2854-2854: Unused function argument: kwargs

(ARG001)


2860-2860: Missing return type annotation for public function update_campaign_amount_on_donation_delete

Add return type annotation: None

(ANN201)


2860-2860: Missing type annotation for function argument sender

(ANN001)


2860-2860: Unused function argument: sender

(ARG001)


2860-2860: Missing type annotation for function argument instance

(ANN001)


2860-2860: Missing type annotation for **kwargs

(ANN003)


2860-2860: Unused function argument: kwargs

(ARG001)

web/views.py

1222-1222: Missing return type annotation for public function create_donation_intent

(ANN201)


1222-1222: Missing type annotation for function argument request

(ANN001)


1222-1222: Missing type annotation for function argument campaign_id

(ANN001)


1250-1250: Do not catch blind exception: Exception

(BLE001)


1305-1305: Missing return type annotation for public function handle_successful_donation

Add return type annotation: None

(ANN201)


1305-1305: Missing type annotation for function argument payment_intent

(ANN001)


1341-1341: Do not catch blind exception: Exception

(BLE001)


1344-1344: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


1344-1344: Logging statement uses f-string

(G004)


1347-1347: Missing return type annotation for public function handle_failed_donation

Add return type annotation: None

(ANN201)


1347-1347: Missing type annotation for function argument payment_intent

(ANN001)


1357-1357: Logging statement uses f-string

(G004)


1358-1358: Do not catch blind exception: Exception

(BLE001)


1360-1360: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


1360-1360: Logging statement uses f-string

(G004)


1395-1395: Missing return type annotation for public function crowdfunding_detail

(ANN201)


1395-1395: Missing type annotation for function argument request

(ANN001)


1395-1395: Missing type annotation for function argument campaign_id

(ANN001)


1407-1407: Missing return type annotation for public function crowdfunding_list

(ANN201)


1407-1407: Missing type annotation for function argument request

(ANN001)


1418-1418: Missing return type annotation for public function crowdfunding_create

(ANN201)


1418-1418: Missing type annotation for function argument request

(ANN001)


1433-1433: Unnecessary else after return statement

Remove unnecessary else

(RET505)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Run Tests
  • GitHub Check: build-and-test
🔇 Additional comments (7)
web/models.py (4)

16-16: No issues with the new signals import.
Clean addition to prepare for the post_save and post_delete handlers.


72-87: No major issues in CampaignDonation.
This model effectively tracks donor details and amounts. The separation from the global Donation model makes sense if they serve different business cases, though any shared logic (e.g., user references or reward processing) could be consolidated if desired.

🧰 Tools
🪛 Ruff (0.8.2)

78-78: Trailing comma missing

Add trailing comma

(COM812)


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

Add return type annotation: str

(ANN204)


2853-2858: Add type annotations and rename unused parameters for clarity.
The same suggestion was previously raised for signals. Reiterating for completeness:

🧰 Tools
🪛 Ruff (0.8.2)

2854-2854: Missing return type annotation for public function update_campaign_amount_on_donation

Add return type annotation: None

(ANN201)


2854-2854: Missing type annotation for function argument sender

(ANN001)


2854-2854: Unused function argument: sender

(ARG001)


2854-2854: Missing type annotation for function argument instance

(ANN001)


2854-2854: Missing type annotation for function argument created

(ANN001)


2854-2854: Unused function argument: created

(ARG001)


2854-2854: Missing type annotation for **kwargs

(ANN003)


2854-2854: Unused function argument: kwargs

(ARG001)


2859-2862: Apply type annotations and rename unused parameters in the delete signal as well.
Mirrors prior feedback for uniformity with the post_save signal.

🧰 Tools
🪛 Ruff (0.8.2)

2860-2860: Missing return type annotation for public function update_campaign_amount_on_donation_delete

Add return type annotation: None

(ANN201)


2860-2860: Missing type annotation for function argument sender

(ANN001)


2860-2860: Unused function argument: sender

(ARG001)


2860-2860: Missing type annotation for function argument instance

(ANN001)


2860-2860: Missing type annotation for **kwargs

(ANN003)


2860-2860: Unused function argument: kwargs

(ARG001)

web/urls.py (2)

41-43: URL patterns for crowdfunding look good!

The URL patterns for listing, creating, and viewing campaigns follow good naming conventions and URL structure. These endpoints properly map to their corresponding view functions in views.py.


61-61: Consider relocating the donation intent URL to group with other crowdfunding URLs.

The URL for creating a donation intent is separated from the other crowdfunding URLs, which might make maintenance more difficult.

Consider moving this URL to be grouped with the other crowdfunding URLs for better organization:

-    path("crowdfunding/<int:campaign_id>/donate/", views.create_donation_intent, name="create_donation_intent"),

Move it to line 44 after the other crowdfunding URLs:

+    path("crowdfunding/<int:campaign_id>/donate/", views.create_donation_intent, name="create_donation_intent"),
web/views.py (1)

1378-1391: Well-implemented payment intent handling in webhook.

The implementation for different payment intent types is clear and well-structured. Good job branching the logic to handle both course and campaign payment intents.

Comment thread web/forms.py
if funding_goal <= 0:
raise forms.ValidationError("Funding goal must be greater than zero")
return funding_goal

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)

Add URL validation for video_url field.

The video_url field should have validation to ensure it contains a valid URL from trusted video sources like YouTube or Vimeo.

def clean_video_url(self) -> str:
    """Validate that video URL is from a trusted source."""
    video_url = self.cleaned_data.get("video_url")
    if video_url:
        import re
        # Check for YouTube or Vimeo URLs
        youtube_pattern = r"^(https?://)?(www\.)?(youtube\.com/watch\?v=|youtu\.be/)[a-zA-Z0-9_-]{11}.*$"
        vimeo_pattern = r"^(https?://)?(www\.)?vimeo\.com/[0-9]{8,}.*$"
        if not (re.match(youtube_pattern, video_url) or re.match(vimeo_pattern, video_url)):
            raise forms.ValidationError("Please enter a valid YouTube or Vimeo URL.")
    return video_url

Comment thread web/forms.py
Comment on lines +312 to +316
help_texts = {
"itemized_budget": "Enter a JSON object with budget items and costs",
"video_url": "Link to a video that explains your campaign",
"image": "Upload an image for your campaign (max size: 5MB)",
}
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)

Move help_texts inside the Meta class and add ClassVar annotation.

For consistency with Django's form convention, the help_texts dictionary should be moved inside the Meta class and annotated with typing.ClassVar.

 class CampaignForm(forms.ModelForm):
     class Meta:
         model = Campaign
         fields = ["title", "description", "funding_goal", "itemized_budget", "video_url", "image"]
         widgets: ClassVar[Dict[str, Any]] = {
             # ... widget definitions ...
         }
+        help_texts: ClassVar[Dict[str, str]] = {
+            "itemized_budget": "Enter a JSON object with budget items and costs",
+            "video_url": "Link to a video that explains your campaign",
+            "image": "Upload an image for your campaign (max size: 5MB)",
+        }

-    help_texts = {
-        "itemized_budget": "Enter a JSON object with budget items and costs",
-        "video_url": "Link to a video that explains your campaign",
-        "image": "Upload an image for your campaign (max size: 5MB)",
-    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
help_texts = {
"itemized_budget": "Enter a JSON object with budget items and costs",
"video_url": "Link to a video that explains your campaign",
"image": "Upload an image for your campaign (max size: 5MB)",
}
from typing import ClassVar, Dict, Any
from django import forms
class CampaignForm(forms.ModelForm):
class Meta:
model = Campaign
fields = ["title", "description", "funding_goal", "itemized_budget", "video_url", "image"]
widgets: ClassVar[Dict[str, Any]] = {
# ... widget definitions ...
}
help_texts: ClassVar[Dict[str, str]] = {
"itemized_budget": "Enter a JSON object with budget items and costs",
"video_url": "Link to a video that explains your campaign",
"image": "Upload an image for your campaign (max size: 5MB)",
}
# Removed the previous class-level help_texts attribute
# (if present outside the Meta class)
🧰 Tools
🪛 Ruff (0.8.2)

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

(RUF012)

Comment thread web/models.py
Comment on lines +28 to +40
class Campaign(models.Model):
"""
Model representing a crowdfunding campaign created by a teacher.
"""

teacher = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="campaigns", help_text="Teacher who created the campaign."
)
title = models.CharField(max_length=255, help_text="Campaign title")
description = models.TextField(help_text="Detailed campaign description")
funding_goal = models.DecimalField(max_digits=10, decimal_places=2, help_text="Target funding amount in dollars")
amount_raised = models.DecimalField(
max_digits=10, decimal_places=2, default=0, help_text="Total funds raised so far"
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 using an atomic update strategy for amount_raised.
Currently, update_amount_raised() recalculates the total from all donations, which may cause concurrency issues under high load if donations are saved in quick succession. Although recalculating might be acceptable for smaller volumes, an atomic approach or incremental updates could enhance performance and consistency for larger-scale scenarios.

🧰 Tools
🪛 Ruff (0.8.2)

34-34: Trailing comma missing

Add trailing comma

(COM812)


40-40: Trailing comma missing

Add trailing comma

(COM812)

Comment thread web/models.py
Comment on lines +54 to +71
def __str__(self):
return self.title

def get_absolute_url(self):
"""
Returns the URL to access a particular campaign's detail view.
"""
return reverse("crowdfunding_detail", kwargs={"campaign_id": self.id})

def update_amount_raised(self):
"""
Recalculate and update the total amount raised from related donations.
"""
total = self.donations.aggregate(total=models.Sum("amount"))["total"] or 0
self.amount_raised = total
self.save()


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)

Add return type annotations to improve clarity.
The functions __str__, get_absolute_url, and update_amount_raised lack return type annotations. Adding them (e.g., -> str or -> None) can help meet static analysis recommendations and make the code more comprehensible.

🧰 Tools
🪛 Ruff (0.8.2)

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

Add return type annotation: str

(ANN204)


57-57: Missing return type annotation for public function get_absolute_url

(ANN201)


63-63: Missing return type annotation for public function update_amount_raised

Add return type annotation: None

(ANN201)

Comment thread web/views.py
Comment on lines +1407 to +1414
def crowdfunding_list(request):
"""
Display all active crowdfunding campaigns.
Active campaigns are those that are approved and live.
"""
campaigns = Campaign.objects.filter(approved=True, live=True).order_by("-created_at")
return render(request, "crowdfunding_list.html", {"campaigns": campaigns})

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)

Add type annotations to crowdfunding_list.

The function is well-implemented but needs type annotations for better code maintainability.

-def crowdfunding_list(request):
+def crowdfunding_list(request: HttpRequest) -> HttpResponse:
    """
    Display all active crowdfunding campaigns.
    Active campaigns are those that are approved and live.
    """
    campaigns = Campaign.objects.filter(approved=True, live=True).order_by("-created_at")
    return render(request, "crowdfunding_list.html", {"campaigns": campaigns})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def crowdfunding_list(request):
"""
Display all active crowdfunding campaigns.
Active campaigns are those that are approved and live.
"""
campaigns = Campaign.objects.filter(approved=True, live=True).order_by("-created_at")
return render(request, "crowdfunding_list.html", {"campaigns": campaigns})
def crowdfunding_list(request: HttpRequest) -> HttpResponse:
"""
Display all active crowdfunding campaigns.
Active campaigns are those that are approved and live.
"""
campaigns = Campaign.objects.filter(approved=True, live=True).order_by("-created_at")
return render(request, "crowdfunding_list.html", {"campaigns": campaigns})
🧰 Tools
🪛 Ruff (0.8.2)

1407-1407: Missing return type annotation for public function crowdfunding_list

(ANN201)


1407-1407: Missing type annotation for function argument request

(ANN001)

Comment thread web/views.py
Comment on lines +1416 to +1438
@login_required
@teacher_required
def crowdfunding_create(request):
"""
View to allow teachers to create a new crowdfunding campaign.
"""
if request.method == "POST":
form = CampaignForm(request.POST, request.FILES)
if form.is_valid():
campaign = form.save(commit=False)
campaign.teacher = request.user
# Set initial approval status (set to False if admin approval is required)
campaign.approved = True # or False if approval workflow is needed
campaign.live = True # or False if campaigns shouldn't be live immediately
campaign.save()
messages.success(request, "Your crowdfunding campaign has been created successfully!")
return redirect("crowdfunding_detail", campaign_id=campaign.id)
else:
messages.error(request, "Please correct the errors below.")
else:
form = CampaignForm()
return render(request, "crowdfunding_create.html", {"form": form})

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)

Improve implementation of crowdfunding_create with type annotations and fix unnecessary else.

The function is well-implemented with proper decorators and error handling, but could use some improvements:

@login_required
@teacher_required
-def crowdfunding_create(request):
+def crowdfunding_create(request: HttpRequest) -> HttpResponse:
    """
    View to allow teachers to create a new crowdfunding campaign.
    """
    if request.method == "POST":
        form = CampaignForm(request.POST, request.FILES)
        if form.is_valid():
            campaign = form.save(commit=False)
            campaign.teacher = request.user
            # Set initial approval status (set to False if admin approval is required)
            campaign.approved = True  # or False if approval workflow is needed
            campaign.live = True  # or False if campaigns shouldn't be live immediately
            campaign.save()
            messages.success(request, "Your crowdfunding campaign has been created successfully!")
            return redirect("crowdfunding_detail", campaign_id=campaign.id)
-        else:
-            messages.error(request, "Please correct the errors below.")
+        messages.error(request, "Please correct the errors below.")
    else:
        form = CampaignForm()
    return render(request, "crowdfunding_create.html", {"form": form})

The else after the return statement is unnecessary since if the form is valid, the function will return before reaching the line where the error message is added.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@login_required
@teacher_required
def crowdfunding_create(request):
"""
View to allow teachers to create a new crowdfunding campaign.
"""
if request.method == "POST":
form = CampaignForm(request.POST, request.FILES)
if form.is_valid():
campaign = form.save(commit=False)
campaign.teacher = request.user
# Set initial approval status (set to False if admin approval is required)
campaign.approved = True # or False if approval workflow is needed
campaign.live = True # or False if campaigns shouldn't be live immediately
campaign.save()
messages.success(request, "Your crowdfunding campaign has been created successfully!")
return redirect("crowdfunding_detail", campaign_id=campaign.id)
else:
messages.error(request, "Please correct the errors below.")
else:
form = CampaignForm()
return render(request, "crowdfunding_create.html", {"form": form})
@login_required
@teacher_required
def crowdfunding_create(request: HttpRequest) -> HttpResponse:
"""
View to allow teachers to create a new crowdfunding campaign.
"""
if request.method == "POST":
form = CampaignForm(request.POST, request.FILES)
if form.is_valid():
campaign = form.save(commit=False)
campaign.teacher = request.user
# Set initial approval status (set to False if admin approval is required)
campaign.approved = True # or False if approval workflow is needed
campaign.live = True # or False if campaigns shouldn't be live immediately
campaign.save()
messages.success(request, "Your crowdfunding campaign has been created successfully!")
return redirect("crowdfunding_detail", campaign_id=campaign.id)
messages.error(request, "Please correct the errors below.")
else:
form = CampaignForm()
return render(request, "crowdfunding_create.html", {"form": form})
🧰 Tools
🪛 Ruff (0.8.2)

1418-1418: Missing return type annotation for public function crowdfunding_create

(ANN201)


1418-1418: Missing type annotation for function argument request

(ANN001)


1433-1433: Unnecessary else after return statement

Remove unnecessary else

(RET505)

Comment thread web/views.py
Comment on lines +1305 to +1345
def handle_successful_donation(payment_intent):
"""
Handle a successful donation payment.
Create a Donation record and update the campaign's amount raised.
"""
try:
campaign_id = payment_intent.metadata.get("campaign_id")
user_id = payment_intent.metadata.get("user_id")
if not campaign_id or not user_id:
# Not a donation payment; you might log this or ignore.
return

# Convert amount from cents to dollars.
# donation_amount = Decimal(payment_intent.amount_received) / 100

# Retrieve campaign and user.
campaign = Campaign.objects.get(id=campaign_id)
# user = User.objects.get(id=user_id)

# # Create the donation record.
# donation = Donation.objects.create(
# campaign=campaign,
# donor_name=user.get_full_name() or user.username,
# donor_email=user.email,
# amount=donation_amount,
# status="completed", # or set appropriate status if your Donation model uses one
# stripe_payment_intent_id=payment_intent.id,
# )

# Update the campaign's amount raised.
campaign.update_amount_raised()

# Optionally, you can send a notification email to the donor or campaign teacher here.
# For example:
# send_donation_confirmation_email(user, campaign, donation)

except Exception as e:
# Log error if needed.
logger = logging.getLogger(__name__)
logger.error(f"Error handling successful donation: {e}")

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.

⚠️ Potential issue

Fix incomplete implementation in handle_successful_donation.

The implementation has several issues:

  1. Contains commented out code that appears to be part of the implementation
  2. Missing type annotations
  3. Uses generic exception handling and f-string in logging

The function has incomplete/commented out code for creating donation records, which needs to be implemented:

-def handle_successful_donation(payment_intent):
+def handle_successful_donation(payment_intent) -> None:
    """
    Handle a successful donation payment.
    Create a Donation record and update the campaign's amount raised.
    """
    try:
        campaign_id = payment_intent.metadata.get("campaign_id")
        user_id = payment_intent.metadata.get("user_id")
        if not campaign_id or not user_id:
            # Not a donation payment; you might log this or ignore.
            return

        # Convert amount from cents to dollars.
-        # donation_amount = Decimal(payment_intent.amount_received) / 100
+        donation_amount = Decimal(payment_intent.amount_received) / 100

        # Retrieve campaign and user.
        campaign = Campaign.objects.get(id=campaign_id)
-        # user = User.objects.get(id=user_id)
+        user = User.objects.get(id=user_id) if user_id else None

-        # # Create the donation record.
-        # donation = Donation.objects.create(
-        #     campaign=campaign,
-        #     donor_name=user.get_full_name() or user.username,
-        #     donor_email=user.email,
-        #     amount=donation_amount,
-        #     status="completed",  # or set appropriate status if your Donation model uses one
-        #     stripe_payment_intent_id=payment_intent.id,
-        # )
+        # Create the donation record.
+        donation = CampaignDonation.objects.create(
+            campaign=campaign,
+            donor_name=user.get_full_name() or user.username if user else "Anonymous",
+            donor_email=user.email if user else None,
+            amount=donation_amount,
+            status="completed",
+            stripe_payment_intent_id=payment_intent.id,
+        )

        # Update the campaign's amount raised.
        campaign.update_amount_raised()

        # Optionally, you can send a notification email to the donor or campaign teacher here.
        # For example:
        # send_donation_confirmation_email(user, campaign, donation)

-    except Exception as e:
+    except (Campaign.DoesNotExist, User.DoesNotExist) as e:
+        logger.error("Campaign or User not found: %s", str(e))
+    except Exception as e:
        # Log error if needed.
        logger = logging.getLogger(__name__)
-        logger.error(f"Error handling successful donation: {e}")
+        logger.exception("Error handling successful donation")

The model referenced in the code should be CampaignDonation based on the context, rather than Donation. Make sure this model exists and is properly imported.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def handle_successful_donation(payment_intent):
"""
Handle a successful donation payment.
Create a Donation record and update the campaign's amount raised.
"""
try:
campaign_id = payment_intent.metadata.get("campaign_id")
user_id = payment_intent.metadata.get("user_id")
if not campaign_id or not user_id:
# Not a donation payment; you might log this or ignore.
return
# Convert amount from cents to dollars.
# donation_amount = Decimal(payment_intent.amount_received) / 100
# Retrieve campaign and user.
campaign = Campaign.objects.get(id=campaign_id)
# user = User.objects.get(id=user_id)
# # Create the donation record.
# donation = Donation.objects.create(
# campaign=campaign,
# donor_name=user.get_full_name() or user.username,
# donor_email=user.email,
# amount=donation_amount,
# status="completed", # or set appropriate status if your Donation model uses one
# stripe_payment_intent_id=payment_intent.id,
# )
# Update the campaign's amount raised.
campaign.update_amount_raised()
# Optionally, you can send a notification email to the donor or campaign teacher here.
# For example:
# send_donation_confirmation_email(user, campaign, donation)
except Exception as e:
# Log error if needed.
logger = logging.getLogger(__name__)
logger.error(f"Error handling successful donation: {e}")
def handle_successful_donation(payment_intent) -> None:
"""
Handle a successful donation payment.
Create a Donation record and update the campaign's amount raised.
"""
try:
campaign_id = payment_intent.metadata.get("campaign_id")
user_id = payment_intent.metadata.get("user_id")
if not campaign_id or not user_id:
# Not a donation payment; you might log this or ignore.
return
# Convert amount from cents to dollars.
donation_amount = Decimal(payment_intent.amount_received) / 100
# Retrieve campaign and user.
campaign = Campaign.objects.get(id=campaign_id)
user = User.objects.get(id=user_id) if user_id else None
# Create the donation record.
donation = CampaignDonation.objects.create(
campaign=campaign,
donor_name=user.get_full_name() or user.username if user else "Anonymous",
donor_email=user.email if user else None,
amount=donation_amount,
status="completed",
stripe_payment_intent_id=payment_intent.id,
)
# Update the campaign's amount raised.
campaign.update_amount_raised()
# Optionally, you can send a notification email to the donor or campaign teacher here.
# For example:
# send_donation_confirmation_email(user, campaign, donation)
except (Campaign.DoesNotExist, User.DoesNotExist) as e:
logger.error("Campaign or User not found: %s", str(e))
except Exception as e:
# Log error if needed.
logger = logging.getLogger(__name__)
logger.exception("Error handling successful donation")
🧰 Tools
🪛 Ruff (0.8.2)

1305-1305: Missing return type annotation for public function handle_successful_donation

Add return type annotation: None

(ANN201)


1305-1305: Missing type annotation for function argument payment_intent

(ANN001)


1341-1341: Do not catch blind exception: Exception

(BLE001)


1344-1344: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


1344-1344: Logging statement uses f-string

(G004)

@A1L13N A1L13N added the fc:9 label Apr 2, 2025
@A1L13N
Copy link
Copy Markdown
Contributor

A1L13N commented Apr 19, 2025

changes were requested 3 weeks ago and no further changes have been made

@A1L13N A1L13N closed this Apr 19, 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.

Classroom Crowdfunding Feature

2 participants