Example #1
0
class DeadlineChangedTrigger(ModelChangedTrigger):
    field = 'deadline'

    effects = [
        TransitionEffect('extend',
                         conditions=[
                             FundingStateMachine.is_complete,
                             FundingStateMachine.is_valid,
                             FundingStateMachine.deadline_in_future,
                             FundingStateMachine.without_approved_payouts
                         ]),
        TransitionEffect('succeed',
                         conditions=[
                             FundingStateMachine.should_finish,
                             FundingStateMachine.target_reached
                         ]),
        TransitionEffect('partial',
                         conditions=[
                             FundingStateMachine.should_finish,
                             FundingStateMachine.target_not_reached
                         ]),
        TransitionEffect('cancel',
                         conditions=[
                             FundingStateMachine.should_finish,
                             FundingStateMachine.no_donations
                         ]),
    ]
Example #2
0
class AssignmentRegistrationOnDateTask(ModelPeriodicTask):
    def get_queryset(self):
        return self.model.objects.filter(
            registration_deadline__lt=timezone.now(),
            end_date_type='on_date',
            status__in=[
                AssignmentStateMachine.full, AssignmentStateMachine.open
            ])

    effects = [
        TransitionEffect(
            'lock',
            conditions=[AssignmentStateMachine.has_new_or_accepted_applicants
                        ]),
        TransitionEffect(
            'expire',
            conditions=[
                AssignmentStateMachine.has_no_new_or_accepted_applicants
            ]),
    ]

    def __str__(self):
        return str(
            _("Make sure users can't sign up after registration date has passed on a task with a set date."
              ))
Example #3
0
class CapacityChangedTrigger(ModelChangedTrigger):
    field = 'capacity'

    effects = [
        TransitionEffect('reopen',
                         conditions=[AssignmentStateMachine.is_not_full]),
        TransitionEffect('lock', conditions=[AssignmentStateMachine.is_full]),
    ]
Example #4
0
class TimeSpentChangedTrigger(ModelChangedTrigger):
    field = 'time_spent'

    effects = [
        TransitionEffect('mark_present',
                         conditions=[ApplicantStateMachine.has_time_spent]),
        TransitionEffect('mark_absent',
                         conditions=[ApplicantStateMachine.has_no_time_spent]),
    ]
Example #5
0
class AccountReviewedTrigger(ModelChangedTrigger):
    field = 'reviewed'

    effects = [
        TransitionEffect(
            'verify', conditions=[PlainPayoutAccountStateMachine.is_reviewed]),
        TransitionEffect(
            'reject',
            conditions=[PlainPayoutAccountStateMachine.is_unreviewed]),
    ]
class FundingFinishedTask(ModelPeriodicTask):
    def get_queryset(self):
        return self.model.objects.filter(deadline__lte=timezone.now(),
                                         status='open')

    effects = [
        TransitionEffect('succeed',
                         conditions=[FundingStateMachine.target_reached]),
        TransitionEffect('partial',
                         conditions=[FundingStateMachine.target_not_reached]),
        TransitionEffect('expire',
                         conditions=[FundingStateMachine.no_donations]),
    ]

    def __str__(self):
        return str(_("Campaign deadline has passed."))
Example #7
0
class StripeBankAccountStateMachine(BankAccountStateMachine):
    model = ExternalAccount

    def account_verified(self):
        """the related connect account is verified"""
        return self.instance.connect_account and self.instance.connect_account.status == 'verified'

    initiate = Transition(
        EmptyState(),
        BankAccountStateMachine.unverified,
        name=_("Initiate"),
        description=_("Bank account details are entered."),
        effects=[TransitionEffect('verify', conditions=[account_verified])])

    reject = Transition([
        BankAccountStateMachine.verified, BankAccountStateMachine.unverified,
        BankAccountStateMachine.incomplete
    ],
                        BankAccountStateMachine.rejected,
                        name=_('Reject'),
                        description=_("Reject bank account"),
                        automatic=True)

    verify = Transition(
        [
            BankAccountStateMachine.incomplete,
            BankAccountStateMachine.unverified
        ],
        BankAccountStateMachine.verified,
        name=_('Verify'),
        description=_("Verify that the bank account is complete."),
        automatic=True,
        effects=[SubmitConnectedActivitiesEffect])
Example #8
0
class RegistrationDeadlineChangedTrigger(ModelChangedTrigger):
    field = 'registration_deadline'

    effects = [
        TransitionEffect('reschedule',
                         conditions=[AssignmentStateMachine.should_open]),
        TransitionEffect('start',
                         conditions=[
                             AssignmentStateMachine.should_start,
                             AssignmentStateMachine.has_accepted_applicants
                         ]),
        TransitionEffect('expire',
                         conditions=[
                             AssignmentStateMachine.should_start,
                             AssignmentStateMachine.has_no_accepted_applicants
                         ]),
    ]
