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
12 changes: 12 additions & 0 deletions app/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _

from .models import Accomplishment
from .models import Affiliate
from .models import Affiliation
from .models import CheckType
Expand Down Expand Up @@ -341,6 +342,17 @@ class UrlStatusTypeAdmin(admin.ModelAdmin):
list_display = ("name", "description")


@admin.register(Accomplishment)
class AccomplishmentAdmin(admin.ModelAdmin):
list_display = (
"title",
"project",
"accomplished_on",
"created_at",
)
list_filter = ("project", "accomplished_on")


@admin.register(Organization)
class OrganizationAdmin(admin.ModelAdmin):
list_display = ("name", "time_zone")
Expand Down
23 changes: 23 additions & 0 deletions app/core/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from rest_framework import serializers
from timezone_field.rest_framework import TimeZoneSerializerField

from core.models import Accomplishment
from core.models import Affiliate
from core.models import Affiliation
from core.models import CheckType
Expand Down Expand Up @@ -461,6 +462,28 @@ class Meta:
read_only_fields = ("uuid", "created_at", "updated_at")


class AccomplishmentSerializer(serializers.ModelSerializer):
"""Used to retrieve accomplishment info"""

class Meta:
model = Accomplishment
fields = (
"uuid",
"created_at",
"updated_at",
"project",
"title",
"description",
"url",
"accomplished_on",
)
read_only_fields = (
"uuid",
"created_at",
"updated_at",
)


class UrlTypeSerializer(serializers.ModelSerializer):
"""Used to retrieve url_type info"""

Expand Down
2 changes: 2 additions & 0 deletions app/core/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.urls import path
from rest_framework import routers

from .views import AccomplishmentViewSet
from .views import AffiliateViewSet
from .views import AffiliationViewSet
from .views import CheckTypeViewSet
Expand Down Expand Up @@ -70,6 +71,7 @@
router.register(r"soc-broads", SocBroadViewSet, basename="soc-broad")
router.register(r"soc-majors", SocMajorViewSet, basename="soc-major")
router.register(r"soc-minors", SocMinorViewSet, basename="soc-minor")
router.register(r"accomplishments", AccomplishmentViewSet, basename="accomplishment")
router.register(r"url-types", UrlTypeViewSet, basename="url-type")
router.register(
r"user-status-types", UserStatusTypeViewSet, basename="user-status-type"
Expand Down
16 changes: 16 additions & 0 deletions app/core/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.response import Response

from ..models import Accomplishment
from ..models import Affiliate
from ..models import Affiliation
from ..models import CheckType
Expand Down Expand Up @@ -47,6 +48,7 @@
from ..models import UserStatusType
from ..models import Win
from ..models import WinType
from .serializers import AccomplishmentSerializer
from .serializers import AffiliateSerializer
from .serializers import AffiliationSerializer
from .serializers import CheckTypeSerializer
Expand Down Expand Up @@ -401,6 +403,20 @@ class AffiliationViewSet(viewsets.ModelViewSet):
serializer_class = AffiliationSerializer


@extend_schema_view(
list=extend_schema(description="Return a list of all accomplishments"),
create=extend_schema(description="Create a new accomplishment"),
retrieve=extend_schema(description="Return the details of an accomplishment"),
destroy=extend_schema(description="Delete an accomplishment"),
update=extend_schema(description="Update an accomplishment"),
partial_update=extend_schema(description="Patch an accomplishment"),
)
class AccomplishmentViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
queryset = Accomplishment.objects.all()
serializer_class = AccomplishmentSerializer


@extend_schema_view(
list=extend_schema(description="Return a list of all the check_type"),
create=extend_schema(description="Create a new check_type"),
Expand Down
30 changes: 30 additions & 0 deletions app/core/migrations/0048_accomplishment_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 4.2.16 on 2026-01-25 17:30

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


class Migration(migrations.Migration):

dependencies = [('core', '0047_win')]

operations = [
migrations.CreateModel(
name='Accomplishment',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('title', models.CharField(db_comment='Title of the accomplishment', help_text='Title of the accomplishment', max_length=255)),
('description', models.TextField(db_comment='Detailed description of the accomplishment', help_text='Detailed description of the accomplishment')),
('url', models.URLField(db_comment='URL link to the accomplishment', help_text='URL link to the accomplishment')),
('accomplished_on', models.DateTimeField(db_comment='Date when the accomplishment was achieved', help_text='Date when the accomplishment was achieved')),
('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')),
],
),
migrations.AddConstraint(
model_name='accomplishment',
constraint=models.UniqueConstraint(fields=('project', 'title'), name='unique_accomplishment_per_project'),
),
]
2 changes: 1 addition & 1 deletion app/core/migrations/max_migration.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0047_win
0048_accomplishment_and_more
42 changes: 42 additions & 0 deletions app/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,48 @@ def __str__(self):
return self.title


