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:
The user does not have permission to join any pools, and an exception is raised.
The user is added to the waiting list, along with any pools he is able to join.
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:
- 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.
- There is only one possible pool left.
The user is registered for this pool.
- 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
- 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)
- 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.