Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/website/webview/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.2.16 on 2025-04-30 14:20

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('shared', '0047_alter_cvederivationclusterproposal_status_and_more'),
]

operations = [
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subscriptions', models.ManyToManyField(related_name='subscribers', to='shared.nixpkgsissue')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
13 changes: 13 additions & 0 deletions src/website/webview/models.py
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
# Create your models here.
from django.contrib.auth.models import User
from django.db import models
from shared.models import NixpkgsIssue


class Profile(models.Model):
"""
Profile associated to a user, storing extra non-auth-related data such as
active issue subscriptions.
"""

user = models.OneToOneField(User, on_delete=models.CASCADE)
subscriptions = models.ManyToManyField(NixpkgsIssue, related_name="subscribers")
21 changes: 21 additions & 0 deletions src/website/webview/templates/issue_detail.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
{% extends "base.html" %}
{% load viewutils %}

{% block title %}
{{ issue }}
{% endblock title %}

{% block content %}

<div id="subscribe-panel">
<form
method="post"
action=""
hx-indicator="find .state-change-indicator"
hx-params="not no-js"
autocomplete="off"
>
<input type="hidden" name="no-js">
{% csrf_token %}
<input type="hidden" name="nixpkgs_issue_id" value="{{ object.id }}">

{% if user|is_subscribed_to:object %}
<button name="subscribe" value="unsubscribe">Unsubscribe</button>
{% elif user.is_authenticated %}
<button name="subscribe" value="subscribe">Subscribe</button>
{% endif %}
</form>
</div>

<h1>{{ object.code }}</h1>

<p>{{ object.description.value }}</p>
Expand Down
16 changes: 16 additions & 0 deletions src/website/webview/templatetags/viewutils.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import datetime
import json
from logging import getLogger
from typing import Any, TypedDict, cast

from django import template
from django.template.context import Context
from shared.auth import isadmin, ismaintainer
from shared.listeners.cache_suggestions import parse_drv_name
from shared.models import NixpkgsIssue
from shared.models.cve import AffectedProduct
from shared.models.linkage import (
CVEDerivationClusterProposal,
)

logger = getLogger(__name__)

register = template.Library()


Expand Down Expand Up @@ -123,6 +127,18 @@ def is_maintainer_or_admin(user: Any) -> bool:
return is_maintainer(user) or is_admin(user)


@register.filter
def is_subscribed_to(user: Any, issue: NixpkgsIssue) -> bool:
if user is None or user.is_anonymous:
return False
else:
profile = user.profile
if profile is None:
return False
else:
return profile.subscriptions.filter(id=issue.id).exists()


@register.inclusion_tag("components/suggestion.html", takes_context=True)
def suggestion(
context: Context,
Expand Down
36 changes: 36 additions & 0 deletions src/website/webview/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@

from django.core.validators import RegexValidator
from django.db import transaction
from django.shortcuts import render
from django.urls import reverse
from shared.github import create_gh_issue
from shared.logs import SuggestionActivityLog
from shared.models.cached import CachedSuggestions

from webview.models import Profile

if typing.TYPE_CHECKING:
# prevent typecheck from failing on some historic type
# https://stackoverflow.com/questions/60271481/django-mypy-valuesqueryset-type-hint
Expand Down Expand Up @@ -426,6 +429,39 @@ def get_cves_for_derivation(self, drv: Any) -> QuerySet | None:
existing_cves = Container.objects.filter(cve__cve_id__in=cves)
return existing_cves or None

def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
if not request.user:
return HttpResponseForbidden()

user = request.user
nixpkgs_issue_id = request.POST.get("nixpkgs_issue_id")
subscribe = request.POST.get("subscribe")
nixpkgs_issue = get_object_or_404(NixpkgsIssue, id=nixpkgs_issue_id)

if subscribe == "subscribe":
profile, _ = Profile.objects.get_or_create(user=user)
profile.subscriptions.add(nixpkgs_issue_id)
profile.save()
elif subscribe == "unsubscribe":
try:
profile = Profile.objects.get(user=user)
profile.subscriptions.remove(nixpkgs_issue)
profile.save()
except Profile.DoesNotExist:
# This can't really happen from the interface since the
# Unsubscribe button is only visible when the user is subscribed
# to said issue. We log it but we don't bother showing the user
# an error message.
logger.error(
f"Tried to unsubscribe user {user.id} from issue #{nixpkgs_issue_id} but user doesn't have a profile"
)
else:
logger.warn(
f"Ignoring subscription action with unexpected `subscribe` value: {subscribe}"
)

return render(request, "issue_detail.html", {"object": nixpkgs_issue})


class NixpkgsIssueListView(ListView):
template_name = "issue_list.html"
Expand Down