Skip to content

Commit

Permalink
Merge pull request #145 from NIAEFEUP/feature/handle-conflicting-exch…
Browse files Browse the repository at this point in the history
…anges

Fix problems with exchange backend
  • Loading branch information
tomaspalma authored Feb 2, 2025
2 parents e1566e3 + 29478e9 commit 3fba801
Show file tree
Hide file tree
Showing 14 changed files with 224 additions and 116 deletions.
41 changes: 34 additions & 7 deletions django/university/controllers/ExchangeController.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from django.core.paginator import Paginator
from university.controllers.ClassController import ClassController
from university.controllers.SigarraController import SigarraController
from university.exchange.utils import ExchangeStatus, check_class_schedule_overlap, course_unit_by_id
from university.models import DirectExchange, DirectExchangeParticipants, ExchangeExpirations, MarketplaceExchange
from django.utils import timezone
Expand Down Expand Up @@ -108,17 +109,10 @@ def getStudentClass(student_schedules, username, courseUnitId):

@staticmethod
def create_direct_exchange_participants(student_schedules, exchanges, inserted_exchanges, exchange_db_model, auth_user):
if ExchangeController.exchange_overlap(student_schedules, auth_user):
return (ExchangeStatus.CLASSES_OVERLAP, None)

for curr_exchange in exchanges:
other_student = curr_exchange["other_student"]["mecNumber"]

course_unit = course_unit_by_id(curr_exchange["courseUnitId"])

if ExchangeController.exchange_overlap(student_schedules, other_student):
return (ExchangeStatus.CLASSES_OVERLAP, None)

