class SemesterStatus(TimeStampModel): company = models.ForeignKey(Company, related_name='semester_statuses', on_delete=models.CASCADE) semester = models.ForeignKey(Semester, on_delete=models.CASCADE) contacted_status = ArrayField( models.CharField(choices=SEMESTER_STATUSES, max_length=64)) contract = FileField(related_name='semester_status_contracts') statistics = FileField(related_name='semester_status_statistics') evaluation = FileField(related_name='semester_status_evaluations') class Meta: unique_together = ('semester', 'company') permission_handler = NestedCompanyPermissionHandler()
class Company(BasisModel): name = models.CharField(max_length=100) student_contact = models.ForeignKey(User, related_name='companies', null=True, on_delete=models.SET_NULL) previous_contacts = models.ManyToManyField(User) description = models.TextField(blank=True) phone = models.CharField(max_length=100, blank=True) company_type = models.CharField(max_length=200, blank=True) website = models.URLField(blank=True) address = models.CharField(max_length=200, blank=True) admin_comment = models.CharField(max_length=100, blank=True) active = models.BooleanField(default=True) payment_mail = models.EmailField(max_length=100, blank=True) comments = GenericRelation(Comment) logo = FileField(related_name='company_logos') class Meta: permission_handler = CompanyPermissionHandler() @property def comment_target(self): return '{0}.{1}-{2}'.format(self._meta.app_label, self._meta.model_name, self.pk) def __str__(self): return self.name
class GalleryPicture(models.Model): """ Store the relation between the gallery and the file in remote storage. Inactive element are only visible for users with can_edit permissions. """ gallery = models.ForeignKey( Gallery, related_name="pictures", on_delete=models.CASCADE ) file = FileField(related_name="gallery_pictures") taggees = models.ManyToManyField("users.User", blank=True) description = models.TextField(blank=True) active = models.BooleanField(default=True) comments = GenericRelation(Comment) class Meta: unique_together = ("gallery", "file") permission_handler = GalleryPicturePermissionHandler() @property def comment_target(self): return "{0}.{1}-{2}".format( self._meta.app_label, self._meta.model_name, self.pk ) def __str__(self): return f"{self.gallery.title}-#{self.pk}" def get_absolute_url(self): return f"{settings.FRONTEND_URL}/photos/{self.gallery.id}/picture/{self.id}/"
class Article(Content, BasisModel, ObjectPermissionsModel): cover = FileField(related_name='article_covers') class Meta: abstract = False def get_absolute_url(self): return f'{settings.FRONTEND_URL}/articles/{self.id}/'
class Page(BasisModel, SlugModel): title = models.CharField('title', max_length=200) content = models.TextField('content') picture = FileField() slug_field = 'title' class Meta: permission_handler = PagePermissionHandler() def __str__(self): return "%s -- %s" % (self.slug, self.title)
class Article(Content, BasisModel, ObjectPermissionsModel): cover = FileField(related_name="article_covers") youtube_url = CharField(max_length=200, default="", validators=[youtube_validator], blank=True) class Meta: abstract = False def get_absolute_url(self): return f"{settings.FRONTEND_URL}/articles/{self.id}/"
class Page(BasisModel, SlugModel, ObjectPermissionsModel): title = models.CharField("title", max_length=200) content = ContentField(allow_images=True) picture = FileField() slug_field = "title" category = models.CharField(max_length=50, choices=constants.CATEGORIES, default=constants.GENERAL) class Meta: abstract = False def __str__(self): return "%s -- %s" % (self.slug, self.title)
class Article(Content, BasisModel, ObjectPermissionsModel): cover = FileField(related_name="article_covers") youtube_url = CharField( max_length=200, default="", validators=[youtube_validator], blank=True ) def save(self, *args, **kwargs): if self.pinned: for pinned_item in Article.objects.filter(pinned=True).exclude(pk=self.pk): pinned_item.pinned = False pinned_item.save() super().save(*args, **kwargs) class Meta: abstract = False def get_absolute_url(self): return f"{settings.FRONTEND_URL}/articles/{self.id}/"
class AbakusGroup(MPTTModel, PersistentModel): name = models.CharField(max_length=80, unique=True, db_index=True) description = models.CharField(blank=True, max_length=200) contact_email = models.EmailField(blank=True) parent = TreeForeignKey( "self", blank=True, null=True, related_name="children", on_delete=models.SET_NULL, ) logo = FileField(related_name="group_pictures") type = models.CharField(max_length=10, choices=constants.GROUP_TYPES, default=constants.GROUP_OTHER) text = models.TextField(blank=True) permissions = ArrayField( models.CharField(validators=[KeywordPermissionValidator()], max_length=50), verbose_name="permissions", default=list, ) objects = AbakusGroupManagerWithoutText() objects_with_text = AbakusGroupManager() show_badge = models.BooleanField(default=True) class Meta: permission_handler = AbakusGroupPermissionHandler() def __str__(self): return self.name @property def is_committee(self): return self.type == constants.GROUP_COMMITTEE @property def is_grade(self): return self.type == constants.GROUP_GRADE @property def leader(self): """Assume there is only one leader, or that we don't care about which leader we get if there is multiple leaders""" membership = self.memberships.filter(role="leader").first() if membership: return membership.user return None @abakus_cached_property def memberships(self): descendants = self.get_descendants(True) return Membership.objects.filter( deleted=False, is_active=True, user__abakus_groups__in=descendants, abakus_group__in=descendants, ) @abakus_cached_property def number_of_users(self): return self.memberships.distinct("user").count() def add_user(self, user, **kwargs): membership, _ = Membership.objects.update_or_create(user=user, abakus_group=self, defaults={ "deleted": False, **kwargs }) return membership def remove_user(self, user): membership = Membership.objects.get(user=user, abakus_group=self) membership.delete() def natural_key(self): return (self.name, ) def restricted_lookup(self): """ Restricted Mail """ memberships = self.memberships.filter(email_lists_enabled=True) return [membership.user for membership in memberships], [] def announcement_lookup(self): memberships = self.memberships return [membership.user for membership in memberships]
class User(PasswordHashUser, GSuiteAddress, AbstractBaseUser, PersistentModel, PermissionsMixin): """ Abakus user model, uses AbstractBaseUser because we use a custom PermissionsMixin. """ username = models.CharField( max_length=50, unique=True, db_index=True, help_text= "Required. 50 characters or fewer. Letters, digits and _ only.", validators=[username_validator, ReservedNameValidator()], error_messages={"unique": "A user with that username already exists."}, ) student_username = models.CharField( max_length=30, unique=True, null=True, help_text="30 characters or fewer. Letters, digits and _ only.", validators=[username_validator, ReservedNameValidator()], error_messages={ "unique": "A user has already verified that student username." }, ) first_name = models.CharField("first name", max_length=50, blank=True) last_name = models.CharField("last name", max_length=30, blank=True) allergies = models.CharField("allergies", max_length=100, blank=True) email = models.EmailField( unique=True, validators=[email_blacklist_validator], error_messages={"unique": "A user with that email already exists."}, ) email_lists_enabled = models.BooleanField(default=True) gender = models.CharField(max_length=50, choices=constants.GENDERS) picture = FileField(related_name="user_pictures") is_active = models.BooleanField( default=True, help_text="Designates whether this user should be treated as " "active. Unselect this instead of deleting accounts.", ) date_joined = models.DateTimeField("date joined", default=timezone.now) date_bumped = models.DateTimeField("date bumped", null=True, default=None) objects = AbakusUserManager() USERNAME_FIELD = "username" REQUIRED_FIELDS = ["email"] backend = "lego.apps.permissions.backends.AbakusPermissionBackend" class Meta: permission_handler = UserPermissionHandler() def clean(self): self.student_username = self.student_username.lower() super(User, self).clean() def get_full_name(self): return f"{self.first_name} {self.last_name}".strip() def get_default_picture(self): if self.gender == constants.MALE: return "default_male_avatar.png" elif self.gender == constants.FEMALE: return "default_female_avatar.png" else: return "default_other_avatar.png" @property def full_name(self): return self.get_full_name() @property def grade(self): return self.abakus_groups.filter(type=constants.GROUP_GRADE).first() @property def profile_picture(self): return self.picture_id or self.get_default_picture() @property def email_address(self): """ Return the address used to reach the user. Some users have a GSuite address and this function is used to decide the correct address to use. """ internal_address = self.internal_email_address if self.is_active and self.crypt_password_hash and internal_address: # Return the internal address if all requirements for a GSuite account are met. return internal_address return self.email @profile_picture.setter def profile_picture(self, value): self.picture = value def is_verified_student(self): return self.student_username is not None def get_short_name(self): return self.first_name def natural_key(self): return (self.username, ) def number_of_penalties(self): # Returns the total penalty weight for this user count = (Penalty.objects.valid().filter(user=self).aggregate( models.Sum("weight"))["weight__sum"]) return count or 0 def restricted_lookup(self): """ Restricted mail """ return [self], [] def announcement_lookup(self): return [self] def unanswered_surveys(self): from lego.apps.surveys.models import Survey from lego.apps.events.models import Registration registrations = Registration.objects.filter(user_id=self.id, presence=PRESENT) unanswered_surveys = (Survey.objects.filter( event__registrations__in=registrations, active_from__lte=timezone.now(), template_type__isnull=True, ).exclude(submissions__user__in=[self]).prefetch_related( "event__registrations", "submissions__user")) return list(unanswered_surveys.values_list("id", flat=True))
class User(PasswordHashUser, GSuiteAddress, AbstractBaseUser, PersistentModel, PermissionsMixin): """ Abakus user model, uses AbstractBaseUser because we use a custom PermissionsMixin. """ username = models.CharField( max_length=50, unique=True, db_index=True, help_text= 'Required. 50 characters or fewer. Letters, digits and _ only.', validators=[username_validator, ReservedNameValidator()], error_messages={ 'unique': 'A user with that username already exists.', }) student_username = models.CharField( max_length=30, unique=True, null=True, help_text='30 characters or fewer. Letters, digits and _ only.', validators=[username_validator, ReservedNameValidator()], error_messages={ 'unique': 'A user has already verified that student username.', }) first_name = models.CharField('first name', max_length=50, blank=True) last_name = models.CharField('last name', max_length=30, blank=True) allergies = models.CharField('allergies', max_length=30, blank=True) email = models.EmailField(unique=True, validators=[email_blacklist_validator], error_messages={ 'unique': 'A user with that email already exists.', }) email_lists_enabled = models.BooleanField(default=True) gender = models.CharField(max_length=50, choices=constants.GENDERS) picture = FileField(related_name='user_pictures') is_active = models.BooleanField( default=True, help_text='Designates whether this user should be treated as ' 'active. Unselect this instead of deleting accounts.') date_joined = models.DateTimeField('date joined', default=timezone.now) objects = AbakusUserManager() USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] backend = 'lego.apps.permissions.backends.AbakusPermissionBackend' class Meta: permission_handler = UserPermissionHandler() def clean(self): self.student_username = self.student_username.lower() super(User, self).clean() def get_full_name(self): return f'{self.first_name} {self.last_name}'.strip() def get_default_picture(self): if self.gender == constants.MALE: return 'default_male_avatar.png' elif self.gender == constants.FEMALE: return 'default_female_avatar.png' else: return 'default_other_avatar.png' @property def full_name(self): return self.get_full_name() @property def grade(self): return self.abakus_groups.filter(type=constants.GROUP_GRADE).first() @property def profile_picture(self): return self.picture_id or self.get_default_picture() @property def email_address(self): """ Return the address used to reach the user. Some users have a GSuite address and this function is used to decide the correct address to use. """ internal_address = self.internal_email_address if self.is_active and self.crypt_password_hash and internal_address: # Return the internal address if all requirements for a GSuite account are met. return internal_address return self.email @profile_picture.setter def profile_picture(self, value): self.picture = value def is_verified_student(self): return self.student_username is not None def get_short_name(self): return self.first_name def natural_key(self): return self.username, def number_of_penalties(self): # Returns the total penalty weight for this user count = Penalty.objects.valid().filter(user=self)\ .aggregate(models.Sum('weight'))['weight__sum'] return count or 0 def restricted_lookup(self): """ Restricted mail """ return [self], [] def announcement_lookup(self): return [self]
class CompanyFile(models.Model): company = models.ForeignKey(Company, related_name='files', on_delete=models.CASCADE) file = FileField()
class Event(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. """ event_type = models.CharField(max_length=50, choices=constants.EVENT_TYPES) location = models.CharField(max_length=100) cover = FileField(related_name='event_covers') start_time = models.DateTimeField(db_index=True) end_time = models.DateTimeField() merge_time = models.DateTimeField(null=True) unregistration_deadline = models.DateTimeField(null=True) registration_deadline_hours = models.PositiveIntegerField( default=constants.REGISTRATION_CLOSE_TIME) @property def registration_close_time(self): return self.start_time - timedelta( hours=self.registration_deadline_hours) penalty_weight = models.PositiveIntegerField(default=1) penalty_weight_on_not_present = models.PositiveIntegerField(default=2) heed_penalties = models.BooleanField(default=True) company = models.ForeignKey(Company, related_name='events', null=True, on_delete=models.SET_NULL) responsible_group = models.ForeignKey('users.AbakusGroup', on_delete=models.SET_NULL, null=True, related_name='events') use_captcha = models.BooleanField(default=True) feedback_description = models.CharField(max_length=255, blank=True) feedback_required = models.BooleanField(default=False) is_priced = models.BooleanField(default=False) use_stripe = models.BooleanField(default=True) price_member = models.PositiveIntegerField(default=0) price_guest = models.PositiveIntegerField(default=0) payment_due_date = models.DateTimeField(null=True) payment_overdue_notified = models.BooleanField(default=False) is_ready = models.BooleanField(default=True) class Meta: permission_handler = EventPermissionHandler() def __str__(self): return self.title def save(self, *args, **kwargs): """ 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 """ with transaction.atomic(): for pool in self.pools.select_for_update().all(): pool.counter = pool.registrations.count() pool.save(update_fields=['counter']) return super().save(*args, **kwargs) def admin_register(self, user, admin_registration_reason, pool=None, feedback=''): """ Used to force registration for a user, even if the event is full or if the user isn't allowed to register. :param user: The user who will be registered :param pool: What pool the registration will be created for :param feedback: Feedback to organizers :return: The registration """ if pool and not self.pools.filter(id=pool.id).exists(): raise NoSuchPool() with transaction.atomic(): registration = self.registrations.get_or_create(event=self, user=user)[0] if registration.pool_id: raise RegistrationExists() if pool: locked_pool = Pool.objects.select_for_update().get(pk=pool.id) locked_pool.increment() registration.add_direct_to_pool( pool, feedback=feedback, admin_registration_reason=admin_registration_reason) else: registration.add_to_waiting_list( feedback=feedback, admin_registration_reason=admin_registration_reason) # Make the user follow the event FollowEvent.objects.get_or_create(follower=user, target=self) handle_event(registration, 'admin_registration') return registration def admin_unregister(self, user, admin_unregistration_reason): with transaction.atomic(): registration = self.registrations.filter(user=user).first() if not registration: raise NoSuchRegistration() self.unregister( registration, admin_unregistration_reason=admin_unregistration_reason) handle_event(registration, 'admin_unregistration') return registration def get_absolute_url(self): return f'{settings.FRONTEND_URL}/events/{self.id}/' def can_register(self, user, pool, future=False, is_admitted=None): if not pool.is_activated and not future: return False if is_admitted is None: is_admitted = self.is_admitted(user) if is_admitted: return False for group in pool.permission_groups.all(): if group in user.all_groups: return True return False def get_earliest_registration_time(self, user, pools=None, penalties=None): if not pools: pools = self.get_possible_pools(user, future=True) if not pools: return None reg_time = min(pool.activation_date for pool in pools) if self.heed_penalties: if not penalties: penalties = user.number_of_penalties() if penalties == 2: return reg_time + timedelta(hours=12) elif penalties == 1: return reg_time + timedelta(hours=3) return reg_time def get_possible_pools(self, user, future=False, all_pools=None, is_admitted=None): if not all_pools: all_pools = self.pools.all() if is_admitted is None: is_admitted = self.is_admitted(user) if is_admitted: return [] queryset = all_pools.filter(permission_groups__in=user.all_groups) if future: return queryset return queryset.filter(activation_date__lte=timezone.now()) def register(self, 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. :param registration: The registration that gets evaluated :return: The registration (in the chosen pool) """ user = registration.user penalties = 0 unanswered_surveys = user.unanswered_surveys() if len(unanswered_surveys) > 0: raise UnansweredSurveyException() if self.heed_penalties: penalties = user.number_of_penalties() current_time = timezone.now() if self.registration_close_time < current_time: raise EventHasClosed() all_pools = self.pools.all() possible_pools = self.get_possible_pools( user, all_pools=all_pools, is_admitted=registration.is_admitted) if not self.is_ready: raise EventNotReady() if not possible_pools: raise ValueError('No available pools') if self.get_earliest_registration_time(user, possible_pools, penalties) > current_time: raise ValueError('Not open yet') # Make the user follow the event FollowEvent.objects.get_or_create(follower=user, target=self) if penalties >= 3: return registration.add_to_waiting_list() # If the event is merged or has only one pool we can skip a lot of logic if all_pools.count() == 1: return registration.add_to_pool(possible_pools[0]) if self.is_merged: with transaction.atomic(): locked_event = Event.objects.select_for_update().get( pk=self.id) is_full = locked_event.is_full if not is_full: return registration.add_direct_to_pool(possible_pools[0]) return registration.add_to_waiting_list() # Calculates which pools that are full or open for registration based on capacity full_pools, open_pools = self.calculate_full_pools(possible_pools) if not open_pools: return registration.add_to_waiting_list() if len(open_pools) == 1: return registration.add_to_pool(open_pools[0]) # Returns a list of the pool(s) with the least amount of potential members exclusive_pools = self.find_most_exclusive_pools(open_pools) if len(exclusive_pools) == 1: chosen_pool = exclusive_pools[0] else: chosen_pool = self.select_highest_capacity(exclusive_pools) return registration.add_to_pool(chosen_pool) def unregister(self, registration, admin_unregistration_reason=''): """ 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. """ if self.start_time < timezone.now(): raise EventHasClosed() # Locks unregister so that no user can register before bump is executed. pool_id = registration.pool_id registration.unregister( is_merged=self.is_merged, admin_unregistration_reason=admin_unregistration_reason) if pool_id: if not admin_unregistration_reason and\ self.heed_penalties and self.passed_unregistration_deadline: if not registration.user.penalties.filter( source_event=self).exists(): Penalty.objects.create( user=registration.user, reason=f'Meldte seg av {self.title} for sent.', weight=1, source_event=self) with transaction.atomic(): locked_event = Event.objects.select_for_update().get( pk=self.id) locked_pool = locked_event.pools.get(id=pool_id) locked_event.check_for_bump_or_rebalance(locked_pool) follow_event_item = FollowEvent.objects.filter( follower=registration.user, target=locked_event).first() if follow_event_item: follow_event_item.delete() def check_for_bump_or_rebalance(self, open_pool): """ 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! :param open_pool: The pool where the unregistration happened. """ if not self.is_full: if self.is_merged: self.bump() elif not open_pool.is_full: for registration in self.waiting_registrations: if open_pool in self.get_possible_pools(registration.user): return self.bump(to_pool=open_pool) self.try_to_rebalance(open_pool=open_pool) def bump(self, to_pool=None): """ Pops the appropriate registration from the waiting list, and moves the registration from the waiting list to `to pool`. :param to_pool: A pool with a free slot. If the event is merged, this will be null. """ if self.waiting_registrations.exists(): first_waiting = self.pop_from_waiting_list(to_pool) if first_waiting: new_pool = None if to_pool: new_pool = to_pool new_pool.increment() else: for pool in self.pools.all(): if self.can_register(first_waiting.user, pool): new_pool = pool new_pool.increment() break first_waiting.pool = new_pool first_waiting.save(update_fields=['pool']) handle_event(first_waiting, 'bump') def early_bump(self, opening_pool): """ 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. :param opening_pool: The pool about to be activated. :return: """ for reg in self.waiting_registrations: if opening_pool.is_full: break if self.heed_penalties and reg.user.number_of_penalties() >= 3: continue if self.can_register(reg.user, opening_pool, future=True): reg.pool = opening_pool reg.save() handle_event(reg, 'bump') self.check_for_bump_or_rebalance(opening_pool) def bump_on_pool_creation_or_expansion(self): """ 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. :return: """ open_pools = [pool for pool in self.pools.all() if not pool.is_full] for pool in open_pools: for reg in self.waiting_registrations: if self.is_full or pool.is_full: break if self.heed_penalties and reg.user.number_of_penalties() >= 3: continue if self.can_register(reg.user, pool, future=True): reg.pool = pool reg.save() handle_event(reg, 'bump') self.check_for_bump_or_rebalance(pool) def try_to_rebalance(self, open_pool): """ 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. :param open_pool: The pool where the unregistration happened. """ balanced_pools = [] bumped = False for waiting_registration in self.waiting_registrations: for full_pool in self.get_possible_pools( waiting_registration.user): if full_pool not in balanced_pools: balanced_pools.append(full_pool) bumped = self.rebalance_pool(from_pool=full_pool, to_pool=open_pool) if bumped: return def rebalance_pool(self, from_pool, to_pool): """ 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)`. :param from_pool: A full pool with waiting registrations. :param to_pool: A pool with one open slot. :return: Boolean, whether or not `bump()` has been called. """ to_pool_permissions = to_pool.permission_groups.all() bumped = False for old_registration in self.registrations.filter(pool=from_pool): moveable = False user_groups = old_registration.user.all_groups for group in to_pool_permissions: if group in user_groups: moveable = True if moveable: old_registration.pool = to_pool old_registration.save() self.bump(to_pool=from_pool) bumped = True return bumped def add_to_waiting_list(self, user): """ Adds a user to the waiting list. :param user: The user that will be registered to the waiting list. :return: A registration for the waiting list, with `pool=null` """ return self.registrations.update_or_create( event=self, user=user, defaults={ 'pool': None, 'status': constants.SUCCESS_REGISTER, 'unregistration_date': None })[0] def pop_from_waiting_list(self, to_pool=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. :param to_pool: The pool we are bumping to. If post-merge, there is no pool. :return: The registration that is first in line for said pool. """ if to_pool: for registration in self.waiting_registrations: if self.heed_penalties: penalties = registration.user.number_of_penalties() earliest_reg = self.get_earliest_registration_time( registration.user, [to_pool], penalties) if penalties < 3 and earliest_reg < timezone.now(): if self.can_register(registration.user, to_pool): return registration elif self.can_register(registration.user, to_pool): return registration return None if self.heed_penalties: for registration in self.waiting_registrations: penalties = registration.user.number_of_penalties() earliest_reg = self.get_earliest_registration_time( registration.user, None, penalties) if penalties < 3 and earliest_reg < timezone.now(): return registration return None return self.waiting_registrations.first() @staticmethod def has_pool_permission(user, pool): for group in pool.permission_groups.all(): if group in user.all_groups: return True return False @staticmethod def calculate_full_pools(pools): full_pools = [] open_pools = [] for pool in pools: if pool.is_full: full_pools.append(pool) else: open_pools.append(pool) return full_pools, open_pools @staticmethod def find_most_exclusive_pools(pools): lowest = float('inf') equal = [] for pool in pools: groups = pool.permission_groups.all() users = sum(g.number_of_users for g in groups) if users == lowest: equal.append(pool) elif users < lowest: equal = [pool] lowest = users return equal @staticmethod def select_highest_capacity(pools): capacities = [pool.capacity for pool in pools] return pools[capacities.index(max(capacities))] def is_admitted(self, user): return self.registrations.filter(user=user).exclude(pool=None).exists() def get_registration(self, user): return self.registrations.filter(user=user).exclude(pool=None).first() def get_price(self, user): if user.is_authenticated and user.is_abakus_member: return self.price_member return self.price_guest def spots_left_for_user(self, user): all_pools = self.pools.all() pools = self.get_possible_pools(user, all_pools=all_pools) if not pools: return None if self.is_merged: return all_pools.annotate(Count('registrations'))\ .aggregate(spots_left=Sum('capacity') - Sum('registrations__count'))['spots_left'] return sum([pool.spots_left() for pool in pools]) @property def is_merged(self): if self.merge_time is None: return False return timezone.now() >= self.merge_time def get_is_full(self, queryset=None): if queryset is None: queryset = self.pools.filter(activation_date__lte=timezone.now()) query = queryset.annotate(Count('registrations')).aggregate( active_capacity=Sum('capacity'), registrations_count=Sum('registrations__count')) active_capacity = query['active_capacity'] or 0 registrations_count = query['registrations_count'] or 0 if active_capacity == 0: return False return active_capacity <= registrations_count @property def is_full(self): return self.get_is_full() @property def active_capacity(self): """ Calculation capacity of pools that are active. """ aggregate = self.pools.all().filter(activation_date__lte=timezone.now())\ .aggregate(Sum('capacity')) return aggregate['capacity__sum'] or 0 @property def total_capacity(self): """ Prefetch friendly calculation of the total possible capacity of the event. """ return sum([pool.capacity for pool in self.pools.all()]) @property def registration_count(self): """ Prefetch friendly counting of registrations for an event. """ return sum( [pool.registrations.all().count() for pool in self.pools.all()]) @property def number_of_registrations(self): """ Registration count guaranteed not to include unregistered users. """ return self.registrations.filter(unregistration_date=None, status__in=[ constants.SUCCESS_REGISTER, constants.FAILURE_UNREGISTER ]).exclude(pool=None).count() @property def unregistered(self): return self.registrations.filter(pool=None, unregistration_date__isnull=False, status=constants.SUCCESS_UNREGISTER) @property def passed_unregistration_deadline(self): if self.unregistration_deadline: return self.unregistration_deadline < timezone.now() return False @property def waiting_registrations(self): return self.registrations.filter(pool=None, unregistration_date=None, status__in=[ constants.SUCCESS_REGISTER, constants.FAILURE_UNREGISTER ]) @property def waiting_registration_count(self): return self.waiting_registrations.count() @property def is_abakom_only(self): return self.require_auth and \ self.can_view_groups.count() == 1 and \ self.can_view_groups.filter(name="Abakom").exists() def set_abakom_only(self, abakom_only): abakom_group = AbakusGroup.objects.get(name="Abakom") if abakom_only: self.require_auth = True self.can_view_groups.add(abakom_group) else: self.require_auth = False self.can_view_groups.remove(abakom_group) self.save() def restricted_lookup(self): """ Restricted Mail """ registrations = self.registrations.filter( status=constants.SUCCESS_REGISTER) return [registration.user for registration in registrations], [] def announcement_lookup(self): registrations = self.registrations.filter( status=constants.SUCCESS_REGISTER) return [registration.user for registration in registrations]