Exemple #1
0
class ProgrammeEventMeta(ContactEmailMixin, EventMetaBase):
    public_from = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name='Ohjelmakartan julkaisuaika',
        help_text='Ohjelmakartta näkyy kansalle tästä eteenpäin.',
    )

    contact_email = models.CharField(
        max_length=255,
        blank=True,
        validators=[contact_email_validator],
        verbose_name='yhteysosoite',
        help_text='Kaikki ohjelmajärjestelmän lähettämät sähköpostiviestit lähetetään tästä '
            'osoitteesta, ja tämä osoite näytetään ohjelmanjärjestäjälle yhteysosoitteena. Muoto: Selite <[email protected]>.',
    )

    accepting_cold_offers_from = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("Accepting cold offers from"),
    )

    accepting_cold_offers_until = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("Accepting cold offers until"),
    )

    schedule_layout = models.CharField(
        max_length=max(len(choice[0]) for choice in SCHEDULE_LAYOUT_CHOICES),
        default='reasonable',
        choices=SCHEDULE_LAYOUT_CHOICES,
        verbose_name=_('Schedule layout'),
        help_text=_(
            'Some events may opt to make their schedule use the full width of the browser window. '
            'This option selects between reasonable width (the default) and full width.'
        ),
    )

    def __init__(self, *args, **kwargs):
        if 'public' in kwargs:
            public = kwargs.pop('public')
            if public:
                kwargs['public_from'] = now()

        super(ProgrammeEventMeta, self).__init__(*args, **kwargs)

    def get_special_programmes(self, include_unpublished=False, **extra_criteria):
        from .room import Room
        from .programme import Programme

        schedule_rooms = Room.objects.filter(view_rooms__view__event=self.event).only('id')
        criteria = dict(category__event=self.event, **extra_criteria)
        if not include_unpublished:
            criteria.update(state='published')
        return Programme.objects.filter(**criteria).exclude(room__in=schedule_rooms)

    @classmethod
    def get_or_create_dummy(cls):
        from core.models import Event
        from django.utils.timezone import now

        event, unused = Event.get_or_create_dummy()
        admin_group, hosts_group = cls.get_or_create_groups(event, ['admins', 'hosts'])

        return cls.objects.get_or_create(
            event=event,
            defaults=dict(
                admin_group=admin_group,
                public_from=now(),
            )
        )

    @property
    def is_public(self):
        return self.public_from is not None and now() > self.public_from

    @property
    def is_accepting_cold_offers(self):
        return is_within_period(
            self.accepting_cold_offers_from,
            self.accepting_cold_offers_until,
        )

    @property
    def is_full_width(self):
        """
        For easy iffability in templates.
        """
        return self.schedule_layout == 'full-width'

    @property
    def default_role(self):
        from .role import Role
        return Role.objects.get(personnel_class__event=self.event, is_default=True)

    @property
    def is_using_alternative_programme_forms(self):
        from .alternative_programme_form import AlternativeProgrammeForm
        return AlternativeProgrammeForm.objects.filter(event=self.event).exists()

    @property
    def default_alternative_programme_form(self):
        from .alternative_programme_form import AlternativeProgrammeForm
        return AlternativeProgrammeForm.objects.filter(event=self.event, slug='default').first()

    def publish(self):
        self.public_from = now()
        self.save()

    def unpublish(self):
        self.public_from = None
        self.save()

    public = alias_property('is_public')

    @property
    def signup_extra_model(self):
        if self.event.labour_event_meta is not None:
            return self.event.labour_event_meta.signup_extra_model
        else:
            from labour.models import EmptySignupExtra
            return EmptySignupExtra
