Exemplo n.º 1
0
class DeliveryNotePaymentWorkflowMixin(object):

    TRANSITION_TARGETS = {
        'prepare_for_shipping': _("Prepare for Shipping"),
    }
    _manual_payment_transitions = TRANSITION_TARGETS.keys()

    def __init__(self, *args, **kwargs):
        if not isinstance(self, BaseOrder):
            raise ImproperlyConfigured("class 'DeliveryNotePaymentWorkflowMixin' is not of type 'BaseOrder'")
        CancelOrderWorkflowMixin.CANCELABLE_SOURCES.update(self._manual_payment_transitions)
        super().__init__(*args, **kwargs)

    @transition(field='status', source=['created', 'no_payment_required'],
                target=RETURN_VALUE('payment_confirmed'))
    def prepare_for_shipping(self):
        """
        Acknowledge the payment. Create Payment-line
        """
        op = OrderPayment.objects.create(
            order=self,
            amount=self._total,
            transaction_id='auto generated',
            payment_method='delivery-note'
        )
        op.save()
Exemplo n.º 2
0
class CancelOrderWorkflowMixin(object):
    """
    Add this class to `settings.SHOP_ORDER_WORKFLOWS` to mix it into your `OrderModel`.
    It adds all the methods required for state transitions, to cancel an order.
    """
    CANCELABLE_SOURCES = {
        'new', 'created', 'payment_confirmed', 'payment_declined',
        'ready_for_delivery'
    }
    TRANSITION_TARGETS = {
        'refund_payment': _("Refund payment"),
        'order_canceled': _("Order Canceled"),
    }

    def cancelable(self):
        return super(
            CancelOrderWorkflowMixin,
            self).cancelable() or self.status in self.CANCELABLE_SOURCES

    @transition(field='status',
                target=RETURN_VALUE(*TRANSITION_TARGETS.keys()),
                conditions=[cancelable],
                custom=dict(admin=True, button_name=_("Cancel Order")))
    def cancel_order(self):
        """
        Signals that an Order shall be canceled.
        """
        self.withdraw_from_delivery()
        if self.amount_paid:
            self.refund_payment()
        return 'refund_payment' if self.amount_paid else 'order_canceled'
Exemplo n.º 3
0
class MultiResultTest(models.Model):
    state = FSMField(default='new')

    @transition(
        field=state,
        source='new',
        target=RETURN_VALUE('for_moderators', 'published'))
    def publish(self, is_public=False):
        return 'published' if is_public else 'for_moderators'

    @transition(
        field=state,
        source='for_moderators',
        target=GET_STATE(
            lambda self, allowed: 'published' if allowed else 'rejected',
            states=['published', 'rejected']
        )
    )
    def moderate(self, allowed):
        pass

    class Meta:
        app_label = 'testapp'
Exemplo n.º 4
0
class ApplicationSubmission(
        WorkflowHelpers,
        BaseStreamForm,
        AccessFormData,
        AbstractFormSubmission,
        metaclass=ApplicationSubmissionMetaclass,
):
    form_data = JSONField(encoder=StreamFieldDataEncoder)
    form_fields = StreamField(ApplicationCustomFormFieldsBlock())
    page = models.ForeignKey('wagtailcore.Page', on_delete=models.PROTECT)
    round = models.ForeignKey('wagtailcore.Page',
                              on_delete=models.PROTECT,
                              related_name='submissions',
                              null=True)
    lead = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        limit_choices_to=LIMIT_TO_STAFF,
        related_name='submission_lead',
        on_delete=models.PROTECT,
    )
    next = models.OneToOneField('self',
                                on_delete=models.CASCADE,
                                related_name='previous',
                                null=True)
    reviewers = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name='submissions_reviewer',
        blank=True,
        through='AssignedReviewers',
    )
    partners = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name='submissions_partner',
        limit_choices_to=LIMIT_TO_PARTNERS,
        blank=True,
    )
    meta_terms = models.ManyToManyField(
        MetaTerm,
        related_name='submissions',
        blank=True,
    )
    flags = GenericRelation(
        Flag,
        content_type_field='target_content_type',
        object_id_field='target_object_id',
        related_query_name='submission',
    )
    activities = GenericRelation(
        'activity.Activity',
        content_type_field='source_content_type',
        object_id_field='source_object_id',
        related_query_name='submission',
    )
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             on_delete=models.SET_NULL,
                             null=True)
    search_data = models.TextField()

    # Workflow inherited from WorkflowHelpers
    status = FSMField(default=INITIAL_STATE, protected=True)

    screening_status = models.ForeignKey(
        'funds.ScreeningStatus',
        related_name='+',
        on_delete=models.SET_NULL,
        verbose_name='screening status',
        null=True,
    )

    is_draft = False

    live_revision = models.OneToOneField(
        'ApplicationRevision',
        on_delete=models.CASCADE,
        related_name='live',
        null=True,
        editable=False,
    )
    draft_revision = models.OneToOneField(
        'ApplicationRevision',
        on_delete=models.CASCADE,
        related_name='draft',
        null=True,
        editable=False,
    )

    # Meta: used for migration purposes only
    drupal_id = models.IntegerField(null=True, blank=True, editable=False)

    objects = ApplicationSubmissionQueryset.as_manager()

    def not_progressed(self):
        return not self.next

    @transition(
        status,
        source='*',
        target=RETURN_VALUE(INITIAL_STATE, 'draft_proposal',
                            'invited_to_proposal'),
        permission=make_permission_check({UserPermissions.ADMIN}),
    )
    def restart_stage(self, **kwargs):
        """
        If running form the console please include your user using the kwarg "by"

        u = User.objects.get(email="<*****@*****.**>")
        for a in ApplicationSubmission.objects.all():
            a.restart_stage(by=u)
            a.save()
        """
        if hasattr(self, 'previous'):
            return 'draft_proposal'
        elif self.next:
            return 'invited_to_proposal'
        return INITIAL_STATE

    @property
    def stage(self):
        return self.phase.stage

    @property
    def phase(self):
        return self.workflow.get(self.status)

    @property
    def active(self):
        return self.status in active_statuses

    def ensure_user_has_account(self):
        if self.user and self.user.is_authenticated:
            self.form_data['email'] = self.user.email
            self.form_data['full_name'] = self.user.get_full_name()
        else:
            # Rely on the form having the following must include fields (see blocks.py)
            email = self.form_data.get('email')
            full_name = self.form_data.get('full_name')

            User = get_user_model()
            if 'skip_account_creation_notification' in self.form_data:
                self.form_data.pop('skip_account_creation_notification', None)
                self.user, _ = User.objects.get_or_create(
                    email=email, defaults={'full_name': full_name})
            else:
                self.user, _ = User.objects.get_or_create_and_notify(
                    email=email,
                    site=self.page.get_site(),
                    defaults={'full_name': full_name})

    def get_from_parent(self, attribute):
        try:

            return getattr(self.round.specific, attribute)
        except AttributeError:
            # We are a lab submission
            return getattr(self.page.specific, attribute)

    @property
    def is_determination_form_attached(self):
        """
        We use old django determination forms but now as we are moving
        to streamfield determination forms which can be created and attached
        to funds in admin.

        This method checks if there are new determination forms attached to the
        submission or we would still use the old determination forms for backward
        compatibility.
        """
        return self.get_from_parent('determination_forms').count() > 0

    def progress_application(self, **kwargs):
        target = None
        for phase in STAGE_CHANGE_ACTIONS:
            transition = self.get_transition(phase)
            if can_proceed(transition):
                # We convert to dict as not concerned about transitions from the first phase
                # See note in workflow.py
                target = dict(PHASES)[phase].stage
        if not target:
            raise ValueError('Incorrect State for transition')

        submission_in_db = ApplicationSubmission.objects.get(id=self.id)
        prev_meta_terms = submission_in_db.meta_terms.all()

        self.id = None
        proposal_form = kwargs.get('proposal_form')
        proposal_form = int(proposal_form) if proposal_form else 0
        self.form_fields = self.get_from_parent('get_defined_fields')(
            target, form_index=proposal_form)

        self.live_revision = None
        self.draft_revision = None
        self.save()
        self.meta_terms.set(prev_meta_terms)

        submission_in_db.next = self
        submission_in_db.save()

    def new_data(self, data):
        self.is_draft = False
        self.form_data = data
        return self

    def from_draft(self):
        self.is_draft = True
        self.form_data = self.deserialised_data(self,
                                                self.draft_revision.form_data,
                                                self.form_fields)
        return self

    def create_revision(self, draft=False, force=False, by=None, **kwargs):
        # Will return True/False if the revision was created or not
        self.clean_submission()
        current_submission = ApplicationSubmission.objects.get(id=self.id)
        current_data = current_submission.form_data
        if current_data != self.form_data or force:
            if self.live_revision == self.draft_revision:
                revision = ApplicationRevision.objects.create(
                    submission=self, form_data=self.form_data, author=by)
            else:
                revision = self.draft_revision
                revision.form_data = self.form_data
                revision.author = by
                revision.save()

            if draft:
                self.form_data = current_submission.form_data
            else:
                self.live_revision = revision
                self.search_data = ' '.join(self.prepare_search_values())

            self.draft_revision = revision
            self.save(skip_custom=True)
            return revision
        return None

    def clean_submission(self):
        self.process_form_data()
        self.ensure_user_has_account()
        self.process_file_data(self.form_data)

    def process_form_data(self):
        for field_name, field_id in self.named_blocks.items():
            response = self.form_data.pop(field_id, None)
            if response:
                self.form_data[field_name] = response

    def save(self, *args, update_fields=list(), skip_custom=False, **kwargs):
        if update_fields and 'form_data' not in update_fields:
            # We don't want to use this approach if the user is sending data
            return super().save(*args, update_fields=update_fields, **kwargs)
        elif skip_custom:
            return super().save(*args, **kwargs)

        if self.is_draft:
            raise ValueError('Cannot save with draft data')

        creating = not self.id

        if creating:
            # We are creating the object default to first stage
            self.workflow_name = self.get_from_parent('workflow_name')
            # Copy extra relevant information to the child
            self.lead = self.get_from_parent('lead')

            # We need the submission id to correctly save the files
            files = self.extract_files()

        self.clean_submission()

        # add a denormed version of the answer for searching
        self.search_data = ' '.join(self.prepare_search_values())

        super().save(*args, **kwargs)

        if creating:
            self.process_file_data(files)
            for reviewer in self.get_from_parent('reviewers').all():
                AssignedReviewers.objects.get_or_create_for_user(
                    reviewer=reviewer, submission=self)
            first_revision = ApplicationRevision.objects.create(
                submission=self,
                form_data=self.form_data,
                author=self.user,
            )
            self.live_revision = first_revision
            self.draft_revision = first_revision
            self.save()

    @property
    def has_all_reviewer_roles_assigned(self):
        return self.assigned.with_roles().count(
        ) == ReviewerRole.objects.count()

    @property
    def community_review(self):
        return self.status in COMMUNITY_REVIEW_PHASES

    @property
    def missing_reviewers(self):
        reviewers_submitted = self.assigned.reviewed().values('reviewer')
        reviewers = self.reviewers.exclude(id__in=reviewers_submitted)
        return reviewers

    @property
    def staff_not_reviewed(self):
        return self.missing_reviewers.staff()

    @property
    def reviewers_not_reviewed(self):
        return self.missing_reviewers.reviewers().exclude(
            id__in=self.staff_not_reviewed)

    def reviewed_by(self, user):
        return self.assigned.reviewed().filter(reviewer=user).exists()

    def flagged_by(self, user):
        return self.flags.filter(user=user, type=Flag.USER).exists()

    @property
    def flagged_staff(self):
        return self.flags.filter(type=Flag.STAFF).exists()

    def has_permission_to_review(self, user):
        if user.is_apply_staff:
            return True

        if user in self.reviewers_not_reviewed:
            return True

        if user.is_community_reviewer and self.user != user and self.community_review and not self.reviewed_by(
                user):
            return True

        return False

    def can_review(self, user):
        if self.reviewed_by(user):
            return False

        return self.has_permission_to_review(user)

    def prepare_search_values(self):
        for field_id in self.question_field_ids:
            field = self.field(field_id)
            data = self.data(field_id)
            value = field.block.get_searchable_content(field.value, data)
            if value:
                if isinstance(value, list):
                    yield ', '.join(value)
                else:
                    yield value

        # Add named fields into the search index
        for field in ['full_name', 'email', 'title']:
            yield getattr(self, field)

    def get_absolute_url(self):
        return reverse('funds:submissions:detail', args=(self.id, ))

    def __str__(self):
        return f'{self.title} from {self.full_name} for {self.page.title}'

    def __repr__(self):
        return f'<{self.__class__.__name__}: {self.user}, {self.round}, {self.page}>'

    @property
    def ready_for_determination(self):
        return self.status in PHASES_MAPPING['ready-for-determination'][
            'statuses']

    @property
    def accepted_for_funding(self):
        accepted = self.status in PHASES_MAPPING['accepted']['statuses']
        return self.in_final_stage and accepted

    @property
    def in_final_stage(self):
        stages = self.workflow.stages

        stage_index = stages.index(self.stage)

        # adjust the index since list.index() is zero-based
        adjusted_index = stage_index + 1

        return adjusted_index == len(stages)

    @property
    def in_internal_review_phase(self):
        return self.status in PHASES_MAPPING['internal-review']['statuses']

    @property
    def in_external_review_phase(self):
        return self.status in PHASES_MAPPING['external-review']['statuses']

    @property
    def is_finished(self):
        accepted = self.status in PHASES_MAPPING['accepted']['statuses']
        dismissed = self.status in PHASES_MAPPING['dismissed']['statuses']
        return accepted or dismissed

    # Methods for accessing data on the submission

    def get_data(self):
        # Updated for JSONField - Not used but base get_data will error
        form_data = self.form_data.copy()
        form_data.update({
            'submit_time': self.submit_time,
        })

        return form_data

    # Template methods for metaclass
    def _get_REQUIRED_display(self, name):
        return self.render_answer(name)

    def _get_REQUIRED_value(self, name):
        return self.data(name)
