Skip to content

Conversation

SahilDhillon21
Copy link
Contributor

@SahilDhillon21 SahilDhillon21 commented Jan 12, 2025

User description

fixes #2560

A notification system is created for the users that looks like this:
screen-capture (1).webm

However, in order to fully close this issue we need to create a cron job via some scheduler to send these reminders to users.
If you can also review #3209 then it can be easily incorporated


PR Type

Enhancement, Tests


Description

  • Introduced a notification system with dynamic dropdown UI.

  • Added backend support for fetching and marking notifications as read.

  • Implemented daily check-in reminders via a management command.

  • Created notification signals for various user activities.


Changes walkthrough 📝

Relevant files
Enhancement
5 files
header.html
Added notification dropdown UI and dynamic behavior.         
+173/-1 
daily_checkin_reminder.py
Added management command for daily check-in reminders.     
+23/-0   
models.py
Added Notification model and organization check-ins field.
+31/-0   
notification_signals.py
Implemented signals to create notifications for user activities.
+80/-0   
user.py
Added views for fetching and marking notifications as read.
+30/-0   
Configuration changes
2 files
urls.py
Added routes for fetching and marking notifications as read.
+4/-0     
admin.py
Registered Notification model in admin panel.                       
+2/-0     
Dependencies
1 files
0231_organization_check_ins_enabled_notification.py
Created migration for Notification model and organization check-ins.
+56/-0   

