Esempio n. 1
0
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()
Esempio n. 2
0
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
Esempio n. 3
0
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}/"
Esempio n. 4
0
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}/'
Esempio n. 5
0
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)
Esempio n. 6
0
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}/"
Esempio n. 7
0
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)
Esempio n. 8
0
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}/"
Esempio n. 9
0
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]
Esempio n. 10
0
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))
Esempio n. 11
0
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]
Esempio n. 12
0
class CompanyFile(models.Model):
    company = models.ForeignKey(Company,
                                related_name='files',
                                on_delete=models.CASCADE)
    file = FileField()
Esempio n. 13
0
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]