Exemplo n.º 5
0
class Socket(Appliance):
    SERVER_MESSAGES = ["on", "off", "toggle", "reset"]
    APPLIANCE_MESSAGES = ["switched-off", "switched-on"]

    state = FSMField(default="off", protected=True)

    class Meta:
        verbose_name = "Socket"
        verbose_name_plural = "Sockets"

    def __str__(self):
        return f"<Socket {self.mqtt_appliance_topic}>"

    @transition(field=state, source="off", target="request-on")
    def on(self):
        mqtt_appliance_publish.send(__name__,
                                    topic=self.mqtt_appliance_topic,
                                    payload="request-on")

    @transition(field=state, source="on", target="request-off")
    def off(self):
        mqtt_appliance_publish.send(__name__,
                                    topic=self.mqtt_appliance_topic,
                                    payload="request-off")

    @transition(field=state, source="request-on", target="on")
    def switchedon(self):
        return

    @transition(field=state, source="request-off", target="off")
    def switchedoff(self):
        return

    @transition(field=state,
                source=["on", "off"],
                target=RETURN_VALUE("request-on", "request-off"))
    def toggle(self):
        new_state = "request-on" if self.state == "off" else "request-off"
        mqtt_appliance_publish.send(__name__,
                                    topic=self.mqtt_appliance_topic,
                                    payload=new_state)
        return new_state

    @transition(field=state,
                source=["request-on", "request-off"],
                target=RETURN_VALUE("on", "off"))
    def reset(self):
        new_state = "on" if self.state == "request-off" else "off"
        logger.warning(
            f"Resetting {self.mqtt_appliance_topic} state to '{new_state}'")

        mqtt_appliance_publish.send(__name__,
                                    topic=f"{self.mqtt_appliance_topic}/reset",
                                    payload=new_state)

        return new_state

    def handle_message(self, topic, payload):
        """
        Handle both appliance and server messages
        """
        method = getattr(self, payload.replace("-", ""))

        if can_proceed(method):
            method()
            self.save()
            logger.info(f"Socket {self.mqtt_appliance_topic}: {self.state}")
        else:
            logger.warning(f"Lamp {self.mqtt_appliance_topic}: '{payload}'"
                           f" impossible, current state is {self.state}")

    def mqtt_server_message(self, topic, payload):
        """
        Handle server mqtt messages
        """
        payload = payload.lower().strip()

        if payload not in self.SERVER_MESSAGES:
            return

        self.handle_message(topic, payload)

    def mqtt_appliance_message(self, topic, payload):
        """
        Handle appliance mqtt messages
        """
        payload = payload.lower().strip()

        if payload not in self.APPLIANCE_MESSAGES:
            return

        self.handle_message(topic, payload)
Exemplo n.º 6
0
class Roller(Appliance):
    SERVER_MESSAGES = ["open", "close"]
    APPLIANCE_MESSAGES = ["opened", "closed"]

    state = FSMField(default="closed", protected=True)

    class Meta:
        verbose_name = "Roller"
        verbose_name_plural = "Rollers"

    def __str__(self):
        return f"<Roller {self.mqtt_appliance_topic}>"

    @transition(field=state, source="closed", target="request-open")
    def open(self):
        mqtt_appliance_publish.send(
            __name__, topic=self.mqtt_appliance_topic, payload="request-open"
        )

    @transition(field=state, source="opened", target="request-close")
    def close(self):
        mqtt_appliance_publish.send(
            __name__, topic=self.mqtt_appliance_topic, payload="request-close"
        )

    @transition(field=state, source="request-open", target="opened")
    def opened(self):
        return

    @transition(field=state, source="request-close", target="closed")
    def closed(self):
        return

    @transition(
        field=state,
        source=["opened", "closed"],
        target=RETURN_VALUE("request-open", "request-close"),
    )
    def toggle(self):
        new_state = "request-open" if self.state == "off" else "request-close"
        mqtt_appliance_publish.send(__name__, topic=self.mqtt_appliance_topic, payload=new_state)
        return new_state

    @transition(
        field=state,
        source=["request-open", "request-close"],
        target=RETURN_VALUE("opened", "closed"),
    )
    def reset(self):
        new_state = "opened" if self.state == "request-close" else "closed"
        logger.warning(f"Resetting {self.mqtt_appliance_topic} state to '{new_state}'")

        mqtt_appliance_publish.send(
            __name__, topic=f"{self.mqtt_appliance_topic}/reset", payload=new_state
        )

        return new_state

    def handle_message(self, topic, payload):
        """
        Handle both appliance and server messages
        """
        method = getattr(self, payload.replace("-", ""))

        if can_proceed(method):
            method()
            self.save()
            logger.info(f"Roller {self.mqtt_appliance_topic}: {self.state}")
        else:
            logger.warning(
                f"Roller {self.mqtt_appliance_topic} can't "
                f"'{payload}', current state is {self.state}"
            )

    def mqtt_server_message(self, topic, payload):
        """
        Handle server mqtt messages
        """
        payload = payload.lower().strip()

        if payload not in self.SERVER_MESSAGES:
            return

        self.handle_message(topic, payload)

    def mqtt_appliance_message(self, topic, payload):
        """
        Handle appliance mqtt messages
        """
        payload = payload.lower().strip()

        if payload not in self.APPLIANCE_MESSAGES:
            return

        self.handle_message(topic, payload)
Exemplo n.º 7
0
class Application(TimestampModelMixin, models.Model):
    """
    Application workflow:
    1. A resident creates an application with NEW state.

    2. A scheduler can:
        2.1. Approve the application
        The application will become APPROVED. The other new applications
        will become POSTPONED.
        2.2. Reject the application
        The state will be changed to REJECTED.

    3. The resident can:
        3.1. Confirm the application
        The application will become CONFIRMED.
        3.2. Cancel the application
        The application will become CANCELLED if shift was not started
        otherwise become FAILED

    4. The scheduler or the resident can cancel the confirmed application
    The application will become FAILED

    5. The scheduler can complete the application after the resident completes
    the shift in real life
    The application will become COMPLETED.

    When a resident/a scheduler cancels the application, all postponed
    applications become new.
    """
    owner = models.ForeignKey('accounts.Resident',
                              related_name='applications',
                              verbose_name='Owner')
    shift = models.ForeignKey('shifts.Shift',
                              related_name='applications',
                              verbose_name='Shift')
    state = FSMIntegerField(verbose_name='State',
                            default=ApplicationStateEnum.NEW,
                            choices=ApplicationStateEnum.CHOICES)
    objects = ApplicationQuerySet.as_manager()

    class Meta:
        verbose_name = 'Application'
        verbose_name_plural = 'Applications'

    def __str__(self):
        return "{0} by {1}".format(self.shift, self.owner)

    @property
    def messages_count(self):
        if hasattr(self, 'annotated_messages_count'):
            return self.annotated_messages_count
        return self.messages.count()

    @property
    def last_message(self):
        """
        Returns last message for the applications (sorted by date)

        TODO: optimize it. Now it creates N+1 queries
        """
        return self.messages.order_by('date_created').last()

    @transition(field=state,
                source=ApplicationStateEnum.NEW,
                target=ApplicationStateEnum.APPROVED,
                permission=can_scheduler_change_application,
                conditions=[lambda self: not self.shift.is_started])
    def approve(self, data):
        """
        Approves the application and postpone all other
        new applications
        """
        new_applications = self.shift.applications.exclude(pk=self.pk).filter(
            state=ApplicationStateEnum.NEW)

        for application in new_applications:
            application.postpone()
            application.save()

        process_approving(self, data['user'], data['text'])

    @transition(field=state,
                source=ApplicationStateEnum.NEW,
                target=ApplicationStateEnum.POSTPONED,
                custom={
                    'viewset': False,
                    'admin': False
                })
    def postpone(self):
        """
        Postpones the application
        """
        process_postponing(self)

    @transition(field=state,
                source=ApplicationStateEnum.POSTPONED,
                target=ApplicationStateEnum.NEW,
                custom={
                    'viewset': False,
                    'admin': False
                })
    def renew(self):
        """
        Renews the application
        """
        process_renewing(self)

    @transition(field=state,
                source=ApplicationStateEnum.NEW,
                target=ApplicationStateEnum.REJECTED,
                permission=can_scheduler_change_application,
                conditions=[lambda self: not self.shift.is_started])
    def reject(self, data):
        """
        Rejects  the application
        """
        process_rejecting(self, data['user'], data['text'])

    @transition(field=state,
                source=ApplicationStateEnum.APPROVED,
                target=ApplicationStateEnum.CONFIRMED,
                permission=can_resident_change_application,
                conditions=[lambda self: not self.shift.is_started])
    def confirm(self, data):
        """
        Confirms the application
        """
        process_confirming(self, data['user'], data['text'])

    @transition(field=state,
                source=[
                    ApplicationStateEnum.APPROVED,
                    ApplicationStateEnum.CONFIRMED,
                ],
                target=RETURN_VALUE(
                    ApplicationStateEnum.CANCELLED,
                    ApplicationStateEnum.FAILED,
                ),
                permission=can_resident_or_scheduler_change_application)
    def cancel(self, data):
        """
        Cancels the application and renew all postponed applications if
        the shift wasn't started
        """
        process_cancelling(self, data['user'], data['text'])

        if not self.shift.is_started:
            postponed_applications = self.shift.applications.filter(
                state=ApplicationStateEnum.POSTPONED)

            for application in postponed_applications:
                application.renew()
                application.save()

            if self.state == ApplicationStateEnum.APPROVED:
                return ApplicationStateEnum.CANCELLED

        return ApplicationStateEnum.FAILED

    @transition(field=state,
                source=ApplicationStateEnum.CONFIRMED,
                target=ApplicationStateEnum.COMPLETED,
                permission=can_scheduler_change_application,
                conditions=[lambda self: self.shift.is_ended])
    def complete(self, data):
        """
        Completes the application
        """
        process_completing(self, data['user'], data['text'])
