Beispiel #1
0
class Batch(models.Model):
    event = models.ForeignKey('core.Event',
                              on_delete=models.CASCADE,
                              related_name='badge_batch_set')

    personnel_class = models.ForeignKey('labour.PersonnelClass',
                                        on_delete=models.CASCADE,
                                        null=True,
                                        blank=True)

    created_at = models.DateTimeField(auto_now_add=True,
                                      verbose_name=_('Created at'))
    updated_at = models.DateTimeField(auto_now=True,
                                      verbose_name=_('Updated at'))
    printed_at = models.DateTimeField(null=True, blank=True)

    is_printed = time_bool_property('printed_at')

    @classmethod
    def create(cls,
               event,
               personnel_class=None,
               max_items=100,
               moon_rune_policy='dontcare'):
        from .badge import Badge

        if personnel_class is not None:
            assert personnel_class.event == event
            badges = Badge.objects.filter(personnel_class=personnel_class)
        else:
            badges = Badge.objects.filter(personnel_class__event=event)

        if moon_rune_policy == 'onlyinclude':
            fields = Badge.get_csv_fields(event)
            test = lambda badge: contains_moon_runes(
                badge.get_printable_text(fields))
        elif moon_rune_policy == 'exclude':
            fields = Badge.get_csv_fields(event)
            test = lambda badge: not contains_moon_runes(
                badge.get_printable_text(fields))
        elif moon_rune_policy == 'dontcare':
            test = lambda badge: True
        else:
            raise NotImplementedError(moon_rune_policy)

        with transaction.atomic():
            batch = cls(personnel_class=personnel_class, event=event)
            batch.save()

            badges = badges.filter(
                **BADGE_ELIGIBLE_FOR_BATCHING).order_by('created_at')
            num_selected_badges = 0

            for badge in badges:
                if test(badge):
                    badge.batch = batch
                    badge.save()

                    num_selected_badges += 1
                    if max_items is not None and num_selected_badges >= max_items:
                        break

        return batch

    def confirm(self):
        self.printed_at = now()
        self.save()

    def cancel(self):
        self.badges.update(batch=None)
        self.delete()

    def can_cancel(self):
        return self.printed_at is not None

    def can_confirm(self):
        return self.printed_at is not None

    def __str__(self):
        return _("Batch %(batch_number)s") % dict(batch_number=self.pk)

    def admin_get_number(self):
        return text_type(self)

    admin_get_number.short_description = _('Batch number')
    admin_get_number.admin_order_field = 'id'

    def admin_get_num_badges(self):
        return self.badges.count()

    admin_get_num_badges.short_description = _('Number of badges')

    class Meta:
        verbose_name = _('Batch')
        verbose_name_plural = _('Batches')
