Practice (project assignment)

The goal here is to draw the contour lines of an initial project architecture on a small yet realistic topic. The context is the choice of a final year project by students.

  • Every student must be assigned to one project. The number of available projects is greater or equal to the number of students.

  • Each project has a title, at least one supervisor, a context, a number of maximum duplication meaning that this project can be assigned between zero to this maximum duplication number.

  • The assignments must not be challengeable by any other assignments where all students would be at least as happy than the current ones with at least one student happier. If this situation were to happen, everybody would agree to accept the other assignments, the original solution would be qualified as unstable.

The users are the supervisors who propose some projects, the students who submit choices and the administrator who runs the assignment once the projects and choices are collected.

Exercise (easy)

There is not a single answer to the following questions, it is usually either a question of compromise or even conventions.

  1. How would you imagine broadly the different features of such an app from a user perspective? (try to think about tasks as uncoupled / isolated as possible)

  2. It is not plausible to try to build the whole app directly. Let’s postpone the decision on the user interfaces (probably web) to avoid tying up our architecture to a specific solution. The goal is to be able to test and possibly substitute components as seamlessly as possible. To start somewhere, what would be the minimal core logic around the project assignment step? Why is it interesting to start there?

Correction
  1. The students:

    • log in the app

    • obtain the project catalog

    • submit their ranking list

    • check their submitted ranking list

    • consult their assignment on one project

    The supervisors:

    • log in the app

    • obtain the project catalog

    • post a project

    • edit a project that they posted

    • remove a project that they posted

    • consult the assignment of all their projects

    The administrator:

    • log in the app

    • obtain the ranking lists

    • obtain the project catalog

    • generate the best assignment list

  2. From the project catalog and all the ranking lists, generate a suitable assignment list. This step does not care about:

    • how the ranking lists or the project catalog were assembled.

    • how the assignment is requested (CLI, web, etc).

    • ‘what’ is assigned to ‘what’, students to projects, candidates to jobs, drivers to cars, etc. Only the notion of ranking is important there.

For every student, try to assign his favorite project. If the project is available, assign the project to the student, otherwise, move to the next preferred project until one available is assigned. Since it might be useful to compare two potential assignments, a student satisfaction score would be useful based on the rank of the assigned project in the choice rankings.

Project assignment graph

Project assignment graph

Exercise (intermediate)

  1. What structure would be appropriate to represent all student choices, assignments and scores?

  2. Define some unit tests and implement them.

  3. Implement the assign logic and test it. Do not attempt to test the solution stability.

Correction
  1. all student choices: A dict of lists {chooser: [choices from least to most preferred]} to be able to drop tested preferred choices in constant time.

    assignments: A dict {choice: chooser} to be able to check in constant time “if the project is already assigned to another student”.

    scores: A dict {chooser: score} to be able to compare in constant time two satisfaction scores from a student.

    • the number of students has to be lower or equal to the number of projects otherwise an exception is raised

    • several small examples can be imagined (2 students, 2 projects; 2 students, 3 projects; etc)

    tests
     1@pytest.mark.parametrize(
     2    "all_choices, error, message",
     3    [
     4        (
     5            {10: [0], 11: [0]},
     6            AssignmentSizeError,
     7            "too many choosers",
     8        ),
     9        (
    10            {10: [0, 1], 11: [0, 2]},
    11            AssignmentInconsistencyError,
    12            "the choices are inconsistent between choosers",
    13        ),
    14        (
    15            {10: [0, 0], 11: [0, 1]},
    16            AssignmentInconsistencyError,
    17            "the choices are inconsistent between choosers",
    18        ),
    19    ],
    20)
    21def test_validate(
    22    all_choices: AllChoices, error: type[AssignmentError], message: str
    23) -> None:
    24    with pytest.raises(error, match=message):
    25        _ = assign(all_choices)
    
    tests
     1@pytest.mark.parametrize(
     2    "all_choices, expected",
     3    [
     4        (
     5            {10: [0]},
     6            ({0: 10}, {10: 0}),
     7        ),
     8        (
     9            {10: [0, 1], 11: [1, 0]},
    10            ({0: 11, 1: 10}, {10: 1, 11: 1}),
    11        ),
    12        (
    13            {10: [0, 1], 11: [0, 1]},
    14            ({0: 11, 1: 10}, {10: 1, 11: 0}),
    15        ),
    16        (
    17            {10: [0, 1, 2], 11: [0, 1, 2], 12: [0, 1, 2]},
    18            ({0: 12, 1: 11, 2: 10}, {10: 2, 11: 1, 12: 0}),
    19        ),
    20        (
    21            {10: [0, 1, 2], 11: [2, 1, 0], 12: [0, 1, 2]},
    22            ({0: 11, 1: 12, 2: 10}, {10: 2, 11: 2, 12: 1}),
    23        ),
    24    ],
    25)
    26def test_assign(all_choices: AllChoices, expected: Assignments) -> None:
    27    assert assign(all_choices) == expected
    
  2. src
     1class AssignmentError(Exception): ...
     2
     3
     4class AssignmentSizeError(AssignmentError): ...
     5
     6
     7class AssignmentInconsistencyError(AssignmentError): ...
     8
     9
    10type Chooser = int  # to be improved later
    11type Choice = int
    12type Choices = list[Choice]  # last is preferred
    13type AllChoices = dict[Chooser, Choices]
    14type Assignments = dict[Choice, Chooser]
    15type Score = int
    16type Scores = dict[Chooser, Score]
    17
    18
    19def assign(all_choices: AllChoices) -> tuple[Assignments, Scores]:
    20    validate(all_choices)
    21
    22    assignments: Assignments = {}
    23    scores: Scores = {}
    24    for chooser, choices in all_choices.items():
    25        is_assigned = False
    26        choices = choices.copy()
    27        while not is_assigned:
    28            preferred_choice: Choice = choices.pop()
    29            if preferred_choice not in assignments:
    30                assignments[preferred_choice] = chooser
    31                scores[chooser] = len(choices)
    32                is_assigned = True
    33
    34    return assignments, scores
    35
    36
    37def validate(all_choices: AllChoices) -> None:
    38    nb_chooser: int = len(all_choices)
    39    previous_sorted_choices = sorted(next(iter(all_choices.values())))
    40    for choices in all_choices.values():
    41        sorted_choices = sorted(choices)
    42        if len(choices) < nb_chooser:
    43            raise AssignmentSizeError("too many choosers")
    44        if sorted_choices != previous_sorted_choices:
    45            raise AssignmentInconsistencyError(
    46                "the choices are inconsistent between choosers"
    47            )
    48
    

Exercise (easy)

Give one example of what would constitute here:

  1. integration testing

  2. performance/stress testing

  3. acceptance/functional testing

Correction
  1. Several other modules will be required to implement all the features imagined before.

    • rankings for example for ranking list validation, edition

    • projects for example for project validation, edition

    • database for the permanence: reading / writing

    In these interactions, it is probable that the first two will require the third. Testing that a valid ranking list is correctly stored it in database would be a required integration test.

  2. The important performances here might be:

    • can the app interface handle the load when every student try to submit their rankings at the same time at the very last minute?

    • can the assignment strategy handle the expected number of students in a reasonable time?

    • can a supervisor (and not a student) insert in the project catalog?

    • can a student modify his rankings (and no one else)?