Skip to content
Snippets Groups Projects
Select Git revision
  • b5deebbc9a6b995f6f91c44b27288e2ec80f6b0c
  • main default protected
  • renovate/django-5.x
  • koma/feature/preference-polling-form
4 results

tests.py

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    serializers.py 15.86 KiB
    from django.apps import apps
    from rest_framework import serializers
    
    from AKModel.models import AK, AKSlot, Event, Room
    from AKModel.serializers import IntListField, StringListField
    
    class ExportRoomInfoSerializer(serializers.ModelSerializer):
        """Serializer of Room objects for the 'info' field.
    
        Used in `ExportRoomSerializer` to serialize Room objects
        for the export to a solver. Part of the implementation of the
        format of the KoMa solver:
        https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
        """
    
        class Meta:
            model = Room
            fields = ["name"]
            read_only_fields = ["name"]
    
    
    class ExportRoomSerializer(serializers.ModelSerializer):
        """Export serializer for Room objects.
    
        Used to serialize Room objects for the export to a solver.
        Part of the implementation of the format of the KoMa solver:
        https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
        """
    
        time_constraints = StringListField(source="get_time_constraints", read_only=True)
        fulfilled_room_constraints = StringListField(
            source="get_fulfilled_room_constraints", read_only=True
        )
        info = ExportRoomInfoSerializer(source="*")
    
        class Meta:
            model = Room
            fields = [
                "id",
                "capacity",
                "time_constraints",
                "fulfilled_room_constraints",
                "info",
            ]
            read_only_fields = [
                "id",
                "capacity",
                "time_constraints",
                "fulfilled_room_constraints",
                "info",
            ]
    
    
    class ExportAKSlotInfoSerializer(serializers.ModelSerializer):
        """Serializer of AKSlot objects for the 'info' field.
    
        Used in `ExportAKSlotSerializer` to serialize AKSlot objects
        for the export to a solver. Part of the implementation of the
        format of the KoMa solver:
        https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
        """
    
        name = serializers.CharField(source="ak.name")
        head = serializers.SerializerMethodField()
        reso = serializers.BooleanField(source="ak.reso")
        description = serializers.CharField(source="ak.description")
        duration_in_hours = serializers.FloatField(source="duration")
        django_ak_id = serializers.IntegerField(source="ak.pk")
        types = StringListField(source="type_names")
    
        def get_head(self, slot: AKSlot) -> str:
            """Get string representation for 'head' field."""
            return ", ".join([str(owner) for owner in slot.ak.owners.all()])
    
        class Meta:
            model = AKSlot
            fields = [
                "name",
                "head",
                "description",
                "reso",
                "duration_in_hours",
                "django_ak_id",
                "types",
            ]
            read_only_fields = [
                "name",
                "head",
                "description",
                "reso",
                "duration_in_hours",
                "django_ak_id",
                "types",
            ]
    
    
    class ExportAKSlotPropertiesSerializer(serializers.ModelSerializer):
        """Serializer of AKSlot objects for the 'properties' field.
    
        Used in `ExportAKSlotSerializer` to serialize AKSlot objects
        for the export to a solver. Part of the implementation of the
        format of the KoMa solver:
        https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
        """
    
        conflicts = IntListField(source="conflict_pks")
        dependencies = IntListField(source="depencency_pks")
    
        class Meta:
            model = AKSlot
            fields = ["conflicts", "dependencies"]
            read_only_fields = ["conflicts", "dependencies"]
    
    
    class ExportAKSlotSerializer(serializers.ModelSerializer):
        """Export serializer for AKSlot objects.
    
        Used to serialize AKSlot objects for the export to a solver.
        Part of the implementation of the format of the KoMa solver:
        https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
        """
    
        duration = serializers.IntegerField(source="export_duration")
        room_constraints = StringListField(source="get_room_constraints")
        time_constraints = StringListField(source="get_time_constraints")
        info = ExportAKSlotInfoSerializer(source="*")
        properties = ExportAKSlotPropertiesSerializer(source="*")
    
        class Meta:
            model = AKSlot
            fields = [
                "id",
                "duration",
                "properties",
                "room_constraints",
                "time_constraints",
                "info",
            ]
            read_only_fields = [
                "id",
                "duration",
                "properties",
                "room_constraints",
                "time_constraints",
                "info",
            ]
    
    
    class ExportParticipantAndDummiesSerializer(serializers.BaseSerializer):
        """Export serializer for EventParticipant objects that includes 'dummy' participants.
    
        This serializer is a work-around to make the solver compatible with the AKOwner model.
    
        Internally, `ExportParticipantSerializer` is used to serialize all EventParticipants of
        the event to serialize. To avoid scheduling conflicts, a 'dummy' participant is then added
        to the list for each AKOwner of the event. These dummy participants only have 'required'
        preference for all AKs of the owner, so the target of the optimization is not impacted.
    
        Part of the implementation of the format of the KoMa solver:
        https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
        """
    
        def create(self, validated_data):
            raise ValueError("`ExportParticipantAndDummiesSerializer` is read-only.")
    
        def to_internal_value(self, data):
            raise ValueError("`ExportParticipantAndDummiesSerializer` is read-only.")
    
        def update(self, instance, validated_data):
            raise ValueError("`ExportParticipantAndDummiesSerializer` is read-only.")
    
        def to_representation(self, instance: Event):
            event = instance
    
            # default case
            real_participants = []
            next_participant_pk = 1
    
            # set variable values if AKPreference app is installed
            if apps.is_installed("AKPreference"):
                # local import to decouple
                # pylint: disable=import-outside-toplevel
                from AKPreference.models import EventParticipant
                from AKPreference.serializers import ExportParticipantSerializer
    
                real_participants = ExportParticipantSerializer(event.participants, many=True).data
                if EventParticipant.objects.exists():
                    next_participant_pk = EventParticipant.objects.latest("pk").pk + 1
    
            dummies = []
            # add one dummy participant per owner
            # this ensures that the hard constraints from each owner are considered
            for new_pk, owner in enumerate(event.owners, next_participant_pk):
                owned_slots = event.slots.filter(ak__owners=owner).order_by().all()
                if not owned_slots:
                    continue
                new_participant_data = {
                    "id": new_pk,
                    "info": {"name": f"{owner} [AKOwner]"},
                    "room_constraints": [],
                    "time_constraints": [],
                    "preferences": [
                        {"ak_id": slot.pk, "required": True, "preference_score": -1}
                        for slot in owned_slots
                    ]
                }
                dummies.append(new_participant_data)
            return real_participants + dummies
    
    
    class ExportEventInfoSerializer(serializers.ModelSerializer):
        """Serializer of an Event object for the 'info' field.
    
        Used in `ExportEventSerializer` to serialize an Event object
        for the export to a solver. Part of the implementation of the
        format of the KoMa solver:
        https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
        """
    
        title = serializers.CharField(source="name")
        contact_email = serializers.EmailField(required=False)
        place = serializers.CharField(required=False)
    
        class Meta:
            model = Event
            fields = ["title", "slug", "contact_email", "place"]
    
    
    class ExportTimeslotBlockSerializer(serializers.BaseSerializer):
        """Read-only serializer for timeslots.
    
        Used to serialize timeslots for the export to a solver.
        Part of the implementation of the format of the KoMa solver:
        https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
        """
    
        def create(self, validated_data):
            raise ValueError("`ExportTimeslotBlockSerializer` is read-only.")
    
        def to_internal_value(self, data):
            raise ValueError("`ExportTimeslotBlockSerializer` is read-only.")
    
        def update(self, instance, validated_data):
            raise ValueError("`ExportTimeslotBlockSerializer` is read-only.")
    
        def to_representation(self, instance: Event):
            """Construct serialized representation of the timeslots of an event."""
            # pylint: disable=import-outside-toplevel
            from AKModel.availability.models import Availability
    
            event = instance
            blocks = list(event.discretize_timeslots())
    
            def _check_event_not_covered(availabilities: list[Availability]) -> bool:
                """Test if event is not covered by availabilities."""
                return not Availability.is_event_covered(event, availabilities)
    
            def _check_akslot_fixed_in_timeslot(
                ak_slot: AKSlot, timeslot: Availability
            ) -> bool:
                """Test if an AKSlot is fixed to overlap a timeslot slot."""
                if not ak_slot.fixed or ak_slot.start is None:
                    return False
    
                fixed_avail = Availability(
                    event=event, start=ak_slot.start, end=ak_slot.end
                )
                return fixed_avail.overlaps(timeslot, strict=True)
    
            def _check_add_constraint(
                slot: Availability, availabilities: list[Availability]
            ) -> bool:
                """Test if object is not available for whole event and may happen during slot."""
                return _check_event_not_covered(availabilities) and slot.is_covered(
                    availabilities
                )
    
            def _generate_time_constraints(
                avail_label: str,
                avail_dict: dict,
                timeslot_avail: Availability,
                prefix: str = "availability",
            ) -> list[str]:
                return [
                    f"{prefix}-{avail_label}-{pk}"
                    for pk, availabilities in avail_dict.items()
                    if _check_add_constraint(timeslot_avail, availabilities)
                ]
    
            timeslots = {
                "info": {"duration": float(event.export_slot)},
                "blocks": [],
            }
    
            ak_availabilities = {
                ak.pk: Availability.union(ak.availabilities.all())
                for ak in AK.objects.filter(event=event).all()
            }
            room_availabilities = {
                room.pk: Availability.union(room.availabilities.all()) for room in event.rooms
            }
            person_availabilities = {
                person.pk: Availability.union(person.availabilities.all())
                for person in event.owners
            }
            participant_availabilities = {
                participant.pk: Availability.union(participant.availabilities.all())
                for participant in event.participants
            }
    
            block_names = []
            for block_idx, block in enumerate(blocks):
                current_block = []
    
                if not block:
                    continue
    
                block_start = block[0].avail.start.astimezone(event.timezone)
                block_end = block[-1].avail.end.astimezone(event.timezone)
    
                start_day = block_start.strftime("%A, %d. %b")
                if block_start.date() == block_end.date():
                    # same day
                    time_str = (
                        block_start.strftime("%H:%M") + " - " + block_end.strftime("%H:%M")
                    )
                else:
                    # different days
                    time_str = (
                        block_start.strftime("%a %H:%M")
                        + " - "
                        + block_end.strftime("%a %H:%M")
                    )
                block_names.append([start_day, time_str])
    
                block_timeconstraints = [
                    f"notblock{idx}" for idx in range(len(blocks)) if idx != block_idx
                ]
    
                for timeslot in block:
                    time_constraints = []
                    # if reso_deadline is set and timeslot ends before it,
                    #   add fulfilled time constraint 'resolution'
                    if (
                        event.reso_deadline is None
                        or timeslot.avail.end < event.reso_deadline
                    ):
                        time_constraints.append("resolution")
    
                    # add fulfilled time constraints for all AKs that cannot happen during full event
                    time_constraints.extend(
                        _generate_time_constraints("ak", ak_availabilities, timeslot.avail)
                    )
    
                    # add fulfilled time constraints for all persons that are not available for full event
                    time_constraints.extend(
                        _generate_time_constraints(
                            "person", person_availabilities, timeslot.avail
                        )
                    )
    
                    # add fulfilled time constraints for all rooms that are not available for full event
                    time_constraints.extend(
                        _generate_time_constraints(
                            "room", room_availabilities, timeslot.avail
                        )
                    )
    
                    # add fulfilled time constraints for all participants that are not available for full event
                    time_constraints.extend(
                        _generate_time_constraints(
                            "participant", participant_availabilities, timeslot.avail
                        )
                    )
    
                    # add fulfilled time constraints for all AKSlots fixed to happen during timeslot
                    time_constraints.extend(
                        [
                            f"fixed-akslot-{slot.id}"
                            for slot in AKSlot.objects.filter(
                                event=event, fixed=True
                            ).exclude(start__isnull=True)
                            if _check_akslot_fixed_in_timeslot(slot, timeslot.avail)
                        ]
                    )
    
                    time_constraints.extend(timeslot.constraints)
                    time_constraints.extend(block_timeconstraints)
                    time_constraints.sort()
    
                    current_block.append(
                        {
                            "id": timeslot.idx,
                            "info": {
                                "start": timeslot.avail.start.astimezone(
                                    event.timezone
                                ).strftime("%Y-%m-%d %H:%M"),
                                "end": timeslot.avail.end.astimezone(
                                    event.timezone
                                ).strftime("%Y-%m-%d %H:%M"),
                            },
                            "fulfilled_time_constraints": time_constraints,
                        }
                    )
    
                timeslots["blocks"].append(current_block)
    
            timeslots["info"]["blocknames"] = block_names
    
            return timeslots
    
    
    class ExportEventSerializer(serializers.ModelSerializer):
        """Export serializer for an Event object.
    
        Used to serialize an Event for the export to a solver.
        Part of the implementation of the format of the KoMa solver:
        https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
        """
    
        info = ExportEventInfoSerializer(source="*")
        rooms = ExportRoomSerializer(many=True)
        aks = ExportAKSlotSerializer(source="slots", many=True)
        participants = ExportParticipantAndDummiesSerializer(source="*")
        timeslots = ExportTimeslotBlockSerializer(source="*")
    
        class Meta:
            model = Event
            fields = ["participants", "rooms", "timeslots", "info", "aks"]
            read_only_fields = ["participants", "rooms", "timeslots", "info", "aks"]