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()
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'
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'
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)
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)
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)
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'])
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)
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]
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, )
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')
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])
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]
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'
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
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()
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