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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.1.12 on 2025-11-12 01:38

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


class Migration(migrations.Migration):

dependencies = [
('exchange', '0008_directexchange_last_validated'),
]

operations = [
migrations.AlterField(
model_name='exchangeadmincourses',
name='exchange_admin',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exchange.exchangeadmin'),
),
migrations.AlterField(
model_name='exchangeadmincourseunits',
name='exchange_admin',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exchange.exchangeadmin'),
),
]
10 changes: 6 additions & 4 deletions django/exchange/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,24 +63,26 @@ class Meta:
managed = True
db_table = 'exchange_admin'


class ExchangeAdminCourseUnits(models.Model):
exchange_admin = models.ForeignKey(ExchangeAdmin, models.DO_NOTHING)
course_unit = models.ForeignKey(CourseUnit, models.DO_NOTHING)
exchange_admin = models.ForeignKey(ExchangeAdmin, on_delete=models.CASCADE)
course_unit = models.ForeignKey(CourseUnit, on_delete=models.DO_NOTHING)

class Meta:
managed = True
db_table = 'exchange_admin_course_units'


class ExchangeAdminCourses(models.Model):
exchange_admin = models.ForeignKey(ExchangeAdmin, models.DO_NOTHING)
course = models.ForeignKey(Course, models.DO_NOTHING)
exchange_admin = models.ForeignKey(ExchangeAdmin, on_delete=models.CASCADE)
course = models.ForeignKey(Course, on_delete=models.DO_NOTHING)

class Meta:
managed = True
db_table = 'exchange_admin_courses'



class ExchangeExpirations(models.Model):
course_unit = models.ForeignKey(CourseUnit, models.DO_NOTHING)
active_date = models.DateTimeField()
Expand Down
2 changes: 2 additions & 0 deletions django/tts_be/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@

EXCHANGE_SEMESTER = CONFIG["EXCHANGE_SEMESTER"]

EXCHANGE_COURSES_LIST = CONFIG.get("EXCHANGE_COURSES_LIST", "22862,22841")

ALLOWED_HOSTS = ['tts.niaefeup.pt', 'tts-staging.niaefeup.pt']

if DEBUG:
Expand Down
2 changes: 1 addition & 1 deletion django/university/controllers/SigarraController.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def get_student_schedule(self, nmec: int) -> SigarraResponse:
(semana_ini, semana_fim) = self.semester_weeks()

response = requests.get(self.student_schedule_url(
nmec,
202208026,
semana_ini,
semana_fim
), cookies=self.cookies)
Expand Down
10 changes: 10 additions & 0 deletions django/university/middleware/is_superuser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.http import HttpResponseForbidden
from functools import wraps

