class StripeSourcePaymentStateMachine(BasePaymentStateMachine): model = StripeSourcePayment charged = State(_('charged'), 'charged') canceled = State(_('canceled'), 'canceled') disputed = State(_('disputed'), 'disputed') def has_charge_token(self): return bool(self.instance.charge_token) def is_not_refunded(self): return self.instance.status not in ['refunded', 'disputed'] authorize = Transition( [BasePaymentStateMachine.new, charged], BasePaymentStateMachine.pending, name=_('Authorize'), automatic=True, effects=[RelatedTransitionEffect('donation', 'succeed')]) succeed = Transition([ BasePaymentStateMachine.new, BasePaymentStateMachine.pending, charged ], BasePaymentStateMachine.succeeded, name=_('Succeed'), automatic=True, effects=[ RelatedTransitionEffect('donation', 'succeed') ]) charge = Transition(BasePaymentStateMachine.new, charged, name=_('Charge'), automatic=True, conditions=[has_charge_token]) cancel = Transition(BasePaymentStateMachine.new, canceled, name=_('Canceled'), automatic=True, effects=[RelatedTransitionEffect('donation', 'fail')]) dispute = Transition( [ BasePaymentStateMachine.new, BasePaymentStateMachine.succeeded, ], disputed, name=_('Dispute'), automatic=True, effects=[RelatedTransitionEffect('donation', 'refund')])
class ContributionStateMachine(ModelStateMachine): new = State(_('new'), 'new', _("The user started a contribution")) succeeded = State(_('succeeded'), 'succeeded', _("The contribution was successful.")) failed = State(_('failed'), 'failed', _("The contribution failed.")) def is_user(self, user): return self.instance.user == user initiate = Transition(EmptyState(), new, name=_('initiate'), description=_('The contribution was created.')) fail = Transition( ( new, succeeded, failed, ), failed, name=_('fail'), description=_( "The contribution failed. It will not be visible in reports."), )
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 BankAccountStateMachine(ModelStateMachine): verified = State( _('verified'), 'verified', _("Bank account is verified") ) incomplete = State( _('incomplete'), 'incomplete', _("Bank account details are missing or incorrect") ) unverified = State( _('unverified'), 'unverified', _("Bank account still needs to be verified") ) rejected = State( _('rejected'), 'rejected', _("Bank account is rejected") ) initiate = Transition( EmptyState(), unverified, name=_("Initiate"), description=_("Bank account details are entered.") ) request_changes = Transition( [verified, unverified], incomplete, name=_('Request changes'), description=_("Bank account is missing details"), automatic=False ) reject = Transition( [verified, unverified, incomplete], rejected, name=_('Reject'), description=_("Reject bank account"), automatic=False, effects=[ RelatedTransitionEffect( 'connect_account', 'reject', description='Reject connected KYC account' ) ] ) verify = Transition( [incomplete, unverified], verified, name=_('Verify'), description=_("Verify that the bank account is complete."), automatic=False, effects=[ SubmitConnectedActivitiesEffect, RelatedTransitionEffect( 'connect_account', 'verify', description='Verify connected KYC account' ) ] )
class PayoutStateMachine(ModelStateMachine): model = Payout new = State( _('new'), 'new', _("Payout has been created") ) approved = State( _('approved'), 'approved', _("Payout has been approved and send to the payout app.") ) scheduled = State( _('scheduled'), 'scheduled', _("Payout has been received by the payout app.") ) started = State( _('started'), 'started', _("Payout was started.") ) succeeded = State( _('succeeded'), 'succeeded', _("Payout was completed successfully.") ) failed = State( _('failed'), 'failed', _("Payout failed.") ) initiate = Transition( EmptyState(), new, name=_("Initiate"), description=_("Create the payout") ) approve = Transition( [new, approved], approved, name=_('Approve'), description=_("Approve the payout so it will be scheduled for execution."), automatic=False, effects=[ SubmitPayoutEffect, SetDateEffect('date_approved') ] ) schedule = Transition( AllStates(), scheduled, name=_('Schedule'), description=_("Schedule payout. Triggered by payout app."), automatic=True, effects=[ ClearPayoutDatesEffect ] ) start = Transition( AllStates(), started, name=_('Start'), description=_("Start payout. Triggered by payout app."), automatic=True, effects=[ SetDateEffect('date_started') ] ) reset = Transition( AllStates(), new, name=_('Reset'), description=_("Payout was rejected by the payout app. " "Adjust information as needed an approve the payout again."), automatic=True, effects=[ ClearPayoutDatesEffect ] ) succeed = Transition( AllStates(), succeeded, name=_('Succeed'), description=_("Payout was successful. Triggered by payout app."), automatic=True, effects=[ SetDateEffect('date_completed') ] ) fail = Transition( AllStates(), failed, name=_('Fail'), description=_("Payout was not successful. " "Contact support to resolve the issue."), automatic=True, )
class BasePaymentStateMachine(ModelStateMachine): new = State( _('new'), 'new', _("Payment was started.") ) pending = State( _('pending'), 'pending', _("Payment is authorised and will probably succeed shortly.") ) succeeded = State( _('succeeded'), 'succeeded', _("Payment is successful.") ) failed = State( _('failed'), 'failed', _("Payment failed.") ) refunded = State( _('refunded'), 'refunded', _("Payment was refunded.") ) refund_requested = State( _('refund requested'), 'refund_requested', _("Platform requested the payment to be refunded. Waiting for payment provider the confirm the refund") ) def donation_not_refunded(self): """donation doesn't have status refunded or activity refunded""" return self.instance.donation.status not in [ DonationStateMachine.refunded.value, DonationStateMachine.activity_refunded.value, ] initiate = Transition( EmptyState(), new, name=_("Initiate"), description=_("Payment started.") ) authorize = Transition( [new], pending, name=_('Authorise'), description=_("Payment has been authorized."), automatic=True, effects=[ RelatedTransitionEffect('donation', 'succeed') ] ) succeed = Transition( [new, pending, failed, refund_requested], succeeded, name=_('Succeed'), description=_("Payment has been completed."), automatic=True, effects=[ RelatedTransitionEffect('donation', 'succeed') ] ) fail = Transition( AllStates(), failed, name=_('Fail'), description=_("Payment failed."), automatic=True, effects=[ RelatedTransitionEffect('donation', 'fail') ] ) request_refund = Transition( succeeded, refund_requested, name=_('Request refund'), description=_("Request to refund the payment."), automatic=False, effects=[ RefundPaymentAtPSPEffect ] ) refund = Transition( [ new, succeeded, refund_requested ], refunded, name=_('Refund'), description=_("Payment was refunded."), automatic=True, effects=[ RelatedTransitionEffect( 'donation', 'refund', conditions=[ donation_not_refunded ] ), ] )
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 ActivityStateMachine(ModelStateMachine): draft = State( _('draft'), 'draft', _('The activity has been created, but not yet completed. The activity manager is still editing the activity.' )) submitted = State( _('submitted'), 'submitted', _('The activity is ready to go online once the initiative has been approved.' )) needs_work = State( _('needs work'), 'needs_work', _('The activity has been submitted but needs adjustments in order to be approved.' )) rejected = State( _('rejected'), 'rejected', _('The activity doesn\'t fit the program or the rules of the game. ' 'The activity won\'t show up on the search page in the front end, ' 'but does count in the reporting. The activity cannot be edited by the activity manager.' )) deleted = State( _('deleted'), 'deleted', _('The activity is not visible in the frontend and does not count in the reporting. ' 'The activity cannot be edited by the activity manager.')) cancelled = State( _('cancelled'), 'cancelled', _('The activity is not executed. The activity won\'t show up on the search page ' 'in the front end, but does count in the reporting. The activity cannot be ' 'edited by the activity manager.')) open = State(_('open'), 'open', _('The activity is accepting new contributions.')) succeeded = State(_('succeeded'), 'succeeded', _('The activity has ended successfully.')) def is_complete(self): """all required information has been submitted""" return not list(self.instance.required) def is_valid(self): """all fields passed validation and are correct""" return not list(self.instance.errors) def initiative_is_approved(self): """the initiative has been approved""" return self.instance.initiative.status == 'approved' def initiative_is_submitted(self): """the initiative has been submitted""" return self.instance.initiative.status in ('submitted', 'approved') def initiative_is_not_approved(self): """the initiative has not yet been approved""" return not self.initiative_is_approved() def is_staff(self, user): """user is a staff member""" return user.is_staff def is_owner(self, user): """user is the owner""" return user == self.instance.owner initiate = Transition(EmptyState(), draft, name=_('Start'), description=_('The acivity will be created.'), effects=[CreateOrganizer]) auto_submit = Transition( [ draft, needs_work, ], submitted, description=_('The acivity will be submitted for review.'), automatic=True, name=_('Submit'), conditions=[is_complete, is_valid], ) submit = Transition( [ draft, needs_work, ], submitted, description=_('Submit the activity for approval.'), automatic=False, name=_('Submit'), conditions=[is_complete, is_valid, initiative_is_submitted], effects=[ TransitionEffect('auto_approve', conditions=[initiative_is_approved]) ]) reject = Transition( [draft, needs_work, submitted], rejected, name=_('Reject'), description= _('Reject in case this acivity doesn\'t fit your program or the rules of the game. ' 'The activity manager will not be able to edit the activity and it won\'t show up ' 'on the search page in the front end. The activity will still be available in the ' 'back office and appear in your reporting.'), automatic=False, permission=is_staff, effects=[RelatedTransitionEffect('organizer', 'fail')]) cancel = Transition([ open, succeeded, ], cancelled, name=_('Cancel'), description=_('Cancel the activity.'), automatic=False, effects=[RelatedTransitionEffect('organizer', 'fail')]) restore = Transition( [rejected, cancelled, deleted], needs_work, name=_('Restore'), description=_( 'The status of the activity is set to "Needs work". The activity manager can edit ' 'the activity again.'), automatic=False, permission=is_staff, effects=[RelatedTransitionEffect('organizer', 'reset')]) delete = Transition( [draft], deleted, name=_('Delete'), automatic=False, permission=is_owner, hide_from_admin=True, description=_( 'Delete the activity if you don\'t want it to appear in your reporting. ' 'The activity will still be available in the back office.'), effects=[RelatedTransitionEffect('organizer', 'fail')]) succeed = Transition( open, succeeded, name=_('Succeed'), automatic=True, )
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'), ] )