class AssignmentRegistrationReminderTask(ModelPeriodicTask): def get_queryset(self): return self.model.objects.filter( date__lte=timezone.now() + timedelta(days=5), status__in=[ AssignmentStateMachine.full, AssignmentStateMachine.open ]) def is_on_date(assignment): """task is on a specific date""" return getattr(assignment, 'end_date_type') == 'on_date' def has_deadline(assignment): """task has a deadline""" return getattr(assignment, 'end_date_type') == 'deadline' effects = [ NotificationEffect(AssignmentReminderDeadline, conditions=[has_deadline]), NotificationEffect(AssignmentReminderOnDate, conditions=[is_on_date]) ] def __str__(self): return str( _("Send a reminder if the task deadline/date is in 5 days."))
class PlainPayoutAccountStateMachine(PayoutAccountStateMachine): model = PlainPayoutAccount verify = Transition( [ PayoutAccountStateMachine.new, PayoutAccountStateMachine.pending, PayoutAccountStateMachine.incomplete, PayoutAccountStateMachine.rejected ], PayoutAccountStateMachine.verified, name=_('Verify'), description=_("Verify the KYC account. You will hereby confirm that you verified the users identity."), automatic=False, permission=PayoutAccountStateMachine.can_approve, effects=[ NotificationEffect(PayoutAccountVerified), DeleteDocumentEffect ] ) reject = Transition( [ PayoutAccountStateMachine.new, PayoutAccountStateMachine.incomplete, PayoutAccountStateMachine.verified ], PayoutAccountStateMachine.rejected, name=_('Reject'), description=_("Reject the payout account. The uploaded ID scan " "will be removed with this step."), automatic=False, effects=[ NotificationEffect(PayoutAccountRejected), DeleteDocumentEffect ] )
def test_notification_effect(self): user = BlueBottleUserFactory.create( email='*****@*****.**' ) event = EventFactory.create( title='Bound to fail', owner=user ) subject = 'Your event "Bound to fail" has been rejected' effect = NotificationEffect(EventRejectedOwnerMessage)(event) self.assertEqual(str(effect), 'Message {} to [email protected]'.format(subject)) effect.execute() self.assertEqual(mail.outbox[0].subject, subject)
class EventReminderTask(ModelPeriodicTask): def get_queryset(self): return self.model.objects.filter(start__lte=timezone.now() + timedelta(days=5), status__in=['open', 'full']) effects = [NotificationEffect(EventReminderMessage)] def __str__(self): return str(_("Send a reminder five days before the event starts."))
class DateChangedTrigger(ModelChangedTrigger): field = 'date' def is_on_date(assignment): """task is on a specific date""" return assignment.end_date_type == 'on_date' def has_deadline(assignment): """task has a deadline""" return assignment.end_date_type == 'deadline' def in_the_future(assignment): """is in the future""" return assignment.date > timezone.now() effects = [ NotificationEffect(AssignmentDeadlineChanged, conditions=[in_the_future, has_deadline]), NotificationEffect(AssignmentDateChanged, conditions=[in_the_future, is_on_date]), TransitionEffect( 'succeed', conditions=[ AssignmentStateMachine.should_finish, AssignmentStateMachine.has_new_or_accepted_applicants ]), TransitionEffect( 'expire', conditions=[ AssignmentStateMachine.should_finish, AssignmentStateMachine.has_no_new_or_accepted_applicants ]), TransitionEffect('reschedule', conditions=[AssignmentStateMachine.should_open]), TransitionEffect('lock', conditions=[AssignmentStateMachine.is_full]), ]
class DateChangedTrigger(ModelChangedTrigger): field = 'start' def in_the_future(event): """is in the future""" return event.start > timezone.now() effects = [ NotificationEffect( EventDateChanged, conditions=[ in_the_future ] ), TransitionEffect('succeed', conditions=[ EventStateMachine.should_finish, EventStateMachine.has_participants ]), TransitionEffect('start', conditions=[ EventStateMachine.should_start, EventStateMachine.has_participants ]), TransitionEffect('expire', conditions=[ EventStateMachine.should_start, EventStateMachine.has_no_participants ]), TransitionEffect('expire', conditions=[ EventStateMachine.should_finish, EventStateMachine.has_no_participants ]), TransitionEffect('reschedule', conditions=[ EventStateMachine.should_open ]), TransitionEffect('lock', conditions=[ EventStateMachine.is_full ]), ]
class ApplicantStateMachine(ContributionStateMachine): model = Applicant accepted = State( _('accepted'), 'accepted', _('The applicant was accepted and will join the activity.')) rejected = State( _('rejected'), 'rejected', _("The applicant was rejected and will not join the activity.")) withdrawn = State( _('withdrawn'), 'withdrawn', _('The applicant withdrew and will no longer join the activity.')) no_show = State(_('no show'), 'no_show', _('The applicant did not contribute to the activity.')) active = State(_('active'), 'active', _('The applicant is currently working on the activity.')) def has_time_spent(self): """time spent is set""" return self.instance.time_spent def has_no_time_spent(self): """time spent is not set""" return not self.instance.time_spent def is_user(self, user): """is applicant""" return self.instance.user == user def is_activity_owner(self, user): """is activity manager or staff member""" return user.is_staff or self.instance.activity.owner == user def assignment_will_become_full(self): """task will be full""" activity = self.instance.activity return activity.capacity == len(activity.accepted_applicants) + 1 def assignment_will_become_open(self): """task will not be full""" activity = self.instance.activity return activity.capacity == len(activity.accepted_applicants) def assignment_is_finished(self): """task is finished""" return self.instance.activity.end < timezone.now() def assignment_is_not_finished(self): "task is not finished" return not self.instance.activity.date < timezone.now() def assignment_will_be_empty(self): """task be empty""" return len(self.instance.activity.accepted_applicants) == 1 def can_accept_applicants(self, user): """can accept applicants""" return user in [ self.instance.activity.owner, self.instance.activity.initiative.activity_manager, self.instance.activity.initiative.owner ] def assignment_is_open(self): """task is open""" return self.instance.activity.status == ActivityStateMachine.open.value initiate = Transition(EmptyState(), ContributionStateMachine.new, name=_('Initiate'), description=_("User applied to join the task."), effects=[ NotificationEffect(AssignmentApplicationMessage), FollowActivityEffect, TransitionEffect( 'succeed', conditions=[assignment_is_finished]) ]) accept = Transition( [ContributionStateMachine.new, rejected], accepted, name=_('Accept'), description=_("Applicant was accepted."), automatic=False, permission=can_accept_applicants, effects=[ TransitionEffect('succeed', conditions=[assignment_is_finished]), RelatedTransitionEffect('activity', 'lock', conditions=[assignment_will_become_full]), RelatedTransitionEffect('activity', 'succeed', conditions=[assignment_is_finished]), NotificationEffect(ApplicantAcceptedMessage) ]) reaccept = Transition(ContributionStateMachine.succeeded, accepted, name=_('Accept'), description=_("Applicant was accepted."), automatic=True, effects=[ RelatedTransitionEffect( 'activity', 'lock', conditions=[assignment_will_become_full]), ClearTimeSpent, ]) reject = Transition([ContributionStateMachine.new, accepted], rejected, name=_('Reject'), description=_("Applicant was rejected."), automatic=False, permission=can_accept_applicants, effects=[ RelatedTransitionEffect('activity', 'reopen'), NotificationEffect(ApplicantRejectedMessage), UnFollowActivityEffect ]) withdraw = Transition( [ContributionStateMachine.new, accepted], withdrawn, name=_('Withdraw'), description=_( "Applicant withdrew and will no longer join the activity."), automatic=False, permission=is_user, hide_from_admin=True, effects=[UnFollowActivityEffect]) reapply = Transition( [withdrawn, ContributionStateMachine.failed], ContributionStateMachine.new, name=_('Reapply'), description=_( "Applicant re-applies for the task after previously withdrawing."), automatic=False, conditions=[assignment_is_open], permission=ContributionStateMachine.is_user, effects=[ FollowActivityEffect, NotificationEffect(AssignmentApplicationMessage) ]) activate = Transition( [ accepted, # ContributionStateMachine.new ], active, name=_('Activate'), description=_("Applicant starts to execute the task."), automatic=True) succeed = Transition( [accepted, active, ContributionStateMachine.new], ContributionStateMachine.succeeded, name=_('Succeed'), description=_("Applicant successfully completed the task."), automatic=True, effects=[SetTimeSpent]) mark_absent = Transition( ContributionStateMachine.succeeded, no_show, name=_('Mark absent'), description=_( "Applicant did not contribute to the task and is marked absent."), automatic=False, permission=is_activity_owner, effects=[ ClearTimeSpent, RelatedTransitionEffect( 'activity', 'cancel', conditions=[assignment_is_finished, assignment_will_be_empty]), UnFollowActivityEffect ]) mark_present = Transition( no_show, ContributionStateMachine.succeeded, name=_('Mark present'), description= _("Applicant did contribute to the task, after first been marked absent." ), automatic=False, permission=is_activity_owner, effects=[ SetTimeSpent, RelatedTransitionEffect('activity', 'succeed', conditions=[assignment_is_finished]), FollowActivityEffect ]) reset = Transition( [ ContributionStateMachine.succeeded, accepted, ContributionStateMachine.failed, ], ContributionStateMachine.new, name=_('Reset'), description=_( "The applicant is reset to new after being successful or failed."), effects=[ClearTimeSpent])
class AssignmentStateMachine(ActivityStateMachine): model = Assignment running = State( _('running'), 'running', _('The task is taking place and people can\'t apply any more.')) full = State( _('full'), 'full', _('The number of people needed is reached and people can\'t apply any more.' )) def should_finish(self): """end date has passed""" return self.instance.end and self.instance.end < timezone.now() def should_start(self): """start date has passed""" return self.instance.start and self.instance.start < timezone.now( ) and not self.should_finish() def has_deadline(self): """has a deadline""" return self.instance.end_date_type == 'deadline' def is_on_date(self): """takes place on a set date""" return self.instance.end_date_type == 'on_date' def should_open(self): """registration deadline is in the future""" return self.instance.start and self.instance.start >= timezone.now( ) and not self.should_finish() def has_accepted_applicants(self): """there are accepted applicants""" return len(self.instance.accepted_applicants) > 0 def has_no_accepted_applicants(self): """there are no accepted applicants""" return len(self.instance.accepted_applicants) == 0 def has_new_or_accepted_applicants(self): """there are accepted applicants""" return len(self.instance.accepted_applicants) > 0 or len( self.instance.new_applicants) > 0 def has_no_new_or_accepted_applicants(self): """there are no accepted applicants""" return len(self.instance.accepted_applicants) == 0 and len( self.instance.new_applicants) == 0 def is_not_full(self): """the task is not full""" return (self.instance.capacity and self.instance.capacity > len( self.instance.accepted_applicants)) def is_full(self): """the task is full""" return (self.instance.capacity and self.instance.capacity <= len( self.instance.accepted_applicants)) start = Transition([ActivityStateMachine.open, full], running, name=_('Start'), description=_("Start the activity."), automatic=True, effects=[ RelatedTransitionEffect('accepted_applicants', 'activate'), ]) lock = Transition( [ActivityStateMachine.open], full, automatic=True, name=_('Fill'), description=_( "People can no longer apply. Triggered when the number of accepted people " "equals the number of people needed."), ) auto_approve = Transition( [ActivityStateMachine.submitted, ActivityStateMachine.rejected], ActivityStateMachine.open, name=_('Approve'), automatic=True, description=_( "The task will be visible in the frontend and people can apply to " "the task."), effects=[ RelatedTransitionEffect('organizer', 'succeed'), RelatedTransitionEffect('applicants', 'reset'), TransitionEffect( 'expire', conditions=[should_finish, has_no_accepted_applicants]), ]) reject = Transition( [ ActivityStateMachine.draft, ActivityStateMachine.needs_work, ActivityStateMachine.submitted ], ActivityStateMachine.rejected, name=_('Reject'), description= _('Reject in case this task doesn\'t fit your program or the rules of the game. ' 'The activity owner will not be able to edit the task and it won\'t show up on ' 'the search page in the front end. The task will still be available in the ' 'back office and appear in your reporting.'), automatic=False, permission=ActivityStateMachine.is_staff, effects=[ RelatedTransitionEffect('organizer', 'fail'), NotificationEffect(AssignmentRejectedMessage), ]) cancel = Transition( [ full, running, ActivityStateMachine.succeeded, ActivityStateMachine.open, ], ActivityStateMachine.cancelled, name=_('Cancel'), description= _('Cancel if the task will not be executed. The activity manager will not be able ' 'to edit the task and it won\'t show up on the search page in the front end. The ' 'task will still be available in the back office and appear in your reporting.' ), automatic=False, effects=[ RelatedTransitionEffect('organizer', 'fail'), RelatedTransitionEffect('accepted_applicants', 'fail'), NotificationEffect(AssignmentCancelledMessage), ]) expire = Transition( [ ActivityStateMachine.submitted, ActivityStateMachine.open, ActivityStateMachine.succeeded ], ActivityStateMachine.cancelled, name=_("Expire"), description= _("The tasks didn\'t have any applicants before the deadline and is cancelled." ), effects=[ NotificationEffect(AssignmentExpiredMessage), ]) reopen = Transition( full, ActivityStateMachine.open, name=_('Reopen'), description=_( 'People can apply to the task again. Triggered when the number of accepted people ' 'become less than the number of people needed.'), automatic=True) reschedule = Transition( [ActivityStateMachine.cancelled, ActivityStateMachine.succeeded], ActivityStateMachine.open, name=_('Reschedule'), description=_("Reschedule the activity for new sign-ups. " "Triggered by a changing to a future date."), automatic=True, effects=[ RelatedTransitionEffect('accepted_applicants', 'reaccept'), ]) succeed = Transition( [ ActivityStateMachine.open, full, running, ActivityStateMachine.cancelled ], ActivityStateMachine.succeeded, name=_('Succeed'), description= _('The task ends and the contributions are counted. Triggered when the task date passes.' ), automatic=True, effects=[ RelatedTransitionEffect('accepted_applicants', 'succeed'), RelatedTransitionEffect('new_applicants', 'succeed'), NotificationEffect(AssignmentCompletedMessage) ]) expire = Transition( ActivityStateMachine.open, ActivityStateMachine.cancelled, name=_('Expire'), description= _("The task didn't have any applicants before the deadline to apply and is cancelled." ), automatic=True, effects=[ RelatedTransitionEffect('organizer', 'fail'), NotificationEffect(AssignmentExpiredMessage), ]) restore = Transition( [ ActivityStateMachine.rejected, ActivityStateMachine.cancelled, ActivityStateMachine.deleted, ], ActivityStateMachine.needs_work, name=_("Restore"), automatic=False, description=_("Restore a cancelled, rejected or deleted task."), effects=[ RelatedTransitionEffect('organizer', 'reset'), RelatedTransitionEffect('accepted_applicants', 'fail') ])
class PayoutAccountStateMachine(ModelStateMachine): new = State( _('new'), 'new', _("Payout account was created.") ) pending = State( _('pending'), 'pending', _("Payout account is pending verification.") ) verified = State( _('verified'), 'verified', _("Payout account has been verified.") ) rejected = State( _('rejected'), 'rejected', _("Payout account was rejected.") ) incomplete = State( _('incomplete'), 'incomplete', _("Payout account is missing information or documents.") ) def can_approve(self, user=None): """is staff user""" return not user or user.is_staff def is_reviewed(self): """has been verified""" return self.instance.reviewed def is_unreviewed(self): """has not been verified""" return not self.instance.reviewed initiate = Transition( EmptyState(), new, name=_("Initiate"), description=_("Payout account has been created") ) submit = Transition( [new, incomplete], pending, name=_('Submit'), description=_("Submit payout account for review."), automatic=False ) verify = Transition( [new, incomplete, rejected, pending], verified, name=_('Verify'), description=_("Verify the payout account."), automatic=False, permission=can_approve, effects=[ NotificationEffect(PayoutAccountVerified), RelatedTransitionEffect('external_accounts', 'verify') ] ) reject = Transition( [new, incomplete, verified, pending], rejected, name=_('Reject'), description=_("Reject the payout account."), automatic=False, effects=[ NotificationEffect(PayoutAccountRejected), RelatedTransitionEffect('external_accounts', 'reject') ] ) set_incomplete = Transition( [new, pending, rejected, verified], incomplete, name=_('Set incomplete'), description=_("Mark the payout account as incomplete. The initiator will have to add more information."), automatic=False )
class DonationStateMachine(ContributionStateMachine): model = Donation refunded = State( _('refunded'), 'refunded', _("The contribution was refunded.") ) activity_refunded = State( _('activity refunded'), 'activity_refunded', _("The contribution was refunded because the activity refunded.") ) def is_successful(self): """donation is successful""" return self.instance.status == ContributionStateMachine.succeeded succeed = Transition( [ ContributionStateMachine.new, ContributionStateMachine.failed ], ContributionStateMachine.succeeded, name=_('Succeed'), description=_("The donation has been completed"), automatic=True, effects=[ NotificationEffect(DonationSuccessActivityManagerMessage), NotificationEffect(DonationSuccessDonorMessage), GenerateDonationWallpostEffect, FollowActivityEffect, UpdateFundingAmountsEffect ] ) fail = Transition( [ ContributionStateMachine.new, ContributionStateMachine.succeeded ], ContributionStateMachine.failed, name=_('Fail'), description=_("The donation failed."), automatic=True, effects=[ RemoveDonationWallpostEffect, UpdateFundingAmountsEffect, RemoveDonationFromPayoutEffect ] ) refund = Transition( [ ContributionStateMachine.new, ContributionStateMachine.succeeded, ], refunded, name=_('Refund'), description=_("Refund this donation."), automatic=True, effects=[ RemoveDonationWallpostEffect, UnFollowActivityEffect, UpdateFundingAmountsEffect, RemoveDonationFromPayoutEffect, NotificationEffect(DonationRefundedDonorMessage) ] ) activity_refund = Transition( ContributionStateMachine.succeeded, activity_refunded, name=_('Activity refund'), description=_("Refund the donation, because the entire activity will be refunded."), automatic=True, effects=[ RelatedTransitionEffect('payment', 'request_refund'), NotificationEffect(DonationActivityRefundedDonorMessage) ] )
class FundingStateMachine(ActivityStateMachine): model = Funding partially_funded = State( _('partially funded'), 'partially_funded', _("The campaign has ended and received donations but didn't reach the target.") ) refunded = State( _('refunded'), 'refunded', _("The campaign has ended and all donations have been refunded.") ) cancelled = State( _('cancelled'), 'cancelled', _("The activity has ended without any donations.") ) def should_finish(self): """the deadline has passed""" return self.instance.deadline and self.instance.deadline < timezone.now() def deadline_in_future(self): """the deadline is in the future""" if self.instance.deadline: return self.instance.deadline > timezone.now() return bool(self.instance.duration) def target_reached(self): """target amount has been reached (100% or more)""" if not self.instance.target: return False return self.instance.amount_raised >= self.instance.target def target_not_reached(self): """target amount has not been reached (less then 100%, but more then 0)""" return self.instance.amount_raised.amount and self.instance.amount_raised < self.instance.target def no_donations(self): """no (successful) donations have been made""" return not self.instance.donations.filter(status='succeeded').count() def without_approved_payouts(self): """hasn't got approved payouts""" return not self.instance.payouts.exclude(status__in=['new', 'failed']).count() def can_approve(self, user): """user has the permission to approve (staff member)""" return user.is_staff def psp_allows_refunding(self): """PSP allows refunding through their API""" return self.instance.bank_account and \ self.instance.bank_account.provider_class and \ self.instance.bank_account.provider_class.refund_enabled submit = Transition( [ActivityStateMachine.draft, ActivityStateMachine.needs_work], ActivityStateMachine.submitted, automatic=False, name=_('Submit'), description=_('The campaign will be submitted for review.'), conditions=[ ActivityStateMachine.is_complete, ActivityStateMachine.is_valid, ActivityStateMachine.initiative_is_submitted ], ) approve = Transition( [ ActivityStateMachine.needs_work, ActivityStateMachine.submitted ], ActivityStateMachine.open, name=_('Approve'), description=_('The campaign will be visible in the frontend and people can donate.'), automatic=False, permission=can_approve, conditions=[ ActivityStateMachine.initiative_is_approved, ActivityStateMachine.is_valid, ActivityStateMachine.is_complete ], effects=[ RelatedTransitionEffect('organizer', 'succeed'), SetDateEffect('started'), SetDeadlineEffect, TransitionEffect( 'expire', conditions=[should_finish] ), NotificationEffect(FundingApprovedMessage) ] ) cancel = Transition( [ ActivityStateMachine.open, ], cancelled, name=_('Cancel'), description=_( 'Cancel if the campaign will not be executed. The activity manager ' 'will not be able to edit the campaign and it won\'t show up on the ' 'search page in the front end. The campaign will still be available ' 'in the back office and appear in your reporting.' ), automatic=False, conditions=[ no_donations ], effects=[ RelatedTransitionEffect('organizer', 'fail'), NotificationEffect(FundingCancelledMessage) ] ) request_changes = Transition( [ ActivityStateMachine.submitted ], ActivityStateMachine.needs_work, name=_('Needs work'), description=_( "The status of the campaign will be set to 'Needs work'. The activity manager " "can edit and resubmit the campaign. Don't forget to inform the activity " "manager of the necessary adjustments." ), automatic=False, permission=can_approve ) reject = Transition( [ ActivityStateMachine.submitted, ActivityStateMachine.draft, ActivityStateMachine.needs_work, ], ActivityStateMachine.rejected, name=_('Reject'), description=_( "Reject in case this campaign doesn\'t fit your program or the rules of the game. " "The activity manager will not be able to edit the campaign and it won\'t show up " "on the search page in the front end. The campaign will still be available in the " "back office and appear in your reporting." ), automatic=False, conditions=[ no_donations ], permission=ActivityStateMachine.is_staff, effects=[ RelatedTransitionEffect('organizer', 'fail'), NotificationEffect(FundingRejectedMessage) ] ) expire = Transition( [ ActivityStateMachine.open, ], ActivityStateMachine.cancelled, name=_('Expire'), description=_("The campaign didn't receive any donations before the deadline and is cancelled."), automatic=True, conditions=[ no_donations, ], effects=[ NotificationEffect(FundingExpiredMessage), RelatedTransitionEffect('organizer', 'fail'), ] ) extend = Transition( [ ActivityStateMachine.succeeded, partially_funded, ActivityStateMachine.cancelled, ], ActivityStateMachine.open, name=_('Extend'), description=_("The campaign will be extended and can receive more donations."), automatic=True, conditions=[ without_approved_payouts, deadline_in_future ], effects=[ DeletePayoutsEffect, NotificationEffect(FundingExtendedMessage) ] ) succeed = Transition( [ ActivityStateMachine.open, partially_funded ], ActivityStateMachine.succeeded, name=_('Succeed'), description=_( "The campaign ends and received donations can be payed out. Triggered when " "the deadline passes." ), automatic=True, effects=[ GeneratePayoutsEffect, NotificationEffect(FundingRealisedOwnerMessage) ] ) recalculate = Transition( [ ActivityStateMachine.succeeded, partially_funded ], ActivityStateMachine.succeeded, name=_('Recalculate'), description=_("The amount of donations received has changed and the payouts will be recalculated."), automatic=False, conditions=[ target_reached ], effects=[ GeneratePayoutsEffect ] ) partial = Transition( [ ActivityStateMachine.open, ActivityStateMachine.succeeded ], partially_funded, name=_('Partial'), description=_("The campaign ends but the target isn't reached."), automatic=True, effects=[ GeneratePayoutsEffect, NotificationEffect(FundingPartiallyFundedMessage) ] ) refund = Transition( [ ActivityStateMachine.succeeded, partially_funded ], refunded, name=_('Refund'), description=_("The campaign will be refunded and all donations will be returned to the donors."), automatic=False, conditions=[ psp_allows_refunding, without_approved_payouts ], effects=[ RelatedTransitionEffect('donations', 'activity_refund'), DeletePayoutsEffect, NotificationEffect(FundingRefundedMessage) ] )
class EventStateMachine(ActivityStateMachine): model = Event def is_full(self): "the event is full" return self.instance.capacity == len(self.instance.participants) def is_not_full(self): "the event is not full" return self.instance.capacity > len(self.instance.participants) def should_finish(self): "the end time has passed" return self.instance.current_end and self.instance.current_end < timezone.now( ) def should_start(self): "the start time has passed" return self.instance.start and self.instance.start < timezone.now( ) and not self.should_finish() def should_open(self): "the start time has not passed" return self.instance.start and self.instance.start > timezone.now() def has_participants(self): """there are participants""" return len(self.instance.participants) > 0 def has_no_participants(self): """there are no participants""" return len(self.instance.participants) == 0 full = State(_('full'), 'full', _('Submit the activity for approval.')) running = State( _('running'), 'running', _('The event is taking place and people can\'t join any more.')) submit = Transition( [ ActivityStateMachine.draft, ActivityStateMachine.needs_work, ], ActivityStateMachine.submitted, description=_('Submit the activity for approval.'), automatic=False, name=_('Submit'), conditions=[ ActivityStateMachine.is_complete, ActivityStateMachine.is_valid, ActivityStateMachine.initiative_is_submitted ], effects=[ TransitionEffect('auto_approve', conditions=[ ActivityStateMachine.initiative_is_approved, should_open ]), TransitionEffect('expire', conditions=[should_start, has_no_participants]), TransitionEffect('expire', conditions=[should_finish, has_no_participants]), TransitionEffect('succeed', conditions=[should_finish, has_participants]), ]) auto_approve = Transition( [ActivityStateMachine.submitted, ActivityStateMachine.rejected], ActivityStateMachine.open, name=_('Approve'), automatic=True, description= _("The event will be visible in the frontend and people can join the event." ), effects=[ RelatedTransitionEffect('organizer', 'succeed'), TransitionEffect('expire', conditions=[should_start, has_no_participants]), TransitionEffect('expire', conditions=[should_finish, has_no_participants]), TransitionEffect('succeed', conditions=[should_finish, has_participants]), ]) cancel = Transition( [ ActivityStateMachine.open, running, full, ActivityStateMachine.succeeded, ], ActivityStateMachine.cancelled, name=_('Cancel'), description= _('Cancel if the event will not be executed. The activity manager will not be ' 'able to edit the event and it won\'t show up on the search page in the front end. ' 'The event will still be available in the back office and appear in your reporting.' ), automatic=False, effects=[ RelatedTransitionEffect('organizer', 'fail'), RelatedTransitionEffect('participants', 'fail'), NotificationEffect(EventCancelledMessage), ]) lock = Transition( [ActivityStateMachine.open, ActivityStateMachine.succeeded], full, name=_("Lock"), description= _("People can no longer join the event. Triggered when the attendee limit is reached." )) reopen = Transition( full, ActivityStateMachine.open, name=_("Reopen"), description=_( "People can join the event again. Triggered when the number of attendees become " "less than the attendee limit.")) reschedule = Transition( [ running, ActivityStateMachine.cancelled, ActivityStateMachine.succeeded ], ActivityStateMachine.open, name=_("Reschedule"), description=_( "People can join the event again, because the date has changed."), effects=[RelatedTransitionEffect('participants', 'reset')]) start = Transition([ActivityStateMachine.open, full], running, name=_("Start"), description=_("Start the event.")) expire = Transition( [ ActivityStateMachine.submitted, ActivityStateMachine.open, ActivityStateMachine.succeeded ], ActivityStateMachine.cancelled, name=_("Expire"), description= _("The event didn\'t have any attendees before the start time and is cancelled." ), effects=[ NotificationEffect(EventExpiredMessage), ]) succeed = Transition( [ full, running, ActivityStateMachine.open, ActivityStateMachine.submitted, ActivityStateMachine.rejected, ActivityStateMachine.cancelled ], ActivityStateMachine.succeeded, name=_("Succeed"), description=_( "The event ends and the contributions are counted. Triggered when the event " "end time passes."), effects=[ NotificationEffect(EventSucceededOwnerMessage), RelatedTransitionEffect('participants', 'succeed') ]) reject = Transition( [ ActivityStateMachine.draft, ActivityStateMachine.needs_work, ActivityStateMachine.submitted ], ActivityStateMachine.rejected, name=_('Reject'), description= _('Reject in case this event doesn\'t fit your program or the rules of the game. ' 'The activity owner will not be able to edit the event and it won\'t show up on ' 'the search page in the front end. The event will still be available in the ' 'back office and appear in your reporting.'), automatic=False, permission=ActivityStateMachine.is_staff, effects=[ RelatedTransitionEffect('organizer', 'fail'), NotificationEffect(EventRejectedOwnerMessage), ]) restore = Transition( [ ActivityStateMachine.rejected, ActivityStateMachine.cancelled, ActivityStateMachine.deleted, ], ActivityStateMachine.needs_work, name=_("Restore"), automatic=False, description=_( "The status of the event is set to 'needs work'. The activity owner can edit " "the event again."), effects=[RelatedTransitionEffect('contributions', 'reset')])
class ParticipantStateMachine(ContributionStateMachine): model = Participant withdrawn = State( _('withdrawn'), 'withdrawn', _("The participant withdrew and will no longer attend the activity")) rejected = State(_('rejected'), 'rejected', _("The participant was rejected and will not attend.")) no_show = State( _('no show'), 'no_show', _("The participant didn't attend the event and was marked absent.")) new = State(_('Joined'), 'new', _("The participant signed up for the event.")) def is_user(self, user): """is the participant""" return self.instance.user == user def is_activity_owner(self, user): """is the activity manager or a staff member""" return user.is_staff or self.instance.activity.owner == user def event_will_become_full(self): "event will be full" activity = self.instance.activity return activity.capacity == len(activity.participants) + 1 def event_will_become_open(self): "event will not be full" activity = self.instance.activity return activity.capacity == len(activity.participants) def event_is_finished(self): "event is finished" return self.instance.activity.current_end < timezone.now() def event_is_not_finished(self): "event is not finished" return not self.instance.activity.start < timezone.now() def event_will_be_empty(self): "event will be empty" return self.instance.activity.participants.exclude( id=self.instance.id).count() == 0 initiate = Transition( EmptyState(), ContributionStateMachine.new, name=_("Join"), description=_( "Participant is created. User signs up for the activity."), effects=[ TransitionEffect('succeed', conditions=[event_is_finished]), RelatedTransitionEffect('activity', 'lock', conditions=[event_will_become_full]), RelatedTransitionEffect('activity', 'succeed', conditions=[event_is_finished]), NotificationEffect(ParticipantApplicationManagerMessage), NotificationEffect(ParticipantApplicationMessage), FollowActivityEffect, ]) withdraw = Transition( ContributionStateMachine.new, withdrawn, name=_('Withdraw'), description=_("Participant withdraws from the activity."), automatic=False, permission=is_user, effects=[ RelatedTransitionEffect('activity', 'reopen', conditions=[event_will_become_open]), UnFollowActivityEffect ]) reapply = Transition( withdrawn, ContributionStateMachine.new, name=_('Join again'), description= _("Participant signs up for the activity again, after previously withdrawing." ), automatic=False, permission=is_user, effects=[ TransitionEffect('succeed', conditions=[event_is_finished]), RelatedTransitionEffect('activity', 'lock', conditions=[event_will_become_full]), NotificationEffect(ParticipantApplicationManagerMessage), NotificationEffect(ParticipantApplicationMessage), FollowActivityEffect ]) reject = Transition(ContributionStateMachine.new, rejected, automatic=False, name=_('Reject'), description=_("Participant is rejected."), effects=[ RelatedTransitionEffect('activity', 'reopen'), NotificationEffect(ParticipantRejectedMessage), UnFollowActivityEffect ], permission=is_activity_owner) accept = Transition( rejected, ContributionStateMachine.new, name=_('Accept'), description=_("Accept a participant after previously being rejected."), automatic=False, effects=[ TransitionEffect('succeed', conditions=[event_is_finished]), RelatedTransitionEffect('activity', 'lock', conditions=[event_will_become_full]), NotificationEffect(ParticipantApplicationMessage), FollowActivityEffect ], permission=is_activity_owner) mark_absent = Transition( ContributionStateMachine.succeeded, no_show, name=_('Mark absent'), description= _("The participant didn't show up at the activity and is marked absent." ), automatic=False, permission=is_activity_owner, effects=[ ResetTimeSpent, RelatedTransitionEffect( 'activity', 'expire', conditions=[event_is_finished, event_will_be_empty]), UnFollowActivityEffect ]) mark_present = Transition( no_show, ContributionStateMachine.succeeded, name=_('Mark present'), description=_( "The participant showed up, after previously marked absent."), automatic=False, permission=is_activity_owner, effects=[ SetTimeSpent, RelatedTransitionEffect('activity', 'succeed', conditions=[event_is_finished]), FollowActivityEffect ]) succeed = Transition( ContributionStateMachine.new, ContributionStateMachine.succeeded, name=_('Succeed'), description=_( "The participant successfully took part in the activity."), effects=[ SetTimeSpent, RelatedTransitionEffect('activity', 'succeed', conditions=[event_is_finished]) ]) reset = Transition( [ ContributionStateMachine.succeeded, ContributionStateMachine.failed, ], ContributionStateMachine.new, name=_('Reset'), description=_( "The participant is reset to new after being successful or failed." ), effects=[ResetTimeSpent, FollowActivityEffect]) fail = Transition( ( ContributionStateMachine.new, ContributionStateMachine.succeeded, ContributionStateMachine.failed, ), ContributionStateMachine.failed, name=_('fail'), description=_( "The contribution failed. It will not be visible in reports."), effects=[ResetTimeSpent, UnFollowActivityEffect])
class ReviewStateMachine(ModelStateMachine): field = 'status' model = Initiative draft = State( _('draft'), 'draft', _('The initiative has been created and is being worked on.') ) submitted = State( _('submitted'), 'submitted', _('The initiative has been submitted and is ready to be reviewed.') ) needs_work = State( _('needs work'), 'needs_work', _('The initiative has been submitted but needs adjustments in order to be approved.') ) rejected = State( _('rejected'), 'rejected', _("The initiative doesn't fit the program or the rules of the game. " "The initiative won't show up on the search page in the front end, " "but does count in the reporting. " "The initiative cannot be edited by the initiator.") ) cancelled = State( _('cancelled'), 'cancelled', _("The initiative is not executed. " "The initiative won't show up on the search page in the front end, " "but does count in the reporting. " "The initiative cannot be edited by the initiator.") ) deleted = State( _('deleted'), 'deleted', _('The initiative is not visible in the frontend and does not count in the reporting. ' 'The initiative cannot be edited by the initiator.') ) approved = State( _('approved'), 'approved', _('The initiative is visible in the frontend and completed activities are open for contributions. ' 'All activities, except the crowdfunding campaigns, ' 'that will be completed at a later stage, ' 'will also be automatically opened up for contributions. ' 'The crowdfunding campaigns must be approved separately.') ) def is_complete(self): """The initiative is complete""" if self.instance.organization and list(self.instance.organization.required): return False if self.instance.organization_contact and list(self.instance.organization_contact.required): return False return not list(self.instance.required) def is_valid(self): """The initiative is valid""" if self.instance.organization and list(self.instance.organization.errors): return False if self.instance.organization_contact and list(self.instance.organization_contact.errors): return False return not list(self.instance.errors) def is_staff(self, user): return user.is_staff initiate = Transition( EmptyState(), draft, name=_('Start'), description=_('The initiative will be created.'), ) submit = Transition( [draft, needs_work], submitted, name=_('Submit'), description=_("The initiative will be submitted for review."), conditions=[is_complete, is_valid], automatic=False, effects=[ RelatedTransitionEffect('activities', 'auto_submit'), ] ) approve = Transition( submitted, approved, name=_('Approve'), description=_("The initiative will be visible in the frontend and " "all completed activities will be open for contributions."), conditions=[is_complete, is_valid], automatic=False, permission=is_staff, effects=[ RelatedTransitionEffect('activities', 'auto_approve'), NotificationEffect(InitiativeApprovedOwnerMessage) ] ) request_changes = Transition( submitted, needs_work, name=_('Needs work'), description=_("The status of the initiative is set to 'Needs work'. " "The initiator can edit and resubmit the initiative. " "Don't forget to inform the initiator of the necessary adjustments."), conditions=[], automatic=False, ) reject = Transition( [ draft, submitted, needs_work, ], rejected, name=_('Reject'), description=_("Reject in case this initiative doesn't fit your program or the rules of the game. " "The initiator will not be able to edit the initiative and " "it won't show up on the search page in the front end. " "The initiative will still be available in the back office and appear in your reporting. "), automatic=False, permission=is_staff, effects=[ RelatedTransitionEffect('activities', 'reject'), NotificationEffect(InitiativeRejectedOwnerMessage) ] ) cancel = Transition( approved, cancelled, name=_('Cancel'), description=_("Cancel if the initiative will not be executed. " "The initiator will not be able to edit the initiative and " "it won't show up on the search page in the front end. " "The initiative will still be available in the back office and appear in your reporting."), automatic=False, effects=[ RelatedTransitionEffect('activities', 'cancel'), NotificationEffect(InitiativeCancelledOwnerMessage) ] ) delete = Transition( draft, deleted, name=_('Delete'), description=_("Delete the initiative if you don't want it to appear in your reporting. " "The initiative will still be available in the back office."), automatic=False, hide_from_admin=True, effects=[ RelatedTransitionEffect('activities', 'delete'), ] ) restore = Transition( [ rejected, cancelled, deleted ], needs_work, name=_('Restore'), description=_("The status of the initiative is set to 'needs work'. " "The initiator can edit and submit the initiative again."), automatic=False, permission=is_staff, effects=[ RelatedTransitionEffect('activities', 'restore'), ] )