-
Notifications
You must be signed in to change notification settings - Fork 13
Automatic subscription to maintained packages #655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
652cd31
dcc4cc3
7fd96cb
82ee088
7bcdd12
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,12 +14,22 @@ def create_package_subscription_notifications( | |
| suggestion: CVEDerivationClusterProposal, | ||
| ) -> None: | ||
| """ | ||
| Create notifications for users subscribed to packages affected by the suggestion. | ||
| Create notifications for users subscribed to packages affected by the suggestion | ||
| and for maintainers of those packages (if they have auto-subscribe enabled). | ||
| """ | ||
| # Extract all affected package names from the suggestion | ||
|
|
||
| # Query package attributes directly from the suggestion's derivations | ||
| affected_packages = list( | ||
| suggestion.derivations.values_list("attribute", flat=True).distinct() | ||
| ) | ||
| cve_id = suggestion.cve.cve_id | ||
|
|
||
| # Query maintainers' GitHub usernames directly from the derivations' metadata | ||
| maintainers_github = list( | ||
| suggestion.derivations.filter(metadata__maintainers__isnull=False) | ||
| .values_list("metadata__maintainers__github", flat=True) | ||
| .distinct() | ||
| ) | ||
|
|
||
| if not affected_packages: | ||
| logger.debug(f"No packages found for suggestion {suggestion.pk}") | ||
|
|
@@ -30,32 +40,71 @@ def create_package_subscription_notifications( | |
| profile__package_subscriptions__overlap=affected_packages | ||
| ).select_related("profile") | ||
|
|
||
| if not subscribed_users.exists(): | ||
| logger.debug(f"No subscribed users found for packages: {affected_packages}") | ||
| return | ||
| # Find maintainers of affected packages from cached suggestion | ||
| maintainer_users = set() | ||
| if maintainers_github: | ||
| maintainer_users = set( | ||
| User.objects.filter( | ||
| username__in=maintainers_github, | ||
| profile__auto_subscribe_to_maintained_packages=True, | ||
| ).select_related("profile") | ||
| ) | ||
|
|
||
| logger.debug( | ||
| f"Found {len(maintainer_users)} maintainers with auto-subscribe enabled for suggestion {suggestion.pk}" | ||
| ) | ||
|
|
||
| # Combine both sets of users, avoiding duplicates | ||
| all_users_to_notify = set(subscribed_users) | maintainer_users | ||
|
|
||
| logger.debug(f"About to notify users about packages: {affected_packages}") | ||
| logger.debug(f"Users to notify: {all_users_to_notify}") | ||
|
|
||
| logger.info( | ||
| f"Creating notifications for {subscribed_users.count()} users for CVE {suggestion.cve.cve_id}" | ||
| f"Creating notifications for {len(all_users_to_notify)} users for CVE {cve_id} " | ||
| f"({len(subscribed_users)} subscribed, {len(maintainer_users)} maintainers)" | ||
| ) | ||
|
|
||
| for user in subscribed_users: | ||
| # Find which of their subscribed packages are actually affected | ||
| user_affected_packages = [ | ||
| pkg | ||
| for pkg in user.profile.package_subscriptions | ||
| if pkg in affected_packages | ||
| ] | ||
| for user in all_users_to_notify: | ||
| # Determine notification reason and affected packages for this user | ||
| user_affected_packages = [] | ||
| notification_reason = [] | ||
|
|
||
| # Check if user is subscribed to any affected packages | ||
| if user in subscribed_users: | ||
|
||
| user_subscribed_packages = [ | ||
| pkg | ||
| for pkg in user.profile.package_subscriptions | ||
| if pkg in affected_packages | ||
| ] | ||
| user_affected_packages.extend(user_subscribed_packages) | ||
| if user_subscribed_packages: | ||
| notification_reason.append("subscribed to") | ||
|
|
||
| # Check if user is a maintainer with auto-subscribe enabled | ||
| if user in maintainer_users: | ||
| # For maintainers, all affected packages are relevant | ||
| maintainer_packages = [ | ||
| pkg for pkg in affected_packages if pkg not in user_affected_packages | ||
| ] | ||
| user_affected_packages.extend(maintainer_packages) | ||
| if maintainer_packages or (user not in subscribed_users): | ||
| notification_reason.append("maintainer of") | ||
|
|
||
| if not user_affected_packages: | ||
| continue | ||
|
|
||
| # Create notification | ||
| try: | ||
| reason_text = " and ".join(notification_reason) | ||
| Notification.objects.create_for_user( | ||
| user=user, | ||
| title=f"New security suggestion affects: {', '.join(user_affected_packages)}", | ||
| message=f"CVE {suggestion.cve.cve_id} may affect packages you're subscribed to. " | ||
| message=f"CVE {cve_id} may affect packages you're {reason_text}. " | ||
| f"Affected packages: {', '.join(user_affected_packages)}. ", | ||
| ) | ||
| logger.debug( | ||
| f"Created notification for user {user.username} for packages: {user_affected_packages}" | ||
| f"Created notification for user {user.username} ({reason_text}) for packages: {user_affected_packages}" | ||
| ) | ||
| except Exception as e: | ||
| logger.error(f"Failed to create notification for user {user.username}: {e}") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Generated by Django 4.2.24 on 2025-11-05 14:27 | ||
|
|
||
| import django.contrib.postgres.fields | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('webview', '0004_remove_profile_subscriptions_and_more'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name='profile', | ||
| name='auto_subscribe_to_maintained_packages', | ||
| field=models.BooleanField(default=True, help_text='Automatically subscribe to notifications for packages this user maintains'), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name='profile', | ||
| name='package_subscriptions', | ||
| field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, help_text="Package attribute names this user has subscribed to manually (e.g., 'firefox', 'chromium')", size=None), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -141,6 +141,60 @@ def _handle_error(self, request: HttpRequest, error_message: str) -> HttpRespons | |
| return redirect(reverse("webview:subscriptions:center")) | ||
|
|
||
|
|
||
| class ToggleAutoSubscribeView(LoginRequiredMixin, TemplateView): | ||
| """Toggle auto-subscription to maintained packages.""" | ||
|
|
||
| template_name = "subscriptions/components/auto_subscribe.html" | ||
|
|
||
| def post(self, request: HttpRequest) -> HttpResponse: | ||
| """Toggle auto-subscription setting.""" | ||
| action = request.POST.get("action", "") | ||
|
|
||
| if action not in ["enable", "disable"]: | ||
| return self._handle_error(request, "Invalid action.") | ||
|
|
||
| profile = request.user.profile | ||
|
|
||
| if action == "enable": | ||
| if profile.auto_subscribe_to_maintained_packages: | ||
| return self._handle_error( | ||
| request, "Auto-subscription is already enabled." | ||
|
||
| ) | ||
| profile.auto_subscribe_to_maintained_packages = True | ||
| else: # disable | ||
| if not profile.auto_subscribe_to_maintained_packages: | ||
| return self._handle_error( | ||
| request, "Auto-subscription is already disabled." | ||
| ) | ||
| profile.auto_subscribe_to_maintained_packages = False | ||
|
|
||
| profile.save(update_fields=["auto_subscribe_to_maintained_packages"]) | ||
|
|
||
| # Handle HTMX vs standard request | ||
| if request.headers.get("HX-Request"): | ||
| return self.render_to_response( | ||
| { | ||
| "auto_subscribe_enabled": profile.auto_subscribe_to_maintained_packages, | ||
| } | ||
| ) | ||
| else: | ||
| return redirect(reverse("webview:subscriptions:center")) | ||
|
|
||
| def _handle_error(self, request: HttpRequest, error_message: str) -> HttpResponse: | ||
| """Handle error responses for both HTMX and standard requests.""" | ||
| if request.headers.get("HX-Request"): | ||
| return self.render_to_response( | ||
| { | ||
| "auto_subscribe_enabled": request.user.profile.auto_subscribe_to_maintained_packages, | ||
| "error_message": error_message, | ||
| } | ||
| ) | ||
| else: | ||
| # Without javascript, we use Django messages for the errors | ||
| messages.error(request, error_message) | ||
| return redirect(reverse("webview:subscriptions:center")) | ||
|
|
||
|
|
||
| class PackageSubscriptionView(LoginRequiredMixin, TemplateView): | ||
| """Display a package subscription page for a specific package.""" | ||
|
|
||
|
|
@@ -164,6 +218,11 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: | |
| else: | ||
| context["is_subscribed"] = False | ||
|
|
||
| # Check if user maintains this package and has automatic subscription enabled | ||
| context["auto_subscribe_enabled"] = ( | ||
| self.request.user.profile.auto_subscribe_to_maintained_packages | ||
| ) | ||
|
|
||
| return context | ||
|
|
||
| def post(self, request: HttpRequest, **kwargs: Any) -> HttpResponse: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| {% load viewutils %} | ||
|
|
||
| <div class="auto-subscribe-toggler toggler {% if auto_subscribe_enabled %}toggler-on{% else %}toggler-off{% endif %}"> | ||
| <div class="toggler-status"> | ||
| <div class="toggler-icon">{% if auto_subscribe_enabled %}✓{% else %}✕{% endif %}</div> | ||
| <div>{% if auto_subscribe_enabled %}You will automatically receive notifications about packages you maintain.{% else %}By default, you won't receive notifications for the packages you maintain.{% endif %}</div> | ||
| </div> | ||
| <form method="post" | ||
| action="{% url 'webview:subscriptions:toggle_auto_subscribe' %}" | ||
| hx-post="{% url 'webview:subscriptions:toggle_auto_subscribe' %}" | ||
| hx-target="closest .auto-subscribe-toggler" | ||
| hx-swap="outerHTML"> | ||
| {% csrf_token %} | ||
| {% if auto_subscribe_enabled %} | ||
| <input type="hidden" name="action" value="disable"> | ||
| <button type="submit">Disable</button> | ||
| {% else %} | ||
| <input type="hidden" name="action" value="enable"> | ||
| <button type="submit">Enable</button> | ||
| {% endif %} | ||
| </form> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be better to have another JOIN here instead of fetching all GitHub handles into Python just to send them back?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in 82ee088