Exemplo n.º 8
0
class Referral(models.Model):
    """
    Our main model. Here we modelize what a Referral is in the first place and provide other
    models it can depend on (eg users or attachments).
    """

    URGENCY_1, URGENCY_2, URGENCY_3 = "u1", "u2", "u3"
    URGENCY_CHOICES = (
        (URGENCY_1, _("Urgent — 1 week")),
        (URGENCY_2, _("Extremely urgent — 3 days")),
        (URGENCY_3, _("Absolute emergency — 24 hours")),
    )

    # Generic fields to build up minimal data on any referral
    id = models.AutoField(
        verbose_name=_("id"),
        help_text=_("Primary key for the referral"),
        primary_key=True,
        editable=False,
    )
    created_at = models.DateTimeField(verbose_name=_("created at"),
                                      auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name=_("updated at"),
                                      auto_now=True)

    # Link the referral with the user who is making it
    # Note: this is optional to support both existing referrals before introduction of this field
    # and deleting users later on while keeping their referrals.
    user = models.ForeignKey(
        verbose_name=_("user"),
        help_text=_("User who created the referral"),
        to=get_user_model(),
        on_delete=models.PROTECT,
        related_name="referrals_created",
    )
    # This field is useful when the actual user above is requesting the referral on behalf of
    # a group of persons or of someone else (eg. for a manager or public official)
    requester = models.CharField(
        verbose_name=_("requester"),
        help_text=_(
            "Identity of the person and service requesting the referral"),
        max_length=500,
    )

    # Referral metadata: helpful to quickly sort through referrals
    topic = models.ForeignKey(
        verbose_name=_("topic"),
        help_text=_(
            "Broad topic to help direct the referral to the appropriate office"
        ),
        to=Topic,
        on_delete=models.PROTECT,
    )
    urgency = models.CharField(
        verbose_name=_("urgency"),
        help_text=_("Urgency level. When do you need the referral?"),
        max_length=2,
        choices=URGENCY_CHOICES,
        blank=True,
    )
    urgency_level = models.ForeignKey(
        verbose_name=_("urgency"),
        help_text=_("Urgency level. When is the referral answer needed?"),
        to=ReferralUrgency,
        on_delete=models.PROTECT,
        related_name="+",
        blank=True,
        null=True,
    )
    urgency_explanation = models.TextField(
        verbose_name=_("urgency explanation"),
        help_text=_("Why is this referral urgent?"),
        blank=True,
    )
    state = FSMField(
        verbose_name=_("referral state"),
        help_text=_("Current treatment status for this referral"),
        default=ReferralState.RECEIVED,
        choices=ReferralState.choices,
    )

    # Unit-related information on the referral
    assignees = models.ManyToManyField(
        verbose_name=_("assignees"),
        help_text=_(
            "Partaj users that have been assigned to work on this referral"),
        to=get_user_model(),
        through="ReferralAssignment",
        through_fields=("referral", "assignee"),
        related_name="referrals_assigned",
    )

    # Actual content of the referral request
    question = models.TextField(
        verbose_name=_("question"),
        help_text=_("Question for which you are requesting the referral"),
    )
    context = models.TextField(
        verbose_name=_("context"),
        help_text=_("Explain the facts and context leading to the referral"),
    )
    prior_work = models.TextField(
        verbose_name=_("prior work"),
        help_text=_(
            "What research did you already perform before the referral?"),
    )

    class Meta:
        db_table = "partaj_referral"
        verbose_name = _("referral")

    def __str__(self):
        """Get the string representation of a referral."""
        return f"{self._meta.verbose_name.title()} #{self.id}"

    def get_human_state(self):
        """
        Get the human readable, localized label for the current state of the Referral.
        """
        return ReferralState(self.state).label

    # Add a short description to label the column in the admin site
    get_human_state.short_description = _("state")

    def get_state_class(self):
        """
        Get the correspond class for state colors.
        """
        state_colors = {
            ReferralState.ANSWERED: "green",
            ReferralState.ASSIGNED: "teal",
            ReferralState.CLOSED: "darkgray",
            ReferralState.INCOMPLETE: "red",
            ReferralState.RECEIVED: "blue",
        }

        return state_colors[self.state]

    @transition(
        field=state,
        source=[ReferralState.ASSIGNED, ReferralState.RECEIVED],
        target=ReferralState.ANSWERED,
    )
    def answer(self, content, attachments, created_by):
        """
        Bring an answer to the referral, marking it as donee.
        """
        answer = ReferralAnswer.objects.create(
            content=content,
            created_by=created_by,
            referral=self,
        )
        for file in attachments:
            ReferralAnswerAttachment.objects.create(
                file=file,
                referral_answer=answer,
            )

        ReferralActivity.objects.create(
            actor=created_by,
            verb=ReferralActivityVerb.ANSWERED,
            referral=self,
            item_content_object=answer,
        )
        # Notify the requester by sending them an email
        Mailer.send_referral_answered(
            answer=answer,
            referral=self,
        )

    @transition(
        field=state,
        source=[ReferralState.ASSIGNED, ReferralState.RECEIVED],
        target=ReferralState.ASSIGNED,
    )
    def assign(self, assignee, created_by):
        """
        Assign the referral to one of the unit's members.
        """
        ReferralAssignment.objects.create(
            assignee=assignee,
            created_by=created_by,
            referral=self,
            unit=self.topic.unit,
        )
        ReferralActivity.objects.create(
            actor=created_by,
            verb=ReferralActivityVerb.ASSIGNED,
            referral=self,
            item_content_object=assignee,
        )
        # Notify the assignee by sending them an email
        Mailer.send_referral_assigned(
            referral=self,
            assignee=assignee,
            assigned_by=created_by,
        )

    @transition(
        field=state,
        source=[ReferralState.RECEIVED],
        target=ReferralState.RECEIVED,
    )
    def send(self):
        """
        Send relevant emails for the newly send referral and create the corresponding activity.
        """
        ReferralActivity.objects.create(
            actor=self.user,
            verb=ReferralActivityVerb.CREATED,
            referral=self,
        )
        # Confirm the referral has been sent to the requester by email
        Mailer.send_referral_saved(self)
        # Also alert the organizers for the relevant unit
        Mailer.send_referral_received(self)

    @transition(
        field=state,
        source=[ReferralState.ASSIGNED],
        target=RETURN_VALUE(ReferralState.RECEIVED, ReferralState.ASSIGNED),
    )
    def unassign(self, assignee, created_by):
        """
        Unassign the referral from a currently assigned member.
        """
        ReferralAssignment.objects.filter(assignee=assignee,
                                          referral=self).delete()
        self.refresh_from_db()
        ReferralActivity.objects.create(
            actor=created_by,
            verb=ReferralActivityVerb.UNASSIGNED,
            referral=self,
            item_content_object=assignee,
        )
        # Check the number of remaining assignments on this referral to determine the next state
        assignment_count = ReferralAssignment.objects.filter(
            referral=self).count()
        return (ReferralState.ASSIGNED
                if assignment_count > 0 else ReferralState.RECEIVED)