inserted_exchanges.append(DirectExchangeParticipants(
participant_name=curr_exchange["other_student"]["name"],
participant_nmec=curr_exchange["other_student"]["mecNumber"],
Expand Down Expand Up @@ -162,3 +156,36 @@ def exchange_overlap(student_schedules, username) -> bool:

return False

@staticmethod
def update_schedule_accepted_exchanges(student, schedule):
accepted_options = DirectExchangeParticipants.objects.filter(participant_nmec=student, accepted=True, direct_exchange__accepted=True)

(status, trailing) = ExchangeController.update_schedule(schedule, accepted_options)
if status == ExchangeStatus.FETCH_SCHEDULE_ERROR:
return (ExchangeStatus.FETCH_SCHEDULE_ERROR, trailing)

return (ExchangeStatus.SUCCESS, None)

@staticmethod
def update_schedule(student_schedule, exchanges):
for exchange in exchanges:
for i, schedule in enumerate(student_schedule):
ocurr_id = int(schedule["ocorrencia_id"])
if ocurr_id == int(exchange.course_unit_id):
class_type = schedule["tipo"]

res = SigarraController().get_class_schedule(int(exchange.course_unit_id), exchange.class_participant_goes_to)
if res.status_code != 200:
return (ExchangeStatus.FETCH_SCHEDULE_ERROR, None)

(tp_schedule, t_schedule) = res.data
tp_schedule.extend(t_schedule)
new_schedules = tp_schedule

for new_schedule in new_schedules:
for turma in new_schedule["turmas"]:
if turma["turma_sigla"] == exchange.class_participant_goes_to and schedule["tipo"] == class_type:
del student_schedule[i]
student_schedule.append(new_schedule)

return (ExchangeStatus.SUCCESS, None)
62 changes: 56 additions & 6 deletions django/university/controllers/ExchangeValidationController.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,60 @@

from university.exchange.utils import exchange_overlap, build_student_schedule_dict, ExchangeStatus, exchange_status_message

from university.models import DirectExchangeParticipants
from university.models import DirectExchangeParticipants, DirectExchange

from django.db import transaction

class ExchangeValidationResponse:
def __init__(self, status: bool, message: ExchangeStatus):
self.status = status
self.message = exchange_status_message(message)

class ExchangeValidationController:
"""
Imagine the scenario where a person has two active pending exchanges:
- 1LEIC01 -> 1LEIC06 (AM I)
- 1LEIC01 -> 1LEIC09 (AM I)
If the user accepts the first exchange and the exchange is accepted, the other exchange will be invalidated and users should be warned about that.
All of the exchanges that include classes that were changed by the accepted exchange need to be revalidated or even canceled.
"""
def cancel_conflicting_exchanges(self, accepted_exchange_id: int):
conflicting_exchanges = []

with transaction.atomic():
accepted_exchange_participants = DirectExchangeParticipants.objects.filter(direct_exchange__id=accepted_exchange_id)
for participant in accepted_exchange_participants:
# 1. Are there any exchanges that include classes that a participant changed from?
conflicting = DirectExchangeParticipants.objects.exclude(direct_exchange__id=accepted_exchange_id).filter(
participant_nmec=participant.participant_nmec,
class_participant_goes_from=participant.class_participant_goes_from
)
conflicting_exchanges.extend(list(map(lambda conflicting_exchange: conflicting_exchange.direct_exchange, conflicting)))

# 2. Revalidate all of the other exchanges who include classes from the participant but not classes
# that are the "class_participant_goes_from" of the exchange
for participant in accepted_exchange_participants:
exchanges = DirectExchange.objects.exclude(id=accepted_exchange_id).filter(
directexchangeparticipants__participant_nmec=participant.participant_nmec
)
exchanges = [exchange for exchange in exchanges if exchange not in conflicting_exchanges]

for exchange in exchanges:
if not self.validate_direct_exchange(exchange.id).status:
conflicting_exchanges.append(exchange)

# 3. Cancel all the conflicting exchanges
for conflicting_exchange in conflicting_exchanges:
self.cancel_exchange(conflicting_exchange)

def cancel_exchange(self, exchange: DirectExchange):
exchange.canceled = True
exchange.save()

"""
This class will contain methods to validate the direct exchanges that are already inside
of the database.
Expand All @@ -20,30 +66,34 @@ class ExchangeValidationController:
will validate the request format.
"""
def validate_direct_exchange(self, exchange_id: int) -> ExchangeValidationResponse:
exchange_participants = list(DirectExchangeParticipants.objects.filter(direct_exchange__id=exchange_id).all())
exchange_participants = DirectExchangeParticipants.objects.filter(direct_exchange__id=exchange_id).all()

# 1. Build new schedule of each student
schedule = {}
for participant in exchange_participants:
if participant.participant_nmec not in schedule.keys():
schedule[participant.participant_nmec] = build_student_schedule_dict(SigarraController().get_student_schedule(int(participant.participant_nmec)).data)

# Get new schedule from accepted changes
ExchangeController.update_schedule_accepted_exchanges(participant.participant_nmec, schedule)

# 2. Check if users are inside classes they will exchange from with
for username in schedule.keys():
participant_entries = list(DirectExchangeParticipants.objects.filter(participant_nmec=username).all())
participant_entries = list(exchange_participants.filter(participant_nmec=username))

for entry in participant_entries:
if (entry.class_participant_goes_from, int(entry.course_unit_id)) not in list(schedule[username].keys()):
return ExchangeValidationResponse(False, ExchangeStatus.STUDENTS_NOT_ENROLLED)

# 3. Alter the schedule of the users according to the exchange metadata
schedule[username][(entry.class_participant_goes_to, int(entry.course_unit_id))] = SigarraController().get_class_schedule(int(entry.course_unit_id), entry.class_participant_goes_to).data
del schedule[username][(entry.class_participant_goes_from, int(entry.course_unit_id))]
class_schedule = SigarraController().get_class_schedule(int(entry.course_unit_id), entry.class_participant_goes_to).data[0][0] # For other courses we will need to have pratical class as a list in the dictionary

schedule[username][(entry.class_participant_goes_to, int(entry.course_unit_id))] = class_schedule
del schedule[username][(entry.class_participant_goes_from, int(entry.course_unit_id))]

# 4. Verify if the exchanges will have overlaps after building the new schedules
for username in schedule.keys():
if exchange_overlap(schedule, username):
if ExchangeController.exchange_overlap(schedule, username):
return ExchangeValidationResponse(False, ExchangeStatus.CLASSES_OVERLAP)

return ExchangeValidationResponse(True, ExchangeStatus.SUCCESS)
3 changes: 3 additions & 0 deletions django/university/controllers/SigarraController.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ def get_course_unit_classes(self, course_unit_id: int) -> SigarraResponse:

return SigarraResponse(response.json(), 200)

"""
Returns a tuple with (pratical class, theoretical class)
"""
def get_class_schedule(self, course_unit_id: int, class_name: str) -> SigarraResponse:
(semana_ini, semana_fim) = self.semester_weeks()

Expand Down
30 changes: 1 addition & 29 deletions django/university/exchange/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import date
from university.controllers.SigarraController import SigarraController
from university.models import CourseUnit, DirectExchangeParticipants, MarketplaceExchange, MarketplaceExchangeClass, Professor, DirectExchange
from university.models import CourseUnit, MarketplaceExchange, MarketplaceExchangeClass, Professor, DirectExchange
from enum import Enum
import requests

Expand Down Expand Up @@ -206,31 +206,3 @@ def convert_sigarra_schedule(schedule_data):
new_schedule_data.append(new_schedule)

return new_schedule_data

def update_schedule_accepted_exchanges(student, schedule):
direct_exchange_ids = DirectExchangeParticipants.objects.filter(
participant_nmec=student, direct_exchange__accepted=True
).values_list('direct_exchange', flat=True)
direct_exchanges = DirectExchange.objects.filter(id__in=direct_exchange_ids).order_by('date')

for exchange in direct_exchanges:
participants = DirectExchangeParticipants.objects.filter(direct_exchange=exchange, participant_nmec=student).order_by('date')
(status, trailing) = update_schedule(schedule, participants)
if status == ExchangeStatus.FETCH_SCHEDULE_ERROR:
return (ExchangeStatus.FETCH_SCHEDULE_ERROR, trailing)

return (ExchangeStatus.SUCCESS, None)

def update_schedule(student_schedule, exchanges):
for exchange in exchanges:
for i, schedule in enumerate(student_schedule):
if schedule["ucurr_sigla"] == exchange.course_unit:
class_type = schedule["tipo"]

# TODO if old_class schedule is different from current schedule, abort

for turma in schedule["turmas"]:
if turma["turma_sigla"] == exchange.class_participant_goes_to and schedule["tipo"] == class_type:
student_schedule[i] = schedule

return (ExchangeStatus.SUCCESS, None)
5 changes: 2 additions & 3 deletions django/university/routes/MarketplaceExchangeView.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from university.controllers.ExchangeController import ExchangeController
from university.controllers.SigarraController import SigarraController
from university.exchange.utils import ExchangeStatus, build_marketplace_submission_schedule, build_student_schedule_dict, exchange_overlap, incorrect_class_error, update_schedule_accepted_exchanges
from university.exchange.utils import ExchangeStatus, build_marketplace_submission_schedule, build_student_schedule_dict, exchange_overlap, incorrect_class_error
from university.models import CourseUnit, MarketplaceExchange, MarketplaceExchangeClass, UserCourseUnits, Class, ExchangeUrgentRequests, ExchangeUrgentRequestOptions
from university.serializers.MarketplaceExchangeClassSerializer import MarketplaceExchangeClassSerializer

Expand Down Expand Up @@ -120,11 +120,10 @@ def submit_marketplace_exchange_request(self, request):
student_schedules[curr_student] = build_student_schedule_dict(sigarra_res.data)

student_schedule = list(student_schedules[curr_student].values())
update_schedule_accepted_exchanges(curr_student, student_schedule)
ExchangeController.update_schedule_accepted_exchanges(curr_student, student_schedule)
student_schedules[curr_student] = build_student_schedule_dict(student_schedule)

(status, new_marketplace_schedule) = build_marketplace_submission_schedule(student_schedules, exchanges, curr_student)
print("Student schedules: ", student_schedules[curr_student])
if status == ExchangeStatus.STUDENTS_NOT_ENROLLED:
return JsonResponse({"error": incorrect_class_error()}, status=400, safe=False)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@
from django.views import View

from university.controllers.AdminExchangeStateChangeController import AdminExchangeStateChangeController
from university.models import DirectExchange, ExchangeUrgentRequests, CourseUnitEnrollments

class AdminExchangeRequestAcceptView(View):
def put(self, request, request_type, id):
AdminExchangeStateChangeController().update_state_to(request_type, id, "treated")

return HttpResponse(status=200)

if request_type == "direct_exchange":
DirectExchange.objects.filter(id=id).update(accepted=True)
elif request_type == "urgent_exchange":
ExchangeUrgentRequests.objects.filter(id=id).update(accepted=True)
elif request_type == "enrollment":
CourseUnitEnrollments.objects.filter(id=id).update(accepted=True)

return HttpResponse(status=200)
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@
from django.views import View

from university.controllers.AdminExchangeStateChangeController import AdminExchangeStateChangeController
from university.models import DirectExchange, ExchangeUrgentRequests, CourseUnitEnrollments

class AdminExchangeRequestRejectView(View):
def put(self, request, request_type, id):
AdminExchangeStateChangeController().update_state_to(request_type, id, "rejected")

if request_type == "direct_exchange":
DirectExchange.objects.filter(id=id).update(accepted=False)
elif request_type == "urgent_exchange":
ExchangeUrgentRequests.objects.filter(id=id).update(accepted=False)
elif request_type == "enrollment":
CourseUnitEnrollments.objects.filter(id=id).update(accepted=False)

return HttpResponse(status=200)
Loading

0 comments on commit 3fba801

Please sign in to comment.