Events

Flow for (un)registration

Upon registration

In the RegistrationCreateAndUpdateSerializer we pull the current user from the validated data, and the event from the view, through self.context['view'].kwargs['event_pk']. The user is sent as the argument to event.register(user).

In the register-method we try to find the optimal pool for the user, if he’s able to register for the event. There are three possible outcomes:

  1. The user does not have permission to join any pools, and an exception is raised.

  2. The user is added to the waiting list, along with any pools he is able to join.

  3. The user is added to a pool, and the registration is successful.

First we iterate over all the pools in the event to build a list of pools the user is able to join. If the list is empty, the user is not allowed to register for the event, and we raise an exception. If the event only has one pool, and the user is able to join it, we can simply check if the pool is full and register the user accordingly. If it’s full a Registration is created, with no pool, and the list of possible pools as waiting_pools. If it’s not full a Registration is created, with the only pool as the pool.

If the event is merged, the situation is similar: we simply check if the event is full, and register the user accordingly. The user is put in the first possible pool, since pools don’t matter post-merge.

If the event isn’t merged we build a list of full pools by popping full pools from the list of possible pools. This is done in a helper-method. There are three possible outcomes:

  1. The list of possible pools is empty.
    • All available pools are full, and from the POV of the user the event is full.

    • The user is registered in the waiting list.

    • The list of full pools is used as the list of pools that the user is waiting for.

  2. There is only one possible pool left.
    • The user is registered for this pool.

  3. There are several pools left
    • The search continues…

The “algorithm” now builds a dictionary where key=pool and value=total users who can join the pool, because we are trying to find the most exclusive pool. If there are several pools with the same exclusivity, the one with the highest capacitiy is chosen. This is done through 3 helper-methods, and the user is now registered.

Upon unregistration

In the unregistration-method the registration belonging to the user is acquired, it’s current pool is stored in a temporary variable, and the fields pool, waiting_list and waiting_pool are cleared. The unregistration date is set, and it is saved.

Then we call check_for_bump(pool), where pool = the temporary pool from earlier. Here we check if there’s room for a bump, and if so we call the bump-method. The from_pool-argument is the pool we unregistered from earlier. If the event is merged, we ignore the pool.

In bump we pop the first person in the waiting list. If from_pool is not None, the first person who can join that pool is popped. This registration has it’s waiting_list, waiting_pool and unregistration_date cleared, and it’s pool set to either from_pool or the first pool that the user can join.

Permissions

Events mostly uses the default permissions in LEGO, but has some custom permission handlers.

Permissions based on event type

For events we want to limit the different types of events a user can create or edit. So for events, one can add append the event type on the create keyword permissions. F.ex: /sudo/admin/events/create/social/ gives the user only access to create events with event_type = social. The same keyword permission is also used for EDIT actions. This means that, if a user has permission to edit an event, they can only change the event_type to the ones specified in their /sudo/admin/events/create/<event_type>/ permissions.

The custom permissions uses a mixture of a custom permission class for the viewswet:

class lego.apps.events.permissions.EventTypePermission

Bases: LegoPermissions

has_permission(request, view)

Return True if permission is granted, False otherwise.

As well as a custom permission handler:

The methods in the custom permission handler that check permisisons based on event_type does not check for object permissions. So any use of lego.apps.events.permissions.EventPermissionHandler.has_event_type_level_permission() must not be used by itself to check permissions on an existing event object.

class lego.apps.events.permissions.EventPermissionHandler

Bases: PermissionHandler[Event]

event_type_keyword_permissions(event_type, perm)

Get the keyword permission string required for a permission for a specific event type

Returns:

The keyword permission the user needs

has_event_type_level_permission(user, request, perm)

Check if a user has the required event_type permissions for a certain action.

WARNING: This method ONLY enforces keyword permissions for event types. It does not check object permissions for the event and shoud not be used on it’s own to check permissions on an object.

Return True if the user has permission to perform the request with the specified event_type

Models

class lego.apps.events.models.Event(*args, **kwargs)

Bases: Content, BasisModel, ObjectPermissionsModel

