Skip to content

Commit 00dd406

Browse files
authored
Merge pull request #51 from Xpirix/api_for_orgs_and_courses
Api for orgs and courses
2 parents 0bc3fbe + 05deb36 commit 00dd406

File tree

7 files changed

+734
-313
lines changed

7 files changed

+734
-313
lines changed

.pre-commit-config.yaml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# See https://pre-commit.com for more information
2+
# See https://pre-commit.com/hooks.html for more hooks
3+
4+
exclude: 'static/'
5+
repos:
6+
# Fix end of files
7+
- repo: https://github.com/pre-commit/pre-commit-hooks
8+
rev: v5.0.0
9+
hooks:
10+
- id: trailing-whitespace
11+
- id: end-of-file-fixer
12+
- id: mixed-line-ending
13+
args:
14+
- '--fix=lf'
15+
16+
# Remove unused imports/variables
17+
- repo: https://github.com/myint/autoflake
18+
rev: v2.3.1
19+
hooks:
20+
- id: autoflake
21+
args:
22+
- "--in-place"
23+
- "--remove-unused-variables"
24+
- "--remove-all-unused-imports"
25+
26+
# Sort imports
27+
- repo: https://github.com/pycqa/isort
28+
rev: "6.0.1"
29+
hooks:
30+
- id: isort
31+
args: ["--profile", "black"]
32+
33+
# Black formatting
34+
- repo: https://github.com/psf/black
35+
rev: 25.1.0
36+
hooks:
37+
- id: black
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from base.models.project import Project
2+
from certification.models import CertifyingOrganisation
3+
from rest_framework import status
4+
from rest_framework.pagination import PageNumberPagination
5+
from rest_framework.response import Response
6+
from rest_framework.views import APIView
7+
8+
from ..serializers.organisation_serializer import CertifyingOrganisationSerializer
9+
10+
11+
class CustomPagination(PageNumberPagination):
12+
page_size = 10 # Default page size
13+
page_size_query_param = (
14+
"page_size" # Allow client to override the page size via query parameter
15+
)
16+
max_page_size = 100 # Maximum limit allowed
17+
18+
def get_paginated_response(self, data):
19+
return Response(
20+
{
21+
"count": self.page.paginator.count,
22+
"page_size": self.get_page_size(self.request),
23+
"next": self.get_next_link(),
24+
"previous": self.get_previous_link(),
25+
"results": data,
26+
}
27+
)
28+
29+
30+
class CertifyingOrganisationApiListView(APIView):
31+
"""
32+
API view to list all certifying organisations with pagination and optional filtering.
33+
34+
Available query parameters:
35+
- country: Filter organisations by one or more country codes (comma-separated, e.g., ZA or ZA,MG,NL) (optional)
36+
- page: Page number to retrieve (default: 1)
37+
- page_size: Number of items per page (default: 10, max: 100)
38+
"""
39+
40+
# Apply pagination class
41+
pagination_class = CustomPagination
42+
43+
def get(self, request):
44+
"""
45+
Handle GET requests to list certifying organisations.
46+
47+
Returns paginated response with:
48+
- count: Total number of items
49+
- page_size: Number of items per page
50+
- next: URL to next page (if exists)
51+
- previous: URL to previous page (if exists)
52+
- results: List of organisations for current page
53+
"""
54+
country_param = request.query_params.get("country")
55+
project = Project.objects.get(slug="qgis")
56+
organisations = CertifyingOrganisation.approved_objects.filter(
57+
project=project, is_archived=False
58+
)
59+
60+
if country_param:
61+
country_codes = [
62+
c.strip().upper() for c in country_param.split(",") if c.strip()
63+
]
64+
organisations = organisations.filter(country__in=country_codes)
65+
66+
# Paginate the queryset
67+
paginator = self.pagination_class()
68+
page = paginator.paginate_queryset(organisations, request)
69+
70+
if page is not None:
71+
serializer = CertifyingOrganisationSerializer(page, many=True)
72+
return paginator.get_paginated_response(serializer.data)
73+
74+
# Fallback for non-paginated response if needed
75+
serializer = CertifyingOrganisationSerializer(organisations, many=True)
76+
return Response(serializer.data, status=status.HTTP_200_OK)
Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# coding=utf-8
22
from datetime import datetime
3+
4+
from base.models.project import Project
35
from django.http import HttpResponse
4-
from rest_framework.views import APIView, Response
56
from rest_framework import status
6-
from base.models.project import Project
7+
from rest_framework.views import APIView, Response
8+
79
from ..models.certifying_organisation import CertifyingOrganisation
810
from ..models.course import Course
911
from ..serializers.course_serializer import CourseSerializer
@@ -14,23 +16,35 @@ class GetUpcomingCourseProject(APIView):
1416
The location is the location of the training center where this course
1517
will be held.
1618
19+
Optional query parameter:
20+
- country: Filter courses by the organisation's country (ISO 3166-1 alpha-2 code).
21+
Supports single or multiple comma-separated codes.
22+
Example: /feed/upcoming-course/?country=ZA or /feed/upcoming-course/?country=ZA,MG,NL
23+
1724
"""
1825

1926
def get(self, request):
27+
country_param = request.GET.get("country", None)
2028
try:
2129
today = datetime.today()
22-
project = Project.objects.get(slug='qgis')
30+
project = Project.objects.get(slug="qgis")
2331
courses = Course.objects.filter(
2432
certifying_organisation__project=project, start_date__gte=today
25-
).order_by(
26-
'certifying_organisation__name', 'start_date'
2733
)
34+
if country_param:
35+
country_list = [
36+
c.strip().upper() for c in country_param.split(",") if c.strip()
37+
]
38+
if country_list:
39+
courses = courses.filter(
40+
certifying_organisation__country__in=country_list
41+
)
42+
courses = courses.order_by("certifying_organisation__name", "start_date")
2843
serializer = CourseSerializer(courses, many=True)
2944
return Response(serializer.data)
3045
except Project.DoesNotExist:
3146
return HttpResponse(
32-
'Project does not exist.',
33-
status=status.HTTP_400_BAD_REQUEST
47+
"Project does not exist.", status=status.HTTP_400_BAD_REQUEST
3448
)
3549

3650

@@ -44,34 +58,30 @@ class GetUpcomingCourseOrganisation(APIView):
4458
def get(self, request, organisation_slug):
4559
today = datetime.today()
4660
try:
47-
project = Project.objects.get(slug='qgis')
61+
project = Project.objects.get(slug="qgis")
4862
except Project.DoesNotExist:
4963
return HttpResponse(
50-
'Project does not exist.',
51-
status=status.HTTP_400_BAD_REQUEST
64+
"Project does not exist.", status=status.HTTP_400_BAD_REQUEST
5265
)
5366

5467
try:
5568
organisation = CertifyingOrganisation.objects.get(
56-
slug=organisation_slug,
57-
project=project
69+
slug=organisation_slug, project=project
5870
)
5971
except Project.DoesNotExist:
6072
return HttpResponse(
61-
'Organisation does not exist.',
62-
status=status.HTTP_400_BAD_REQUEST
73+
"Organisation does not exist.", status=status.HTTP_400_BAD_REQUEST
6374
)
6475

6576
try:
6677
courses = Course.objects.filter(
6778
certifying_organisation=organisation, start_date__gte=today
68-
).order_by('start_date')
79+
).order_by("start_date")
6980
serializer = CourseSerializer(courses, many=True)
7081
return Response(serializer.data)
7182
except Course.DoesNotExist:
7283
return HttpResponse(
73-
'Course does not exist.',
74-
status=status.HTTP_400_BAD_REQUEST
84+
"Course does not exist.", status=status.HTTP_400_BAD_REQUEST
7585
)
7686

7787

@@ -85,18 +95,15 @@ class GetPastCourseProject(APIView):
8595
def get(self, request):
8696
try:
8797
today = datetime.today()
88-
project = Project.objects.get(slug='qgis')
98+
project = Project.objects.get(slug="qgis")
8999
courses = Course.objects.filter(
90100
certifying_organisation__project=project, end_date__lte=today
91-
).order_by(
92-
'certifying_organisation__name', 'start_date'
93-
)
101+
).order_by("certifying_organisation__name", "start_date")
94102
serializer = CourseSerializer(courses, many=True)
95103
return Response(serializer.data)
96104
except Project.DoesNotExist:
97105
return HttpResponse(
98-
'Project does not exist.',
99-
status=status.HTTP_400_BAD_REQUEST
106+
"Project does not exist.", status=status.HTTP_400_BAD_REQUEST
100107
)
101108

102109

@@ -110,32 +117,28 @@ class GetPastCourseOrganisation(APIView):
110117
def get(self, request, organisation_slug):
111118
today = datetime.today()
112119
try:
113-
project = Project.objects.get(slug='qgis')
120+
project = Project.objects.get(slug="qgis")
114121
except Project.DoesNotExist:
115122
return HttpResponse(
116-
'Project does not exist.',
117-
status=status.HTTP_400_BAD_REQUEST
123+
"Project does not exist.", status=status.HTTP_400_BAD_REQUEST
118124
)
119125

120126
try:
121127
organisation = CertifyingOrganisation.objects.get(
122-
slug=organisation_slug,
123-
project=project
128+
slug=organisation_slug, project=project
124129
)
125130
except Project.DoesNotExist:
126131
return HttpResponse(
127-
'Organisation does not exist.',
128-
status=status.HTTP_400_BAD_REQUEST
132+
"Organisation does not exist.", status=status.HTTP_400_BAD_REQUEST
129133
)
130134

131135
try:
132136
courses = Course.objects.filter(
133137
certifying_organisation=organisation, end_date__lte=today
134-
).order_by('start_date')
138+
).order_by("start_date")
135139
serializer = CourseSerializer(courses, many=True)
136140
return Response(serializer.data)
137141
except Course.DoesNotExist:
138142
return HttpResponse(
139-
'Course does not exist.',
140-
status=status.HTTP_400_BAD_REQUEST
143+
"Course does not exist.", status=status.HTTP_400_BAD_REQUEST
141144
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from certification.models import CertifyingOrganisation
2+
from rest_framework import serializers
3+
4+
5+
class CertifyingOrganisationSerializer(serializers.ModelSerializer):
6+
"""Serializer for Certifying Organisation model."""
7+
8+
country_name = serializers.SerializerMethodField()
9+
country_code = serializers.SerializerMethodField()
10+
11+
class Meta:
12+
model = CertifyingOrganisation
13+
fields = [
14+
"name",
15+
"country_name",
16+
"country_code",
17+
"organisation_email",
18+
"url",
19+
"address",
20+
"organisation_phone",
21+
]
22+
23+
def get_country_name(self, obj):
24+
"""Get the name of the country."""
25+
return obj.country.name if obj.country else None
26+
27+
def get_country_code(self, obj):
28+
"""Get the ISO 3166-1 alpha-2 code of the country."""
29+
return obj.country.code if obj.country else None
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# coding=utf-8
2+
import logging
3+
4+
from certification.tests.model_factories import (
5+
CertificateTypeF,
6+
CertifyingOrganisationF,
7+
CourseConvenerF,
8+
CourseF,
9+
CourseTypeF,
10+
ProjectCertificateTypeF,
11+
ProjectF,
12+
TrainingCenterF,
13+
UserF,
14+
)
15+
from django.test import TestCase, override_settings
16+
from django.test.client import Client
17+
from django.urls import reverse
18+
19+
20+
class TestCourseApiView(TestCase):
21+
"""Test that Course API View works."""
22+
23+
@override_settings(
24+
VALID_DOMAIN=[
25+
"testserver",
26+
]
27+
)
28+
def setUp(self):
29+
"""
30+
Setup before each test
31+
We force the locale to en otherwise it will use
32+
the locale of the host running the tests and we
33+
will get unpredictable results / 404s
34+
"""
35+
36+
self.client = Client()
37+
self.client.post("/set_language/", data={"language": "en"})
38+
logging.disable(logging.CRITICAL)
39+
self.user = UserF.create(
40+
**{"username": "anita", "password": "password", "is_staff": True}
41+
)
42+
self.user.set_password("password")
43+
self.user.save()
44+
self.project = ProjectF.create()
45+
self.certifying_organisation = CertifyingOrganisationF.create(
46+
project=self.project
47+
)
48+
self.training_center = TrainingCenterF.create(
49+
certifying_organisation=self.certifying_organisation
50+
)
51+
self.course_convener = CourseConvenerF.create(
52+
certifying_organisation=self.certifying_organisation
53+
)
54+
self.course_type = CourseTypeF.create(
55+
certifying_organisation=self.certifying_organisation
56+
)
57+
self.course = CourseF.create(
58+
certifying_organisation=self.certifying_organisation,
59+
training_center=self.training_center,
60+
course_convener=self.course_convener,
61+
course_type=self.course_type,
62+
)
63+
self.certificate_type = CertificateTypeF.create()
64+
self.project_cert_type = ProjectCertificateTypeF.create(
65+
project=self.project, certificate_type=self.certificate_type
66+
)
67+
68+
@override_settings(
69+
VALID_DOMAIN=[
70+
"testserver",
71+
]
72+
)
73+
def tearDown(self):
74+
"""
75+
Teardown after each test.
76+
77+
:return:
78+
"""
79+
80+
self.course.delete()
81+
self.certifying_organisation.delete()
82+
self.project.delete()
83+
self.user.delete()
84+
85+
@override_settings(
86+
VALID_DOMAIN=[
87+
"testserver",
88+
]
89+
)
90+
def test_upcoming_courses_api(self):
91+
"""
92+
Test the upcoming courses API endpoint.
93+
"""
94+
response = self.client.get(reverse("feed-upcoming-project-course"))
95+
self.assertEqual(response.status_code, 200)
96+
self.assertContains(response, self.course.name)

0 commit comments

Comments
 (0)