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
5 changes: 5 additions & 0 deletions docs/application-project-submission-rfc.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ PROCOLLAB работает с кейс-чемпионатами, хакатон
- `created_at`
- `updated_at`

MVP implementation note: `team` is intentionally not added in the first
`Application` model PR because the `Team` model does not exist yet. Until the
Team PR lands, backend validation treats `Application` as an individual
application and requires `user`.

Связи:

- принадлежит одному `Program`;
Expand Down
39 changes: 39 additions & 0 deletions partner_programs/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from core.utils import XlsxFileToExport, build_xlsx_download_response
from mailing.views import MailingTemplateRender
from partner_programs.models import (
Application,
PartnerProgram,
PartnerProgramField,
PartnerProgramFieldValue,
Expand All @@ -21,6 +22,44 @@
from partner_programs.services import prepare_project_scores_export_data


@admin.register(Application)
class ApplicationAdmin(admin.ModelAdmin):
list_display = (
"id",
"program",
"user",
"created_by",
"status",
"project",
"submitted_at",
"created_at",
"updated_at",
)
list_filter = (
"status",
"program",
"created_at",
)
search_fields = (
"user__email",
"created_by__email",
"program__name",
"program__tag",
"project__name",
)
raw_id_fields = (
"program",
"user",
"created_by",
"project",
)
readonly_fields = (
"created_at",
"updated_at",
)
date_hierarchy = "created_at"


class PartnerProgramMaterialInline(admin.StackedInline):
model = PartnerProgramMaterial
extra = 1
Expand Down
104 changes: 104 additions & 0 deletions partner_programs/migrations/0018_application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Generated by Django 4.2.24 on 2026-07-20

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


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("partner_programs", "0017_alter_partnerprogramproject_options"),
("projects", "0033_delete_projectnews"),
]

operations = [
migrations.CreateModel(
name="Application",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"status",
models.CharField(
choices=[
("draft", "Draft"),
("submitted", "Submitted"),
("approved", "Approved"),
("rejected", "Rejected"),
("withdrawn", "Withdrawn"),
("cancelled", "Cancelled"),
],
default="draft",
max_length=16,
),
),
("form_data", models.JSONField(blank=True, default=dict)),
("submitted_at", models.DateTimeField(blank=True, null=True)),
("approved_at", models.DateTimeField(blank=True, null=True)),
("rejected_at", models.DateTimeField(blank=True, null=True)),
("withdrawn_at", models.DateTimeField(blank=True, null=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"created_by",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="created_program_applications",
to=settings.AUTH_USER_MODEL,
),
),
(
"program",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="applications",
to="partner_programs.partnerprogram",
),
),
(
"project",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="applications",
to="projects.project",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="program_applications",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Application",
"verbose_name_plural": "Applications",
},
),
migrations.AddConstraint(
model_name="application",
constraint=models.UniqueConstraint(
condition=models.Q(
("status__in", ("draft", "submitted", "approved")),
("user__isnull", False),
),
fields=("user", "program"),
name="uniq_active_application_user_program",
),
),
]
118 changes: 118 additions & 0 deletions partner_programs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,124 @@ def is_project_submission_open(self) -> bool:
return deadline is None or deadline >= timezone.now()


class Application(models.Model):
"""
MVP application model for participation in a partner program.

Team applications are intentionally deferred until the Team model exists.
For now, an application is individual and must have `user`.
"""

STATUS_DRAFT = "draft"
STATUS_SUBMITTED = "submitted"
STATUS_APPROVED = "approved"
STATUS_REJECTED = "rejected"
STATUS_WITHDRAWN = "withdrawn"
STATUS_CANCELLED = "cancelled"

ACTIVE_STATUSES = (
STATUS_DRAFT,
STATUS_SUBMITTED,
STATUS_APPROVED,
)

STATUS_CHOICES = (
(STATUS_DRAFT, "Draft"),
(STATUS_SUBMITTED, "Submitted"),
(STATUS_APPROVED, "Approved"),
(STATUS_REJECTED, "Rejected"),
(STATUS_WITHDRAWN, "Withdrawn"),
(STATUS_CANCELLED, "Cancelled"),
)

program = models.ForeignKey(
PartnerProgram,
on_delete=models.CASCADE,
related_name="applications",
)
user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
related_name="program_applications",
null=True,
blank=True,
)
created_by = models.ForeignKey(
User,
on_delete=models.PROTECT,
related_name="created_program_applications",
)
status = models.CharField(
max_length=16,
choices=STATUS_CHOICES,
default=STATUS_DRAFT,
)
form_data = models.JSONField(default=dict, blank=True)
project = models.ForeignKey(
Project,
on_delete=models.SET_NULL,
related_name="applications",
null=True,
blank=True,
)
submitted_at = models.DateTimeField(null=True, blank=True)
approved_at = models.DateTimeField(null=True, blank=True)
rejected_at = models.DateTimeField(null=True, blank=True)
withdrawn_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def clean(self):
super().clean()

errors = {}

if not self.user_id:
errors["user"] = (
"User is required for MVP applications until Team is implemented."
)

if self.user_id and self.program_id and self.status in self.ACTIVE_STATUSES:
duplicate_qs = Application.objects.filter(
user_id=self.user_id,
program_id=self.program_id,
status__in=self.ACTIVE_STATUSES,
)
if self.pk:
duplicate_qs = duplicate_qs.exclude(pk=self.pk)
if duplicate_qs.exists():
errors["user"] = (
"User already has an active application for this program."
)

if errors:
raise ValidationError(errors)

def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)

class Meta:
verbose_name = "Application"
verbose_name_plural = "Applications"
constraints = [
models.UniqueConstraint(
fields=["user", "program"],
condition=models.Q(
user__isnull=False,
status__in=("draft", "submitted", "approved"),
),
name="uniq_active_application_user_program",
),
]

def __str__(self):
return (
f"Application<{self.pk}> "
f"user={self.user_id} program={self.program_id} status={self.status}"
)


class PartnerProgramUserProfile(models.Model):
"""
PartnerProgramUserProfile model
Expand Down
Loading
Loading