Skip to content

Commit ef2e97b

Browse files
drakeredwind01fyliu
authored andcommitted
Add Accomplishment model and API endpoints for Feature #48
1 parent 4780d10 commit ef2e97b

10 files changed

Lines changed: 159 additions & 1 deletion

File tree

app/core/admin.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.contrib.auth.forms import UsernameField
77
from django.utils.translation import gettext_lazy as _
88

9+
from .models import Accomplishment
910
from .models import Affiliate
1011
from .models import Affiliation
1112
from .models import CheckType
@@ -341,6 +342,17 @@ class UrlStatusTypeAdmin(admin.ModelAdmin):
341342
list_display = ("name", "description")
342343

343344

345+
@admin.register(Accomplishment)
346+
class AccomplishmentAdmin(admin.ModelAdmin):
347+
list_display = (
348+
"title",
349+
"project",
350+
"accomplished_on",
351+
"created_at",
352+
)
353+
list_filter = ("project", "accomplished_on")
354+
355+
344356
@admin.register(Organization)
345357
class OrganizationAdmin(admin.ModelAdmin):
346358
list_display = ("name", "time_zone")

app/core/api/serializers.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from rest_framework import serializers
22
from timezone_field.rest_framework import TimeZoneSerializerField
33

4+
from core.models import Accomplishment
45
from core.models import Affiliate
56
from core.models import Affiliation
67
from core.models import CheckType
@@ -461,6 +462,28 @@ class Meta:
461462
read_only_fields = ("uuid", "created_at", "updated_at")
462463

463464

465+
class AccomplishmentSerializer(serializers.ModelSerializer):
466+
"""Used to retrieve accomplishment info"""
467+
468+
class Meta:
469+
model = Accomplishment
470+
fields = (
471+
"uuid",
472+
"created_at",
473+
"updated_at",
474+
"project",
475+
"title",
476+
"description",
477+
"url",
478+
"accomplished_on",
479+
)
480+
read_only_fields = (
481+
"uuid",
482+
"created_at",
483+
"updated_at",
484+
)
485+
486+
464487
class UrlTypeSerializer(serializers.ModelSerializer):
465488
"""Used to retrieve url_type info"""
466489

app/core/api/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.urls import path
22
from rest_framework import routers
33

4+
from .views import AccomplishmentViewSet
45
from .views import AffiliateViewSet
56
from .views import AffiliationViewSet
67
from .views import CheckTypeViewSet
@@ -70,6 +71,7 @@
7071
router.register(r"soc-broads", SocBroadViewSet, basename="soc-broad")
7172
router.register(r"soc-majors", SocMajorViewSet, basename="soc-major")
7273
router.register(r"soc-minors", SocMinorViewSet, basename="soc-minor")
74+
router.register(r"accomplishments", AccomplishmentViewSet, basename="accomplishment")
7375
router.register(r"url-types", UrlTypeViewSet, basename="url-type")
7476
router.register(
7577
r"user-status-types", UserStatusTypeViewSet, basename="user-status-type"

app/core/api/views.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from rest_framework.permissions import IsAuthenticatedOrReadOnly
1515
from rest_framework.response import Response
1616

17+
from ..models import Accomplishment
1718
from ..models import Affiliate
1819
from ..models import Affiliation
1920
from ..models import CheckType
@@ -47,6 +48,7 @@
4748
from ..models import UserStatusType
4849
from ..models import Win
4950
from ..models import WinType
51+
from .serializers import AccomplishmentSerializer
5052
from .serializers import AffiliateSerializer
5153
from .serializers import AffiliationSerializer
5254
from .serializers import CheckTypeSerializer
@@ -401,6 +403,20 @@ class AffiliationViewSet(viewsets.ModelViewSet):
401403
serializer_class = AffiliationSerializer
402404

403405

406+
@extend_schema_view(
407+
list=extend_schema(description="Return a list of all accomplishments"),
408+
create=extend_schema(description="Create a new accomplishment"),
409+
retrieve=extend_schema(description="Return the details of an accomplishment"),
410+
destroy=extend_schema(description="Delete an accomplishment"),
411+
update=extend_schema(description="Update an accomplishment"),
412+
partial_update=extend_schema(description="Patch an accomplishment"),
413+
)
414+
class AccomplishmentViewSet(viewsets.ModelViewSet):
415+
permission_classes = [IsAuthenticated]
416+
queryset = Accomplishment.objects.all()
417+
serializer_class = AccomplishmentSerializer
418+
419+
404420
@extend_schema_view(
405421
list=extend_schema(description="Return a list of all the check_type"),
406422
create=extend_schema(description="Create a new check_type"),
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 4.2.16 on 2026-01-25 17:30
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import uuid
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [('core', '0047_win')]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='Accomplishment',
15+
fields=[
16+
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
17+
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
18+
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
19+
('title', models.CharField(db_comment='Title of the accomplishment', help_text='Title of the accomplishment', max_length=255)),
20+
('description', models.TextField(db_comment='Detailed description of the accomplishment', help_text='Detailed description of the accomplishment')),
21+
('url', models.URLField(db_comment='URL link to the accomplishment', help_text='URL link to the accomplishment')),
22+
('accomplished_on', models.DateTimeField(db_comment='Date when the accomplishment was achieved', help_text='Date when the accomplishment was achieved')),
23+
('project', models.ForeignKey(db_comment='Project this accomplishment belongs to', help_text='Project this accomplishment belongs to', on_delete=django.db.models.deletion.PROTECT, related_name='accomplishments', to='core.project')),
24+
],
25+
),
26+
migrations.AddConstraint(
27+
model_name='accomplishment',
28+
constraint=models.UniqueConstraint(fields=('project', 'title'), name='unique_accomplishment_per_project'),
29+
),
30+
]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0047_win
1+
0048_accomplishment_and_more

