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
3 changes: 2 additions & 1 deletion backend/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from users.views import UserViewSet
from contributions.views import ContributionTypeViewSet, ContributionViewSet, EvidenceViewSet, SubmittedContributionViewSet, StewardSubmissionViewSet, MissionViewSet
from contributions.views import ContributionTypeViewSet, ContributionViewSet, EvidenceViewSet, SubmittedContributionViewSet, StewardSubmissionViewSet, MissionViewSet, StartupRequestViewSet
from leaderboard.views import GlobalLeaderboardMultiplierViewSet, LeaderboardViewSet
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView
from .metrics_views import ActiveValidatorsView, ContributionTypesStatsView
Expand All @@ -17,6 +17,7 @@
router.register(r'submissions', SubmittedContributionViewSet, basename='submission')
router.register(r'steward-submissions', StewardSubmissionViewSet, basename='steward-submission')
router.register(r'missions', MissionViewSet, basename='mission')
router.register(r'startup-requests', StartupRequestViewSet, basename='startup-request')

# The API URLs are now determined automatically by the router
urlpatterns = [
Expand Down
37 changes: 36 additions & 1 deletion backend/contributions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from django.core.exceptions import ValidationError
from django.contrib.auth import get_user_model
from datetime import datetime
from .models import Category, ContributionType, Contribution, SubmittedContribution, Evidence, ContributionHighlight, Mission
from .models import Category, ContributionType, Contribution, SubmittedContribution, Evidence, ContributionHighlight, Mission, StartupRequest
from .validator_forms import CreateValidatorForm
from leaderboard.models import GlobalLeaderboardMultiplier

Expand Down Expand Up @@ -628,3 +628,38 @@ def get_status(self, obj):
else:
return format_html('<span style="color: red;">●</span> Inactive')
get_status.short_description = 'Status'


@admin.register(StartupRequest)
class StartupRequestAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'get_status', 'order', 'created_at')
list_filter = ('is_active', 'created_at')
search_fields = ('title', 'description', 'short_description')
readonly_fields = ('id', 'created_at', 'updated_at')
list_editable = ('order',)
ordering = ('order', '-created_at')

fieldsets = (
(None, {
'fields': ('id', 'title', 'is_active', 'order')
}),
('Content', {
'fields': ('short_description', 'description'),
'description': 'Short description is shown in the listing. Full description supports Markdown.'
}),
('Documents', {
'fields': ('documents',),
'description': 'JSON array of document objects: [{"title": "...", "url": "...", "type": "pdf|image"}]'
}),
('Metadata', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)

def get_status(self, obj):
if obj.is_active:
return format_html('<span style="color: green;">●</span> Active')
else:
return format_html('<span style="color: red;">●</span> Inactive')
get_status.short_description = 'Status'
32 changes: 32 additions & 0 deletions backend/contributions/migrations/0030_startuprequest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 5.2.10 on 2026-01-22 16:33

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('contributions', '0029_submittedcontribution_assigned_to_and_more'),
]

operations = [
migrations.CreateModel(
name='StartupRequest',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('title', models.CharField(help_text='Title of the startup request', max_length=200)),
('description', models.TextField(help_text='Full description (supports Markdown)')),
('short_description', models.CharField(help_text='Brief description shown in listing (plain text)', max_length=300)),
('documents', models.JSONField(blank=True, default=list, help_text='Array of document objects: [{title, url, type}]')),
('is_active', models.BooleanField(default=True, help_text='Whether this startup request is currently visible')),
('order', models.PositiveIntegerField(default=0, help_text='Display order (lower numbers appear first)')),
],
options={
'verbose_name': 'Startup Request',
'verbose_name_plural': 'Startup Requests',
'ordering': ['order', '-created_at'],
},
),
]
48 changes: 47 additions & 1 deletion backend/contributions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,5 +505,51 @@ def get_active_highlights(cls, contribution_type=None, user=None, limit=5):
'contribution__user',
'contribution__contribution_type'
)

return queryset[:limit]