Exemple #2
0
class Signup(models.Model, CsvExportMixin):
    person = models.ForeignKey('core.Person', related_name='signups')
    event = models.ForeignKey('core.Event')

    personnel_classes = models.ManyToManyField(
        'labour.PersonnelClass',
        blank=True,
        verbose_name='Henkilöstöluokat',
        help_text=
        'Mihin henkilöstöryhmiin tämä henkilö kuuluu? Henkilö saa valituista ryhmistä '
        'ylimmän mukaisen badgen.',
    )

    job_categories = models.ManyToManyField(
        'labour.JobCategory',
        verbose_name='Haettavat tehtävät',
        help_text=
        'Valitse kaikki ne tehtävät, joissa olisit valmis työskentelemään '
        'tapahtumassa. Huomaathan, että sinulle tarjottavia tehtäviä voi rajoittaa se, '
        'mitä pätevyyksiä olet ilmoittanut sinulla olevan. Esimerkiksi järjestyksenvalvojaksi '
        'voivat ilmoittautua ainoastaan JV-kortilliset.',
        related_name='signup_set')

    notes = models.TextField(
        blank=True,
        verbose_name='Käsittelijän merkinnät',
        help_text=
        ('Tämä kenttä ei normaalisti näy henkilölle itselleen, mutta jos tämä '
         'pyytää henkilörekisteriotetta, kentän arvo on siihen sisällytettävä.'
         ),
    )

    created_at = models.DateTimeField(auto_now_add=True, verbose_name='Luotu')
    updated_at = models.DateTimeField(auto_now=True, verbose_name='Päivitetty')

    job_categories_accepted = models.ManyToManyField(
        'labour.JobCategory',
        blank=True,
        related_name='accepted_signup_set',
        verbose_name='Hyväksytyt tehtäväalueet',
        help_text=
        'Tehtäväalueet, joilla hyväksytty vapaaehtoistyöntekijä tulee työskentelemään. '
        'Tämän perusteella henkilölle mm. lähetetään oman tehtäväalueensa työvoimaohjeet. '
        'Harmaalla merkityt tehtäväalueet ovat niitä, joihin hakija ei ole itse hakenut.'
    )

    job_categories_rejected = models.ManyToManyField(
        'labour.JobCategory',
        blank=True,
        related_name='+',
        verbose_name=_('Rejected job categories'),
        help_text=
        _('The workforce manager may use this field to inform other workforce managers that '
          'this applicant will not be accepted to a certain job category. This field is not visible '
          'to the applicant, but should they request a record of their own information, this field will '
          'be included.'))

    xxx_interim_shifts = models.TextField(
        blank=True,
        null=True,
        default="",
        verbose_name="Työvuorot",
        help_text=
        ("Tämä tekstikenttä on väliaikaisratkaisu, jolla vänkärin työvuorot voidaan "
         "merkitä Kompassiin ja lähettää vänkärille työvoimaviestissä jo ennen kuin "
         "lopullinen työvuorotyökalu on käyttökunnossa."),
    )

    alternative_signup_form_used = models.ForeignKey(
        'labour.AlternativeSignupForm',
        blank=True,
        null=True,
        verbose_name="Ilmoittautumislomake",
        help_text=
        ("Tämä kenttä ilmaisee, mitä ilmoittautumislomaketta hakemuksen täyttämiseen käytettiin. "
         "Jos kenttä on tyhjä, käytettiin oletuslomaketta."),
    )

    job_title = models.CharField(
        max_length=JOB_TITLE_LENGTH,
        blank=True,
        default='',
        verbose_name="Tehtävänimike",
        help_text=
        ("Printataan badgeen ym. Asetetaan automaattisesti hyväksyttyjen tehtäväalueiden perusteella, "
         "mikäli kenttä jätetään tyhjäksi."),
    )

    is_active = models.BooleanField(verbose_name='Aktiivinen', default=True)

    time_accepted = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name='Hyväksytty',
    )

    time_confirmation_requested = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name='Vahvistusta vaadittu',
    )

    time_finished = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name='Vuorot valmiit',
    )

    time_complained = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name='Vuoroista reklamoitu',
    )

    time_cancelled = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name='Peruutettu',
    )

    time_rejected = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name='Hylätty',
    )

    time_arrived = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name='Saapunut tapahtumaan',
    )

    time_work_accepted = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name='Työpanos hyväksytty',
    )

    time_reprimanded = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name='Työpanoksesta esitetty moite',
    )

    is_accepted = time_bool_property('time_accepted')
    is_confirmation_requested = time_bool_property(
        'time_confirmation_requested')
    is_finished = time_bool_property('time_finished')
    is_complained = time_bool_property('time_complained')
    is_cancelled = time_bool_property('time_cancelled')
    is_rejected = time_bool_property('time_rejected')
    is_arrived = time_bool_property('time_arrived')
    is_work_accepted = time_bool_property('time_work_accepted')
    is_workaccepted = alias_property(
        'is_work_accepted')  # for automagic groupiness
    is_reprimanded = time_bool_property('time_reprimanded')

    is_new = property(lambda self: self.state == 'new')
    is_applicants = alias_property(
        'is_active')  # group is called applicants for historical purposes
    is_confirmation = alias_property('is_confirmation_requested')
    is_processed = property(lambda self: self.state != 'new')

    class Meta:
        verbose_name = _('signup')
        verbose_name_plural = _('signups')

    def __str__(self):
        p = self.person.full_name if self.person else 'None'
        e = self.event.name if self.event else 'None'

        return '{p} / {e}'.format(**locals())

    @property
    def personnel_class(self):
        """
        The highest personnel class of this Signup (possibly None).
        """
        return self.personnel_classes.first()

    @property
    def signup_extra_model(self):
        return self.event.labour_event_meta.signup_extra_model

    @property
    def signup_extra(self):
        if not hasattr(self, '_signup_extra'):
            SignupExtra = self.signup_extra_model
            self._signup_extra = SignupExtra.for_signup(self)

        return self._signup_extra

    def get_first_categories(self):
        return self.job_categories.all()[:NUM_FIRST_CATEGORIES]

    @property
    def is_more_categories(self):
        return self.job_categories.count() > NUM_FIRST_CATEGORIES

    def get_redacted_category_names(self):
        return ', '.join(
            cat.name
            for cat in self.job_categories.all()[NUM_FIRST_CATEGORIES:])

    @property
    def job_categories_label(self):
        if self.state == 'new':
            return 'Haetut tehtävät'
        else:
            return 'Hyväksytyt tehtävät'

    @property
    def job_category_accepted_labels(self):
        state = self.state
        label_class = SIGNUP_STATE_LABEL_CLASSES[state]

        if state == 'new':
            label_texts = [cat.name for cat in self.get_first_categories()]
            labels = [(label_class, label_text, None)
                      for label_text in label_texts]

            if self.is_more_categories:
                labels.append(
                    (label_class, '...', self.get_redacted_category_names()))

        elif state == 'cancelled':
            labels = [(label_class, 'Peruutettu', None)]

        elif state == 'rejected':
            labels = [(label_class, 'Hylätty', None)]

        elif state == 'beyond_logic':
            labels = [(label_class, 'Perätilassa', None)]

        elif self.is_accepted:
            label_texts = [
                cat.name for cat in self.job_categories_accepted.all()
            ]
            labels = [(label_class, label_text, None)
                      for label_text in label_texts]

        else:
            from warnings import warn
            warn('Unknown state: {state}'.format(self=self))
            labels = []

        return labels

    @property
    def personnel_class_labels(self):
        label_texts = [pc.name for pc in self.personnel_classes.all()]
        return [('label-default', label_text, None)
                for label_text in label_texts]

    @property
    def some_job_title(self):
        """
        Tries to figure a job title for this worker using the following methods in this order

        1. A manually set job title
        2. The title of the job category the worker is accepted into
        3. A generic job title
        """

        if self.job_title:
            return self.job_title
        elif self.job_categories_accepted.exists():
            return self.job_categories_accepted.first().name
        else:
            return 'Työvoima'

    @property
    def granted_privileges(self):
        if 'access' not in settings.INSTALLED_APPS:
            return []

        from access.models import GrantedPrivilege

        return GrantedPrivilege.objects.filter(
            person=self.person,
            privilege__group_privileges__group__in=self.person.user.groups.all(
            ),
            privilege__group_privileges__event=self.event,
        )

    @property
    def potential_privileges(self):
        if 'access' not in settings.INSTALLED_APPS:
            return []

        from access.models import Privilege

        return Privilege.get_potential_privileges(
            person=self.person, group_privileges__event=self.event)

    @classmethod
    def get_or_create_dummy(cls, accepted=False):
        from core.models import Person, Event
        from .job_category import JobCategory

        person, unused = Person.get_or_create_dummy()
        event, unused = Event.get_or_create_dummy()
        job_category, unused = JobCategory.get_or_create_dummy()

        signup, created = Signup.objects.get_or_create(person=person,
                                                       event=event)
        extra = signup.signup_extra
        signup.job_categories = [job_category]
        extra.save()

        if accepted:
            signup.job_categories_accepted = signup.job_categories.all()
            signup.personnel_classes.add(
                signup.job_categories.first().personnel_classes.first())
            signup.state = 'accepted'
            signup.save()
            signup.apply_state()

        return signup, created

    @classmethod
    def get_state_query_params(cls, state):
        flag_values = STATE_FLAGS_BY_NAME[state]
        assert len(STATE_TIME_FIELDS) == len(flag_values)

        query_params = []

        for time_field_name, flag_value in zip(STATE_TIME_FIELDS, flag_values):
            time_field_preposition = '{}__isnull'.format(time_field_name)
            query_params.append((time_field_preposition, not flag_value))

        # First state flag is not a time bool field, but an actual bona fide boolean field.
        # Also "is null" semantics mean that flag values are flipped, so we need to backflip it.
        query_params[0] = ('is_active', not query_params[0][1])

        return OrderedDict(query_params)

    @classmethod
    def mass_reject(cls, signups):
        return cls._mass_state_change('new', 'rejected', signups)

    @classmethod
    def mass_request_confirmation(cls, signups):
        return cls._mass_state_change('accepted', 'confirmation', signups)

    @classmethod
    def filter_signups_for_mass_send_shifts(cls, signups):
        return signups.filter(
            **cls.get_state_query_params('accepted')).exclude(
                xxx_interim_shifts='',
                shifts__isnull=True,
            )

    @classmethod
    def mass_send_shifts(cls, signups):
        return cls._mass_state_change(
            old_state='accepted',
            new_state='finished',
            signups=signups,
            filter_func=cls.filter_signups_for_mass_send_shifts)

    @classmethod
    def _mass_state_change(cls,
                           old_state,
                           new_state,
                           signups,
                           filter_func=None):
        if filter_func is None:
            signups = signups.filter(
                **Signup.get_state_query_params(old_state))
        else:
            signups = filter_func(signups)

        for signup in signups:
            signup.state = new_state
            signup.save()
            signup.apply_state()

        return signups

    def apply_state(self):
        self.apply_state_sync()

        if 'background_tasks' in settings.INSTALLED_APPS:
            from ..tasks import signup_apply_state
            signup_apply_state.delay(self.pk)
        else:
            self._apply_state()

    def apply_state_sync(self):
        self.apply_state_ensure_job_categories_accepted_is_set()
        self.apply_state_ensure_personnel_class_is_set()

        self.signup_extra.apply_state()

        self.apply_state_create_badges()

    def _apply_state(self):
        self.apply_state_group_membership()
        self.apply_state_email_aliases()
        self.apply_state_send_messages()

    def apply_state_group_membership(self):
        from .job_category import JobCategory
        from .personnel_class import PersonnelClass

        groups_to_add = set()
        groups_to_remove = set()

        for group_suffix in SIGNUP_STATE_GROUPS:
            should_belong_to_group = getattr(
                self, 'is_{group_suffix}'.format(group_suffix=group_suffix))
            group = self.event.labour_event_meta.get_group(group_suffix)

            if should_belong_to_group:
                groups_to_add.add(group)
            else:
                groups_to_remove.add(group)

        for job_category in JobCategory.objects.filter(event=self.event):
            should_belong_to_group = self.job_categories_accepted.filter(
                pk=job_category.pk).exists()
            group = self.event.labour_event_meta.get_group(job_category.slug)

            if should_belong_to_group:
                groups_to_add.add(group)
            else:
                groups_to_remove.add(group)

        for personnel_class in PersonnelClass.objects.filter(
                event=self.event, app_label='labour'):
            should_belong_to_group = self.personnel_classes.filter(
                pk=personnel_class.pk).exists()
            group = self.event.labour_event_meta.get_group(
                personnel_class.slug)

            if should_belong_to_group:
                groups_to_add.add(group)
            else:
                groups_to_remove.add(group)

        ensure_user_group_membership(self.person.user, groups_to_add,
                                     groups_to_remove)

    def apply_state_email_aliases(self):
        if 'access' not in settings.INSTALLED_APPS:
            return

        from access.models import GroupEmailAliasGrant
        GroupEmailAliasGrant.ensure_aliases(self.person)

    def apply_state_send_messages(self, resend=False):
        if 'mailings' not in settings.INSTALLED_APPS:
            return

        from mailings.models import Message
        Message.send_messages(self.event, 'labour', self.person)

    def apply_state_ensure_job_categories_accepted_is_set(self):
        if self.is_accepted and not self.job_categories_accepted.exists(
        ) and self.job_categories.count() == 1:
            self.job_categories_accepted.add(self.job_categories.get())

    def apply_state_ensure_personnel_class_is_set(self):
        for app_label in self.job_categories_accepted.values_list(
                'app_label', flat=True).distinct():
            if self.personnel_classes.filter(app_label=app_label).exists():
                continue

            any_jca = self.job_categories_accepted.filter(
                app_label=app_label).first()
            personnel_class = any_jca.personnel_classes.first()
            self.personnel_classes.add(personnel_class)

    def apply_state_create_badges(self):
        if 'badges' not in settings.INSTALLED_APPS:
            return

        if self.event.badges_event_meta is None:
            return

        from badges.models import Badge

        Badge.ensure(event=self.event, person=self.person)

    def get_previous_and_next_signup(self):
        queryset = self.event.signup_set.order_by('person__surname',
                                                  'person__first_name',
                                                  'id').all()
        return get_previous_and_next(queryset, self)

    @property
    def _state_flags(self):
        # The Grand Order is defined here.
        return (
            self.is_active,
            self.is_accepted,
            self.is_confirmation_requested,
            self.is_finished,
            self.is_complained,
            self.is_arrived,
            self.is_work_accepted,
            self.is_reprimanded,
            self.is_rejected,
            self.is_cancelled,
        )

    @_state_flags.setter
    def _state_flags(self, flags):
        # These need to be in the Grand Order.
        (
            self.is_active,
            self.is_accepted,
            self.is_confirmation_requested,
            self.is_finished,
            self.is_complained,
            self.is_arrived,
            self.is_work_accepted,
            self.is_reprimanded,
            self.is_rejected,
            self.is_cancelled,
        ) = flags

    @property
    def state(self):
        return STATE_NAME_BY_FLAGS[self._state_flags]

    @state.setter
    def state(self, new_state):
        self._state_flags = STATE_FLAGS_BY_NAME[new_state]

    @property
    def next_states(self):
        cur_state = self.state

        states = []

        if cur_state == 'new':
            states.extend(('accepted', 'rejected', 'cancelled'))
        elif cur_state == 'accepted':
            states.extend(('finished', 'confirmation', 'cancelled'))
        elif cur_state == 'confirmation':
            states.extend(('accepted', 'cancelled'))
        elif cur_state == 'finished':
            states.extend(('arrived', 'complained', 'no_show', 'relieved'))
        elif cur_state == 'complained':
            states.extend(('finished', 'relieved'))
        elif cur_state == 'arrived':
            states.extend(('honr_discharged', 'dish_discharged', 'relieved'))
        elif cur_state == 'beyond_logic':
            states.extend((
                'new',
                'accepted',
                'finished',
                'complained',
                'rejected',
                'cancelled',
                'arrived',
                'honr_discharged',
                'no_show',
            ))

        if cur_state != 'beyond_logic':
            states.append('beyond_logic')

        return states

    @property
    def next_states_buttons(self):
        return [
            StateTransition(self, to_state) for to_state in self.next_states
        ]

    @property
    def formatted_state(self):
        return dict(SIGNUP_STATE_NAMES).get(self.state, '')

    @property
    def state_label_class(self):
        return SIGNUP_STATE_LABEL_CLASSES[self.state]

    @property
    def state_description(self):
        return SIGNUP_STATE_DESCRIPTIONS.get(self.state, '')

    @property
    def state_times(self):
        return [(
            self._meta.get_field(field_name).verbose_name,
            getattr(self, field_name, None),
        ) for field_name in STATE_TIME_FIELDS
                if getattr(self, field_name, None)]

    @property
    def person_messages(self):
        if 'mailings' in settings.INSTALLED_APPS:
            if getattr(self, '_person_messages', None) is None:
                self._person_messages = self.person.personmessage_set.filter(
                    message__recipient__event=self.event,
                    message__recipient__app_label='labour',
                ).order_by('-created_at')

            return self._person_messages
        else:
            return []

    @property
    def have_person_messages(self):
        if 'mailings' in settings.INSTALLED_APPS:
            return self.person_messages.exists()
        else:
            return False

    @property
    def applicant_has_actions(self):
        return any([
            self.applicant_can_edit,
            self.applicant_can_confirm,
            self.applicant_can_cancel,
        ])

    @property
    def applicant_can_edit(self):
        return self.state == 'new' and self.is_registration_open

    @property
    def is_registration_open(self):
        if self.alternative_signup_form_used is not None:
            return self.alternative_signup_form_used.is_active
        else:
            return self.event.labour_event_meta.is_registration_open

    @property
    def applicant_can_confirm(self):
        return self.state == 'confirmation'

    def confirm(self):
        assert self.state == 'confirmation'

        self.state = 'accepted'
        self.save()
        self.apply_state()

    @property
    def applicant_can_cancel(self):
        return self.is_active and not self.is_cancelled and not self.is_rejected and \
            not self.is_arrived

    @property
    def formatted_personnel_classes(self):
        from .job_category import format_job_categories
        return format_job_categories(self.personnel_classes.all())

    @property
    def formatted_job_categories_accepted(self):
        from .job_category import format_job_categories
        return format_job_categories(self.job_categories_accepted.all())

    @property
    def formatted_job_categories(self):
        from .job_category import format_job_categories
        return format_job_categories(self.job_categories.all())

    @property
    def formatted_shifts(self):
        parts = []

        if self.xxx_interim_shifts:
            parts.append(self.xxx_interim_shifts)

        parts.extend(text_type(shift) for shift in self.shifts.all())

        return "\n\n".join(part for part in parts if part)

    # for admin
    @property
    def full_name(self):
        return self.person.full_name

    @property
    def info_links(self):
        from .info_link import InfoLink

        return InfoLink.objects.filter(
            event=self.event,
            group__user=self.person.user,
        )

    @property
    def email_address(self):
        from access.models import EmailAlias

        email_alias = EmailAlias.objects.filter(
            type__domain__organization=self.event.organization,
            person=self.person,
        ).order_by('type__priority').first()  # TODO order

        return email_alias.email_address if email_alias else self.person.email

    @classmethod
    def get_csv_fields(cls, event):
        if getattr(event, '_signup_csv_fields', None) is None:
            from core.models import Person

            event._signup_csv_fields = []

            related_models = [Person, Signup]

            fields_to_skip = [
                # useless & non-serializable
                (Person, 'user'),
                (Signup, 'person'),

                # too official
                (Person, 'official_first_names'),
                (Person, 'muncipality'),
            ]

            SignupExtra = event.labour_event_meta.signup_extra_model
            if SignupExtra is not None:
                related_models.append(SignupExtra)
                fields_to_skip.extend([
                    (SignupExtra, 'event'),
                    (SignupExtra, 'person'),
                ])

            # XXX HACK jv-kortin numero
            if 'labour_common_qualifications' in settings.INSTALLED_APPS:
                from labour_common_qualifications.models import JVKortti
                related_models.append(JVKortti)
                fields_to_skip.append((JVKortti, 'personqualification'))

            for model in related_models:
                for field in model._meta.fields:
                    if (model, field.name) in fields_to_skip:
                        continue

                    event._signup_csv_fields.append((model, field))

                for field in model._meta.many_to_many:
                    event._signup_csv_fields.append((model, field))

        return event._signup_csv_fields

    def get_csv_related(self):
        from core.models import Person
        related = {Person: self.person}

        signup_extra_model = self.signup_extra_model
        if signup_extra_model:
            related[signup_extra_model] = self.signup_extra

        # XXX HACK jv-kortin numero
        if 'labour_common_qualifications' in settings.INSTALLED_APPS:
            from labour_common_qualifications.models import JVKortti
            try:
                jv_kortti = JVKortti.objects.get(
                    personqualification__person=self.person)
                related[JVKortti] = jv_kortti
            except JVKortti.DoesNotExist:
                related[JVKortti] = None

        return related

    def as_dict(self):
        # XXX?
        signup_extra = self.signup_extra
        shift_wishes = signup_extra.shift_wishes if signup_extra.get_field(
            'shift_wishes') else ''
        total_work = signup_extra.total_work if signup_extra.get_field(
            'total_work') else ''
        shift_type = signup_extra.get_shift_type_display(
        ) if signup_extra.get_field('shift_type') else ''

        return dict(
            id=self.person.id,
            fullName=self.person.full_name,
            shiftWishes=shift_wishes,
            totalWork=total_work,
            currentlyAssigned=self.shifts.all().aggregate(
                sum_hours=Coalesce(Sum('hours'), 0))['sum_hours'],
            shiftType=shift_type,
        )

    @classmethod
    def for_signup(cls, signup):
        """
        Surveys make use of this method.
        """
        return signup