Exemplo n.º 9
0
class ApplicationSubmission(
        WorkflowHelpers,
        BaseStreamForm,
        AccessFormData,
        AbstractFormSubmission,
        metaclass=ApplicationSubmissionMetaclass,
):
    field_template = 'funds/includes/submission_field.html'

    form_data = JSONField(encoder=StreamFieldDataEncoder)
    form_fields = StreamField(ApplicationCustomFormFieldsBlock())
    page = models.ForeignKey('wagtailcore.Page', on_delete=models.PROTECT)
    round = models.ForeignKey('wagtailcore.Page', on_delete=models.PROTECT, related_name='submissions', null=True)
    lead = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        limit_choices_to=LIMIT_TO_STAFF,
        related_name='submission_lead',
        on_delete=models.PROTECT,
    )
    next = models.OneToOneField('self', on_delete=models.CASCADE, related_name='previous', null=True)
    reviewers = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name='submissions_reviewer',
        limit_choices_to=LIMIT_TO_STAFF_AND_REVIEWERS,
        blank=True,
    )
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
    search_data = models.TextField()

    # Workflow inherited from WorkflowHelpers
    status = FSMField(default=INITIAL_STATE, protected=True)

    is_draft = False

    live_revision = models.OneToOneField(
        'ApplicationRevision',
        on_delete=models.CASCADE,
        related_name='live',
        null=True,
        editable=False,
    )
    draft_revision = models.OneToOneField(
        'ApplicationRevision',
        on_delete=models.CASCADE,
        related_name='draft',
        null=True,
        editable=False,
    )

    # Meta: used for migration purposes only
    drupal_id = models.IntegerField(null=True, blank=True, editable=False)

    objects = ApplicationSubmissionQueryset.as_manager()

    def not_progressed(self):
        return not self.next

    @transition(
        status, source='*',
        target=RETURN_VALUE(INITIAL_STATE, 'draft_proposal', 'invited_to_proposal'),
        permission=make_permission_check({UserPermissions.ADMIN}),
    )
    def restart_stage(self, **kwargs):
        """
        If running form the console please include your user using the kwarg "by"

        u = User.objects.get(email="<*****@*****.**>")
        for a in ApplicationSubmission.objects.all():
            a.restart_stage(by=u)
            a.save()
        """
        if hasattr(self, 'previous'):
            return 'draft_proposal'
        elif self.next:
            return 'invited_to_proposal'
        return INITIAL_STATE

    @property
    def stage(self):
        return self.phase.stage

    @property
    def phase(self):
        return self.workflow.get(self.status)

    @property
    def active(self):
        return self.status in active_statuses

    def ensure_user_has_account(self):
        if self.user and self.user.is_authenticated:
            self.form_data['email'] = self.user.email
            self.form_data['full_name'] = self.user.get_full_name()
        else:
            # Rely on the form having the following must include fields (see blocks.py)
            email = self.form_data.get('email')
            full_name = self.form_data.get('full_name')

            User = get_user_model()
            if 'skip_account_creation_notification' in self.form_data:
                self.form_data.pop('skip_account_creation_notification', None)
                self.user, _ = User.objects.get_or_create(
                    email=email,
                    defaults={'full_name': full_name}
                )
            else:
                self.user, _ = User.objects.get_or_create_and_notify(
                    email=email,
                    site=self.page.get_site(),
                    defaults={'full_name': full_name}
                )

    def get_from_parent(self, attribute):
        try:

            return getattr(self.round.specific, attribute)
        except AttributeError:
            # We are a lab submission
            return getattr(self.page.specific, attribute)

    def progress_application(self, **kwargs):
        target = None
        for phase in STAGE_CHANGE_ACTIONS:
            transition = self.get_transition(phase)
            if can_proceed(transition):
                # We convert to dict as not concerned about transitions from the first phase
                # See note in workflow.py
                target = dict(PHASES)[phase].stage
        if not target:
            raise ValueError('Incorrect State for transition')

        submission_in_db = ApplicationSubmission.objects.get(id=self.id)

        self.id = None
        self.form_fields = self.get_from_parent('get_defined_fields')(target)

        self.live_revision = None
        self.draft_revision = None
        self.save()

        submission_in_db.next = self
        submission_in_db.save()

    def new_data(self, data):
        self.is_draft = False
        self.form_data = data
        return self

    def from_draft(self):
        self.is_draft = True
        self.form_data = self.deserialised_data(self.draft_revision.form_data, self.form_fields)
        return self

    def create_revision(self, draft=False, force=False, by=None, **kwargs):
        # Will return True/False if the revision was created or not
        self.clean_submission()
        current_submission = ApplicationSubmission.objects.get(id=self.id)
        current_data = current_submission.form_data
        if current_data != self.form_data or force:
            if self.live_revision == self.draft_revision:
                revision = ApplicationRevision.objects.create(submission=self, form_data=self.form_data, author=by)
            else:
                revision = self.draft_revision
                revision.form_data = self.form_data
                revision.author = by
                revision.save()

            if draft:
                self.form_data = current_submission.form_data
            else:
                self.live_revision = revision

            self.draft_revision = revision
            self.save()
            return revision
        return None

    def clean_submission(self):
        self.process_form_data()
        self.ensure_user_has_account()
        self.process_file_data(self.form_data)

    def process_form_data(self):
        for field_name, field_id in self.named_blocks.items():
            response = self.form_data.pop(field_id, None)
            if response:
                self.form_data[field_name] = response

    def extract_files(self):
        files = {}
        for field in self.form_fields:
            if isinstance(field.block, UploadableMediaBlock):
                files[field.id] = self.data(field.id) or []
                self.form_data.pop(field.id, None)
        return files

    def process_file_data(self, data):
        for field in self.form_fields:
            if isinstance(field.block, UploadableMediaBlock):
                file = self.process_file(data.get(field.id, []))
                folder = os.path.join('submission', str(self.id), field.id)
                try:
                    file.save(folder)
                except AttributeError:
                    for f in file:
                        f.save(folder)
                self.form_data[field.id] = file

    def save(self, *args, update_fields=list(), **kwargs):
        if update_fields and 'form_data' not in update_fields:
            # We don't want to use this approach if the user is sending data
            return super().save(*args, update_fields=update_fields, **kwargs)

        if self.is_draft:
            raise ValueError('Cannot save with draft data')

        creating = not self.id

        if creating:
            # We are creating the object default to first stage
            self.workflow_name = self.get_from_parent('workflow_name')
            # Copy extra relevant information to the child
            self.lead = self.get_from_parent('lead')

            # We need the submission id to correctly save the files
            files = self.extract_files()

        self.clean_submission()

        # add a denormed version of the answer for searching
        self.search_data = ' '.join(self.prepare_search_values())

        super().save(*args, **kwargs)

        if creating:
            self.process_file_data(files)
            self.reviewers.set(self.get_from_parent('reviewers').all())
            first_revision = ApplicationRevision.objects.create(
                submission=self,
                form_data=self.form_data,
                author=self.user,
            )
            self.live_revision = first_revision
            self.draft_revision = first_revision
            self.save()

    @property
    def missing_reviewers(self):
        return self.reviewers.exclude(id__in=self.reviews.submitted().values('author'))

    @property
    def staff_not_reviewed(self):
        return self.missing_reviewers.staff()

    @property
    def reviewers_not_reviewed(self):
        return self.missing_reviewers.reviewers().exclude(id__in=self.staff_not_reviewed)

    def reviewed_by(self, user):
        return self.reviews.submitted().filter(author=user).exists()

    def has_permission_to_review(self, user):
        if user.is_apply_staff:
            return True

        if user in self.reviewers_not_reviewed:
            return True

        return False

    def can_review(self, user):
        if self.reviewed_by(user):
            return False

        return self.has_permission_to_review(user)

    def prepare_search_values(self):
        for field_id in self.question_field_ids:
            field = self.field(field_id)
            data = self.data(field_id)
            value = field.block.get_searchable_content(field.value, data)
            if value:
                if isinstance(value, list):
                    yield ', '.join(value)
                else:
                    yield value

        # Add named fields into the search index
        for field in ['full_name', 'email', 'title']:
            yield getattr(self, field)

    def get_absolute_url(self):
        return reverse('funds:submissions:detail', args=(self.id,))

    def __str__(self):
        return f'{self.title} from {self.full_name} for {self.page.title}'

    def __repr__(self):
        return f'<{self.__class__.__name__}: {self.user}, {self.round}, {self.page}>'

    # Methods for accessing data on the submission

    def get_data(self):
        # Updated for JSONField - Not used but base get_data will error
        form_data = self.form_data.copy()
        form_data.update({
            'submit_time': self.submit_time,
        })

        return form_data

    # Template methods for metaclass
    def _get_REQUIRED_display(self, name):
        return self.render_answer(name)

    def _get_REQUIRED_value(self, name):
        return self.form_data[name]