Beispiel #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
Beispiel #3
0
class Badge(models.Model, CsvExportMixin):
    person = models.ForeignKey(
        'core.Person',
        null=True,
        blank=True,
        verbose_name=_('Person'),
    )

    personnel_class = models.ForeignKey(
        'labour.PersonnelClass',
        verbose_name=_('Personnel class'),
    )

    printed_separately_at = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_('Printed separately at'),
    )

    revoked_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        related_name='badges_revoked',
        verbose_name=_('Revoked by'),
    )
    revoked_at = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_('Revoked at'),
    )

    arrived_at = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_('Arrived at'),
    )

    first_name = models.CharField(
        blank=True,
        max_length=1023,
        verbose_name=_('First name'),
    )
    is_first_name_visible = models.BooleanField(
        default=True,
        verbose_name=_('Is first name visible'),
    )

    surname = models.CharField(
        blank=True,
        max_length=1023,
        verbose_name=_('Surname'),
    )
    is_surname_visible = models.BooleanField(
        default=True,
        verbose_name=_('Is surname visible'),
    )

    nick = models.CharField(
        blank=True,
        max_length=1023,
        verbose_name=_('Nick name'),
        help_text=
        _('If you only have a single piece of information to print on the badge, use this field.'
          ),
    )
    is_nick_visible = models.BooleanField(
        default=True,
        verbose_name=_('Is nick visible'),
    )

    job_title = models.CharField(
        max_length=63,
        blank=True,
        default='',
        verbose_name=_('Job title'),
        help_text=_('Please stay civil with the job title field.'),
    )

    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        related_name='badges_created',
        verbose_name=_('Created by'),
    )
    created_at = models.DateTimeField(
        auto_now_add=True,
        verbose_name=_('Created at'),
    )
    updated_at = models.DateTimeField(
        auto_now=True,
        verbose_name=_('Updated at'),
    )

    batch = models.ForeignKey(
        'badges.Batch',
        null=True,
        blank=True,
        db_index=True,
        verbose_name=_('Printing batch'),
        on_delete=models.SET_NULL,
    )

    is_revoked = time_bool_property('revoked_at')
    is_printed = time_bool_property('printed_at')
    is_printed_separately = time_bool_property('printed_separately_at')
    is_arrived = time_bool_property('arrived_at')

    @property
    def printed_at(self):
        if self.printed_separately_at:
            return self.printed_separately_at
        elif self.batch:
            return self.batch.printed_at
        else:
            return None

    @property
    def formatted_printed_at(self):
        # XXX not really "formatted"
        return self.printed_at if self.printed_at is not None else ''

    @classmethod
    def get_or_create_dummy(cls):
        from core.models import Person
        from labour.models import PersonnelClass

        person, unused = Person.get_or_create_dummy()
        personnel_class, unused = PersonnelClass.get_or_create_dummy()

        return cls.objects.get_or_create(
            person=person,
            personnel_class=personnel_class,
        )

    @classmethod
    def ensure(cls, event, person):
        """
        Makes sure the person has a badge of the correct class and up-to-date information for a given event.
        """

        from badges.utils import default_badge_factory

        assert person is not None

        with transaction.atomic():
            try:
                existing_badge = cls.objects.get(
                    personnel_class__event=event,
                    person=person,
                    revoked_at__isnull=True,
                )
            except cls.DoesNotExist:
                existing_badge = None

            expected_badge_opts = default_badge_factory(event=event,
                                                        person=person)

            if existing_badge:
                # There is an existing un-revoked badge. Check that its information is correct.
                if any(
                        getattr(existing_badge, key) != value
                        for key, value in expected_badge_opts.items()):
                    existing_badge.revoke()
                else:
                    return existing_badge, False

            if expected_badge_opts.get('personnel_class') is None:
                # They should not have a badge.
                return None, False

            badge_opts = dict(expected_badge_opts, person=person)

            badge = cls(**badge_opts)
            badge.save()

            return badge, True

    @classmethod
    def get_csv_fields(cls, event):
        from labour.models import PersonnelClass

        meta = event.badges_event_meta
        if meta.badge_layout == 'trad':
            # Chief Technology Officer
            # Santtu Pajukanta
            # Japsu
            return [
                (cls, 'personnel_class_name'),
                (BadgePrivacyAdapter, 'surname'),
                (BadgePrivacyAdapter, 'first_name'),
                (BadgePrivacyAdapter, 'nick'),
                (cls, 'job_title'),
            ]
        elif meta.badge_layout == 'nick':
            # JAPSU
            # Santtu Pajukanta
            # Chief Technology Officer
            # -OR-
            # SANTTU
            # Pajukanta
            # Chief Technology Officer
            return [
                (cls, 'personnel_class_name'),
                (BadgePrivacyAdapter, 'nick_or_first_name'),
                (BadgePrivacyAdapter, 'surname_or_full_name'),
                (cls, 'job_title'),
            ]
        else:
            raise NotImplementedError(meta.badge_layout)

    def get_csv_related(self):
        from core.models import Person

        return {
            BadgePrivacyAdapter: BadgePrivacyAdapter(self),
        }

    def get_name_fields(self):
        return [
            (self.surname.strip(), self.is_surname_visible),
            (self.first_name.strip(), self.is_first_name_visible),
            (self.nick.strip(), self.is_nick_visible),
        ]

    @property
    def personnel_class_name(self):
        return self.personnel_class.name if self.personnel_class else ''

    @property
    def event(self):
        return self.personnel_class.event

    @property
    def meta(self):
        return self.event.badges_event_meta

    @property
    def signup(self):
        from labour.models import Signup

        if self.person is None:
            return None

        return Signup.objects.filter(event=self.event,
                                     person=self.person).first()

    @property
    def signup_extra(self):
        if not hasattr(self, '_signup_extra'):
            if self.person_id is None or self.event.labour_event_meta is None:
                self._signup_extra = None
            else:
                SignupExtra = self.event.labour_event_meta.signup_extra_model

                try:
                    self._signup_extra = SignupExtra.get_for_event_and_person(
                        self.event, self.person)
                except SignupExtra.DoesNotExist:
                    self._signup_extra = None

        return self._signup_extra

    @property
    def event_name(self):
        return self.personnel_class.event.name if self.personnel_class else ''

    def get_printable_text(self, fields):
        return '\n'.join(
            str(value) for value in self.get_csv_row(self.event, fields,
                                                     'comma_separated'))

    def to_html_print(self):
        def format_name_field(value, is_visible):
            if is_visible:
                return '<strong>{value}</strong>'.format(value=escape(value))
            else:
                return escape(value)

        vars = dict(
            surname=format_name_field(self.surname.strip(),
                                      self.is_surname_visible),
            first_name=format_name_field(self.first_name.strip(),
                                         self.is_first_name_visible),
            nick=format_name_field(self.nick.strip(), self.is_nick_visible),
        )

        if self.nick:
            return "{surname}, {first_name}, {nick}".format(**vars)
        else:
            return "{surname}, {first_name}".format(**vars)

    def revoke(self, user=None):
        """
        Revoke the badge.

        When a badge that is not yet assigned to a batch or printed separately is revoked, it is
        removed altogether.

        When a badge that is already assigned to a batch or printed separately is revoked, it will be
        marked as such but not removed, because it needs to be manually removed from distribution.

        Note that the batch does not need to be marked as printed yet for a badge to stay around revoked,
        because a batch that is already created but not yet printed may have been downloaded as Excel
        already. A Batch should never change after being created.
        """
        assert not self.is_revoked

        if self.is_printed_separately or self.batch:
            self.is_revoked = True
            self.revoked_by = user
            self.save()
            return self
        else:
            self.delete()
            return None

    def unrevoke(self):
        assert self.is_revoked
        self.is_revoked = False
        self.revoked_by = None
        self.save()
        return self

    def admin_get_full_name(self):
        if self.nick:
            return '{self.first_name} "{self.nick}" {self.surname}'.format(
                self=self)
        else:
            return '{self.first_name} {self.surname}'.format(self=self)

    admin_get_full_name.short_description = _('Name')
    admin_get_full_name.admin_order_field = ('surname', 'first_name', 'nick')

    def __str__(self):
        return "{person_name} ({personnel_class_name}, {event_name})".format(
            person_name=self.admin_get_full_name(),
            personnel_class_name=self.personnel_class_name,
            event_name=self.event_name,
        )