class StartupRequest(BaseModel):
"""
Represents a startup idea/opportunity for the community to pursue.
Displayed in the builder contributions section as informational content.
"""
title = models.CharField(
max_length=200,
help_text="Title of the startup request"
)
description = models.TextField(
help_text="Full description (supports Markdown)"
)
short_description = models.CharField(
max_length=300,
help_text="Brief description shown in listing (plain text)"
)
documents = models.JSONField(
default=list,
blank=True,
help_text="Array of document objects: [{title, url, type}]"
)
is_active = models.BooleanField(
default=True,
help_text="Whether this startup request is currently visible"
)
order = models.PositiveIntegerField(
default=0,
help_text="Display order (lower numbers appear first)"
)

class Meta:
ordering = ['order', '-created_at']
verbose_name = "Startup Request"
verbose_name_plural = "Startup Requests"

def __str__(self):
return self.title

@classmethod
def get_active_requests(cls):
"""
Get all active startup requests ordered by display order.
"""
return cls.objects.filter(is_active=True).order_by('order', '-created_at')
5 changes: 4 additions & 1 deletion backend/contributions/recaptcha_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from django_recaptcha.fields import ReCaptchaField as DjangoReCaptchaField
from django_recaptcha.widgets import ReCaptchaV2Checkbox

from tally.middleware.tracing import trace_external


class ReCaptchaField(serializers.Field):
"""
Expand Down Expand Up @@ -55,7 +57,8 @@ def to_internal_value(self, data):
try:
# Use django-recaptcha's validation
# The field expects the token value directly
cleaned_value = self.django_field.clean(data)
with trace_external('recaptcha', 'verify'):
cleaned_value = self.django_field.clean(data)
return cleaned_value
except Exception as e:
# Handle various validation errors
Expand Down
28 changes: 26 additions & 2 deletions backend/contributions/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from rest_framework import serializers
from .models import ContributionType, Contribution, SubmittedContribution, Evidence, ContributionHighlight, Mission
from .models import ContributionType, Contribution, SubmittedContribution, Evidence, ContributionHighlight, Mission, StartupRequest
from users.serializers import UserSerializer, LightUserSerializer
from users.models import User
from .recaptcha_field import ReCaptchaField
Expand Down Expand Up @@ -85,7 +85,7 @@ class ContributionTypeSerializer(serializers.ModelSerializer):
class Meta:
model = ContributionType
fields = [
'id', 'name', 'description', 'category', 'min_points', 'max_points',
'id', 'name', 'slug', 'description', 'category', 'min_points', 'max_points',
'current_multiplier', 'is_submittable', 'examples',
'created_at', 'updated_at'
]
Expand Down Expand Up @@ -619,3 +619,27 @@ class Meta:

def get_is_active(self, obj):
return obj.is_active()


class StartupRequestListSerializer(serializers.ModelSerializer):
"""
Lightweight serializer for listing startup requests.
"""
class Meta:
model = StartupRequest
fields = ['id', 'title', 'short_description', 'is_active', 'order', 'created_at']
read_only_fields = ['id', 'created_at']


class StartupRequestDetailSerializer(serializers.ModelSerializer):
"""
Full serializer for startup request detail view.
Includes all fields for rendering the full page with markdown and documents.
"""
class Meta:
model = StartupRequest
fields = [
'id', 'title', 'description', 'short_description',
'documents', 'is_active', 'order', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at']
3 changes: 2 additions & 1 deletion backend/contributions/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .views import (
ContributionTypeViewSet, ContributionViewSet, EvidenceViewSet,
SubmittedContributionViewSet, SubmissionListView, submission_review_view,
MissionViewSet
MissionViewSet, StartupRequestViewSet
)

app_name = 'contributions'
Expand All @@ -15,6 +15,7 @@
router.register(r'evidence', EvidenceViewSet)
router.register(r'submissions', SubmittedContributionViewSet, basename='submission')
router.register(r'missions', MissionViewSet, basename='mission')
router.register(r'startup-requests', StartupRequestViewSet, basename='startup-request')

urlpatterns = [
# API URLs
Expand Down
Loading