class LabourEventMeta(ContactEmailMixin, EventMetaBase):
    signup_extra_content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE)

    registration_opens = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("Registration opens"),
    )
    public_from = alias_property('registration_opens')

    registration_closes = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("Registration closes"),
    )
    public_until = alias_property('registration_closes')

    work_begins = models.DateTimeField(verbose_name='Ensimmäiset työvuorot alkavat')
    work_ends = models.DateTimeField(verbose_name='Viimeiset työvuorot päättyvät')

    monitor_email = models.CharField(
        max_length=255,
        blank=True,
        verbose_name='tarkkailusähköposti',
        help_text='Kaikki työvoimajärjestelmän lähettämät sähköpostiviestit lähetetään myös '
            'tähän osoitteeseen.',
    )

    contact_email = models.CharField(
        max_length=255,
        blank=True,
        validators=[contact_email_validator,],
        verbose_name='yhteysosoite',
        help_text='Kaikki työvoimajärjestelmän lähettämät sähköpostiviestit lähetetään tästä '
            'osoitteesta, ja tämä osoite näytetään työvoimalle yhteysosoitteena. Muoto: Selite <[email protected]>.',
    )

    signup_message = models.TextField(
        null=True,
        blank=True,
        default='',
        verbose_name='Ilmoittautumisen huomautusviesti',
        help_text='Tämä viesti näytetään kaikille työvoimailmoittautumisen alussa. Käytettiin '
            'esimerkiksi Tracon 9:ssä kertomaan, että työvoimahaku on avoinna enää JV:ille ja '
            'erikoistehtäville.',
    )

    work_certificate_signer = models.TextField(
        null=True,
        blank=True,
        default='',
        verbose_name='Työtodistuksen allekirjoittaja',
        help_text='Tämän kentän sisältö näkyy työtodistuksen allekirjoittajan nimenselvennyksenä. '
            'On suositeltavaa sisällyttää tähän omalle rivilleen allekirjoittajan tehtävänimike.'
    )

    use_cbac = True

    class Meta:
        verbose_name = _('labour event meta')
        verbose_name_plural = _('labour event metas')

    def __str__(self):
        return self.event.name if self.event else 'None'

    @property
    def signup_extra_model(self):
        return self.signup_extra_content_type.model_class()

    @classmethod
    def events_registration_open(cls):
        from core.models import Event
        t = now()
        return Event.objects.filter(
            laboureventmeta__registration_opens__isnull=False,
            laboureventmeta__registration_opens__lte=t,
        ).exclude(
            laboureventmeta__registration_closes__isnull=False,
            laboureventmeta__registration_closes__lte=t,
        )

    @classmethod
    def get_or_create_dummy(cls):
        from django.contrib.contenttypes.models import ContentType
        from core.models import Event
        from .signup_extras import EmptySignupExtra

        event, unused = Event.get_or_create_dummy()
        content_type = ContentType.objects.get_for_model(EmptySignupExtra)
        admin_group, = LabourEventMeta.get_or_create_groups(event, ['admins'])

        t = now()

        labour_event_meta, created = cls.objects.get_or_create(
            event=event,
            defaults=dict(
                admin_group=admin_group,
                signup_extra_content_type=content_type,
                registration_opens=t - timedelta(days=60),
                registration_closes=t + timedelta(days=60),
                work_begins=event.start_time - timedelta(days=1),
                work_ends=event.end_time + timedelta(days=1),
                contact_email='*****@*****.**',
                monitor_email='*****@*****.**',
            )
        )

        labour_event_meta.create_groups()

        return labour_event_meta, created

    @classmethod
    def get_or_create_groups(cls, event, job_categories_or_suffixes):
        suffixes = [
            jc_or_suffix if isinstance(jc_or_suffix, str) else jc_or_suffix.slug
            for jc_or_suffix in job_categories_or_suffixes
        ]

        groups = super(LabourEventMeta, cls).get_or_create_groups(event, suffixes)

        if 'mailings' in settings.INSTALLED_APPS:
            from mailings.models import RecipientGroup
            from .job_category import JobCategory
            from .personnel_class import PersonnelClass

            for jc_or_suffix, group in zip(job_categories_or_suffixes, groups):
                if isinstance(jc_or_suffix, JobCategory):
                    verbose_name = jc_or_suffix.name
                    job_category = jc_or_suffix
                    personnel_class = None
                elif isinstance(jc_or_suffix, PersonnelClass):
                    verbose_name = jc_or_suffix.name
                    job_category = None
                    personnel_class = jc_or_suffix
                else:
                    verbose_name = GROUP_VERBOSE_NAMES_BY_SUFFIX[jc_or_suffix]
                    job_category = None
                    personnel_class = None

                RecipientGroup.objects.get_or_create(
                    event=event,
                    app_label='labour',
                    group=group,
                    defaults=dict(
                        job_category=job_category,
                        personnel_class=personnel_class,
                        verbose_name=verbose_name,
                    ),
                )

        return groups

    def create_groups_async(self):
        if 'background_tasks' in settings.INSTALLED_APPS:
            from ..tasks import labour_event_meta_create_groups
            labour_event_meta_create_groups.delay(self.pk)
        else:
            self.create_groups()

    def create_groups(self):
        from .job_category import JobCategory
        from .personnel_class import PersonnelClass

        job_categories_or_suffixes = list(SIGNUP_STATE_GROUPS)
        job_categories_or_suffixes.extend(JobCategory.objects.filter(event=self.event))
        job_categories_or_suffixes.extend(PersonnelClass.objects.filter(event=self.event, app_label='labour'))
        return LabourEventMeta.get_or_create_groups(self.event, job_categories_or_suffixes)

    @property
    def is_registration_open(self):
        return is_within_period(self.registration_opens, self.registration_closes)

    is_public = alias_property('is_registration_open')

    def publish(self):
        """
        Used by the start/stop signup period view to start the signup period. Returns True
        if the user needs to be warned about a certain corner case where information was lost.
        """
        warn = False

        if self.public_until and self.public_until <= now():
            self.public_until = None
            warn = True

        self.public_from = now()
        self.save()

        return warn

    def unpublish(self):
        """
        Used by the start/stop signup period view to end the signup period. We prefer setting
        public_until to clearing public_from because this causes less information loss.
        """
        self.public_until = now()
        self.save()

    def is_person_signed_up(self, person):
        return self.event.signup_set.filter(person=person).exists()

    def get_signup_for_person(self, person):
        from .signup import Signup

        try:
            return self.event.signup_set.get(person=person)
        except Signup.DoesNotExist:
            return Signup(person=person, event=self.event)

    @property
    def work_hours(self):
        return full_hours_between(self.work_begins, self.work_ends)

    @property
    def applicants_group(self):
        return self.get_group('applicants')

    @property
    def accepted_group(self):
        return self.get_group('accepted')

    @property
    def finished_group(self):
        return self.get_group('finished')

    @property
    def rejected_group(self):
        return self.get_group('rejected')