Exemplo n.º 10
0
class Referral(models.Model):
    """
    Our main model. Here we modelize what a Referral is in the first place and provide other
    models it can depend on (eg users or attachments).
    """

    URGENCY_1, URGENCY_2, URGENCY_3 = "u1", "u2", "u3"
    URGENCY_CHOICES = (
        (URGENCY_1, _("Urgent — 1 week")),
        (URGENCY_2, _("Extremely urgent — 3 days")),
        (URGENCY_3, _("Absolute emergency — 24 hours")),
    )

    # Generic fields to build up minimal data on any referral
    id = models.AutoField(
        verbose_name=_("id"),
        help_text=_("Primary key for the referral"),
        primary_key=True,
        editable=False,
    )
    created_at = models.DateTimeField(verbose_name=_("created at"),
                                      auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name=_("updated at"),
                                      auto_now=True)
    sent_at = models.DateTimeField(
        verbose_name=_("sent at"),
        blank=True,
        null=True,
    )

    # Link the referral with the user who is making it
    # Note: this is optional to support both existing referrals before introduction of this field
    # and deleting users later on while keeping their referrals.
    user = models.ForeignKey(
        verbose_name=_("user"),
        help_text=_("User who created the referral"),
        to=get_user_model(),
        on_delete=models.PROTECT,
        related_name="referrals_created",
        blank=True,
        null=True,
    )
    # Link the referral with the users who are identified as the requesters
    users = models.ManyToManyField(
        verbose_name=_("users"),
        help_text=_(
            "Users who are registered as requesters for this referral"),
        to=get_user_model(),
        through="ReferralUserLink",
        through_fields=("referral", "user"),
        related_name="referrals_requested",
        blank=True,
        null=True,
    )

    # Referral metadata: helpful to quickly sort through referrals
    topic = models.ForeignKey(
        verbose_name=_("topic"),
        help_text=_(
            "Broad topic to help direct the referral to the appropriate office"
        ),
        to=Topic,
        on_delete=models.PROTECT,
        blank=True,
        null=True,
    )
    urgency = models.CharField(
        verbose_name=_("urgency"),
        help_text=_("Urgency level. When do you need the referral?"),
        max_length=2,
        choices=URGENCY_CHOICES,
        blank=True,
        null=True,
    )
    urgency_level = models.ForeignKey(
        verbose_name=_("urgency"),
        help_text=_("Urgency level. When is the referral answer needed?"),
        to="ReferralUrgency",
        on_delete=models.PROTECT,
        related_name="+",
        blank=True,
        null=True,
    )

    urgency_explanation = models.TextField(
        verbose_name=_("urgency explanation"),
        help_text=_("Why is this referral urgent?"),
        blank=True,
        null=True,
    )

    state = FSMField(
        verbose_name=_("referral state"),
        help_text=_("Current treatment status for this referral"),
        default=ReferralState.DRAFT,
        choices=ReferralState.choices,
    )

    # Unit-related information on the referral
    units = models.ManyToManyField(
        verbose_name=_("units"),
        help_text=_("Partaj units that have been assigned to this referral"),
        to="Unit",
        through="ReferralUnitAssignment",
        through_fields=("referral", "unit"),
        related_name="referrals_assigned",
        blank=True,
        null=True,
    )
    assignees = models.ManyToManyField(
        verbose_name=_("assignees"),
        help_text=_(
            "Partaj users that have been assigned to work on this referral"),
        to=get_user_model(),
        through="ReferralAssignment",
        through_fields=("referral", "assignee"),
        related_name="referrals_assigned",
        blank=True,
        null=True,
    )

    # Actual content of the referral request
    object = models.CharField(
        verbose_name=_("object"),
        help_text=_("Brief sentence describing the object of the referral"),
        max_length=60,
        blank=True,
        null=True,
    )
    question = models.TextField(
        verbose_name=_("question"),
        help_text=_("Question for which you are requesting the referral"),
        blank=True,
        null=True,
    )
    context = models.TextField(
        verbose_name=_("context"),
        help_text=_("Explain the facts and context leading to the referral"),
        blank=True,
        null=True,
    )
    prior_work = models.TextField(
        verbose_name=_("prior work"),
        help_text=_(
            "What research did you already perform before the referral?"),
        blank=True,
        null=True,
    )

    class Meta:
        db_table = "partaj_referral"
        verbose_name = _("referral")

    def __str__(self):
        """Get the string representation of a referral."""
        return f"{self._meta.verbose_name.title()} #{self.id}"

    def save(self, *args, **kwargs):
        """
        Override the default save method to update the Elasticsearch entry for the
        referral whenever it is updated.
        """
        super().save(*args, **kwargs)
        # There is a necessary circular dependency between the referral indexer and
        # the referral model (and models in general)
        # We handled it by importing the indexer only at the point we need it here.
        # pylint: disable=import-outside-toplevel
        from ..indexers import ReferralsIndexer

        ReferralsIndexer.update_referral_document(self)

    def get_human_state(self):
        """
        Get the human readable, localized label for the current state of the Referral.
        """
        return ReferralState(self.state).label

    # Add a short description to label the column in the admin site
    get_human_state.short_description = _("state")

    def get_state_class(self):
        """
        Get the correspond class for state colors.
        """
        state_colors = {
            ReferralState.ANSWERED: "green",
            ReferralState.ASSIGNED: "teal",
            ReferralState.CLOSED: "darkgray",
            ReferralState.DRAFT: "red",
            ReferralState.RECEIVED: "blue",
        }

        return state_colors[self.state]

    def get_due_date(self):
        """
        Use the linked ReferralUrgency to calculate the expected answer date from the day the
        referral was created.
        """
        if self.urgency_level and self.sent_at:
            return self.sent_at + self.urgency_level.duration

        return None

    def get_users_text_list(self):
        """
        Return a comma-separated list of all users linked to the referral.
        """
        return ", ".join([user.get_full_name() for user in self.users.all()])

    @transition(
        field=state,
        source=[
            ReferralState.ANSWERED,
            ReferralState.ASSIGNED,
            ReferralState.IN_VALIDATION,
            ReferralState.PROCESSING,
            ReferralState.RECEIVED,
        ],
        target=RETURN_VALUE(
            ReferralState.ANSWERED,
            ReferralState.ASSIGNED,
            ReferralState.IN_VALIDATION,
            ReferralState.PROCESSING,
            ReferralState.RECEIVED,
        ),
    )
    def add_requester(self, requester, created_by):
        """
        Add a new user to the list of requesters for a referral.
        """
        ReferralUserLink.objects.create(referral=self, user=requester)
        ReferralActivity.objects.create(
            actor=created_by,
            verb=ReferralActivityVerb.ADDED_REQUESTER,
            referral=self,
            item_content_object=requester,
        )
        # Notify the newly added requester by sending them an email
        Mailer.send_referral_requester_added(
            referral=self,
            contact=requester,
            created_by=created_by,
        )
        return self.state

    @transition(
        field=state,
        source=[
            ReferralState.ASSIGNED,
            ReferralState.IN_VALIDATION,
            ReferralState.PROCESSING,
            ReferralState.RECEIVED,
        ],
        target=RETURN_VALUE(
            ReferralState.ASSIGNED,
            ReferralState.IN_VALIDATION,
            ReferralState.PROCESSING,
        ),
    )
    def assign(self, assignee, created_by, unit):
        """
        Assign the referral to one of the unit's members.
        """
        assignment = ReferralAssignment.objects.create(
            assignee=assignee,
            created_by=created_by,
            referral=self,
            unit=unit,
        )
        ReferralActivity.objects.create(
            actor=created_by,
            verb=ReferralActivityVerb.ASSIGNED,
            referral=self,
            item_content_object=assignee,
        )
        # Notify the assignee by sending them an email
        Mailer.send_referral_assigned(
            referral=self,
            assignment=assignment,
            assigned_by=created_by,
        )

        if self.state in [
                ReferralState.IN_VALIDATION, ReferralState.PROCESSING
        ]:
            return self.state

        return ReferralState.ASSIGNED

    @transition(
        field=state,
        source=[
            ReferralState.ASSIGNED,
            ReferralState.IN_VALIDATION,
            ReferralState.PROCESSING,
            ReferralState.RECEIVED,
        ],
        target=RETURN_VALUE(
            ReferralState.ASSIGNED,
            ReferralState.IN_VALIDATION,
            ReferralState.PROCESSING,
            ReferralState.RECEIVED,
        ),
    )
    def assign_unit(
        self,
        unit,
        created_by,
        assignunit_explanation,
    ):
        """
        Add a unit assignment to the referral.
        """
        assignment = ReferralUnitAssignment.objects.create(
            referral=self,
            unit=unit,
        )
        ReferralActivity.objects.create(
            actor=created_by,
            verb=ReferralActivityVerb.ASSIGNED_UNIT,
            referral=self,
            item_content_object=unit,
            message=assignunit_explanation,
        )
        Mailer.send_referral_assigned_unit(
            referral=self,
            assignment=assignment,
            assignunit_explanation=assignunit_explanation,
            assigned_by=created_by,
        )
        return self.state

    @transition(
        field=state,
        source=[
            ReferralState.ASSIGNED,
            ReferralState.IN_VALIDATION,
            ReferralState.PROCESSING,
            ReferralState.RECEIVED,
        ],
        target=RETURN_VALUE(
            ReferralState.ASSIGNED,
            ReferralState.IN_VALIDATION,
            ReferralState.PROCESSING,
        ),
    )
    def draft_answer(self, answer):
        """
        Create a draft answer to the Referral. If there is no current assignee, we'll auto-assign
        the person who created the draft.
        """
        # If the referral is not already assigned, self-assign it to the user who created
        # the answer
        if not ReferralAssignment.objects.filter(referral=self).exists():
            # Get the first unit from referral linked units the user is a part of.
            # Having a user in two different units both assigned on the same referral is a very
            # specific edge case and picking between those is not an important distinction.
            unit = answer.referral.units.filter(
                members__id=answer.created_by.id).first()
            ReferralAssignment.objects.create(
                assignee=answer.created_by,
                created_by=answer.created_by,
                referral=self,
                unit=unit,
            )
            ReferralActivity.objects.create(
                actor=answer.created_by,
                verb=ReferralActivityVerb.ASSIGNED,
                referral=self,
                item_content_object=answer.created_by,
            )

        # Create the activity. Everything else was handled upstream where the ReferralAnswer
        # instance was created
        ReferralActivity.objects.create(
            actor=answer.created_by,
            verb=ReferralActivityVerb.DRAFT_ANSWERED,
            referral=self,
            item_content_object=answer,
        )

        if self.state in [
                ReferralState.IN_VALIDATION, ReferralState.PROCESSING
        ]:
            return self.state

        return ReferralState.PROCESSING

    @transition(
        field=state,
        source=ReferralState.IN_VALIDATION,
        target=ReferralState.IN_VALIDATION,
    )
    def perform_answer_validation(self, validation_request, state, comment):
        """
        Provide a response to the validation request, setting the state according to
        the validator's choice and registering their comment.
        """
        ReferralAnswerValidationResponse.objects.create(
            validation_request=validation_request,
            state=state,
            comment=comment,
        )
        verb = (ReferralActivityVerb.VALIDATED
                if state == ReferralAnswerValidationResponseState.VALIDATED
                else ReferralActivityVerb.VALIDATION_DENIED)
        ReferralActivity.objects.create(
            actor=validation_request.validator,
            verb=verb,
            referral=self,
            item_content_object=validation_request,
        )

        # Notify all the assignees of the validation response with different emails
        # depending on the response state
        assignees = [
            assignment.assignee
            for assignment in ReferralAssignment.objects.filter(referral=self)
        ]
        Mailer.send_validation_performed(
            validation_request=validation_request,
            assignees=assignees,
            is_validated=state ==
            ReferralAnswerValidationResponseState.VALIDATED,
        )

    @transition(
        field=state,
        source=[
            ReferralState.IN_VALIDATION,
            ReferralState.PROCESSING,
        ],
        target=ReferralState.ANSWERED,
    )
    def publish_answer(self, answer, published_by):
        """
        Mark the referral as done by picking and publishing an answer.
        """
        # Create the published answer to our draft and attach all the relevant attachments
        published_answer = ReferralAnswer.objects.create(
            content=answer.content,
            created_by=answer.created_by,
            referral=self,
            state=ReferralAnswerState.PUBLISHED,
        )
        for attachment in answer.attachments.all():
            attachment.referral_answers.add(published_answer)
            attachment.save()
        # Update the draft answer with a reference to its published version
        answer.published_answer = published_answer
        answer.save()
        # Create the publication activity
        ReferralActivity.objects.create(
            actor=published_by,
            verb=ReferralActivityVerb.ANSWERED,
            referral=self,
            item_content_object=published_answer,
        )
        # Notify the requester by sending them an email
        Mailer.send_referral_answered(
            answer=answer,
            referral=self,
        )

    @transition(
        field=state,
        source=[
            ReferralState.ASSIGNED,
            ReferralState.IN_VALIDATION,
            ReferralState.PROCESSING,
            ReferralState.RECEIVED,
        ],
        target=RETURN_VALUE(
            ReferralState.ASSIGNED,
            ReferralState.IN_VALIDATION,
            ReferralState.PROCESSING,
            ReferralState.RECEIVED,
        ),
    )
    def remove_requester(self, referral_user_link, created_by):
        """
        Remove a user from the list of requesters for a referral.
        """
        requester = referral_user_link.user
        referral_user_link.delete()
        ReferralActivity.objects.create(
            actor=created_by,
            verb=ReferralActivityVerb.REMOVED_REQUESTER,
            referral=self,
            item_content_object=requester,
        )
        return self.state

    @transition(
        field=state,
        source=[
            ReferralState.IN_VALIDATION,
            ReferralState.PROCESSING,
        ],
        target=ReferralState.IN_VALIDATION,
    )
    def request_answer_validation(self, answer, requested_by, validator):
        """
        Request a validation for an existing answer. Represent the request through a validation
        request object and an activity, and send the email to the validator.
        """
        validation_request = ReferralAnswerValidationRequest.objects.create(
            validator=validator,
            answer=answer,
        )
        activity = ReferralActivity.objects.create(
            actor=requested_by,
            verb=ReferralActivityVerb.VALIDATION_REQUESTED,
            referral=self,
            item_content_object=validation_request,
        )
        Mailer.send_validation_requested(
            validation_request=validation_request,
            activity=activity,
        )

    @transition(
        field=state,
        source=[ReferralState.DRAFT],
        target=ReferralState.RECEIVED,
    )
    def send(self, created_by):
        """
        Send relevant emails for the newly sent referral and create the corresponding activity.
        """
        ReferralActivity.objects.create(
            actor=created_by,
            verb=ReferralActivityVerb.CREATED,
            referral=self,
        )
        # Confirm the referral has been sent to the requester by email
        Mailer.send_referral_saved(self, created_by)
        # Send this email to all owners of the unit(s) (admins are not supposed to receive
        # email notifications)
        for unit in self.units.all():
            contacts = unit.members.filter(
                unitmembership__role=UnitMembershipRole.OWNER)
            for contact in contacts:
                Mailer.send_referral_received(self, contact=contact, unit=unit)

    @transition(
        field=state,
        source=[
            ReferralState.ASSIGNED,
            ReferralState.PROCESSING,
            ReferralState.IN_VALIDATION,
        ],
        target=RETURN_VALUE(
            ReferralState.RECEIVED,
            ReferralState.ASSIGNED,
            ReferralState.PROCESSING,
            ReferralState.IN_VALIDATION,
        ),
    )
    def unassign(self, assignment, created_by):
        """
        Unassign the referral from a currently assigned member.
        """
        assignee = assignment.assignee
        assignment.delete()
        self.refresh_from_db()
        ReferralActivity.objects.create(
            actor=created_by,
            verb=ReferralActivityVerb.UNASSIGNED,
            referral=self,
            item_content_object=assignee,
        )
        # Check the number of remaining assignments on this referral to determine the next state
        assignment_count = ReferralAssignment.objects.filter(
            referral=self).count()

        if self.state == ReferralState.ASSIGNED and assignment_count == 0:
            return ReferralState.RECEIVED

        return self.state

    @transition(
        field=state,
        source=[
            ReferralState.RECEIVED,
            ReferralState.ASSIGNED,
            ReferralState.PROCESSING,
            ReferralState.IN_VALIDATION,
        ],
        target=RETURN_VALUE(
            ReferralState.RECEIVED,
            ReferralState.ASSIGNED,
            ReferralState.PROCESSING,
            ReferralState.IN_VALIDATION,
        ),
    )
    def unassign_unit(self, assignment, created_by):
        """
        Remove a unit assignment from the referral.
        """
        unit = assignment.unit

        if self.units.count() <= 1:
            raise TransitionNotAllowed()

        if self.assignees.filter(unitmembership__unit=unit):
            raise TransitionNotAllowed()

        assignment.delete()
        self.refresh_from_db()
        ReferralActivity.objects.create(
            actor=created_by,
            verb=ReferralActivityVerb.UNASSIGNED_UNIT,
            referral=self,
            item_content_object=unit,
        )

        return self.state

    @transition(
        field=state,
        source=[
            ReferralState.RECEIVED,
            ReferralState.ASSIGNED,
            ReferralState.PROCESSING,
            ReferralState.IN_VALIDATION,
        ],
        target=RETURN_VALUE(
            ReferralState.RECEIVED,
            ReferralState.ASSIGNED,
            ReferralState.PROCESSING,
            ReferralState.IN_VALIDATION,
        ),
    )
    def change_urgencylevel(self, new_urgency_level,
                            new_referralurgency_explanation, created_by):
        """
        Perform the urgency level change, keeping a history object to
        show the relevant information on the referral activity.
        """
        old_urgency_level = self.urgency_level
        self.urgency_level = new_urgency_level

        referral_urgencylevel_history = ReferralUrgencyLevelHistory.objects.create(
            referral=self,
            old_referral_urgency=old_urgency_level,
            new_referral_urgency=new_urgency_level,
            explanation=new_referralurgency_explanation,
        )

        ReferralActivity.objects.create(
            actor=created_by,
            verb=ReferralActivityVerb.URGENCYLEVEL_CHANGED,
            referral=self,
            item_content_object=referral_urgencylevel_history,
        )

        # Define all users who need to receive emails for this referral
        contacts = [*self.users.all()]
        if self.assignees.count() > 0:
            contacts = contacts + list(self.assignees.all())
        else:
            for unit in self.units.all():
                contacts = contacts + [
                    membership.user
                    for membership in unit.get_memberships().filter(
                        role=UnitMembershipRole.OWNER)
                ]

        # Remove the actor from the list of contacts, and use a set to deduplicate entries
        contacts = set(
            filter(lambda contact: contact.id != created_by.id, contacts))

        for target in contacts:
            Mailer.send_referral_changeurgencylevel(
                contact=target,
                referral=self,
                history_object=referral_urgencylevel_history,
                created_by=created_by,
            )

        return self.state

    @transition(
        field=state,
        source=[
            ReferralState.RECEIVED,
            ReferralState.ASSIGNED,
            ReferralState.PROCESSING,
            ReferralState.IN_VALIDATION,
        ],
        target=ReferralState.CLOSED,
    )
    def close_referral(self, close_explanation, created_by):
        """
        Close the referral and create the relevant activity.
        """

        ReferralActivity.objects.create(
            actor=created_by,
            verb=ReferralActivityVerb.CLOSED,
            referral=self,
            message=close_explanation,
        )

        # Define all users who need to receive emails for this referral
        contacts = [*self.users.all()]
        if self.assignees.count() > 0:
            contacts = contacts + list(self.assignees.all())
        else:
            for unit in self.units.all():
                contacts = contacts + [
                    membership.user
                    for membership in unit.get_memberships().filter(
                        role=UnitMembershipRole.OWNER)
                ]

        # Remove the actor from the list of contacts, and use a set to deduplicate entries
        contacts = set(
            filter(lambda contact: contact.id != created_by.id, contacts))

        for contact in contacts:
            Mailer.send_referral_closed(
                contact=contact,
                referral=self,
                close_explanation=close_explanation,
                closed_by=created_by,
            )