Example #9
0
class AmountChangedTrigger(ModelChangedTrigger):
    field = 'target'

    effects = [
        TransitionEffect('succeed',
                         conditions=[
                             FundingStateMachine.should_finish,
                             FundingStateMachine.target_reached
                         ]),
        TransitionEffect('partial',
                         conditions=[
                             FundingStateMachine.should_finish,
                             FundingStateMachine.target_not_reached
                         ]),
        TransitionEffect('cancel',
                         conditions=[
                             FundingStateMachine.should_finish,
                             FundingStateMachine.no_donations
                         ]),
    ]
Example #10
0
class EventFinishedTask(ModelPeriodicTask):
    def get_queryset(self):
        return self.model.objects.filter(
            end__lte=timezone.now(), status__in=['running', 'open', 'full'])

    effects = [
        TransitionEffect('succeed',
                         conditions=[
                             EventStateMachine.should_finish,
                             EventStateMachine.has_participants
                         ]),
        TransitionEffect('cancel',
                         conditions=[
                             EventStateMachine.should_finish,
                             EventStateMachine.has_no_participants
                         ]),
    ]

    def __str__(self):
        return str(_("Finish an event when end time has passed."))
Example #11
0
class EventStartTask(ModelPeriodicTask):
    def get_queryset(self):
        return self.model.objects.filter(start__lte=timezone.now(),
                                         status__in=['open', 'full'])

    effects = [
        TransitionEffect('start',
                         conditions=[
                             EventStateMachine.should_start,
                             EventStateMachine.has_participants
                         ]),
        TransitionEffect('expire',
                         conditions=[
                             EventStateMachine.should_start,
                             EventStateMachine.has_no_participants
                         ]),
    ]

    def __str__(self):
        return str(_("Start an event when start time ha passed."))
Example #12
0
class AssignmentFinishedDeadlineTask(ModelPeriodicTask):
    def get_queryset(self):
        return self.model.objects.filter(date__lt=timezone.now(),
                                         end_date_type='deadline',
                                         status__in=[
                                             AssignmentStateMachine.running,
                                             AssignmentStateMachine.full,
                                             AssignmentStateMachine.open
                                         ])

    effects = [
        TransitionEffect(
            'succeed',
            conditions=[AssignmentStateMachine.has_new_or_accepted_applicants
                        ]),
        TransitionEffect(
            'expire',
            conditions=[
                AssignmentStateMachine.has_no_new_or_accepted_applicants
            ]),
    ]

    def __str__(self):
        return str(_("Finish a task when deadline has passed."))
Example #13
0
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]),
    ]
Example #14
0
class AssignmentFinishedOnDateTask(ModelPeriodicTask):
    def get_queryset(self):
        return self.model.objects.filter(
            date__lte=timezone.now() - timedelta(hours=1) * F('duration'),
            end_date_type='on_date',
            status__in=[
                AssignmentStateMachine.running, AssignmentStateMachine.full,
                AssignmentStateMachine.open
            ])

    effects = [
        TransitionEffect(
            'succeed',
            conditions=[AssignmentStateMachine.has_new_or_accepted_applicants
                        ]),
        TransitionEffect(
            'expire',
            conditions=[
                AssignmentStateMachine.has_no_new_or_accepted_applicants
            ]),
    ]

    def __str__(self):
        return str(_("Finish a task after it has ended (date + duration)."))
Example #15
0
class AssignmentStartOnDateTask(ModelPeriodicTask):
    def get_queryset(self):
        return self.model.objects.filter(date__lte=timezone.now(),
                                         end_date_type='on_date',
                                         status__in=[
                                             AssignmentStateMachine.full,
                                             AssignmentStateMachine.open
                                         ])

    effects = [
        TransitionEffect(
            'start',
            conditions=[AssignmentStateMachine.has_accepted_applicants]),
    ]

    def __str__(self):
        return str(_("Start a task on a set date."))
Example #16
0
class AssignmentStartDeadlineTask(ModelPeriodicTask):
    def get_queryset(self):
        return self.model.objects.filter(
            registration_deadline__lte=timezone.now(),
            end_date_type='deadline',
            status__in=[
                AssignmentStateMachine.full, AssignmentStateMachine.open
            ])

    effects = [
        TransitionEffect(
            'start',
            conditions=[AssignmentStateMachine.has_accepted_applicants]),
    ]

    def __str__(self):
        return str(
            _("Start a task with deadline after registration deadline has passed."
              ))
Example #17
0
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
        ]),
    ]
Example #18
0
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])
Example #19
0
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')
        ])
Example #20
0
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)
        ]
    )
Example #21
0
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,
    )
Example #22
0
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')])
Example #23
0
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])