예제 #1
0
class AssignmentRegistrationReminderTask(ModelPeriodicTask):
    def get_queryset(self):
        return self.model.objects.filter(
            date__lte=timezone.now() + timedelta(days=5),
            status__in=[
                AssignmentStateMachine.full, AssignmentStateMachine.open
            ])

    def is_on_date(assignment):
        """task is on a specific date"""
        return getattr(assignment, 'end_date_type') == 'on_date'

    def has_deadline(assignment):
        """task has a deadline"""
        return getattr(assignment, 'end_date_type') == 'deadline'

    effects = [
        NotificationEffect(AssignmentReminderDeadline,
                           conditions=[has_deadline]),
        NotificationEffect(AssignmentReminderOnDate, conditions=[is_on_date])
    ]

    def __str__(self):
        return str(
            _("Send a reminder if the task deadline/date is in 5 days."))
예제 #2
0
class PlainPayoutAccountStateMachine(PayoutAccountStateMachine):
    model = PlainPayoutAccount
    verify = Transition(
        [
            PayoutAccountStateMachine.new,
            PayoutAccountStateMachine.pending,
            PayoutAccountStateMachine.incomplete,
            PayoutAccountStateMachine.rejected
        ],
        PayoutAccountStateMachine.verified,
        name=_('Verify'),
        description=_("Verify the KYC account. You will hereby confirm that you verified the users identity."),
        automatic=False,
        permission=PayoutAccountStateMachine.can_approve,
        effects=[
            NotificationEffect(PayoutAccountVerified),
            DeleteDocumentEffect
        ]
    )
    reject = Transition(
        [
            PayoutAccountStateMachine.new,
            PayoutAccountStateMachine.incomplete,
            PayoutAccountStateMachine.verified
        ],
        PayoutAccountStateMachine.rejected,
        name=_('Reject'),
        description=_("Reject the payout account. The uploaded ID scan "
                      "will be removed with this step."),
        automatic=False,
        effects=[
            NotificationEffect(PayoutAccountRejected),
            DeleteDocumentEffect
        ]
    )
예제 #3
0
 def test_notification_effect(self):
     user = BlueBottleUserFactory.create(
         email='*****@*****.**'
     )
     event = EventFactory.create(
         title='Bound to fail',
         owner=user
     )
     subject = 'Your event "Bound to fail" has been rejected'
     effect = NotificationEffect(EventRejectedOwnerMessage)(event)
     self.assertEqual(str(effect), 'Message {} to [email protected]'.format(subject))
     effect.execute()
     self.assertEqual(mail.outbox[0].subject, subject)
예제 #4
0
class EventReminderTask(ModelPeriodicTask):
    def get_queryset(self):
        return self.model.objects.filter(start__lte=timezone.now() +
                                         timedelta(days=5),
                                         status__in=['open', 'full'])

    effects = [NotificationEffect(EventReminderMessage)]

    def __str__(self):
        return str(_("Send a reminder five days before the event starts."))
예제 #5
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]),
    ]
예제 #6
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
        ]),
    ]
예제 #7
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])
예제 #8
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')
        ])
예제 #9
0
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
    )
예제 #10
0
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)
        ]
    )
예제 #11
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)
        ]
    )
예제 #12
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')])
예제 #13
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])
예제 #14
0
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'),
        ]
    )