Exemplo n.º 11
0
class StudentCharacter(student_character_factory(), StrDefaultReprMixin):
    TO_LEARN = 10
    IN_PROGRESS = 20
    MASTERED = 30
    STATE_CHOICES = [
        (TO_LEARN, 'To Learn'),
        (IN_PROGRESS, 'In Progress'),
        (MASTERED, 'Mastered'),
    ]
    state = FSMIntegerField(choices=STATE_CHOICES, default=TO_LEARN)
    student = models.ForeignKey(Student,
                                on_delete=models.CASCADE,
                                related_name='student_characters',
                                related_query_name='student_character')
    character = models.ForeignKey(Character,
                                  on_delete=models.CASCADE,
                                  related_name='student_characters',
                                  related_query_name='student_character')
    time_added = models.DateField(auto_now_add=True)
    of = factory_student_character_manager_of
    objects = StudentCharacterManager()

    @transition(state, source=TO_LEARN, target=IN_PROGRESS)
    def _learn_update(self):
        pass

    def learn_update(self):
        self._learn_update()
        self.save()

    @transition(state,
                source=IN_PROGRESS,
                target=RETURN_VALUE(IN_PROGRESS, MASTERED))
    def _test_review_update(self, is_correct, field_name):
        """
        N (default: 2) correct answers in a row of a field masters this field
        incorrect answers increases N up to a maximum of 4
        mastering all TEST_FIELDS masters the character
        """
        if is_correct:
            field_in_a_row = getattr(self, field_name + '_in_a_row') + 1
            setattr(self, field_name + '_in_a_row', field_in_a_row)
            if field_in_a_row == \
                    getattr(self, field_name + '_in_a_row_required'):
                setattr(self, field_name + '_mastered', True)
                if all(
                        getattr(self, f'{test_field}_mastered')
                        for test_field in Character.TEST_FIELDS):
                    return self.MASTERED
        else:  # answer incorrect
            setattr(self, field_name + '_in_a_row', 0)
            field_in_a_row_required = \
                getattr(self, field_name + '_in_a_row_required')
            field_in_a_row_required = min(
                field_in_a_row_required + 1,
                learning_process.LearningProcess.MAX_IN_A_ROW_REQUIRED)
            setattr(self, field_name + '_in_a_row_required',
                    field_in_a_row_required)
        setattr(self, field_name + '_time_last_studied', timezone.now())
        return self.IN_PROGRESS

    def test_review_update(self, is_correct, field_name):
        self._test_review_update(is_correct, field_name)
        self.save()

    def __repr__(self):
        return f"<sc {self.pk}:{self.student}'s {self.character}>"

    class Meta:
        # for effectively selecting random choices from mastered scs
        ordering = ['-state']
        unique_together = ('student', 'character')
Exemplo n.º 12
0
class SiteJoinRequest(models.Model):
    """
    Join request statuses

    new -> just created
    rejected -> site coordinator rejected joinig
    approved -> site coordinator approved joining
    confirmed -> doctor confirmed joining and shared all patients
    """
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)
    state = FSMIntegerField(default=JoinStateEnum.NEW)
    doctor = models.ForeignKey('accounts.Doctor')
    site = models.ForeignKey('accounts.Site')

    class Meta:
        unique_together = (
            'doctor',
            'site', )

    @transition(
        field=state,
        source=JoinStateEnum.NEW,
        target=JoinStateEnum.REJECTED,
        permission=is_site_coordinator)
    def reject(self):
        """
        There is no side effects for the decline action
        """
        CoordinatorRejectedEmail(
            context={'site_title': self.site.title,
                     'doctor': self.doctor}).send([self.doctor.email])

    @transition(
        field=state,
        source=JoinStateEnum.NEW,
        target=RETURN_VALUE(JoinStateEnum.APPROVED, JoinStateEnum.CONFIRMED),
        permission=is_site_coordinator)
    def approve(self):
        has_patients = self.doctor.patients.exists()
        CoordinatorApprovedEmail(
            context={'site_title': self.site.title,
                     'has_patients': has_patients,
                     'doctor': self.doctor}).send([self.doctor.email])
        if not has_patients:
            self.doctor.my_coordinator_id = self.site.site_coordinator_id
            self.doctor.save()
            return JoinStateEnum.CONFIRMED

        return JoinStateEnum.APPROVED

    @transition(
        field=state,
        source=JoinStateEnum.APPROVED,
        target=JoinStateEnum.CONFIRMED,
        permission=lambda instance, user: instance.doctor_id == user.id,
        custom={'args_serializer': ConfirmArgsSerializer})
    def confirm(self, encrypted_keys=None):
        encrypted_keys = encrypted_keys or {}
        patients_ids = list(
            self.doctor.doctortopatient_set.values_list(
                'patient_id', flat=True))

        doctor_id = self.site.site_coordinator_id

        if set(patients_ids) != set(encrypted_keys.keys()):
            raise TransitionNotAllowed(
                "Not enough encrypted keys", object=self, method=self.confirm)

        DoctorToPatient.objects.bulk_create([
            DoctorToPatient(
                doctor_id=doctor_id,
                patient_id=patient_id,
                encrypted_key=encrypted_key)
            for patient_id, encrypted_key in encrypted_keys.items()
        ])

        self.doctor.my_coordinator_id = self.site.site_coordinator_id
        self.doctor.save()

        DoctorSharedPatientsEmail(context={
            'site_title': self.site.title,
            'doctor': self.doctor
        }).send([Doctor.objects.get(id=self.site.site_coordinator_id).email])
Exemplo n.º 13
0
class LearningProcess(models.Model):
    # algorithm constants
    MAX_INTERVAL_SECONDS = 120
    DEFAULT_IN_A_ROW_REQUIRED = 2
    MAX_IN_A_ROW_REQUIRED = 2
    ADDED_DURATION = timedelta(seconds=30)
    MIN_SC_IN_PROGRESS_CNT = 3
    MAX_SC_IN_PROGRESS_CNT = 10
    LEARN_PROB = 1 / 3
    MAX_RANDOM_CHOICES = 20
    DECIDE = 10
    # states
    START_LEARN = 20
    DONE_LEARN = 30
    START_RELEARN = 40
    DONE_RELEARN = 50
    TOLERANT_REVIEW = 60
    TEST_REVIEW = 70
    FINISH = 80
    CHOICES = [(DECIDE, 'decide'),
               (START_LEARN, 'start_learn'),
               (DONE_LEARN, 'done_learn'),
               (START_RELEARN, 'start_relearn'),
               (DONE_RELEARN, 'done_relearn'),
               (TOLERANT_REVIEW, 'tolerant_review'),
               (TEST_REVIEW, 'test_review'),
               (FINISH, 'finish')]
    state = FSMIntegerField(choices=CHOICES, default=DECIDE)
    # fields
    student = models.OneToOneField('classroom.Student',
                                   on_delete=models.CASCADE,
                                   primary_key=True, related_name='+')
    character = models.ForeignKey(Character, related_name='+',
                                  null=True, on_delete=models.SET_NULL)
    sc_tags = models.ManyToManyField('learning.StudentCharacterTag',
                                     related_name='+')
    review_field_index = models.PositiveSmallIntegerField(
        default=0, null=True, blank=True,
        validators=[MaxValueValidator(len(Character.TEST_FIELDS) - 1)]
    )
    review_answer_index = models.PositiveSmallIntegerField(default=0)
    # stats
    last_study_time = models.DateTimeField(auto_now=True)
    duration = models.DurationField(default=timedelta(0))

    @transition(field=state, source="*",
                target=RETURN_VALUE(TEST_REVIEW, START_LEARN, FINISH))
    def _decide(self):
        scs = learning.models.StudentCharacter.of(sc_tags=self.sc_tags.all())
        sc_to_learn = scs.get_one_to_learn()
        sc_to_review, self.review_field_index = scs.get_to_review()
        sc_in_progress_cnt = scs.count_all_in_progress()
        if not sc_to_review and not sc_to_learn:
            return self.FINISH
        if not sc_to_learn \
                or sc_in_progress_cnt >= LearningProcess.MAX_SC_IN_PROGRESS_CNT:
            self.character = sc_to_review.character
            return self.TEST_REVIEW
        if sc_in_progress_cnt <= LearningProcess.MIN_SC_IN_PROGRESS_CNT \
                or random.random() > LearningProcess.LEARN_PROB:
            self.character = sc_to_learn.character
            return self.START_LEARN
        self.character = sc_to_review.character
        return self.TEST_REVIEW

    def _generate_review(self):
        choices, ans_index = learning.models.StudentCharacter.of(
            student=self.student).generate_choices(
                self.character, Character.TEST_FIELDS[self.review_field_index])
        self.review_answer_index = ans_index
        question = self.character.generate_question(self.review_field_index)
        return 'review', question, choices

    @transition(field=state, source=START_LEARN, target=DONE_LEARN)
    def _start_learn(self):
        return 'learn', self.character, None

    @transition(field=state, source=DONE_LEARN, target=TOLERANT_REVIEW)
    def _done_learn(self):
        learning.models.StudentCharacter.of(self.student, self.character)\
            .learn_update()
        self.review_field_index = 0

    @transition(field=state, source=START_RELEARN, target=DONE_RELEARN)
    def _start_relearn(self):
        return 'learn', self.character, None

    @transition(field=state, source=DONE_RELEARN, target=DECIDE)
    def _done_relearn(self):
        pass

    def _finish(self):
        return None, None, None

    def get_action(self):
        """
        This should be called with any GET request while learning
        returns (None, None, None), or ('learn', character, None), or
        ('review', question, choices)
        """
        ACTIONS = {
            self.DECIDE: self._decide,
            self.START_LEARN: self._start_learn,
            self.DONE_LEARN: self._done_learn,
            self.START_RELEARN: self._start_relearn,
            self.DONE_RELEARN: self._done_relearn,
            self.TEST_REVIEW: self._generate_review,
            self.TOLERANT_REVIEW: self._generate_review,
            self.FINISH: self._finish,
        }
        for i in range(10):
            action = ACTIONS[self.state]()
            if isinstance(action, tuple):
                self._update_duration()
                self.save()
                return action
        raise Exception('InternalError: infinite loop')

    @transition(field=state, source=(TEST_REVIEW, TOLERANT_REVIEW),
                target=RETURN_VALUE(START_RELEARN, TOLERANT_REVIEW, DECIDE))
    def _check_answer(self, ans_index):
        is_correct = (ans_index == self.review_answer_index)
        if self.state == self.TEST_REVIEW:
            learning.models.StudentCharacter.objects.get(
                student=self.student, character=self.character
            ).test_review_update(is_correct,
                                 Character.TEST_FIELDS[self.review_field_index])
            if is_correct:
                return self.DECIDE
            else:
                return self.START_RELEARN
        else: # we are doing tolerant_review
            self.review_field_index += 1
            if self.review_field_index == len(Character.TEST_FIELDS):
                return self.DECIDE
            return self.TOLERANT_REVIEW

    def check_answer(self, ans_index):
        """
        This should be called with any POST request while learning
        :returns the correct ans_index
        """
        self._check_answer(ans_index)
        self._update_duration()
        self.save()
        return self.review_answer_index

    def start(self, sc_tags_filter=[]):
        """
        This function resets the LearningProcess
        """
        self.sc_tags.set(
            learning.models.StudentCharacterTag.objects.filter_by_pk(
            sc_tags_filter)
        )
        self.state = self.DECIDE
        self.duration = timedelta(0)
        self.save()

    def _update_duration(self):
        delta_time = timezone.now() - self.last_study_time
        if delta_time > timedelta(seconds=self.MAX_INTERVAL_SECONDS):
            delta_time = timedelta(seconds=self.MAX_INTERVAL_SECONDS)
            # TODO issue a warning like Membean
        self.duration += delta_time
        self.student.update_duration(delta_time)

    @property
    def duration_seconds(self):
        return self.duration.total_seconds()

    @classmethod
    def of(cls, student):
        """convenient get_or_create"""
        return cls.objects.get_or_create(student=student)[0]