An event has a type (e.g. Company presentation, Party. Eventually, each type of event might have slightly different ‘requirements’ or fields. For example, a company presentation will be connected to a company from our company database.

An event has between 1 and X pools, each with their own capacity, to separate users based on groups. At merge_time all pools are combined into one.

An event has a waiting list, filled with users who register after the event is full.

exception DoesNotExist

Bases: ObjectDoesNotExist

exception MultipleObjectsReturned

Bases: MultipleObjectsReturned

property active_capacity: int

Calculation capacity of pools that are active.

add_to_waiting_list(user: User) Registration

Adds a user to the waiting list.

Parameters:

user – The user that will be registered to the waiting list.

Returns:

A registration for the waiting list, with pool=null

admin_register(admin_user: User, user: User, admin_registration_reason: str, pool: Pool | None = None, feedback: str = '') Registration

Used to force registration for a user, even if the event is full or if the user isn’t allowed to register.

Parameters:
  • user – The user who will be registered

  • pool – What pool the registration will be created for

  • feedback – Feedback to organizers

Returns:

The registration

bump(to_pool: Pool | None = None) None

Pops the appropriate registration from the waiting list, and moves the registration from the waiting list to to pool.

Parameters:

to_pool – A pool with a free slot. If the event is merged, this will be null.

bump_on_pool_creation_or_expansion() None

Used when a pool’s capacity is expanded or a new pool is created, so that waiting registrations are bumped before anyone else can fill the open spots. This is done on event update.

This method does the same as early_bump, but only accepts people that can be bumped now, not people that can be bumped in the future.

check_for_bump_or_rebalance(open_pool: Pool) None

Checks if there is an available spot in the event. If so, and the event is merged, bumps the first person in the waiting list. If the event isn’t merged, bumps the first user in the waiting list who is able to join open_pool. If no one is waiting for open_pool, check if anyone is waiting for any of the other pools and attempt to rebalance.

NOTE: Remember to lock the event using select_for_update! AND lock the corresponding pools by including all pools in the select statement.

Parameters:

open_pool – The pool where the unregistration happened.

early_bump(opening_pool: Pool) None

Used when bumping users from waiting list to a pool that is about to be activated, using an async task. This is done to make sure these existing registrations are given the spot ahead of users that register at activation time.

Parameters:

opening_pool – The pool about to be activated.

property number_of_registrations: int

Registration count guaranteed not to include unregistered users.

pop_from_waiting_list(to_pool: Pool | None = None) Registration | None

Pops the first user in the waiting list that can join to_pool. If from_pool=None, pops the first user in the waiting list overall.

Parameters:

to_pool – The pool we are bumping to. If post-merge, there is no pool.

Returns:

The registration that is first in line for said pool.

rebalance_pool(from_pool: Pool, to_pool: Pool) bool

Iterates over registrations in a full pool, and checks if they can be moved to the open pool. If possible, moves a registration and calls bump(from_pool).

Parameters:
  • from_pool – A full pool with waiting registrations.

  • to_pool – A pool with one open slot.

Returns:

Boolean, whether or not bump() has been called.

register(registration: Registration) Registration

Evaluates a pending registration for the event, and automatically selects the optimal pool for the registration.

First checks if there exist any legal pools for the pending registration, raises an exception if not.

If there is only one possible pool, checks if the pool is full and registers for the waiting list or the pool accordingly.

If the event is merged, and it isn’t full, joins any pool. Otherwise, joins the waiting list.

If the event isn’t merged, checks if the pools that the pending registration can possibly join are full or not. If all are full, a registration for the waiting list is created. If there’s only one pool that isn’t full, register for it.

If there’s more than one possible pool that isn’t full, calculates the total amount of users that can join each pool, and selects the most exclusive pool. If several pools have the same exclusivity, selects the biggest pool of these.

Parameters:

registration – The registration that gets evaluated

Returns:

The registration (in the chosen pool)

property registration_count: int

Prefetch friendly counting of registrations for an event.

restricted_lookup() tuple[list[lego.apps.users.models.User], list]

Restricted Mail

save(*args: Any, **kwargs: Any) None

By re-setting the pool counters on save, we can ensure that counters are updated if an event that has been merged gets un-merged. We want to avoid having to increment counters when registering after merge_time for performance reasons

property total_capacity: int

Prefetch friendly calculation of the total possible capacity of the event.

try_to_rebalance(open_pool: Pool) None

Pull the top waiting registrations for all pools, and try to move users in the pools they are waiting for to open_pool so that someone can be bumped.

Parameters:

open_pool – The pool where the unregistration happened.

unregister(registration: Registration, updated_by: User | None = None, admin_unregistration_reason: str = '') None

Pulls the registration, and clears relevant fields. Sets unregistration date. If the user was in a pool, and not in the waiting list, notifies the waiting list that there might be a bump available.

class lego.apps.events.models.Pool(*args, **kwargs)

Bases: BasisModel

Pool which keeps track of users able to register from different grades/groups.

exception DoesNotExist

Bases: ObjectDoesNotExist

exception MultipleObjectsReturned

Bases: MultipleObjectsReturned

class lego.apps.events.models.Registration(*args, **kwargs)

Bases: BasisModel

A registration for an event. Can be connected to either a pool or a waiting list.

exception DoesNotExist

Bases: ObjectDoesNotExist

exception MultipleObjectsReturned

Bases: MultipleObjectsReturned

handle_user_penalty(presence: PRESENCE_CHOICES) None

Previous penalties related to the event are deleted since the newest presence is the only one that matters

save(*args: Any, **kwargs: Any) None

Save the current instance. Override this in a subclass if you want to control the saving process.

The ‘force_insert’ and ‘force_update’ parameters can be used to insist that the “save” must be an SQL insert or update (or equivalent for non-SQL backends), respectively. Normally, they should not be set.

set_presence(presence: PRESENCE_CHOICES) None

Wrap this method in a transaction