Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 9 additions & 9 deletions backend/src/apps/owasp/api/internal/nodes/certificate.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@
class CertificateNode:
"""Certificate node."""

@strawberry_django.field
def tier(self, root: Certificate) -> str:
"""Resolve the human-readable tier level (e.g. 'Level 1')."""
return root.get_tier_display()
@strawberry_django.field(select_related=["github_user"])
def github_user(self, root: Certificate) -> UserNode:
"""Resolve the associated GitHub user."""
return root.github_user

@strawberry_django.field
def is_verified(self, root: Certificate) -> bool:
"""Resolve whether the certificate is active/verified."""
return not root.is_revoked
return root.is_verified

@strawberry_django.field(select_related=["github_user"])
def github_user(self, root: Certificate) -> UserNode:
"""Resolve the associated GitHub user."""
return root.github_user
@strawberry_django.field
def tier(self, root: Certificate) -> str:
"""Resolve the human-readable tier level (e.g. 'Level 1')."""
return root.get_tier_display()
20 changes: 16 additions & 4 deletions backend/src/apps/owasp/api/internal/queries/certificate.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
"""OWASP certificate GraphQL queries."""

import re

import strawberry
import strawberry_django
from django.core.exceptions import ValidationError

from apps.nest.api.internal.permissions import IsAuthenticated
from apps.owasp.api.internal.nodes.certificate import CertificateNode
from apps.owasp.models.crp.certificate import Certificate
from apps.owasp.models.crp.certificate import (
CERTIFICATE_ID_ALPHABET,
CERTIFICATE_ID_LENGTH,
Certificate,
)

CERTIFICATE_ID_RE = re.compile(
rf"^[{re.escape(CERTIFICATE_ID_ALPHABET)}]{{{CERTIFICATE_ID_LENGTH}}}$"
)


@strawberry.type
Expand All @@ -15,12 +24,15 @@ class CertificateQuery:

@strawberry_django.field
def certificate(self, certificate_id: str) -> CertificateNode | None:
"""Resolve certificate by raw ID."""
"""Resolve certificate by ID."""
if not CERTIFICATE_ID_RE.fullmatch(certificate_id):
Comment thread
anurag2787 marked this conversation as resolved.
return None

try:
return Certificate.objects.select_related(
"github_user",
).get(id=certificate_id)
except (Certificate.DoesNotExist, ValidationError, ValueError):
except Certificate.DoesNotExist:
return None

@strawberry_django.field(permission_classes=[IsAuthenticated])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Generated by Django 6.0.8 on 2026-08-13 09:03

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


class Migration(migrations.Migration):

dependencies = [
('github', '0044_user_indexes'),
('owasp', '0075_alter_certificate_id'),
]

operations = [
migrations.RemoveConstraint(
model_name='certificate',
name='unique_active_cert_per_tier',
),
migrations.AddField(
model_name='certificate',
name='chapter',
field=models.ForeignKey(blank=True, help_text='Associated chapter', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='certificates', to='owasp.chapter'),
),
migrations.AddField(
model_name='certificate',
name='issuer',
field=models.ForeignKey(blank=True, help_text='Issuer GitHub user (for generic certificates)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='issued_certificates', to='github.user'),
),
migrations.AddField(
model_name='certificate',
name='message',
field=models.TextField(blank=True, default='', help_text='Customizable certificate message', verbose_name='Message'),
),
migrations.AddField(
model_name='certificate',
name='project',
field=models.ForeignKey(blank=True, help_text='Associated project', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='certificates', to='owasp.project'),
),
migrations.RenameField(
model_name='certificate',
old_name='github_user',
new_name='recipient',
Comment thread
anurag2787 marked this conversation as resolved.
Outdated
),
migrations.AddField(
model_name='certificate',
name='title',
field=models.CharField(blank=True, default='', help_text='Certificate title', max_length=255, verbose_name='Title'),
),
migrations.AlterField(
model_name='certificate',
name='score',
field=models.PositiveIntegerField(blank=True, help_text="The contributor's score when the certificate was issued", null=True, verbose_name='Score'),
),
migrations.AlterField(
model_name='certificate',
name='tier',
field=models.CharField(blank=True, choices=[('level_1', 'Level 1'), ('level_2', 'Level 2'), ('level_3', 'Level 3'), ('level_4', 'Level 4')], default='', help_text='The tier at which the certificate was issued', max_length=20, verbose_name='Tier'),
),
migrations.AddConstraint(
model_name='certificate',
constraint=models.UniqueConstraint(condition=models.Q(('is_revoked', False), models.Q(('tier', ''), _negated=True)), fields=('recipient', 'tier'), name='unique_active_cert_per_tier', violation_error_message='Cannot have multiple active certificates for same tier'),
Comment thread
anurag2787 marked this conversation as resolved.
Outdated
),
]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
5 changes: 5 additions & 0 deletions backend/src/apps/owasp/models/crp/certificate.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ class Meta:
help_text="Whether the certificate has been revoked",
)

@property
def is_verified(self) -> bool:
"""Return whether the certificate is active/verified (not revoked)."""
return not self.is_revoked

def __str__(self) -> str:
"""Return human-readable representation."""
status = "Revoked" if self.is_revoked else "Active"
Expand Down
Loading