Exemplo n.º 14
0
class ManualPaymentWorkflowMixin(object):
    """
    Add this class to `settings.SHOP_ORDER_WORKFLOWS` to mix it into your `OrderModel`.
    It adds all the methods required for state transitions, when used with the
    `ForwardFundPayment` provider from above.
    """
    TRANSITION_TARGETS = {
        'awaiting_payment': _("Awaiting a forward fund payment"),
        'prepayment_deposited': _("Prepayment deposited"),
        'no_payment_required': _("No Payment Required"),
    }
    _manual_payment_transitions = TRANSITION_TARGETS.keys()

    def __init__(self, *args, **kwargs):
        if not isinstance(self, BaseOrder):
            raise ImproperlyConfigured(
                "class 'ManualPaymentWorkflowMixin' is not of type 'BaseOrder'"
            )
        CancelOrderWorkflowMixin.CANCELABLE_SOURCES.update(
            self._manual_payment_transitions)
        super(ManualPaymentWorkflowMixin, self).__init__(*args, **kwargs)

    @transition(field='status',
                source=['created'],
                target='no_payment_required')
    def no_payment_required(self):
        """
        Signals that an Order can proceed directly, by confirming a payment of value zero.
        """

    @transition(field='status', source=['created'], target='awaiting_payment')
    def awaiting_payment(self):
        """
        Signals that the current Order awaits a payment.
        Invoked by ForwardFundPayment.get_payment_request.
        """

    def payment_deposited(self):
        if hasattr(self, 'amount_paid'):
            del self.amount_paid
        return self.amount_paid > 0

    @transition(field='status',
                source=['awaiting_payment'],
                target=RETURN_VALUE('awaiting_payment',
                                    'prepayment_deposited'),
                conditions=[payment_deposited],
                custom=dict(admin=True, button_name=_("Payment Received")))
    def prepayment_deposited(self):
        """
        Signals that the current Order received a payment.
        """
        return 'prepayment_deposited' if self.is_fully_paid(
        ) else 'awaiting_payment'

    @transition(field='status',
                source=['prepayment_deposited', 'no_payment_required'],
                custom=dict(auto=True))
    def acknowledge_prepayment(self):
        """
        Acknowledge the payment. This method is invoked automatically.
        """
        self.acknowledge_payment()

    @transition(field='status',
                source='refund_payment',
                target=RETURN_VALUE('refund_payment', 'order_canceled'),
                custom=dict(admin=True, button_name=_("Mark as Refunded")))
    def payment_refunded(self):
        """
        Signals that the payment for this Order has been refunded manually.
        """
        return 'refund_payment' if self.amount_paid else 'order_canceled'
Exemplo n.º 15
0
class Session(models.Model):
    """
	Holds the information about the interaction between user (student) and system
	Primary key = default django primary key
	"""

    TERMINAL_STATES = ['timeout', 'finished', 'canceled', 'error_terminated']

    start_time = models.DateTimeField(default=datetime.datetime.now)
    state = FSMField(default='session_started')
    last_action_time = models.DateTimeField(auto_now=True)
    student_card = models.ForeignKey(StudentCard,
                                     on_delete=models.SET_NULL,
                                     blank=True,
                                     null=True,
                                     related_name='+')
    raspi_tag = models.ForeignKey(RaspiTag,
                                  on_delete=models.SET_NULL,
                                  blank=True,
                                  null=True,
                                  related_name='+')

    class Meta:
        ordering = ['id']

    @staticmethod
    def get_active_session():
        return Session.objects.exclude(
            state__in=Session.TERMINAL_STATES).last()

    def get_active_board(self):
        if self.raspi_tag is None:
            return None
        return self.raspi_tag.board

    def get_active_student(self):
        if self.student_card is None:
            return None
        return self.student_card.student

    def board_returned(self):
        try:
            student = self.get_active_student()
            boards = self.student_card.student.get_student_boards()
        except AttributeError:
            return False

        scanned_board = self.get_active_board()

        # return loaned board that is assigned on student- OK
        if scanned_board in boards.values():
            # create action in Action model with returning operation and timestamp
            Action.return_board_action(student=student, board=scanned_board)
            return True
        else:
            return False

    def board_loaned(self):
        scanned_board = self.get_active_board()

        if scanned_board.board_status != BoardStatus.ACTIVE:
            return 'error'
        try:
            student = self.get_active_student()
            boards = student.get_student_boards()
        except AttributeError:
            return "error"

        # if student has 2 boards, they can not loan one more
        if len(boards) == 2:
            return 'maximum_boards_reached'
        elif len(boards) == 1:
            if "lab" in boards:
                loaned_type = BoardType.LAB_LOAN
            elif "home" in boards:
                loaned_type = BoardType.HOME_LOAN
            else:
                return 'status_error'
        else:
            loaned_type = 'empty'

        if scanned_board.board_type == loaned_type:
            return 'same_bord_type'

        if scanned_board.board_type == BoardType.LAB_LOAN:
            operation = Operation.LAB_LOAN
        elif scanned_board.board_type == BoardType.HOME_LOAN:
            if not student.is_home_loan_enabled:
                return 'home_loan_disabled'
            operation = Operation.HOME_LOAN

        # create action in Action model with loan operation and timestamp
        Action.loan_board_action(student=student,
                                 board=scanned_board,
                                 operation=operation)
        return 'loaned'

    def clean(self):
        open_session = Session.objects.exclude(
            state__in=Session.TERMINAL_STATES).count()
        if open_session != 0:
            raise ValidationError('Active session already exists!')
        super().clean()

    # =================== Django Finite State Machine ===================================

    @transition(field=state,
                source='session_started',
                target=RETURN_VALUE('valid_student_card',
                                    'unknown_student_card'))
    def student_card_inserted(self, card_uid):
        try:
            card = StudentCard.objects.get(uid=card_uid)
            if card.student is not None:
                self.student_card = card
                return 'valid_student_card'
        except StudentCard.student.RelatedObjectDoesNotExist:
            return 'unknown_student_card'
        except StudentCard.DoesNotExist:
            return 'unknown_student_card'

    @transition(field=state,
                source='valid_student_card',
                target=RETURN_VALUE('valid_rfid', 'unknown_rfid'))
    def rfid_inserted(self, uid):
        try:
            tag = RaspiTag.objects.get(uid=uid)
            if tag.board is not None:
                self.raspi_tag = tag
                return 'valid_rfid'
        except RaspiTag.board.RelatedObjectDoesNotExist:
            return 'unknown_rfid'
        except RaspiTag.DoesNotExist:
            return 'unknown_rfid'

    @transition(field=state,
                source='valid_rfid',
                target=RETURN_VALUE('rfid_state_loaned', 'rfid_state_active',
                                    'status_error'))
    def get_rfid_status(self):
        board = self.get_active_board()
        if board is not None:
            if board.board_status == BoardStatus.LOANED:
                return 'rfid_state_loaned'
            elif board.board_status == BoardStatus.ACTIVE:
                return 'rfid_state_active'
            else:
                return 'status_error'
        else:
            return 'status_error'

    @transition(field=state,
                source='rfid_state_loaned',
                target=RETURN_VALUE('returned', 'return_error'))
    def loaned_board_returned(self):
        if self.board_returned():
            # After the new Action in DB was created with board_returned()
            # the board_status will be set to Active again
            Board.return_board(self.raspi_tag)
            return 'returned'
        else:
            return 'return_error'

    @transition(field=state,
                source='rfid_state_active',
                target=RETURN_VALUE('loaned', 'home_loan_disabled', 'error',
                                    'maximum_boards_reached',
                                    'same_bord_type'))
    def loan_active_board(self):
        result = self.board_loaned()
        if result == 'loaned':
            # After the new Action in DB was created with board_loaned()
            # the board_status will be set to Loaned
            Board.loan_board(self.raspi_tag)
        return result

    @transition(field=state, source='*', target='timeout')
    def timeout(self):
        # transition into final state
        pass

    @transition(field=state, source='*', target='canceled')
    def session_canceled(self):
        # transition into final state
        pass

    # SUCCESS FINISH
    @transition(field=state, source=['returned', 'loaned'], target='finished')
    def session_finished(self):
        # finish session, terminal state for successful scenario
        pass

    # ERROR TERMINATION
    @transition(field=state,
                source=[
                    'unknown_student_card', 'unknown_rfid', 'status_error',
                    'home_loan_disabled', 'maximum_boards_reached',
                    'same_bord_type', 'return_error'
                ],
                target='error_terminated')
    def session_terminated(self):
        # change error state to terminated state to allow new session to be started
        pass