def superuser_required(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.is_superuser:
return HttpResponseForbidden("Superuser permissions required")
return view_func(request, *args, **kwargs)
return _wrapped_view
88 changes: 88 additions & 0 deletions django/university/routes/admin/AdminExchangeAdminsView.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from django.db.models import Q
from django.http import JsonResponse
from django.contrib.auth import get_user_model
from django.core.paginator import Paginator
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from exchange.models import ExchangeAdmin


## repeated code may move to better place later
def apply_search_filter(queryset, q):
q = q.strip()
if not q:
return queryset

parts = q.split()
if len(parts) == 2:
first, last = parts
return queryset.filter(
Q(first_name__icontains=first) & Q(last_name__icontains=last)
)
else:
return queryset.filter(
Q(username__icontains=q) |
Q(first_name__icontains=q) |
Q(last_name__icontains=q) |
Q(email__icontains=q)
)


class AdminExchangeAdminsView(APIView):
permission_classes = [IsAuthenticated]

def get(self, request):
UserModel = get_user_model()
admin_usernames = ExchangeAdmin.objects.values_list("username", flat=True)

users_qs = UserModel.objects.filter(username__in=admin_usernames).values(
"id", "username", "first_name", "last_name", "email", "is_active", "date_joined"
).order_by('username')

q = request.GET.get('q', '').strip()
users_qs = apply_search_filter(users_qs, q)

page = int(request.GET.get('page', 1))
page_size = min(int(request.GET.get('page_size', 10)), 100)
paginator = Paginator(users_qs, page_size)
page_obj = paginator.get_page(page)

admins = []
for u in page_obj.object_list:
u = dict(u)
u["date_joined"] = str(u.get("date_joined")) if u.get("date_joined") else None
admins.append(u)

return JsonResponse({
"admins": admins,
"total_pages": paginator.num_pages,
"current_page": page_obj.number,
"total_count": paginator.count
})

def post(self, request):
username = request.data.get('username')
if not username:
return JsonResponse({'error': 'Username required'}, status=400)

UserModel = get_user_model()
try:
UserModel.objects.get(username=username)
except UserModel.DoesNotExist:
return JsonResponse({'error': 'User not found'}, status=404)

if ExchangeAdmin.objects.filter(username=username).exists():
return JsonResponse({'error': 'User is already an admin'}, status=400)

ExchangeAdmin.objects.create(username=username)
return JsonResponse({'message': 'Admin added successfully'})

def delete(self, request, username):
if not username:
return JsonResponse({'error': 'Username required'}, status=400)

deleted, _ = ExchangeAdmin.objects.filter(username=username).delete()
if deleted == 0:
return JsonResponse({'error': 'Admin not found'}, status=404)

return JsonResponse({'message': 'Admin removed successfully'})
51 changes: 51 additions & 0 deletions django/university/routes/admin/AdminExchangeCandidatesView.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from django.db.models import Q
from django.http import JsonResponse
from django.contrib.auth import get_user_model
from django.core.paginator import Paginator
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from exchange.models import ExchangeAdmin

## repeated code may move to better place later
def apply_search_filter(queryset, q):
q = q.strip()
if not q:
return queryset

parts = q.split()
if len(parts) == 2:
first, last = parts
return queryset.filter(
Q(first_name__icontains=first) & Q(last_name__icontains=last)
)
else:
return queryset.filter(
Q(username__icontains=q) |
Q(first_name__icontains=q) |
Q(last_name__icontains=q) |
Q(email__icontains=q)
)


class AdminExchangeCandidatesView(APIView):
permission_classes = [IsAuthenticated]

def get(self, request):
UserModel = get_user_model()
admin_usernames = ExchangeAdmin.objects.values_list("username", flat=True)

users_qs = UserModel.objects.exclude(username__in=admin_usernames).values(
"id", "username", "first_name", "last_name", "email", "is_active", "date_joined"
).order_by('username')

q = request.GET.get('q', '').strip()
limit = min(int(request.GET.get('limit', 10)), 100)
users_qs = apply_search_filter(users_qs, q)

candidates = []
for u in users_qs[:limit]:
u = dict(u)
u["date_joined"] = str(u.get("date_joined")) if u.get("date_joined") else None
candidates.append(u)

return JsonResponse({"candidates": candidates})
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from django.http import JsonResponse
from django.db import models
from django.db.models import Case, When, IntegerField
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from django.conf import settings

from university.models import CourseUnit


class AdminExchangeCourseUnitsSearchView(APIView):
"""Return course units filtered by search query.

This endpoint requires authentication. It returns a JSON object with a
`course_units` array containing basic fields for each course unit.
"""

permission_classes = [IsAuthenticated]

def get(self, request):
exchange_courses_list = settings.EXCHANGE_COURSES_LIST
allowed_course_ids = [int(x.strip()) for x in exchange_courses_list.split(',') if x.strip()]

course_units_qs = CourseUnit.objects.filter(coursemetadata__course__id__in=allowed_course_ids).distinct().values(
"id", "name", "acronym", "course__name", "course__acronym", "semester", "year"
)

q = request.GET.get('q', '').strip()
limit = min(int(request.GET.get('limit', 10)), 100)
if q:
course_units_qs = course_units_qs.annotate(
priority=Case(
When(acronym__iexact=q, then=4),
When(course__acronym__iexact=q, then=3),
When(acronym__icontains=q, then=2),
When(course__acronym__icontains=q, then=1),
default=0,
output_field=IntegerField()
)
).filter(
models.Q(name__icontains=q) |
models.Q(acronym__icontains=q) |
models.Q(course__name__icontains=q) |
models.Q(course__acronym__icontains=q)
).order_by('-priority', 'acronym')[:limit]

course_units = []
for cu in course_units_qs:
cu = dict(cu)
cu["course_name"] = cu.pop("course__name")
cu["course_acronym"] = cu.pop("course__acronym")
course_units.append(cu)

return JsonResponse({"course_units": course_units}, safe=False)
61 changes: 56 additions & 5 deletions django/university/routes/admin/AdminExchangeCourseUnitsView.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,66 @@
from rest_framework.views import APIView
from django.http import HttpResponse, JsonResponse
from rest_framework import status
from django.conf import settings

from university.models import CourseUnit
from university.models import CourseUnit, CourseMetadata

from exchange.models import ExchangeAdminCourseUnits
from exchange.models import ExchangeAdmin, ExchangeAdminCourseUnits

class AdminExchangeCourseUnitsView(APIView):
def get(self, request):
user = request.user

admin_exchange_course_units = ExchangeAdminCourseUnits.objects.filter(exchange_admin__username=user.username)
# Allow admins to view other admins' course units
admin_username = request.GET.get('admin_username', request.user.username)

admin_exchange_course_units = ExchangeAdminCourseUnits.objects.filter(exchange_admin__username=admin_username)
course_units = list(CourseUnit.objects.filter(id__in=admin_exchange_course_units.values_list("course_unit_id", flat=True)).values())

return JsonResponse(course_units, safe=False)

def post(self, request):
exchange_courses_list = settings.EXCHANGE_COURSES_LIST
allowed_course_ids = [int(x.strip()) for x in exchange_courses_list.split(',') if x.strip()]

course_unit_id = request.data.get('course_unit_id')
admin_username = request.data.get('admin_username', request.user.username)
if not course_unit_id:
return JsonResponse({'error': 'course_unit_id required'}, status=status.HTTP_400_BAD_REQUEST)

try:
course_unit = CourseUnit.objects.get(id=course_unit_id)
except CourseUnit.DoesNotExist:
return JsonResponse({'error': 'Course unit not found'}, status=status.HTTP_404_NOT_FOUND)

# Check if the course unit belongs to an allowed course
course_metadata = CourseMetadata.objects.filter(course_unit=course_unit).first()
if not course_metadata or course_metadata.course.id not in allowed_course_ids:
return JsonResponse({'error': 'Course unit not allowed for exchange'}, status=status.HTTP_400_BAD_REQUEST)

try:
admin = ExchangeAdmin.objects.get(username=admin_username)
except ExchangeAdmin.DoesNotExist:
return JsonResponse({'error': 'Admin not found'}, status=status.HTTP_404_NOT_FOUND)

if ExchangeAdminCourseUnits.objects.filter(exchange_admin=admin, course_unit=course_unit).exists():
return JsonResponse({'error': 'Course unit already assigned'}, status=status.HTTP_400_BAD_REQUEST)

ExchangeAdminCourseUnits.objects.create(exchange_admin=admin, course_unit=course_unit)
return JsonResponse({'success': True})

def delete(self, request, course_unit_id=None):
if not course_unit_id:
return JsonResponse({'error': 'course_unit_id required'}, status=status.HTTP_400_BAD_REQUEST)

admin_username = request.GET.get('admin_username', request.user.username)

try:
admin = ExchangeAdmin.objects.get(username=admin_username)
course_unit = CourseUnit.objects.get(id=course_unit_id)
relation = ExchangeAdminCourseUnits.objects.get(exchange_admin=admin, course_unit=course_unit)
relation.delete()
return JsonResponse({'success': True})
except (ExchangeAdmin.DoesNotExist, CourseUnit.DoesNotExist, ExchangeAdminCourseUnits.DoesNotExist):
return JsonResponse({'error': 'Not found'}, status=status.HTTP_404_NOT_FOUND)



Loading