Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
169c532
refactor: projects pages
remigermain Apr 17, 2026
5380e98
change serializers
remigermain Apr 20, 2026
1f46a3f
Merge branch 'main' of github.com:CyberCRI/projects-backend into refa…
remigermain Apr 21, 2026
2ded7dc
add members and filter/irderubg
remigermain Apr 24, 2026
a98d31f
fix: filters
remigermain Apr 24, 2026
fffef47
announcements
remigermain Apr 27, 2026
05fc6f6
fix: members reviews
remigermain May 7, 2026
5d54c14
fix: similars
remigermain May 13, 2026
5524c9a
get info fro modules
remigermain May 15, 2026
17daac0
fix: search api
remigermain May 19, 2026
5dee807
clean role group members
remigermain May 20, 2026
7135a30
optimize query
remigermain May 21, 2026
53e2758
fix: duplicate linked
remigermain May 21, 2026
38c4455
search filters/linked/follow cleanup
remigermain May 21, 2026
5b509b3
lint view
remigermain May 22, 2026
2ffbc4a
fix: members/groups queryset
remigermain May 26, 2026
1311e28
i18n
remigermain May 26, 2026
071402d
Revert "i18n"
remigermain May 29, 2026
059bd29
cleanuo
remigermain May 29, 2026
d1730c7
cleanup tests
remigermain May 29, 2026
896656d
cleanup tests
remigermain May 29, 2026
ed58762
cleanup tests
remigermain May 29, 2026
2e6be26
cleanup tests
remigermain May 29, 2026
7a3cb88
Merge branch 'main' of github.com:CyberCRI/projects-backend into refa…
remigermain May 29, 2026
875d5f7
fix: typos
remigermain May 29, 2026
0093b75
test: fix errors
remigermain Jun 1, 2026
aff175f
test: fix peoplegroup
remigermain Jun 2, 2026
3e4f3b3
test: fix tests
remigermain Jun 2, 2026
bdb52b3
test: cleanup mixins
remigermain Jun 2, 2026
a6897f7
test: fix announcements
remigermain Jun 2, 2026
a5c37b7
test: fix comments
remigermain Jun 2, 2026
fcfa63f
test: fix attachments
remigermain Jun 2, 2026
8551ae5
feat: optimize search tags wikimedia
remigermain Jun 3, 2026
49526cc
fix: tags search
remigermain Jun 3, 2026
bd54be4
test: fix create googlegroup
remigermain Jun 3, 2026
a302930
fix: tags search
remigermain Jun 9, 2026
5f9ac47
test: fix tests
remigermain Jun 10, 2026
116531e
pre-models
remigermain Jun 10, 2026
af398d5
add modules tabs
remigermain Jun 11, 2026
d159d83
clenaup models
remigermain Jun 11, 2026
c391b28
feat: add filter viewset
remigermain Jun 15, 2026
264a827
cleanup
remigermain Jun 15, 2026
d793b36
Merge branch 'main' of github.com:CyberCRI/projects-backend into feat…
remigermain Jun 19, 2026
8acb0e8
Merge branch 'main' of github.com:CyberCRI/projects-backend into feat…
remigermain Jun 22, 2026
d4e551b
revert: test type projectab
remigermain Jun 22, 2026
541ceea
test: fix owner
remigermain Jun 22, 2026
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
15 changes: 14 additions & 1 deletion apps/commons/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from apps.accounts.permissions import ProjectNestedPermission
from apps.organizations.models import Organization
from apps.organizations.utils import get_below_hierarchy_codes
from apps.projects.models import Project
from apps.projects.models import Project, ProjectTab

from .mixins import HasMultipleIDs

Expand Down Expand Up @@ -185,6 +185,19 @@ def get_permissions(self):
return [ProjectNestedPermission(), *super().get_permissions()]


class NestedProjectTabViewMixins:
tab: ProjectTab
project: Project

def initial(self, request, *args, **kwargs):

self.tab = get_object_or_404(
self.project.modules_by_user(request.user).tabs(), pk=kwargs["tab_id"]
)

super().initial(request, *args, **kwargs)


