Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable user-facing changes to this project will be documented in this file.

## Unreleased

- Stewards reviewing submissions can now copy a compact AI-ready bundle (user, contribution, mission, state, submitter notes, evidence URLs, staff reply, proposal, and internal CRM notes) from a copy icon next to each submission's title; the copy aborts with a warning if internal notes can't be loaded so the clipboard payload is never silently incomplete. Evidence URL types gain an admin-editable "Allow duplicate" flag that exempts URLs of that type (for example shared GitHub repositories) from duplicate detection across both manual and automated review (ac68ec7)
- Contributions explorer now shows highlighted contributions again, the page footer is flush to the bottom and CTAs users to submit a contribution, and the highlights section collapses to a single compact line when empty so contributions get more room. The Submit Contribution form no longer shows misleading "Please add a description" errors for evidence URLs, detects URL types correctly when users omit `https://`, and disables the submit button when a required GitHub or X URL has no linked social account (b249787)
- Highlights and All Contributions are now a single filterable Contributions explorer at `/all-contributions` with a category pill toggle, type and mission dropdowns, a debounced search bar that supports `sort:` syntax, a Both / Highlights only / Contributions only view switch, and shareable URL state. Highlights show as a horizontal slider with arrows in the default Both view; non-submittable contribution types (badges, journey rewards) are hidden from the public list, consecutive contributions of the same type are no longer stacked, and the Dashboard highlights strip now shows the latest 10 sorted by date (c9f7b88)
- Metrics dashboard Pending review tile no longer collapses to zero when filters are applied; it now shows submissions created in the selected range that are still awaiting a decision (97dc404)
Expand Down
5 changes: 3 additions & 2 deletions backend/contributions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,9 @@ class Media:

@admin.register(EvidenceURLType)
class EvidenceURLTypeAdmin(admin.ModelAdmin):
list_display = ('name', 'slug', 'is_generic', 'order', 'ownership_social_account', 'created_at')
list_filter = ('is_generic',)
list_display = ('name', 'slug', 'is_generic', 'allow_duplicate', 'order', 'ownership_social_account', 'created_at')
list_filter = ('is_generic', 'allow_duplicate')
list_editable = ('allow_duplicate',)
search_fields = ('name', 'slug', 'description')
readonly_fields = ('created_at', 'updated_at')
prepopulated_fields = {'slug': ('name',)}
Expand Down
16 changes: 15 additions & 1 deletion backend/contributions/management/commands/review_submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,16 @@ def _check_single_url_duplicate(submission, evidence, normalized,

Returns (template_label, crm_reason) if duplicate, or None if unique.
"""
# URL types flagged as duplicate-allowed are exempt. Fall back to
# pattern detection when url_type is missing (legacy rows or evidence
# copied via bulk_create paths that didn't populate the FK).
if evidence.url_type_id and evidence.url_type and evidence.url_type.allow_duplicate:
return None
if not evidence.url_type_id and evidence.url:
from contributions.url_utils import detect_url_type
detected = detect_url_type(evidence.url)
if detected and detected.allow_duplicate:
return None
# Check converted/accepted contributions (always deterministic)
if normalized in accepted_urls:
return (
Expand Down Expand Up @@ -351,7 +361,9 @@ def _build_url_lookup(self):
accepted_urls: set of normalized URLs from accepted contributions
submitted_created_at: dict mapping submission ID → created_at
"""
# URLs from pending/accepted submitted contributions
# URLs from pending/accepted submitted contributions.
# Evidence whose url_type allows duplicates is excluded so those
# URLs never participate in duplicate detection.
submitted = (
Evidence.objects
.filter(
Expand All @@ -360,6 +372,7 @@ def _build_url_lookup(self):
],
url__gt='',
)
.exclude(url_type__allow_duplicate=True)
.values_list(
'url',
'submitted_contribution_id',
Expand All @@ -377,6 +390,7 @@ def _build_url_lookup(self):
_normalize_url(url) for url in
Evidence.objects
.filter(contribution__isnull=False, url__gt='')
.exclude(url_type__allow_duplicate=True)
.values_list('url', flat=True)
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('contributions', '0054_submittedcontribution_is_interesting'),
]

operations = [
migrations.AddField(
model_name='evidenceurltype',
name='allow_duplicate',
field=models.BooleanField(
default=False,
help_text=(
'If True, URLs of this type are exempt from duplicate '
'checking against other submissions and contributions. '
'Useful for shared resources like GitHub repositories.'
),
),
),
]
6 changes: 6 additions & 0 deletions backend/contributions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,12 @@ class EvidenceURLType(BaseModel):
blank=True,
help_text="Social account type for ownership checks: 'twitter' or 'github'"
)
allow_duplicate = models.BooleanField(
default=False,
help_text="If True, URLs of this type are exempt from duplicate "
"checking against other submissions and contributions. "
"Useful for shared resources like GitHub repositories."
)

class Meta:
ordering = ['order', 'name']
Expand Down
137 changes: 137 additions & 0 deletions backend/contributions/tests/test_review_submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -847,3 +847,140 @@ def test_update_rejects_unknown_evidence_id_without_saving_changes(self):
self.submission.refresh_from_db()
self.assertEqual(self.submission.notes, 'Original notes')
self.assertEqual(self.submission.evidence_items.count(), 1)