app/core/models.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,48 @@ def __str__(self):
524524
return self.title
525525

526526

527+
class Accomplishment(AbstractBaseModel):
528+
"""
529+
Project accomplishments and milestones
530+
"""
531+
532+
project = models.ForeignKey(
533+
Project,
534+
on_delete=models.PROTECT,
535+
related_name="accomplishments",
536+
db_comment="Project this accomplishment belongs to",
537+
help_text="Project this accomplishment belongs to",
538+
)
539+
title = models.CharField(
540+
max_length=255,
541+
db_comment="Title of the accomplishment",
542+
help_text="Title of the accomplishment",
543+
)
544+
description = models.TextField(
545+
db_comment="Detailed description of the accomplishment",
546+
help_text="Detailed description of the accomplishment",
547+
)
548+
url = models.URLField(
549+
db_comment="URL link to the accomplishment",
550+
help_text="URL link to the accomplishment",
551+
)
552+
accomplished_on = models.DateTimeField(
553+
db_comment="Date when the accomplishment was achieved",
554+
help_text="Date when the accomplishment was achieved",
555+
)
556+
557+
class Meta:
558+
constraints = [
559+
models.UniqueConstraint(
560+
fields=["project", "title"],
561+
name="unique_accomplishment_per_project",
562+
)
563+
]
564+
565+
def __str__(self):
566+
return self.title
567+
568+
527569
class ProjectProgramAreaXref(AbstractBaseModel):
528570
project = models.ForeignKey(Project, on_delete=models.CASCADE)
529571
program_area = models.ForeignKey(ProgramArea, on_delete=models.CASCADE)

app/core/tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from constants import admin_project
55
from constants import practice_lead_project
66

7+
from ..models import Accomplishment
78
from ..models import Affiliate
89
from ..models import Affiliation
910
from ..models import CheckType
@@ -417,6 +418,17 @@ def url_status_type(db):
417418
)
418419

419420

421+
@pytest.fixture
422+
def accomplishment(project):
423+
return Accomplishment.objects.create(
424+
project=project,
425+
title="Test Accomplishment",
426+
description="This is a test accomplishment",
427+
url="https://example.com",
428+
accomplished_on="2025-09-11T18:05:00Z",
429+
)
430+
431+
420432
@pytest.fixture
421433
def organization():
422434
return Organization.objects.create(

app/core/tests/test_api.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
SOC_BROAD_URL = reverse("soc-broad-list")
4848
SOC_MAJOR_URL = reverse("soc-major-list")
4949
SOC_MINORS_URL = reverse("soc-minor-list")
50+
ACCOMPLISHMENT_URL = reverse("accomplishment-list")
5051
URL_TYPE_URL = reverse("url-type-list")
5152
PROJECT_STACK_ELEMENTS_URL = reverse("project-stack-element-list")
5253
URL_STATUS_TYPES_URL = reverse("url-status-type-list")
@@ -512,6 +513,21 @@ def test_soc_minor_soc_major_relationship(auth_client, soc_minor, soc_major):
512513
assert soc_major_exists is True
513514

514515

516+
def test_accomplishment(auth_client, project):
517+
"""Test that we can create a accomplishment"""
518+
519+
payload = {
520+
"project": project.uuid,
521+
"title": "Test title",
522+
"description": "Test description",
523+
"url": "https://redwind01.com",
524+
"accomplished_on": "2024-01-01T18:00:00Z",
525+
}
526+
res = auth_client.post(ACCOMPLISHMENT_URL, payload)
527+
assert res.status_code == status.HTTP_201_CREATED
528+
assert res.data["title"] == payload["title"]
529+
530+
515531
def test_project_sdg_xref(auth_client, project, sdg, sdg1):
516532
def get_object(objects, target_uuid):
517533
for obj in objects:

app/core/tests/test_models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,3 +622,8 @@ def test_win_type_str(win_type):
622622
def test_win_type_fields(win_type):
623623
assert win_type.name == "funding"
624624
assert win_type.display_text == "Funding / Grant awarded"
625+
626+
627+
def test_accomplishment_str(accomplishment):
628+
# __str__ returns the name
629+
assert str(accomplishment) == "Test Accomplishment"

0 commit comments

Comments
 (0)