class Accomplishment(AbstractBaseModel):
"""
Project accomplishments and milestones
"""

project = models.ForeignKey(
Project,
on_delete=models.PROTECT,
related_name="accomplishments",
db_comment="Project this accomplishment belongs to",
help_text="Project this accomplishment belongs to",
)
title = models.CharField(
max_length=255,
db_comment="Title of the accomplishment",
help_text="Title of the accomplishment",
)
description = models.TextField(
db_comment="Detailed description of the accomplishment",
help_text="Detailed description of the accomplishment",
)
url = models.URLField(
db_comment="URL link to the accomplishment",
help_text="URL link to the accomplishment",
)
accomplished_on = models.DateTimeField(
db_comment="Date when the accomplishment was achieved",
help_text="Date when the accomplishment was achieved",
)

class Meta:
constraints = [
models.UniqueConstraint(
fields=["project", "title"],
name="unique_accomplishment_per_project",
)
]

def __str__(self):
return self.title


class ProjectProgramAreaXref(AbstractBaseModel):
project = models.ForeignKey(Project, on_delete=models.CASCADE)
program_area = models.ForeignKey(ProgramArea, on_delete=models.CASCADE)
Expand Down
12 changes: 12 additions & 0 deletions app/core/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from constants import admin_project
from constants import practice_lead_project

from ..models import Accomplishment
from ..models import Affiliate
from ..models import Affiliation
from ..models import CheckType
Expand Down Expand Up @@ -417,6 +418,17 @@ def url_status_type(db):
)


@pytest.fixture
def accomplishment(project):
return Accomplishment.objects.create(
project=project,
title="Test Accomplishment",
description="This is a test accomplishment",
url="https://example.com",
accomplished_on="2025-09-11T18:05:00Z",
)


@pytest.fixture
def organization():
return Organization.objects.create(
Expand Down
16 changes: 16 additions & 0 deletions app/core/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
SOC_BROAD_URL = reverse("soc-broad-list")
SOC_MAJOR_URL = reverse("soc-major-list")
SOC_MINORS_URL = reverse("soc-minor-list")
ACCOMPLISHMENT_URL = reverse("accomplishment-list")
URL_TYPE_URL = reverse("url-type-list")
PROJECT_STACK_ELEMENTS_URL = reverse("project-stack-element-list")
URL_STATUS_TYPES_URL = reverse("url-status-type-list")
Expand Down Expand Up @@ -512,6 +513,21 @@ def test_soc_minor_soc_major_relationship(auth_client, soc_minor, soc_major):
assert soc_major_exists is True


def test_accomplishment(auth_client, project):
"""Test that we can create a accomplishment"""

payload = {
"project": project.uuid,
"title": "Test title",
"description": "Test description",
"url": "https://redwind01.com",
"accomplished_on": "2024-01-01T18:00:00Z",
}
res = auth_client.post(ACCOMPLISHMENT_URL, payload)
assert res.status_code == status.HTTP_201_CREATED
assert res.data["title"] == payload["title"]


def test_project_sdg_xref(auth_client, project, sdg, sdg1):
def get_object(objects, target_uuid):
for obj in objects:
Expand Down
5 changes: 5 additions & 0 deletions app/core/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,3 +622,8 @@ def test_win_type_str(win_type):
def test_win_type_fields(win_type):
assert win_type.name == "funding"
assert win_type.display_text == "Funding / Grant awarded"


def test_accomplishment_str(accomplishment):
# __str__ returns the name
assert str(accomplishment) == "Test Accomplishment"