Skip to content
Open
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ docs/build
*.pyc

.vscode
TODO.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks you have a local file to ignore. It shouldn't be committed to the project though.

You can move this line to .git/info/exclude so it applies only to your local repo.

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 @@ -332,6 +333,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 @@ -445,6 +446,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 @@ -67,6 +68,7 @@
router.register(r"project-urls", ProjectUrlViewSet, basename="project-url")
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 @@ -16,6 +16,7 @@

from core.models import WinType

from ..models import Accomplishment
from ..models import Affiliate
from ..models import Affiliation
from ..models import CheckType
Expand Down Expand Up @@ -46,6 +47,7 @@
from ..models import UserCheck
from ..models import UserPermission
from ..models import UserStatusType
from .serializers import AccomplishmentSerializer
from .serializers import AffiliateSerializer
from .serializers import AffiliationSerializer
from .serializers import CheckTypeSerializer
Expand Down Expand Up @@ -398,6 +400,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
33 changes: 33 additions & 0 deletions app/core/migrations/0046_accomplishment_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 4.2.16 on 2026-01-16 06:58

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


class Migration(migrations.Migration):

dependencies = [('core', '0045_wintype')]

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')),
],
options={
'indexes': [models.Index(fields=['project'], name='core_accomp_project_idx'), models.Index(fields=['accomplished_on'], name='core_accomp_date_idx')],
},
),
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 @@
0045_wintype
0046_accomplishment_and_more
48 changes: 47 additions & 1 deletion app/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

class AbstractBaseModel(models.Model):
"""
Base abstract model, that has `uuid` instead of `id` and included `created_at`, `updated_at` fields.
Base abstract model, that has `uuid` instead of `id` fields.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines shouldn't be changed since it's not part of the issue.

"""

uuid = models.UUIDField(
Expand Down Expand Up @@ -507,6 +507,52 @@ 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:
indexes = [
models.Index(fields=["project"], name="core_accomp_project_idx"),
models.Index(fields=["accomplished_on"], name="core_accomp_date_idx"),
]
Comment on lines +541 to +544
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not necessary to add these indexes. Django already adds some default ones that overlap these.

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 @@ -406,6 +407,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 @@ -45,6 +45,7 @@
PROJECT_URLS_URL = reverse("project-url-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 @@ -480,6 +481,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