Need help?
  • Type /help how to ... in the comments thread for any questions about PR-Agent usage.
  • Check out the documentation for more information.
  • Summary by CodeRabbit

    • New Features
      • Introduced a comprehensive notifications system on the website with a bell icon and dropdown that displays real-time updates and an unread counter.
      • Enabled users to mark notifications as read and delete individual alerts.
      • Added automated daily check-in reminders for eligible users.
      • Enhanced notifications triggered by key user events, improving timely updates and overall engagement.

    DonnieBLT
    DonnieBLT previously approved these changes Jan 16, 2025
    @SahilDhillon21 SahilDhillon21 marked this pull request as ready for review January 16, 2025 11:36
    @SahilDhillon21
    Copy link
    Contributor Author

    I have added the command to the run_daily script, and also an optional link field to each notification. Should we add other notifications such as issue likes/comments, streaks etc?

    @DonnieBLT DonnieBLT enabled auto-merge (squash) January 19, 2025 16:05
    @DonnieBLT
    Copy link
    Collaborator

    Yes notifications for all of those will be good. Thanks!

    auto-merge was automatically disabled January 25, 2025 09:46

    Head branch was pushed to by a user without write access

    @SahilDhillon21
    Copy link
    Contributor Author

    Fixed all the issues, please verify!

    @SahilDhillon21
    Copy link
    Contributor Author

    Also added the checkin_reminder_notification command in the run_daily schedule

    @SahilDhillon21
    Copy link
    Contributor Author

    @DonnieBLT Can this please be merged since it frequently gets merge conflicts due to the model addition

    Copy link
    Collaborator

    @DonnieBLT DonnieBLT left a comment

    Choose a reason for hiding this comment

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

    we would only want to show checkin reminders to users who belong to an organization that has check ins turned on.

    @DonnieBLT
    Copy link
    Collaborator

    I merged the conflict, hopefully that was right.

    @SahilDhillon21
    Copy link
    Contributor Author

    Thanks a lot for that! I'll make sure to finish this up as soon as possible

    Copy link
    Collaborator

    @DonnieBLT DonnieBLT left a comment

    Choose a reason for hiding this comment

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

    • merge migrations
    • fix conflicts

    Copy link
    Contributor

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    🎫 Ticket compliance analysis 🔶

    2560 - Partially compliant

    Compliant requirements:

    • Implement a daily prompt feature to remind users to log their stand-up report.
    • Provide options for notifications via in-app.

    Non-compliant requirements:

    • Provide options for notifications via email.

    Requires further human verification:

    • Ensure the feature is automated (e.g., using a cron job or scheduler).
    ⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
    🧪 No relevant tests
    🔒 Security concerns

    CSRF vulnerability:
    The JavaScript code for fetching and marking notifications as read relies on the CSRF token, but there is no explicit validation or error handling for CSRF token mismatches, which could lead to potential vulnerabilities.

    ⚡ Recommended focus areas for review

    Possible Issue

    The fetchNotifications function in the JavaScript code does not handle cases where the data.notifications field is missing or malformed, which could lead to runtime errors.

    document.addEventListener("DOMContentLoaded", function () {
        const notificationButton = document.getElementById("notificationButton");
        const notificationDropdown = document.getElementById("notificationDropdown");
        const unreadCount = document.getElementById("unreadCount");
        const notificationList = document.getElementById("notificationList");
    
        async function fetchNotifications() {
            try {
                const response = await fetch("{% url 'fetch_notifications' %}", {
                    method: "GET",
                    headers: {
                        "X-CSRFToken": "{{ csrf_token }}",
                    },
                });
    
                if (!response.ok) {
                    throw new Error("Failed to fetch notifications");
                }
    
                const data = await response.json();
                const notifications = data.notifications;
    
                // Clear existing notifications
                notificationList.innerHTML = "";
    
                // Update unread count
                const unreadNotifications = notifications.filter(notification => !notification.is_read);
                if (unreadNotifications.length > 0) {
                    unreadCount.textContent = unreadNotifications.length;
                    unreadCount.classList.remove("hidden"); // Show the badge
                } else {
                    unreadCount.textContent = ""; // Ensure no text is displayed
                    unreadCount.classList.add("hidden"); // Hide the badge
                }
    
                // Populate notification list
                if (notifications.length === 0) {
                    notificationList.innerHTML = "<p class='text-sm text-gray-500'>No new notifications.</p>";
                    return;
                }
    
                notifications.forEach(notification => {
                    const notificationItem = document.createElement("div");
                    notificationItem.classList.add(
                        "flex", "items-start", "gap-3", "p-2", "rounded-lg",
                        !notification.is_read ? "bg-gray-50" : "border", // Differentiate read/unread
                        !notification.is_read ? "font-semibold" : "font-normal" // Bold for unread
                    );
    
                    notificationItem.innerHTML = `
                        <div class="flex-shrink-0">
                            ${getNotificationIcon(notification.notification_type)}
                        </div>
                        <div class="flex-1">
                            <p class="text-sm">${notification.message}</p>
                            <p class="text-xs text-gray-500">${timeSince(notification.created_at)} ago</p>
                        </div>
                    `;
    
                    notificationList.appendChild(notificationItem);
                });
            } catch (error) {
                console.error(error);
            }
        }
    
        // Helper function to get notification icon based on type
        function getNotificationIcon(type) {
            switch (type) {
                case "alert":
                    return '<i class="fa fa-exclamation-circle text-red-500 fa-lg"></i>';
                case "reminder":
                    return '<i class="fa fa-clock text-yellow-500 fa-lg"></i>';
                case "promo":
                    return '<i class="fa fa-tag text-green-500 fa-lg"></i>';
                case "reward":
                    return '<i class="fa fa-gift text-purple-500 fa-lg"></i>';
                default:
                    return '<i class="fa fa-info-circle text-blue-500 fa-lg"></i>';
            }
        }
    
        // Helper function to calculate time since a date
        function timeSince(dateString) {
            const date = new Date(dateString);
            const now = new Date();
            const seconds = Math.floor((now - date) / 1000);
    
            let interval = Math.floor(seconds / 31536000);
            if (interval > 1) return interval + " years";
    
            interval = Math.floor(seconds / 2592000);
            if (interval > 1) return interval + " months";
    
            interval = Math.floor(seconds / 86400);
            if (interval > 1) return interval + " days";
    
            interval = Math.floor(seconds / 3600);
            if (interval > 1) return interval + " hours";
    
            interval = Math.floor(seconds / 60);
            if (interval > 1) return interval + " minutes";
    
            return Math.floor(seconds) + " seconds";
        }
    
        // Fetch notifications on page load to initialize the unread count
        fetchNotifications();
    
        // Event listener for notification button
        notificationButton.addEventListener("click", function () {
            notificationDropdown.classList.toggle("hidden");
    
            if (!notificationDropdown.classList.contains("hidden")) {
                // Fetch notifications when dropdown is opened
                fetchNotifications();
    
                // Mark all notifications as read
                fetch("{% url 'mark_all_read' %}", {
                    method: "PATCH",
                    headers: {
                        "X-CSRFToken": "{{ csrf_token }}",
                        "Content-Type": "application/json",
                    },
                })
                    .then(response => response.json())
                    .then(data => {
                        if (data.status === "success") {
                            // Reset unread count
                            unreadCount.textContent = "";
                            unreadCount.classList.add("hidden"); // Hide the badge
                            // Update styles for all notifications to reflect "read" state
                            document.querySelectorAll(".bg-gray-50").forEach(el => {
                                el.classList.remove("bg-gray-50", "font-semibold");
                                el.classList.add("border", "font-normal");
                            });
                        }
                    });
            }
        });
    
        // Close dropdown when clicking outside
        document.addEventListener("click", function (event) {
            const isClickInside = notificationButton.contains(event.target) || notificationDropdown.contains(event.target);
            if (!isClickInside) {
                notificationDropdown.classList.add("hidden");
            }
        });
    });
    API Error Handling

    The mark_as_read view does not handle cases where the PATCH request body is malformed or missing required fields, which could lead to unexpected behavior.

    @login_required
    def fetch_notifications(request):
        notifications = Notification.objects.filter(user=request.user).order_by("is_read", "-created_at")
    
        notifications_data = [
            {
                "message": notification.message,
                "created_at": notification.created_at,
                "is_read": notification.is_read,
                "notification_type": notification.notification_type,
                "link": notification.link,
            }
            for notification in notifications
        ]
    
        return JsonResponse({"notifications": notifications_data}, safe=False)
    
    
    @login_required
    def mark_as_read(request):
        if request.method == "PATCH":
            try:
                notifications = Notification.objects.filter(user=request.user, is_read=False)
                notifications.update(is_read=True)
                return JsonResponse({"status": "success"})
            except Notification.DoesNotExist:
                return JsonResponse({"status": "error", "message": "Unable to fetch notifications"}, status=404)

    Copy link
    Contributor

    PR Code Suggestions ✨

    @SahilDhillon21
    Copy link
    Contributor Author

    Does this look fine @DonnieBLT ? Since users are already connected to organizations, there was no need to connect the DailyStatusReport directly to orgs as we could access it from report -> user -> send notif if user's org has checkins enabled
    Can still add if needed

    @DonnieBLT
    Copy link
    Collaborator

    @coderabbitai is there a new pr for this? Can we close this one?

    Copy link
    Contributor

    coderabbitai bot commented Mar 15, 2025

    Tip

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

    🧩 Analysis chain

    I need to check if there's a new PR that replaces this one before answering.


    🏁 Script executed:

    #!/bin/bash
    # Check status of the current PR
    echo "Checking status of PR #3216"
    gh pr view 3216 --json state,title,url
    
    # Check for newer PRs that might be related to notifications
    echo -e "\nSearching for related open PRs"
    gh pr list --search "notifications in:title,body" --state open --json number,title,url,createdAt | jq '[.[] | select(.number != 3216)]'
    

    Length of output: 631


    @DonnieBLT Based on my investigation, PR #3216 "Notification system" is still open and appears to be the current PR for implementing the notification system. I don't see any newer PRs that specifically replace this functionality.

    The only other open PR mentioning notifications (#3809) is about Bounty Bot enhancements and Slack integration, which seems to be a different feature.

    From the comment chain, it appears SahilDhillon21 has been working on addressing your feedback and was waiting for your final approval. Unless you're aware of another PR that specifically replaces this functionality, I would recommend keeping this PR open and reviewing the current implementation to determine if it's ready to be merged.

    @DonnieBLT
    Copy link
    Collaborator

    Oh @SahilDhillon21 I see this is current! Will review

    Copy link
    Contributor

    coderabbitai bot commented Mar 15, 2025

    Walkthrough

    This pull request integrates a notification system into the application. It introduces new URL endpoints and view functions to fetch, mark as read, and delete notifications, along with corresponding JavaScript functionality in the header for real-time user interaction. Additionally, it creates a new Notification model (with its database migration), extends the Organization model, and registers Notification in the admin interface. New management commands and Django signal receivers have been added to generate notifications—whether from daily check-ins or various user events—ensuring that notifications are created and managed throughout the system.

    Changes

    File(s) Change Summary
    blt/urls.py Added URL patterns and imports for notification endpoints: fetch, mark all as read, and delete notifications.
    website/admin.py Imported and registered the Notification model with Django admin.
    website/management/commands/daily_checkin_reminder.py
    website/management/commands/run_daily.py
    Introduced a new management command to send daily check-in reminders and integrated error handling for its execution.
    website/migrations/...0231_organization_check_ins_enabled_notification.py
    website/models.py
    Added a Boolean field (check_ins_enabled) to the Organization model and created the Notification model with fields (message, timestamps, is_read, is_deleted, notification_type, link, user) and helper methods.
    website/notification_signals.py Implemented Django signal receivers that generate notifications for events such as points rewards, join requests, kudos, post creation, and badge awards.
    website/templates/includes/header.html Updated the header template to include a notifications dropdown with a bell icon, unread count, and JavaScript functions to fetch, format, and delete notifications.
    website/views/user.py Added view functions for fetching notifications (returning JSON), marking notifications as read, and deleting notifications (marking them as deleted).

    Sequence Diagram(s)

    sequenceDiagram
        participant User
        participant Browser
        participant Server as Django View
        participant DB as Database
    
        User->>Browser: Clicks notification button
        Browser->>Server: GET /notifications/fetch/
        Server->>DB: Query unread notifications
        DB-->>Server: Return notifications list
        Server-->>Browser: JSON response with notifications
        Browser->>User: Renders notification dropdown
    
        User->>Browser: Click delete on a notification
        Browser->>Server: DELETE /notifications/delete_notification/<id>
        Server->>DB: Mark notification as deleted
        DB-->>Server: Confirmation
        Server-->>Browser: Success response
        Browser->>User: Update notification list
    
    Loading
    sequenceDiagram
        participant CLI
        participant RunDailyCmd as run_daily Command
        participant DailyCheck as daily_checkin_reminder Command
        participant DB as Database
    
        CLI->>RunDailyCmd: Execute run_daily command
        RunDailyCmd->>DailyCheck: Call daily_checkin_reminder (within try block)
        DailyCheck->>DB: Query user profiles with check-ins enabled
        DB-->>DailyCheck: Return eligible user list
        DailyCheck->>DB: Bulk create Notification records
        DB-->>DailyCheck: Confirmation
        DailyCheck-->>RunDailyCmd: Return success message
        RunDailyCmd->>CLI: Print final status
    
    Loading
    sequenceDiagram
        participant Event as Trigger (e.g., Kudos/Post/etc.)
        participant SignalReceiver as Django Signal Handler
        participant DB as Database
    
        Event->>SignalReceiver: Emit Django signal on event creation
        SignalReceiver->>DB: Create a Notification record
        DB-->>SignalReceiver: Notification saved
        SignalReceiver-->>Event: (Notification available for later fetch)
    
    Loading

    Tip

    ⚡🧪 Multi-step agentic review comment chat (experimental)
    • We're introducing multi-step agentic chat in review comments. This experimental feature enhances review discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments.
      - To enable this feature, set early_access to true under in the settings.
    ✨ 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 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.

    @DonnieBLT
    Copy link
    Collaborator

    @coderabbitai please review and approve if it looks good

    @DonnieBLT DonnieBLT enabled auto-merge (squash) March 15, 2025 07:08
    @DonnieBLT DonnieBLT dismissed their stale review March 15, 2025 07:08

    Was updated

    Copy link
    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

    🧹 Nitpick comments (15)
    website/management/commands/daily_checkin_reminder.py (1)

    6-23: Command implementation looks good with a few potential improvements.

    The command follows Django's management command pattern and correctly creates notification objects for users who belong to teams with check-ins enabled.

    Some suggestions to consider:

    1. Add validation to handle the case when userprofiles_with_checkins is empty
    2. Consider adding a timestamp check to prevent duplicate reminders on the same day
    3. Use Django's reverse() function for generating the URL instead of hardcoding "/add-sizzle-checkin/"
    4. Consider checking if users already have pending reminders before creating new ones
     def handle(self, *args, **options):
         userprofiles_with_checkins = UserProfile.objects.filter(team__check_ins_enabled=True)
    +    if not userprofiles_with_checkins.exists():
    +        self.stdout.write(self.style.WARNING("No users found with check-ins enabled."))
    +        return
    +        
         notifications = [
             Notification(
                 user=userprofile.user,
                 message=f"This is a reminder to add your daily check-in for {userprofile.team.name}",
                 notification_type="reminder",
    -            link="/add-sizzle-checkin/",
    +            link=reverse("add_sizzle_checkin"),
             )
             for userprofile in userprofiles_with_checkins
         ]
         Notification.objects.bulk_create(notifications)
         self.stdout.write(
             self.style.SUCCESS(f"Sent check-in reminder notifications to {len(userprofiles_with_checkins)} users.")
         )
    blt/urls.py (1)

    638-640: URL patterns for notification endpoints look good with minor consistency issue.

    The notification endpoints are properly configured. For consistency with other URL patterns in this file, consider adding a trailing slash to the mark_all_read endpoint.

     path("notifications/fetch/", fetch_notifications, name="fetch_notifications"),
    -path("notifications/mark_all_read", mark_as_read, name="mark_all_read"),
    +path("notifications/mark_all_read/", mark_as_read, name="mark_all_read"),
     path("notifications/delete_notification/<int:notification_id>", delete_notification, name="delete_notification"),
    website/models.py (1)

    2304-2329: Notification model implementation is well-structured

    The Notification model has all the necessary fields to support the notification system requirements, with appropriate field types and constraints. The soft delete implementation is a good practice for preserving data while allowing users to "delete" notifications from their view.

    One suggestion for improvement:

    Consider adding an index on the user and is_deleted fields since these will be frequently queried together:

    class Meta:
        ordering = ["is_read", "-created_at"]
    +   indexes = [
    +       models.Index(fields=["user", "is_deleted"], name="notif_user_deleted_idx"),
    +   ]
    website/views/user.py (3)

    1124-1141: Consider implementing pagination for notifications

    The fetch_notifications function returns all notifications for a user in a single JSON response. For users with many notifications, this could lead to performance issues.

    Consider implementing pagination to limit the number of notifications fetched at once:

    @login_required
    def fetch_notifications(request):
    +   page = int(request.GET.get('page', 1))
    +   page_size = int(request.GET.get('page_size', 10))
    +   
    +   start = (page - 1) * page_size
    +   end = page * page_size
    +   
        notifications = Notification.objects.filter(user=request.user, is_deleted=False).order_by("is_read", "-created_at")
    +   total_count = notifications.count()
    +   notifications = notifications[start:end]
    
        notifications_data = [
            {
                "id": notification.id,
                "message": notification.message,
                "created_at": notification.created_at,
                "is_read": notification.is_read,
                "notification_type": notification.notification_type,
                "link": notification.link,
            }
            for notification in notifications
        ]
    
    -   return JsonResponse({"notifications": notifications_data}, safe=False)
    +   return JsonResponse({
    +       "notifications": notifications_data,
    +       "pagination": {
    +           "total": total_count,
    +           "page": page,
    +           "page_size": page_size,
    +           "total_pages": (total_count + page_size - 1) // page_size
    +       }
    +   }, safe=False)

    1143-1161: Simplify mark_as_read implementation

    The function has unnecessary JSON parsing that's not used in the function logic.

    @login_required
    def mark_as_read(request):
        if request.method == "PATCH":
            try:
    -           if request.body and request.content_type == "application/json":
    -               try:
    -                   json.loads(request.body)
    -               except json.JSONDecodeError:
    -                   return JsonResponse({"status": "error", "message": "Invalid JSON in request body"}, status=400)
    
                notifications = Notification.objects.filter(user=request.user, is_read=False)
                notifications.update(is_read=True)
                return JsonResponse({"status": "success"})
            except Exception as e:
                logger.error(f"Error marking notifications as read: {e}")
                return JsonResponse(
                    {"status": "error", "message": "An error occured while marking notifications as read"}, status=400
                )

    1163-1180: Consider using transaction for delete_notification

    Using a database transaction would ensure atomicity for this operation.

    @login_required
    def delete_notification(request, notification_id):
        if request.method == "DELETE":
            try:
    +           with transaction.atomic():
                    notification = get_object_or_404(Notification, id=notification_id, user=request.user)
                    notification.is_deleted = True
                    notification.save()
    
                return JsonResponse({"status": "success", "message": "Notification deleted successfully"})
            except Exception as e:
                logger.error(f"Error deleting notification: {e}")
                return JsonResponse(
                    {"status": "error", "message": "An error occured while deleting notification, please try again."},
                    status=400,
                )
        else:
            return JsonResponse({"status": "error", "message": "Invalid request method"}, status=405)

    Additionally, don't forget to import the transaction module:

    from django.db.models import Count, F, Q, Sum
    +from django.db import transaction
    from django.db.models.functions import ExtractMonth
    website/templates/includes/header.html (5)

    123-144: UI implementation looks good but needs cross-browser testing

    The notification dropdown UI structure is well-organized using appropriate HTML elements and Tailwind CSS classes. The bell icon with an unread count badge is a standard pattern that users will recognize.

    Consider adding ARIA attributes for accessibility:

    -<button id="notificationButton"
    -        class="relative p-2 rounded-full hover:bg-gray-200 transition-colors duration-200">
    +<button id="notificationButton"
    +        class="relative p-2 rounded-full hover:bg-gray-200 transition-colors duration-200"
    +        aria-label="Notifications"
    +        aria-haspopup="true"
    +        aria-expanded="false">

    Please verify this implementation works correctly across different browsers, especially for older versions of Safari and Firefox.


    707-720: Notification type icon mapping is well-implemented

    The function provides appropriate icons for different notification types.

    Consider adding a fallback default icon at the end of the function to handle any future notification types:

    function getNotificationIcon(type) {
        switch (type) {
            case "alert":
                return '<i class="fa fa-exclamation-circle text-red-500 fa-lg"></i>';
            case "reminder":
                return '<i class="fa fa-clock text-yellow-500 fa-lg"></i>';
            case "promo":
                return '<i class="fa fa-tag text-green-500 fa-lg"></i>';
            case "reward":
                return '<i class="fa fa-gift text-purple-500 fa-lg"></i>';
            default:
                return '<i class="fa fa-info-circle text-blue-500 fa-lg"></i>';
        }
    }

    745-781: Delete notification implementation uses proper error handling

    The function properly handles various error cases and provides appropriate feedback to users.

    Remove the console.log statement on line 698 as it's not needed in production code:

    -console.log("Deleting notification with ID:", notificationId);

    785-817: Effective event listeners for notification dropdown

    The code properly handles opening/closing the dropdown and marks notifications as read when viewed.

    Consider adding keyboard navigation support for accessibility. Users should be able to tab through notifications and use Enter to interact with them.

    document.addEventListener("DOMContentLoaded", function () {
        // Existing code...
        
    +   // Add keyboard support for notifications
    +   notificationList.addEventListener("keydown", function(event) {
    +       if (event.key === "Enter" && event.target.classList.contains("notification-item")) {
    +           if (event.target.dataset.link) {
    +               window.location.href = event.target.dataset.link;
    +           }
    +       }
    +   });
    });

    662-689: Good notification item creation, but consider extracting as a separate function

    The notification item creation logic is well implemented but embedded within the fetchNotifications function.

    Consider extracting this logic into a separate function for better maintainability:

    +function createNotificationItem(notification) {
    +    const notificationItem = document.createElement("div");
    +    notificationItem.classList.add(
    +        "flex", "items-start", "gap-3", "p-3", "rounded-lg", "hover:bg-gray-100", "transition-colors",
    +        !notification.is_read ? "bg-gray-50" : "bg-white", 
    +        !notification.is_read ? "font-semibold" : "font-normal" 
    +    );
    +
    +    if (notification.link) {
    +        notificationItem.addEventListener("click", () => {
    +            window.location.href = notification.link;
    +        });
    +        notificationItem.classList.add("cursor-pointer");
    +    }
    +
    +    notificationItem.innerHTML = `
    +        <div class="flex-shrink-0">
    +            ${getNotificationIcon(notification.notification_type)}
    +        </div>
    +        <div class="flex-1">
    +            <p class="text-sm">${notification.message}</p>
    +            <p class="text-xs text-gray-500">${timeSince(notification.created_at)} ago</p>
    +        </div>
    +        <button class="delete-notification p-1 text-gray-400 hover:text-red-500 transition-colors" data-notification-id="${notification.id}">
    +            <i class="fa fa-times"></i>
    +        </button>
    +    `;
    +    
    +    return notificationItem;
    +}
    
    // Then in fetchNotifications:
    -    notifications.forEach(notification => {
    -        const notificationItem = document.createElement("div");
    -        // ... (existing code)
    -        notificationList.appendChild(notificationItem);
    -    });
    +    notifications.forEach(notification => {
    +        const notificationItem = createNotificationItem(notification);
    +        notificationList.appendChild(notificationItem);
    +    });
    website/notification_signals.py (4)

    34-35: Redundant list conversion

    Converting to a set and then back to a list can be simplified.

    -recipients = list(set(recipients))
    +recipients = list(set(recipients))  # Remove duplicates

    While this line works correctly to remove duplicates, adding a comment would help clarify the purpose.


    70-81: Typo in function name

    There's a spelling error in the function name.

    -def notify_user_on_badge_recieved(sender, instance, created, **kwargs):
    +def notify_user_on_badge_received(sender, instance, created, **kwargs):

    77-79: Inconsistent URL construction approach

    Some functions use string formatting for URLs while others use reverse().

    Use the reverse() function consistently for all URL generation:

    -link = f"/profile/{instance.user.username}"
    +link = reverse("profile", kwargs={"username": instance.user.username})

    This makes URL generation consistent and ensures URLs will be updated if routes change.


    1-81: Overall signal handler design consideration

    The signal handlers are well structured, but they lack error handling which can cause issues if notification creation fails.

    Wrap the notification creation in try-except blocks to prevent main operations from failing:

    @receiver(post_save, sender=UserBadge)
    def notify_user_on_badge_received(sender, instance, created, **kwargs):
        """
        Signal to notify users when they receive a new badge.
        """
        if created:
            try:
                message = f"You have just earned a new badge: {instance.badge.title}!"
                link = reverse("profile", kwargs={"username": instance.user.username})
                Notification.objects.create(
                    user=instance.user, 
                    message=message, 
                    notification_type="reward", 
                    link=link
                )
            except Exception as e:
                logger.error(f"Failed to create badge notification: {e}")
                # Main operation (badge creation) should still succeed

    For production systems, consider moving notification creation to asynchronous tasks using Celery or Django Q to prevent notifications from impacting the performance of main operations.

    📜 Review details

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

    📥 Commits

    Reviewing files that changed from the base of the PR and between 244d359 and 794e4c6.

    📒 Files selected for processing (9)
    • blt/urls.py (2 hunks)
    • website/admin.py (2 hunks)
    • website/management/commands/daily_checkin_reminder.py (1 hunks)
    • website/management/commands/run_daily.py (1 hunks)
    • website/migrations/0231_organization_check_ins_enabled_notification.py (1 hunks)
    • website/models.py (2 hunks)
    • website/notification_signals.py (1 hunks)
    • website/templates/includes/header.html (3 hunks)
    • website/views/user.py (2 hunks)
    ⏰ Context from checks skipped due to timeout of 90000ms (4)
    • GitHub Check: Run Tests
    • GitHub Check: docker-test
    • GitHub Check: Analyze (python)
    • GitHub Check: Analyze (javascript-typescript)
    🔇 Additional comments (11)
    website/management/commands/run_daily.py (1)

    44-47: LGTM! Integration for the daily check-in reminders.

    The integration of the daily check-in reminder command follows the established pattern in this file. The error handling is consistent with other commands and the error message is clear.

    website/admin.py (2)

    44-44: LGTM! Notification model import added correctly.

    The Notification model is properly added to the imports list.


    762-762: LGTM! Notification model registered correctly.

    The Notification model is properly registered with the Django admin site.

    blt/urls.py (1)

    302-302: LGTM! Proper import of notification-related view functions.

    The view functions for handling notifications are properly imported.

    Also applies to: 304-304, 310-310

    website/models.py (1)

    175-177: LGTM: Organization model extension looks good

    Adding check_ins_enabled flag to the Organization model properly supports the check-in reminder feature requirement.

    website/migrations/0231_organization_check_ins_enabled_notification.py (1)

    1-56: Migration file correctly reflects model changes

    The migration properly adds the check_ins_enabled field to the Organization model and creates the new Notification model with all necessary fields and meta options.

    website/views/user.py (1)

    45-45: LGTM: Correctly imported Notification model

    The Notification model is properly imported to support the new notification functionality.

    website/templates/includes/header.html (2)

    623-705: Robust notification fetching implementation with proper error handling

    The fetchNotifications function is well-structured with thorough error handling and data validation. It properly updates the UI based on the fetched data.


    722-744: Effective time formatting function

    The timeSince function effectively formats timestamps into human-readable relative time strings.

    website/notification_signals.py (2)

    1-6: Good imports organization

    The imports are appropriately organized, with Django core imports first followed by application imports.


    8-21: Points reward notification implementation is effective

    The signal handler properly creates a notification when points are awarded, with a clear message that includes the points and reason.

    Copy link
    Contributor

    coderabbitai bot commented Mar 15, 2025

    🧩 Analysis chain

    @DonnieBLT I've reviewed the notification system implementation in PR #3216 and found it to be well-structured and comprehensive. Here's my assessment:

    Strengths

    • Well-designed Notification model with appropriate fields (user, message, created_date, is_read, link)
    • Clean signal implementation for creating notifications on various user activities
    • Good UI integration with dropdown in header and unread counter
    • Proper API endpoints for fetching and marking notifications as read
    • Daily check-in reminder functionality via management command

    Minor Concerns

    • No dedicated test files for the notification functionality
    • Some views could benefit from additional error handling

    Overall, the implementation meets the requirements specified in the PR objectives and appears to be production-ready. The code is well-organized with proper separation of concerns.

    I recommend approving this PR.


    🏁 Script executed:

    #!/bin/bash
    # Get models.py to check the Notification model
    echo "===== models.py ====="
    git show HEAD:website/models.py | grep -A 40 "class Notification"
    
    # Get header.html to check notification UI
    echo -e "\n\n===== header.html notification section ====="
    git show HEAD:website/templates/header.html | grep -A 70 -B 10 "notification"
    
    # Get notification_signals.py
    echo -e "\n\n===== notification_signals.py ====="
    git show HEAD:website/notification_signals.py
    
    # Get user.py views for notifications
    echo -e "\n\n===== user.py notification views ====="
    git show HEAD:website/views/user.py | grep -A 40 -B 5 "def get_notifications\|def mark_notification_as_read"
    
    # Get daily_checkin_reminder.py
    echo -e "\n\n===== daily_checkin_reminder.py ====="
    git show HEAD:website/management/commands/daily_checkin_reminder.py
    

    Length of output: 5640


    🏁 Script executed:

    #!/bin/bash
    # Get urls.py to check notification routes
    echo "===== urls.py notification routes ====="
    git show HEAD:website/urls.py | grep -A 10 -B 10 "notification"
    
    # Check for any test files related to notifications
    echo -e "\n\n===== Tests for notifications ====="
    fd -t f "test.*notification.*\.py" || echo "No specific notification test files found"
    

    Length of output: 334


    🏁 Script executed:

    #!/bin/bash
    # Find and check the run_daily script
    echo "===== Finding run_daily script ====="
    fd "run_daily"
    
    # If found, examine its content
    echo -e "\n\n===== run_daily script content ====="
    fd "run_daily" -x cat {} \; || echo "run_daily script not found in the current branch"
    

    Length of output: 2211


    🏁 Script executed:

    #!/bin/bash
    # Check for apps.py configuration
    echo "===== apps.py configuration ====="
    git show HEAD:website/apps.py || echo "apps.py not found"
    
    # Look at the migration file for the notification model
    echo -e "\n\n===== Notification migration file ====="
    git show HEAD:website/migrations/0231_organization_check_ins_enabled_notification.py
    
    # Check for signals registration in any __init__.py files
    echo -e "\n\n===== Signals registration ====="
    fd "__init__\.py" --exec grep -l "notification_signals" {} \;
    

    Length of output: 2769


    @DonnieBLT Upon further review of the PR #3216, I can confirm the notification system looks well-implemented:

    What's Implemented Well

    • Notification Model: Comprehensive model with fields for user, message, created_at, is_read, is_deleted, notification_type, and link
    • Signal Handlers: Well-designed signal handlers for points rewards, join requests, kudos, posts, and badges
    • Daily Check-in Reminder: Implemented as a management command that sends notifications to users in organizations with check-ins enabled
    • Integration with Scheduled Tasks: The daily_checkin_reminder command has been properly added to the run_daily script with error handling

    Potential Improvements

    I noticed that notification_signals.py might not be imported in the WebsiteConfig.ready() method in apps.py (alongside the other signal imports). This could potentially prevent the signals from being registered.

    However, this is a minor issue that can be addressed in a follow-up PR if needed. The core notification functionality appears to be implemented correctly and meets the requirements described in the PR objectives.

    I'm approving this PR as it adds valuable functionality to the application.

    ✅ Actions performed

    Comments resolved and changes approved.

    @DonnieBLT DonnieBLT merged commit e39a306 into OWASP-BLT:main Mar 15, 2025
    10 checks passed
    @SahilDhillon21
    Copy link
    Contributor Author

    That's such a cool bot 😦

    @SahilDhillon21 SahilDhillon21 removed their assignment Mar 15, 2025
    @SahilDhillon21 SahilDhillon21 deleted the notification-prompts branch June 2, 2025 09:42
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    None yet
    Projects
    None yet
    2 participants