class RuleDuplicateUrlAllowDuplicateTest(Tier1RuleTestBase):
"""Tests for ``allow_duplicate`` exemption in ``rule_duplicate_evidence_url``.

The flag is admin-configurable (no data migration). ``setUp`` flips
github-repo into permissive mode so the exemption can be exercised.
Covers (a) lookup-time exclusion of permissive evidence,
(b) per-URL short-circuit when the new evidence's url_type is permissive,
(c) fallback to ``detect_url_type`` when ``url_type`` is null on the new
evidence, and (d) that other URL types still get rejected normally.
"""

REPO_URL = 'https://github.com/genlayer/studio'
PR_URL = 'https://github.com/genlayer/studio/pull/42'

def setUp(self):
super().setUp()
from contributions.models import EvidenceURLType
EvidenceURLType.objects.filter(slug='github-repo').update(
allow_duplicate=True,
)

def _build_lookup(self):
from contributions.management.commands.review_submissions import Command
return Command()._build_url_lookup()

def test_permissive_evidence_excluded_from_lookup(self):
"""Stored evidence with ``url_type.allow_duplicate=True`` must be
absent from ``url_to_sub_ids`` so subsequent submissions of the
same URL do not even see a candidate match."""
sub = self._create_submission()
self._add_evidence(sub, url=self.REPO_URL)
url_to_sub_ids, accepted_urls, _ = self._build_lookup()
self.assertNotIn(_normalize_url(self.REPO_URL), url_to_sub_ids)
self.assertNotIn(_normalize_url(self.REPO_URL), accepted_urls)

def test_non_permissive_evidence_still_in_lookup(self):
"""A github-pr URL is not duplicate-allowed and must still appear
in the lookup (control case)."""
sub = self._create_submission()
self._add_evidence(sub, url=self.PR_URL)
url_to_sub_ids, _, _ = self._build_lookup()
self.assertIn(_normalize_url(self.PR_URL), url_to_sub_ids)

def test_duplicate_repo_url_passes_review(self):
"""Two pending submissions of the same github-repo URL must both
pass the duplicate rule (because the URL type allows duplicates)."""
first = self._create_submission(
created_at=timezone.now() - timezone.timedelta(hours=2),
)
self._add_evidence(first, url=self.REPO_URL)
second = self._create_submission(user=self.other_user)
ev2 = self._add_evidence(second, url=self.REPO_URL)
url_to_sub_ids, accepted_urls, created_at = self._build_lookup()
result = rule_duplicate_evidence_url(
second, [ev2], url_to_sub_ids, accepted_urls,
submitted_created_at=created_at,
)
self.assertIsNone(result)

def test_duplicate_pr_url_still_rejects(self):
"""github-pr is not permissive — duplicate must still reject so
that the exemption is truly per-type, not blanket-github."""
first = self._create_submission(
created_at=timezone.now() - timezone.timedelta(hours=2),
)
self._add_evidence(first, url=self.PR_URL)
second = self._create_submission(user=self.other_user)
ev2 = self._add_evidence(second, url=self.PR_URL)
url_to_sub_ids, accepted_urls, created_at = self._build_lookup()
result = rule_duplicate_evidence_url(
second, [ev2], url_to_sub_ids, accepted_urls,
submitted_created_at=created_at,
)
self.assertIsNotNone(result)

def test_null_url_type_falls_back_to_detect(self):
"""When the new submission's evidence has ``url_type=None`` (legacy
or bulk-created without it), the rule must fall back to
``detect_url_type(evidence.url)`` and recognise permissive URLs."""
# Existing pending repo evidence (correctly excluded from lookup
# because its detected url_type is permissive).
first = self._create_submission(
created_at=timezone.now() - timezone.timedelta(hours=2),
)
self._add_evidence(first, url=self.REPO_URL)

# New submission whose evidence row has no url_type FK.
second = self._create_submission(user=self.other_user)
ev2 = self._add_evidence(second, url=self.REPO_URL)
Evidence.objects.filter(pk=ev2.pk).update(url_type=None)
ev2.refresh_from_db()

url_to_sub_ids, accepted_urls, created_at = self._build_lookup()
result = rule_duplicate_evidence_url(
second, [ev2], url_to_sub_ids, accepted_urls,
submitted_created_at=created_at,
)
self.assertIsNone(result)

def test_accepted_contribution_evidence_preserves_url_type(self):
"""When a submission is accepted, copied Evidence rows on the
Contribution must carry both ``url_type`` and ``normalized_url``
— bulk_create skips Evidence.save(), so the view must populate
them explicitly. Without this, accepted evidence with a permissive
type would silently degrade to null and bypass the lookup-time
exclusion."""
from contributions.url_utils import normalize_url
sub = self._create_submission()
ev = self._add_evidence(sub, url=self.REPO_URL)
# Sanity: source evidence has detected url_type and normalized_url.
self.assertIsNotNone(ev.url_type_id)
self.assertEqual(ev.normalized_url, normalize_url(self.REPO_URL))

# Mimic the views.py accept flow for evidence copying.
contribution = Contribution.objects.create(
user=sub.user,
contribution_type=sub.contribution_type,
points=10,
multiplier_at_creation=1,
frozen_global_points=10,
)
Evidence.objects.bulk_create([
Evidence(
contribution=contribution,
description=ev.description,
url=ev.url,
file=ev.file,
url_type=ev.url_type,
normalized_url=ev.normalized_url,
)
])

copied = Evidence.objects.get(contribution=contribution)
self.assertEqual(copied.url_type_id, ev.url_type_id)
self.assertEqual(copied.normalized_url, normalize_url(self.REPO_URL))
Loading