Exemplo n.º 16
0
class Relationship(ManageableModel):
    start_date = models.DateTimeField(default=now)
    end_date = models.DateTimeField(null=True, blank=True)
    effective_start_date = models.DateTimeField(null=True, blank=True)
    effective_end_date = models.DateTimeField(null=True, blank=True)
    review_date = models.DateTimeField(null=True, blank=True)

    suspended_until = models.DateTimeField(null=True, blank=True)

    comment = models.TextField(blank=True)

    dependent_on = models.ForeignKey('self', null=True, blank=True)

    state = FSMField(max_length=16, choices=RELATIONSHIP_STATE_CHOICES, db_index=True, protected=True)
    suspended = FSMBooleanField(db_index=True, default=False, protected=True)

    delayed_save = GenericRelation(DelayedSave)

    class Meta:
        abstract = True

    def schedule_resave(self):
        now = timezone.now()
        dates = [self.start_date,
                 self.end_date,
                 self.effective_start_date,
                 self.effective_end_date,
                 self.review_date,
                 self.suspended_until]
        dates = sorted(d for d in dates if d and d > now)

        if dates:
            try:
                delayed_save = self.delayed_save.get()
            except DelayedSave.DoesNotExist:
                delayed_save = DelayedSave(object=self)
            delayed_save.when = dates[0]
            delayed_save.save()
        elif self.delayed_save.exists():
            self.delayed_save.get().delete()

    @transition(field=suspended, source=True, target=False)
    def unsuspend(self):
        self.suspended_until = None
        if self.state == 'suspended':
            self._unsuspend_state()

    @transition(field=suspended, source=False, target=True)
    def suspend(self, until=None):
        self.suspended_until = until
        if self.state == 'active':
            self._suspend_state()

    @transition(field=state, source='suspended', target='active')
    def _unsuspend_state(self):
        pass

    @transition(field=state, source='active', target='suspended')
    def _suspend_state(self):
        pass

    @transition(field=state, source='offered', target=RETURN_VALUE(),
                permission=is_owning_identity)
    def accept(self):
        return self._time_has_passed(now_active=True)

    @transition(field=state, source='offered', target='rejected',
                permission=is_owning_identity)
    def reject(self):
        pass

    @transition(field=state, source='*', target=RETURN_VALUE())
    def _time_has_passed(self, now_active=False):
        start_date = self.effective_start_date or self.start_date
        end_date = self.effective_end_date or self.end_date
        now = timezone.now()

        if self.state == 'suspended' and self.suspended_until and self.suspended_until < now:
            self.unsuspend()

        if now_active or self.state in {'forthcoming', 'active', 'historic', ''}:
            if start_date <= now and (not end_date or now <= end_date):
                return 'active' if not self.suspended else 'suspended'
            elif now < start_date:
                return 'forthcoming'
            elif end_date < now:
                return 'historic'
        else:
            return self.state

    def save(self, *args, **kwargs):
        self._time_has_passed()
        super().save(*args, **kwargs)
        self.schedule_resave()
Exemplo n.º 17
0
class Appearance(TimeStampedModel):
    """
    An appearance of a competitor on stage.

    The Appearance is meant to be a private resource.
    """

    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
    )

    STATUS = Choices(
        (0, 'new', 'New',),
        (7, 'built', 'Built',),
        (10, 'started', 'Started',),
        (20, 'finished', 'Finished',),
        (25, 'variance', 'Variance',),
        (30, 'verified', 'Verified',),
    )

    status = FSMIntegerField(
        help_text="""DO NOT CHANGE MANUALLY unless correcting a mistake.  Use the buttons to change state.""",
        choices=STATUS,
        default=STATUS.new,
    )

    num = models.IntegerField(
        null=True,
        blank=True,
    )

    draw = models.IntegerField(
        null=True,
        blank=True,
    )

    actual_start = models.DateTimeField(
        help_text="""
            The actual appearance window.""",
        null=True,
        blank=True,
    )

    actual_finish = models.DateTimeField(
        help_text="""
            The actual appearance window.""",
        null=True,
        blank=True,
    )

    pos = models.IntegerField(
        help_text='Actual Participants-on-Stage',
        null=True,
        blank=True,
    )

    legacy_group = models.CharField(
        max_length=255,
        blank=True,
        null=True,
    )

    # Privates
    rank = models.IntegerField(
        null=True,
        blank=True,
    )

    mus_points = models.IntegerField(
        null=True,
        blank=True,
    )

    per_points = models.IntegerField(
        null=True,
        blank=True,
    )

    sng_points = models.IntegerField(
        null=True,
        blank=True,
    )

    tot_points = models.IntegerField(
        null=True,
        blank=True,
    )

    mus_score = models.FloatField(
        null=True,
        blank=True,
    )

    per_score = models.FloatField(
        null=True,
        blank=True,
    )

    sng_score = models.FloatField(
        null=True,
        blank=True,
    )

    tot_score = models.FloatField(
        null=True,
        blank=True,
    )

    mus_rank = models.IntegerField(
        null=True,
        blank=True,
    )

    per_rank = models.IntegerField(
        null=True,
        blank=True,
    )

    sng_rank = models.IntegerField(
        null=True,
        blank=True,
    )

    tot_rank = models.IntegerField(
        null=True,
        blank=True,
    )

    variance_report = models.FileField(
        null=True,
        blank=True,
    )

    # Appearance FKs
    round = models.ForeignKey(
        'Round',
        related_name='appearances',
        on_delete=models.CASCADE,
    )

    competitor = models.ForeignKey(
        'Competitor',
        related_name='appearances',
        on_delete=models.CASCADE,
    )

    # Relations
    statelogs = GenericRelation(
        StateLog,
        related_query_name='appearances',
    )

    @cached_property
    def round__kind(self):
        return self.round.kind

    # Appearance Internals
    def clean(self):
        if self.competitor:
            if self.competitor.group.kind != self.round.session.kind:
                raise ValidationError(
                    {'competitor': 'Competitor kind must match session'}
                )


    class Meta:
        ordering = [
            '-round__num',
            'num',
        ]
        unique_together = (
            ('round', 'num',),
        )

    class JSONAPIMeta:
        resource_name = "appearance"

    def __str__(self):
        return "{0} {1}".format(
            str(self.competitor),
            str(self.round),
        )

    # Methods
    def get_variance(self):
        Score = apps.get_model('api.score')
        Panelist = apps.get_model('api.panelist')
        songs = self.songs.order_by('num')
        scores = Score.objects.filter(
            kind=Score.KIND.official,
            song__in=songs,
        ).order_by(
            'category',
            'panelist__person__last_name',
            'song__num',
        )
        panelists = self.round.panelists.filter(
            kind=Panelist.KIND.official,
            category__gt=Panelist.CATEGORY.ca,
        ).order_by(
            'category',
            'person__last_name',
        )
        variances = []
        for song in songs:
            variances.extend(song.dixons)
            variances.extend(song.asterisks)
        variances = list(set(variances))
        context = {
            'appearance': self,
            'songs': songs,
            'scores': scores,
            'panelists': panelists,
            'variances': variances,
        }
        rendered = render_to_string('variance.html', context)
        pdf = pydf.generate_pdf(rendered, enable_smart_shrinking=False)
        content = ContentFile(pdf)
        return content


    def mock(self):
        # Mock Appearance
        Chart = apps.get_model('api.chart')
        prelim = self.competitor.entry.prelim
        if self.competitor.group.kind == self.competitor.group.KIND.chorus:
            pos = self.competitor.group.members.filter(
                status=self.competitor.group.members.model.STATUS.active,
            ).count()
            self.pos = pos
        if not prelim:
            average = self.competitor.group.competitors.filter(
                status=self.competitor.group.competitors.model.STATUS.finished,
            ).aggregate(avg=Avg('tot_score'))['avg']
            if average:
                prelim = average
            else:
                prelim = randint(65, 80)
        songs = self.songs.all()
        for song in songs:
            song.chart = Chart.objects.filter(
                status=Chart.STATUS.active
            ).order_by("?").first()
            song.save()
            scores = song.scores.all()
            for score in scores:
                d = randint(-4, 4)
                score.points = prelim + d
                score.save()
        if self.status == self.STATUS.new:
            raise RuntimeError("Out of state")
        if self.status == self.STATUS.built:
            self.start()
            self.finish()
            self.verify()
            return
        if self.status == self.STATUS.started:
            self.finish()
            self.verify()
            return
        if self.status == self.STATUS.finished:
            self.verify()
            return


    def calculate(self):
        Score = apps.get_model('api.score')
        tot = Sum('points')
        mus = Sum('points', filter=Q(category=Score.CATEGORY.music))
        per = Sum('points', filter=Q(category=Score.CATEGORY.performance))
        sng = Sum('points', filter=Q(category=Score.CATEGORY.singing))
        officials = Score.objects.filter(
            song__appearance=self,
            kind=Score.KIND.official,
        ).annotate(
            tot=tot,
            mus=mus,
            per=per,
            sng=sng,
        )
        tot = officials.aggregate(
            sum=Sum('points'),
            avg=Avg('points'),
        )
        mus = officials.filter(
            category=Score.CATEGORY.music,
        ).aggregate(
            sum=Sum('points'),
            avg=Avg('points'),
        )
        per = officials.filter(
            category=Score.CATEGORY.performance,
        ).aggregate(
            sum=Sum('points'),
            avg=Avg('points'),
        )
        sng = officials.filter(
            category=Score.CATEGORY.singing,
        ).aggregate(
            sum=Sum('points'),
            avg=Avg('points'),
        )
        self.tot_points = tot['sum']
        self.tot_score = tot['avg']
        self.mus_points = mus['sum']
        self.mus_score = mus['avg']
        self.per_points = per['sum']
        self.per_score = per['avg']
        self.sng_points = sng['sum']
        self.sng_score = sng['avg']

    def check_variance(self):
        variance = False
        for song in self.songs.all():
            song.calculate()
            asterisks = song.get_asterisks()
            if asterisks:
                song.asterisks = asterisks
                variance = True
            dixons = song.get_dixons()
            if dixons:
                song.dixons = dixons
                variance = True
            song.save()
        return variance


    # Appearance Permissions
    @staticmethod
    @allow_staff_or_superuser
    @authenticated_users
    def has_read_permission(request):
        return True

    @allow_staff_or_superuser
    @authenticated_users
    def has_object_read_permission(self, request):
        return any([
            self.round.status == self.round.STATUS.finished,
            self.round.session.convention.assignments.filter(
                person__user=request.user,
                status__gt=0,
                category__lte=10,
            ),
        ])

    @staticmethod
    @allow_staff_or_superuser
    @authenticated_users
    def has_write_permission(request):
        return any([
            request.user.is_round_manager,
        ])

    @allow_staff_or_superuser
    @authenticated_users
    def has_object_write_permission(self, request):
        return any([
            all([
                self.round.session.convention.assignments.filter(
                    person__user=request.user,
                    status__gt=0,
                    category__lte=10,
                ),
                self.round.status != self.round.STATUS.finished,
            ]),
        ])

    # Appearance Conditions
    def can_verify(self):
        try:
            if self.competitor.group.kind == self.competitor.group.KIND.chorus and not self.pos:
                is_pos = False
            else:
                is_pos = True
        except AttributeError:
            is_pos = False
        return all([
            is_pos,
        ])

    # Appearance Transitions
    @fsm_log_by
    @transition(field=status, source=[STATUS.new], target=STATUS.built)
    def build(self, *args, **kwargs):
        Grid = apps.get_model('api.grid')
        Panelist = apps.get_model('api.panelist')
        grid, created = Grid.objects.get_or_create(
            round=self.round,
            num=self.num,
        )
        grid.appearance = self
        grid.save()
        panelists = self.round.panelists.filter(
            category__gt=Panelist.CATEGORY.ca,
        )
        i = 1
        while i <= 2:  # Number songs constant
            song = self.songs.create(
                num=i
            )
            for panelist in panelists:
                song.scores.create(
                    category=panelist.category,
                    kind=panelist.kind,
                    panelist=panelist,
                )
            i += 1
        return

    @fsm_log_by
    @transition(field=status, source=[STATUS.built], target=STATUS.started)
    def start(self, *args, **kwargs):
        self.actual_start = now()
        return

    @fsm_log_by
    @transition(field=status, source=[STATUS.started, STATUS.verified], target=STATUS.finished)
    def finish(self, *args, **kwargs):
        self.actual_finish = now()
        return

    @fsm_log_by
    @transition(
        field=status,
        source=[STATUS.finished, STATUS.verified, STATUS.variance],
        target=RETURN_VALUE(STATUS.variance, STATUS.verified,),
    )
    def verify(self, *args, **kwargs):
        if self.status == self.STATUS.finished:
            variance = self.check_variance()
            if variance:
                content = self.get_variance()
                self.variance_report.save(
                    "{0}-variance-report".format(
                        slugify(self.competitor.group.name),
                    ),
                    content,
                )
        else:
            variance = None
        self.calculate()
        self.competitor.calculate()
        self.competitor.save()
        django_rq.enqueue(
            self.competitor.save_csa,
        )
        return self.STATUS.variance if variance else self.STATUS.verified