class NestedPeopleGroupViewMixins:
def initial(self, request, *args, **kwargs):
self.people_group = get_object_or_404(
Expand Down
3 changes: 2 additions & 1 deletion apps/modules/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .group import PeopleGroupModules
from .project import ProjectModules
from .tab import TabModules

__all__ = ["PeopleGroupModules", "ProjectModules"]
__all__ = ["PeopleGroupModules", "ProjectModules", "TabModules"]
4 changes: 4 additions & 0 deletions apps/modules/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Location,
Project,
ProjectMessage,
ProjectTab,
)


Expand Down Expand Up @@ -114,3 +115,6 @@ def reviews(self) -> QuerySet[Review]:

def messages(self) -> QuerySet[ProjectMessage]:
return self.instance.messages.all()

def tabs(self) -> QuerySet[ProjectTab]:
return self.instance.additional_tabs.all()
12 changes: 12 additions & 0 deletions apps/modules/tab.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.db.models import QuerySet

from apps.modules.base import AbstractModules, register_module
from apps.projects.models import ProjectTab, ProjectTabItem


@register_module(ProjectTab)
class TabModules(AbstractModules):
instance: ProjectTab

def items(self) -> QuerySet[ProjectTabItem]:
return self.instance.items.all()
27 changes: 26 additions & 1 deletion apps/projects/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
)
from apps.organizations.utils import get_below_hierarchy_codes

from .models import Location, Project
from .models import Location, Project, ProjectTab, ProjectTabItem


class ProjectFilterMixin(filters.FilterSet):
Expand Down Expand Up @@ -118,3 +118,28 @@ class ProjectGroupsFilter(filters.FilterSet):
class Meta:
model = PeopleGroup
fields = ("role",)


class ProjectTabFilter(filters.FilterSet):
type = filters.CharFilter() # noqa: A003
show_preview = filters.BooleanFilter()

class Meta:
model = ProjectTab
fields = ("type", "show_preview")


class ProjectTabItemFilter(filters.FilterSet):
from_date = filters.CharFilter(method="range_filter_from", label="form_date")
to_date = filters.CharFilter(method="range_filter_to", label="to_date")

class Meta:
model = ProjectTabItem
fields = ("from_date", "to_date")

def range_filter_from(self, queryset, name, value):
return queryset.filter(created_at__gte=value)

def range_filter_to(self, queryset, name, value):
# same above but with start_date
return queryset.filter(created_at__lte=value)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 6.0.4 on 2026-06-15 13:44

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("projects", "0003_alter_location_type"),
]

operations = [
migrations.AddField(
model_name="projecttab",
name="show_preview",
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name="projecttab",
name="description",
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name="projecttab",
name="icon",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name="projecttabitem",
name="content",
field=models.TextField(blank=True, null=True),
),
]
11 changes: 7 additions & 4 deletions apps/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,9 @@ def is_owned_by(self, user: "ProjectUser") -> bool:
return self.author == user


class ProjectTab(HasAutoTranslatedFields, ProjectRelated, models.Model):
class ProjectTab(
HasRelatedModules, HasAutoTranslatedFields, ProjectRelated, models.Model
):
"""A tab in the project page.

Attributes
Expand Down Expand Up @@ -1037,9 +1039,10 @@ class TabType(models.TextChoices):
max_length=32, choices=TabType.choices, default=TabType.TEXT
)
title = models.CharField(max_length=255)
description = models.TextField(blank=True)
icon = models.CharField(max_length=255, blank=True)
description = models.TextField(blank=True, null=True)
icon = models.CharField(max_length=255, blank=True, null=True)
images = models.ManyToManyField("files.Image", related_name="project_tabs")
show_preview = models.BooleanField(default=True)

def get_related_project(self) -> Project:
"""Return the projects related to this model."""
Expand Down Expand Up @@ -1072,7 +1075,7 @@ class ProjectTabItem(HasAutoTranslatedFields, ProjectRelated, models.Model):
"projects.ProjectTab", on_delete=models.CASCADE, related_name="items"
)
title = models.CharField(max_length=255)
content = models.TextField(blank=True)
content = models.TextField(blank=True, null=True)
images = models.ManyToManyField("files.Image", related_name="project_tab_items")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
Expand Down
4 changes: 3 additions & 1 deletion apps/projects/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,7 @@ def get_string_images_kwargs(

@auto_translated
class ProjectTabSerializer(
ModulesSerializers,
StringsImagesSerializer,
serializers.ModelSerializer,
):
Expand All @@ -914,13 +915,14 @@ class ProjectTabSerializer(

class Meta:
model = ProjectTab
read_only_fields = ["id"]
read_only_fields = ["id", "modules"]
fields = read_only_fields + [
"type",
"title",
"description",
"icon",
"images",
"show_preview",
]

def validate_type(self, value: str):
Expand Down
9 changes: 6 additions & 3 deletions apps/projects/tests/views/test_project_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,16 @@ def test_retrieve_project_tab(self, role, retrieved_tabs):
user = self.get_parameterized_test_user(role, instances=[project])
self.client.force_authenticate(user)
response = self.client.get(reverse("ProjectTab-list", args=(project.id,)))
self.assertEqual(response.status_code, status.HTTP_200_OK)
content = response.json()["results"]
if publication_status in retrieved_tabs:
self.assertEqual(response.status_code, status.HTTP_200_OK)
content = response.json()["results"]
self.assertEqual(len(content), 1)
self.assertEqual(content[0]["id"], tab.id)
else:
self.assertEqual(len(content), 0)
self.assertIn(
response.status_code,
(status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN),
)


class UpdateProjectTabTestCase(JwtAPITestCase):
Expand Down
6 changes: 4 additions & 2 deletions apps/projects/tests/views/test_project_tab_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ def setUpTestData(cls):
(TestRoles.ANONYMOUS, ("public",)),
(TestRoles.DEFAULT, ("public",)),
(TestRoles.SUPERADMIN, ("public", "org", "private")),
(TestRoles.OWNER, ("public", "org", "private")),
(TestRoles.ORG_ADMIN, ("public", "org", "private")),
(TestRoles.ORG_FACILITATOR, ("public", "org", "private")),
(TestRoles.ORG_USER, ("public", "org")),
Expand All @@ -73,7 +72,10 @@ def test_retrieve_project_tab_image(self, role, retrieved_images):
if publication_status in retrieved_images:
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
else:
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertIn(
response.status_code,
(status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN),
)


class CreateProjectTabImageTestCase(JwtAPITestCase):
Expand Down
9 changes: 6 additions & 3 deletions apps/projects/tests/views/test_project_tab_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,19 @@ def test_retrieve_project_tab_items(self, role, retrieved_items):
response = self.client.get(
reverse("ProjectTabItem-list", args=(project.id, tab.id))
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
content = response.json()["results"]
if publication_status in retrieved_items:
self.assertEqual(response.status_code, status.HTTP_200_OK)
content = response.json()["results"]
self.assertEqual(len(content), 2)
self.assertSetEqual(
{item["id"] for item in content}, {item.id for item in item}
)
self.assertGreater(content[0]["created_at"], content[1]["created_at"])
else:
self.assertEqual(len(content), 0)
self.assertIn(
response.status_code,
(status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN),
)


class UpdateProjectTabItemTestCase(JwtAPITestCase):
Expand Down
6 changes: 4 additions & 2 deletions apps/projects/tests/views/test_project_tab_item_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ def setUpTestData(cls):
(TestRoles.ANONYMOUS, ("public",)),
(TestRoles.DEFAULT, ("public",)),
(TestRoles.SUPERADMIN, ("public", "org", "private")),
(TestRoles.OWNER, ("public", "org", "private")),
(TestRoles.ORG_ADMIN, ("public", "org", "private")),
(TestRoles.ORG_FACILITATOR, ("public", "org", "private")),
(TestRoles.ORG_USER, ("public", "org")),
Expand All @@ -85,7 +84,10 @@ def test_retrieve_tab_item_image(self, role, retrieved_images):
if publication_status in retrieved_images:
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
else:
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertIn(
response.status_code,
(status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN),
)


class CreateProjectTabItemImageTestCase(JwtAPITestCase):
Expand Down
Loading
Loading