Пример #1
0
class Event(models.Model):
    id = HashidAutoField(primary_key=True, )
    STATE = Choices(
        (0, 'new', 'New'),
        (10, 'active', 'Active'),
        (20, 'archived', 'Archived'),
    )
    state = FSMIntegerField(
        choices=STATE,
        default=STATE.new,
    )
    name = models.CharField(
        max_length=100,
        blank=False,
    )
    year = models.IntegerField(
        null=False,
        blank=False,
    )
    deadline = models.DateField(
        blank=False,
        null=False,
    )
    date = models.DateField(
        blank=False,
        null=False,
    )
    created = models.DateTimeField(auto_now_add=True, )
    updated = models.DateTimeField(auto_now=True, )

    def __str__(self):
        return f"{self.year}"
Пример #2
0
class SynchronizableMixin(models.Model):
    class Meta(object):
        abstract = True

    state = FSMIntegerField(
        default=SynchronizationStates.SYNCING_SCHEDULED,
        choices=SynchronizationStates.CHOICES,
    )

    @transition(field=state,
                source=SynchronizationStates.SYNCING_SCHEDULED,
                target=SynchronizationStates.SYNCING)
    def begin_syncing(self):
        pass

    @transition(field=state,
                source=SynchronizationStates.IN_SYNC,
                target=SynchronizationStates.SYNCING_SCHEDULED)
    def schedule_syncing(self):
        pass

    @transition(field=state,
                source=SynchronizationStates.SYNCING,
                target=SynchronizationStates.IN_SYNC)
    def set_in_sync(self):
        pass

    @transition(field=state, source='*', target=SynchronizationStates.ERRED)
    def set_erred(self):
        pass
Пример #3
0
class Order(ConcurrentTransitionMixin, Model):
    class Meta:
        app_label = APP_LABEL
        abstract = True

    NEW = STATE.NEW
    PROCESSING = STATE.PROCESSING
    PROCESSED = STATE.PROCESSED
    ERROR = STATE.ERROR

    CHOICES = STATE.CHOICES

    id = BigIntegerField(primary_key=True, editable=False)
    email = EmailField()
    first_name = CharField(max_length=254)
    last_name = CharField(max_length=254)
    received = DateTimeField(default=timezone.now)
    status = FSMIntegerField(choices=CHOICES, default=NEW, protected=True)

    @transition(field=status, source=NEW, target=PROCESSING, on_error=ERROR)
    def start_processing(self):
        logger.debug('Processing order %s' % self.id)

    @transition(field=status,
                source=PROCESSING,
                target=PROCESSED,
                on_error=ERROR)
    def finish_processing(self):
        logger.debug('Finishing order %s' % self.id)

    @transition(field=status, source=PROCESSING, target=ERROR)
    def fail(self):
        logger.debug('Failed to process order %s' % self.id)
Пример #4
0
class OrderItem(ConcurrentTransitionMixin, Model):
    class Meta:
        app_label = APP_LABEL
        abstract = True

    NEW = STATE.NEW
    PROCESSING = STATE.PROCESSING
    PROCESSED = STATE.PROCESSED
    ERROR = STATE.ERROR

    CHOICES = STATE.CHOICES

    sku = CharField(max_length=254)
    email = EmailField()
    status = FSMIntegerField(choices=CHOICES, default=NEW, protected=True)

    @transition(field=status, source=NEW, target=PROCESSING, on_error=ERROR)
    def start_processing(self):
        logger.debug('Processing item %s for order %s' %
                     (self.id, self.order.id))

    @transition(field=status,
                source=PROCESSING,
                target=PROCESSED,
                on_error=ERROR)
    def finish_processing(self):
        logger.debug('Finishing item %s for order %s' %
                     (self.id, self.order.id))

    @transition(field=status, source=PROCESSING, target=ERROR)
    def fail(self):
        logger.debug('Failed to process item %s '
                     'for order %s' % (self.id, self.order.id))
Пример #5
0
class Task(models.Model):
    NEW = 0
    IN_PROGRESS = 1
    DONE = 2
    STATES_CHOICES = (
        (NEW, 'New'),
        (IN_PROGRESS, 'In Progress'),
        (DONE, 'Done'),
    )
    title = models.CharField(max_length=255)
    description = models.TextField()
    state = FSMIntegerField(choices=STATES_CHOICES, default=NEW)
    linked_task = models.OneToOneField('self',
                                       null=True,
                                       blank=True,
                                       on_delete=models.SET_NULL)

    @transition(field=state, source=NEW, target=IN_PROGRESS)
    def in_progress(self):
        return 'In progress'

    @transition(field=state, source=IN_PROGRESS, target=DONE)
    def done(self):
        return 'Done'

    class Meta:
        indexes = [models.Index(fields=('title', ))]
        ordering = ('title', )
Пример #6
0
class Order(core_models.UuidMixin, structure_models.TimeStampedModel):
    class States(object):
        DRAFT = 1
        REQUESTED_FOR_APPROVAL = 2
        EXECUTING = 3
        DONE = 4
        TERMINATED = 5

        CHOICES = (
            (DRAFT, 'draft'),
            (REQUESTED_FOR_APPROVAL, 'requested for approval'),
            (EXECUTING, 'executing'),
            (DONE, 'done'),
            (TERMINATED, 'terminated'),
        )

    created_by = models.ForeignKey(core_models.User, related_name='orders')
    approved_by = models.ForeignKey(core_models.User,
                                    blank=True,
                                    null=True,
                                    related_name='+')
    approved_at = models.DateTimeField(editable=False, null=True, blank=True)
    project = models.ForeignKey(structure_models.Project)
    state = FSMIntegerField(default=States.DRAFT, choices=States.CHOICES)
    total_cost = models.DecimalField(max_digits=22,
                                     decimal_places=10,
                                     null=True,
                                     blank=True)

    class Permissions(object):
        customer_path = 'project__customer'
        project_path = 'project'

    class Meta(object):
        verbose_name = _('Order')
        ordering = ('created', )

    @classmethod
    def get_url_name(cls):
        return 'marketplace-order'

    @transition(field=state,
                source=States.DRAFT,
                target=States.REQUESTED_FOR_APPROVAL)
    def set_state_requested_for_approval(self):
        pass

    @transition(field=state,
                source=States.REQUESTED_FOR_APPROVAL,
                target=States.EXECUTING)
    def set_state_executing(self):
        pass

    @transition(field=state, source=States.EXECUTING, target=States.DONE)
    def set_state_done(self):
        pass

    @transition(field=state, source='*', target=States.TERMINATED)
    def set_state_terminated(self):
        pass
Пример #7
0
class ReviewStateMixin(models.Model):
    class Meta:
        abstract = True

    class States:
        DRAFT = 1
        PENDING = 2
        APPROVED = 3
        REJECTED = 4
        CANCELED = 5

        CHOICES = (
            (DRAFT, 'draft'),
            (PENDING, 'pending'),
            (APPROVED, 'approved'),
            (REJECTED, 'rejected'),
            (CANCELED, 'canceled'),
        )

    state = FSMIntegerField(default=States.DRAFT, choices=States.CHOICES)

    def submit(self):
        self.state = self.States.PENDING
        self.save(update_fields=['state'])

    def cancel(self):
        self.state = self.States.CANCELED
        self.save(update_fields=['state'])
Пример #8
0
class Thread(models.Model):
    id = HashidAutoField(primary_key=True, )
    STATE = Choices((0, 'new', 'New'), )
    state = FSMIntegerField(
        choices=STATE,
        default=STATE.new,
    )
    account = models.ForeignKey(
        'app.Account',
        on_delete=models.SET_NULL,
        related_name='threads',
        null=True,
    )
    topic = models.ForeignKey(
        'app.Topic',
        on_delete=models.SET_NULL,
        related_name='threads',
        null=True,
        blank=True,
    )
    created = models.DateTimeField(auto_now_add=True, )
    updated = models.DateTimeField(auto_now=True, )

    def __str__(self):
        return f"{self.id}"
Пример #9
0
class Issue(models.Model):
    """
    Describes single problem.

    Single issue may contain multiple photos in some range
    (e.g. when there are many potholes on some location)
    """
    STATUS_DRAFT = 0
    STATUS_READY = 1
    STATUS_WAITING_FOR_ANSWER = 2
    STATUS_CHOICES = (
        (STATUS_DRAFT, 'draft'),
        (STATUS_READY, 'ready'),
        (STATUS_WAITING_FOR_ANSWER, 'waiting_for_the_answer'),
    )

    state = FSMIntegerField(default=STATUS_DRAFT, choices=STATUS_CHOICES)

    @transition(field=state, source=STATUS_DRAFT, target=STATUS_READY)
    def publish(self):
        pass

    @transition(field=state, source=STATUS_READY, target=STATUS_WAITING_FOR_ANSWER)
    def send(self):
        pass
Пример #10
0
class Order(models.Model):

    STATUS_CREATED = 0
    STATUS_PAID = 1
    STATUS_FULFILLED = 2
    STATUS_CANCELLED = 3
    STATUS_RETURNED = 4

    STATUS_CHOICES = (
        (STATUS_CREATED, 'created'),
        (STATUS_PAID, 'paid'),
        (STATUS_FULFILLED, 'fulfilled'),
        (STATUS_CANCELLED, 'cancelled'),
        (STATUS_RETURNED, 'returned'),
    )

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    plants = models.ManyToManyField(Plant,
                                    related_name='order_plants',
                                    blank=True)
    total_ammount = models.PositiveSmallIntegerField(editable=False)
    added_on = models.DateTimeField(auto_now_add=True)
    updated_on = models.DateTimeField(auto_now=True)
    is_active = models.BooleanField(default=True)
    user = models.ForeignKey(User,
                             related_name='orders',
                             on_delete=models.CASCADE)
    status = FSMIntegerField(choices=STATUS_CHOICES,
                             default=STATUS_CREATED,
                             protected=True,
                             editable=False)

    def __str__(self):
        return self.user.email

    @cached_property
    def all_plants(self):
        return self.plants.all()

    @transition(field=status, source=STATUS_CREATED, target=STATUS_PAID)
    def pay(self, amount):
        self.amount = amount
        pass

    @transition(field=status, source=STATUS_PAID, target=STATUS_FULFILLED)
    def fulfill_order(self):
        pass

    @transition(field=status,
                source=[STATUS_CREATED, STATUS_PAID],
                target=STATUS_CANCELLED)
    def cancel_order(self):
        pass

    @transition(field=status, source=STATUS_FULFILLED, target=STATUS_RETURNED)
    def return_order(self):
        pass
Пример #11
0
class OrderItem(core_models.UuidMixin, core_models.ErrorMessageMixin,
                structure_models.TimeStampedModel, ScopeMixin):
    class States(object):
        PENDING = 1
        EXECUTING = 2
        DONE = 3
        ERRED = 4
        TERMINATED = 5

        CHOICES = (
            (PENDING, 'pending'),
            (EXECUTING, 'executing'),
            (DONE, 'done'),
            (ERRED, 'erred'),
            (TERMINATED, 'terminated'),
        )

        TERMINAL_STATES = set([DONE, ERRED, TERMINATED])

    order = models.ForeignKey(Order, related_name='items')
    offering = models.ForeignKey(Offering)
    attributes = BetterJSONField(blank=True, default=dict)
    cost = models.DecimalField(max_digits=22,
                               decimal_places=10,
                               null=True,
                               blank=True)
    plan = models.ForeignKey('Plan', null=True, blank=True)
    objects = managers.MixinManager('scope')
    state = FSMIntegerField(default=States.PENDING, choices=States.CHOICES)
    tracker = FieldTracker()

    class Meta(object):
        verbose_name = _('Order item')
        ordering = ('created', )

    @transition(field=state,
                source=[States.PENDING, States.ERRED],
                target=States.EXECUTING)
    def set_state_executing(self):
        pass

    @transition(field=state, source=States.EXECUTING, target=States.DONE)
    def set_state_done(self):
        pass

    @transition(field=state, source='*', target=States.ERRED)
    def set_state_erred(self):
        pass

    @transition(field=state, source='*', target=States.TERMINATED)
    def set_state_terminated(self):
        pass

    def set_state(self, state):
        getattr(self, 'set_state_' + state)()
        self.save(update_fields=['state'])
Пример #12
0
class BlogPostWithIntegerField(models.Model):
    state = FSMIntegerField(default=BlogPostStateEnum.NEW)

    @transition(field=state, source=BlogPostStateEnum.NEW, target=BlogPostStateEnum.PUBLISHED)
    def publish(self):
        pass

    @transition(field=state, source=BlogPostStateEnum.PUBLISHED, target=BlogPostStateEnum.HIDDEN)
    def hide(self):
        pass
Пример #13
0
class Payment(LoggableMixin, TimeStampedModel, UuidMixin):
    class Permissions(object):
        customer_path = 'customer'

    class States(object):
        INIT = 0
        CREATED = 1
        APPROVED = 2
        CANCELLED = 3
        ERRED = 4

    STATE_CHOICES = (
        (States.INIT, 'Initial'),
        (States.CREATED, 'Created'),
        (States.APPROVED, 'Approved'),
        (States.ERRED, 'Erred'),
    )

    state = FSMIntegerField(default=States.INIT, choices=STATE_CHOICES)

    customer = models.ForeignKey(Customer)
    amount = models.DecimalField(max_digits=9, decimal_places=2)

    # Payment ID is persistent identifier of payment
    backend_id = models.CharField(max_length=255, null=True)

    # Token is temporary identifier of payment
    token = models.CharField(max_length=255, null=True)

    # URL is fetched from backend
    approval_url = models.URLField()

    def __str__(self):
        return "%s %.2f %s" % (self.modified, self.amount, self.customer.name)

    def get_log_fields(self):
        return ('uuid', 'customer', 'amount', 'modified', 'status')

    @transition(field=state, source=States.INIT, target=States.CREATED)
    def set_created(self):
        pass

    @transition(field=state, source=States.CREATED, target=States.APPROVED)
    def set_approved(self):
        pass

    @transition(field=state, source=States.CREATED, target=States.CANCELLED)
    def set_cancelled(self):
        pass

    @transition(field=state, source='*', target=States.ERRED)
    def set_erred(self):
        pass
Пример #14
0
class Venta(models.Model):

    ESTADO_CREADO = 0
    ESTADO_PAGADO = 1
    ESTADO_FACTURADO = 2
    ESTADO_FINALIZADO = 3
    ESTADO_CANCELADO = -1
    ESTADO_ANULADO = -2

    ESTADOS = (
        (ESTADO_CREADO, 'creado'),
        (ESTADO_PAGADO, 'pagado'),
        (ESTADO_FACTURADO, 'facturado'),
        (ESTADO_FINALIZADO, 'finalizado'),
        (ESTADO_CANCELADO, 'cancelado'),
        (ESTADO_ANULADO, 'anulado'),
    )

    id = models.AutoField(primary_key=True)
    codigo = models.IntegerField(null=False)
    fecha_hora = models.DateTimeField()
    total = models.DecimalField(max_digits=50, decimal_places=2, null=False)
    estado = FSMIntegerField(choices=ESTADOS, default=ESTADO_CREADO, protected=True)
    factura_nombre = models.CharField(null=True, max_length=100)
    factura_nit = models.IntegerField(null=True)
    motivo_anulacion = models.CharField(max_length=400, null=True)

    class Meta:
        db_table = 'ventas_ventas'

    @transition(estado, source=ESTADO_CREADO, target=ESTADO_PAGADO)
    def pagar(self):
        pass

    @transition(estado, source=ESTADO_PAGADO, target=ESTADO_FACTURADO)
    def facturar(self, nombre, nit):
        self.factura_nombre = nombre
        self.factura_nit = nit
        pass

    @transition(estado, source=[ESTADO_PAGADO, ESTADO_FACTURADO], target=ESTADO_FINALIZADO)
    def finalizar(self):
        pass

    @transition(estado, source=ESTADO_CREADO, target=ESTADO_CANCELADO)
    def cancelar(self):
        pass

    @transition(estado, source=[ESTADO_FINALIZADO, ESTADO_PAGADO, ESTADO_FACTURADO], target=ESTADO_ANULADO)
    def anular(self, motivo):
        self.motivo_anulacion = motivo
        pass
Пример #15
0
class Resident(ResidentNotificationSettingsMixin, ResidentProfileSettingsMixin,
               User):
    specialities = models.ManyToManyField(Speciality,
                                          verbose_name='Specialities',
                                          related_name='residents',
                                          blank=True)
    residency_program = models.TextField(verbose_name='Residency program',
                                         null=True,
                                         blank=True)
    residency_years = models.PositiveIntegerField(
        verbose_name='Residency years', null=True, blank=True)
    state = FSMIntegerField(verbose_name='State',
                            default=ResidentStateEnum.NEW,
                            choices=ResidentStateEnum.CHOICES)
    cv_link = models.CharField(max_length=200,
                               verbose_name='Link to CV',
                               null=True,
                               blank=True)

    objects = ResidentManager()

    class Meta:
        verbose_name = 'Resident'
        verbose_name_plural = 'Residents'

    @property
    def is_approved(self):
        return self.state == ResidentStateEnum.APPROVED

    @transition(field=state,
                source=[ResidentStateEnum.NEW, ResidentStateEnum.REJECTED],
                target=ResidentStateEnum.PROFILE_FILLED,
                permission=is_resident)
    def fill_profile(self, profile_data):
        for field, value in profile_data.items():
            setattr(self, field, value)

        process_resident_profile_filling(self)

    @transition(field=state,
                source=ResidentStateEnum.PROFILE_FILLED,
                target=ResidentStateEnum.APPROVED,
                permission=is_account_manager)
    def approve(self):
        process_resident_approving(self)

    @transition(field=state,
                source=ResidentStateEnum.PROFILE_FILLED,
                target=ResidentStateEnum.REJECTED,
                permission=is_account_manager)
    def reject(self):
        process_resident_rejecting(self)
Пример #16
0
class IssueStatus(models.Model):
    """ This model is needed in order to understand whether the issue has been solved or not.

        The field of resolution does not give an exact answer since may be the same in both cases.
    """

    class Types:
        RESOLVED = 0
        CANCELED = 1

    TYPE_CHOICES = (
        (Types.RESOLVED, 'Resolved'),
        (Types.CANCELED, 'Canceled'),
    )

    name = models.CharField(
        max_length=255, help_text='Status name in Jira.', unique=True
    )
    type = FSMIntegerField(default=Types.RESOLVED, choices=TYPE_CHOICES)

    @classmethod
    def check_success_status(cls, status):
        """ Check an issue has been resolved.

            True if an issue resolved.
            False if an issue canceled.
            None in all other cases.
        """
        if (
            not cls.objects.filter(type=cls.Types.RESOLVED).exists()
            or not cls.objects.filter(type=cls.Types.CANCELED).exists()
        ):
            logger.critical(
                'There is no information about statuses of an issue. '
                'Please, add resolved and cancelled statuses in admin. '
                'Otherwise, you cannot use processes that need this information.'
            )
            return
        try:
            issue_status = cls.objects.get(name=status)
            if issue_status.type == cls.Types.RESOLVED:
                return True
            if issue_status.type == cls.Types.CANCELED:
                return False
        except cls.DoesNotExist:
            return

    class Meta:
        verbose_name = _('Issue status')
        verbose_name_plural = _('Issue statuses')
Пример #17
0
class SynchronizableMixin(ErrorMessageMixin):
    class Meta(object):
        abstract = True

    state = FSMIntegerField(
        default=SynchronizationStates.CREATION_SCHEDULED,
        choices=SynchronizationStates.CHOICES,
    )

    @transition(field=state,
                source=SynchronizationStates.CREATION_SCHEDULED,
                target=SynchronizationStates.CREATING)
    def begin_creating(self):
        pass

    @transition(field=state,
                source=SynchronizationStates.SYNCING_SCHEDULED,
                target=SynchronizationStates.SYNCING)
    def begin_syncing(self):
        pass

    @transition(field=state,
                source=SynchronizationStates.IN_SYNC,
                target=SynchronizationStates.SYNCING_SCHEDULED)
    def schedule_syncing(self):
        pass

    @transition(field=state,
                source=SynchronizationStates.NEW,
                target=SynchronizationStates.CREATION_SCHEDULED)
    def schedule_creating(self):
        pass

    @transition(
        field=state,
        source=[SynchronizationStates.SYNCING, SynchronizationStates.CREATING],
        target=SynchronizationStates.IN_SYNC)
    def set_in_sync(self):
        pass

    @transition(field=state, source='*', target=SynchronizationStates.ERRED)
    def set_erred(self):
        pass

    @transition(field=state,
                source=SynchronizationStates.ERRED,
                target=SynchronizationStates.IN_SYNC)
    def set_in_sync_from_erred(self):
        self.error_message = ''
Пример #18
0
class ArticleInteger(models.Model):
    STATE_ONE = 1
    STATE_TWO = 2

    STATES = (
        (STATE_ONE, 'one'),
        (STATE_TWO, 'two'),
    )

    state = FSMIntegerField(choices=STATES, default=STATE_ONE)

    @fsm_log_by
    @transition(field=state, source=STATE_ONE, target=STATE_TWO)
    def change_to_two(self, by=None):
        pass
Пример #19
0
class Message(models.Model):
    id = HashidAutoField(primary_key=True, )
    STATE = Choices(
        (0, 'new', 'New'),
        (10, 'sent', 'Sent'),
    )
    state = FSMIntegerField(
        choices=STATE,
        default=STATE.new,
    )
    sid = models.CharField(
        max_length=100,
        blank=True,
    )
    to_phone = PhoneNumberField(
        blank=True,
        null=True,
    )
    from_phone = PhoneNumberField(
        blank=True,
        null=True,
    )
    body = models.TextField(blank=True, )
    DIRECTION = Choices(
        (10, 'inbound', 'Inbound'),
        (20, 'outbound', 'Outbound'),
    )
    direction = models.IntegerField(
        choices=DIRECTION,
        null=True,
        blank=True,
    )
    raw = models.JSONField(
        blank=True,
        null=True,
    )
    thread = models.ForeignKey(
        'app.Thread',
        on_delete=models.SET_NULL,
        related_name='messages',
        null=True,
    )
    created = models.DateTimeField(auto_now_add=True, )
    updated = models.DateTimeField(auto_now=True, )

    def __str__(self):
        return f"{self.id}"
Пример #20
0
class Topic(models.Model):
    id = HashidAutoField(primary_key=True, )
    STATE = Choices((0, 'new', 'New'), )
    state = FSMIntegerField(
        choices=STATE,
        default=STATE.new,
    )
    name = models.CharField(
        max_length=100,
        blank=False,
    )
    body = models.TextField(
        max_length=1600,
        blank=False,
    )
    created = models.DateTimeField(auto_now_add=True, )
    updated = models.DateTimeField(auto_now=True, )

    def __str__(self):
        return f"{self.name}"
Пример #21
0
class Assignment(models.Model):
    id = HashidAutoField(primary_key=True, )
    STATE = Choices((0, 'new', 'New'), )
    state = FSMIntegerField(
        choices=STATE,
        default=STATE.new,
    )
    recipient = models.ForeignKey(
        'app.Recipient',
        on_delete=models.CASCADE,
        related_name='assignments',
    )
    team = models.ForeignKey(
        'app.Team',
        on_delete=models.CASCADE,
        related_name='assignments',
    )
    created = models.DateTimeField(auto_now_add=True, )
    updated = models.DateTimeField(auto_now=True, )

    def __str__(self):
        return f"{self.id}"
Пример #22
0
class WebhookData(ConcurrentTransitionMixin, Model):
    """Abstract base class for webhook data."""
    class Meta:
        app_label = APP_LABEL
        abstract = True

    NEW = STATE.NEW
    PROCESSING = STATE.PROCESSING
    PROCESSED = STATE.PROCESSED
    ERROR = STATE.ERROR

    CHOICES = STATE.CHOICES

    # We know we always want to record the webhook source, and the
    # date we received it.
    status = FSMIntegerField(choices=CHOICES, default=NEW, protected=True)
    source = GenericIPAddressField(null=True)
    received = DateTimeField(default=timezone.now)
    headers = JSONField()
    # This is for storing the webhook payload exactly as received
    # (i.e. from request.body), which comes in handy for signature
    # verification.
    body = BinaryField()

    @transition(field=status, source=NEW, target=PROCESSING, on_error=ERROR)
    def start_processing(self):
        logger.debug('Processing webhook %s' % self.id)

    @transition(field=status,
                source=PROCESSING,
                target=PROCESSED,
                on_error=ERROR)
    def finish_processing(self):
        logger.debug('Finishing webhook %s' % self.id)

    @transition(field=status, source=PROCESSING, target=ERROR)
    def fail(self):
        logger.debug('Failed to process webhook %s' % self.id)
Пример #23
0
class Post(BaseModel):
    class StatusChoice(models.IntegerChoices):
        DRAFT = 0, _('Draft')
        PUBLISHED = 1, _('Published')

    title = models.CharField(max_length=255)
    slug = models.SlugField(max_length=255, unique=True)
    author = models.ForeignKey(User,
                               on_delete=models.CASCADE,
                               related_name='blog_posts')
    body = models.TextField()
    publish = models.DateTimeField(null=True, blank=True)
    status = FSMIntegerField(choices=StatusChoice.choices,
                             default=StatusChoice.DRAFT)

    tags = TaggableManager(through=UUIDTaggedItem)

    objects = PostQueryset.as_manager()

    class Meta:
        ordering = ('-publish', )

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):

        if not self.pk:
            self.slug = f'{slugify(self.title)}-{utils.get_uuid()}'

        if not self.publish and self.status in [self.StatusChoice.PUBLISHED]:
            self.publish = timezone.now()

        return super().save(*args, **kwargs)

    def get_similar_posts(self):
        return Post.objects.get_similar_posts(self)
Пример #24
0
class OrderItem(ConcurrentTransitionMixin, models.Model):
    class Meta:
        app_label = APP_LABEL
        constraints = [
            models.UniqueConstraint(fields=['order', 'sku', 'email'],
                                    name='unique_order_sku_email')
        ]

    NEW = STATE.NEW
    PROCESSING = STATE.PROCESSING
    PROCESSED = STATE.PROCESSED
    ERROR = STATE.ERROR

    CHOICES = STATE.CHOICES

    order = models.ForeignKey(Order, on_delete=models.PROTECT)
    sku = models.CharField(max_length=254)
    email = models.EmailField()
    status = FSMIntegerField(choices=CHOICES, default=NEW, protected=True)

    @transition(field=status, source=NEW, target=PROCESSING, on_error=ERROR)
    def start_processing(self):
        logger.debug('Processing item %s for order %s' %
                     (self.id, self.order.id))

    @transition(field=status,
                source=PROCESSING,
                target=PROCESSED,
                on_error=ERROR)
    def finish_processing(self):
        logger.debug('Finishing item %s for order %s' %
                     (self.id, self.order.id))

    @transition(field=status, source=PROCESSING, target=ERROR)
    def fail(self):
        logger.debug('Failed to process item %s '
                     'for order %s' % (self.id, self.order.id))
Пример #25
0
class Order(models.Model):
    STATUS_CREATED = 0
    STATUS_PAID = 1
    STATUS_FULFILLED = 2
    STATUS_CANCELLED = 3
    STATUS_RETURNED = 4
    STATUS_CHOICES = (
        (STATUS_CREATED, 'Created'),
        (STATUS_PAID, 'Paid'),
        (STATUS_FULFILLED, 'Fulfilled'),
        (STATUS_CANCELLED, 'Cancelled'),
        (STATUS_RETURNED, 'Returned'),
    )
    amount = models.DecimalField(max_digits=9, decimal_places=2)
    product = models.CharField(max_length=200)
    status = FSMIntegerField(choices=STATUS_CHOICES,
                             default=STATUS_CREATED,
                             protected=True)

    @transition(field=status, source=STATUS_CREATED, target=STATUS_PAID)
    def pay(self):
        print("Pay the order")

    @transition(field=status, source=STATUS_PAID, target=STATUS_FULFILLED)
    def fulfill(self):
        print("Fulfill the order")

    @transition(field=status,
                source=[STATUS_CREATED, STATUS_PAID],
                target=STATUS_CANCELLED)
    def cancel(self):
        print("Cancel the order")

    @transition(field=status, source=STATUS_FULFILLED, target=STATUS_RETURNED)
    def return_order(self):
        print("Return the order")
Пример #26
0
class StateMixin(ErrorMessageMixin):
    class States:
        CREATION_SCHEDULED = 5
        CREATING = 6
        UPDATE_SCHEDULED = 1
        UPDATING = 2
        DELETION_SCHEDULED = 7
        DELETING = 8
        OK = 3
        ERRED = 4

        CHOICES = (
            (CREATION_SCHEDULED, 'Creation Scheduled'),
            (CREATING, 'Creating'),
            (UPDATE_SCHEDULED, 'Update Scheduled'),
            (UPDATING, 'Updating'),
            (DELETION_SCHEDULED, 'Deletion Scheduled'),
            (DELETING, 'Deleting'),
            (OK, 'OK'),
            (ERRED, 'Erred'),
        )

    class Meta:
        abstract = True

    state = FSMIntegerField(
        default=States.CREATION_SCHEDULED,
        choices=States.CHOICES,
    )

    @property
    def human_readable_state(self):
        return force_text(dict(self.States.CHOICES)[self.state])

    @transition(field=state,
                source=States.CREATION_SCHEDULED,
                target=States.CREATING)
    def begin_creating(self):
        pass

    @transition(field=state,
                source=States.UPDATE_SCHEDULED,
                target=States.UPDATING)
    def begin_updating(self):
        pass

    @transition(field=state,
                source=States.DELETION_SCHEDULED,
                target=States.DELETING)
    def begin_deleting(self):
        pass

    @transition(field=state,
                source=[States.OK, States.ERRED],
                target=States.UPDATE_SCHEDULED)
    def schedule_updating(self):
        pass

    @transition(field=state,
                source=[States.OK, States.ERRED],
                target=States.DELETION_SCHEDULED)
    def schedule_deleting(self):
        pass

    @transition(field=state,
                source=[States.UPDATING, States.CREATING],
                target=States.OK)
    def set_ok(self):
        pass

    @transition(field=state, source='*', target=States.ERRED)
    def set_erred(self):
        pass

    @transition(field=state, source=States.ERRED, target=States.OK)
    def recover(self):
        pass

    @classmethod
    @lru_cache(maxsize=1)
    def get_all_models(cls):
        return [model for model in apps.get_models() if issubclass(model, cls)]
Пример #27
0
class Subscription(models.Model):
    state = FSMIntegerField(
        default=State.ACTIVE,
        choices=State.choices(),
        protected=True,
        help_text="The current status of the subscription. May not be modified directly.",
    )
    start = models.DateTimeField(default=timezone.now, help_text="When the subscription begins")
    end = models.DateTimeField(help_text="When the subscription ends")
    reference = models.TextField(max_length=100, help_text="Free text field for user references")
    last_updated = models.DateTimeField(
        auto_now=True, help_text="Keeps track of when a record was last updated"
    )
    reason = models.TextField(help_text="Reason for state change, if applicable.")

    objects = SubscriptionManager.from_queryset(SubscriptionQuerySet)()

    class Meta:
        indexes = [
            models.Index(fields=["state"], name="subscription_state_idx"),
            models.Index(fields=["end"], name="subscription_end_idx"),
            models.Index(fields=["last_updated"], name="subscription_last_updated_idx"),
        ]
        get_latest_by = "start"
        permissions = (("can_update_state", "Can update subscription state"),)

    def __str__(self):
        return "[{}] {}: {:%Y-%m-%d} to {:%Y-%m-%d}".format(
            self.pk, self.get_state_display(), as_date(self.start), as_date(self.end)
        )

    def can_proceed(self, transition_method):
        return can_proceed(transition_method)

    @transition(field=state, source=State.ACTIVE, target=State.EXPIRING)
    def cancel_autorenew(self):
        self.reason = ""

    @post_transition(cancel_autorenew)
    def post_cancel_autorenew(self):
        self.save()
        signals.autorenew_canceled.send_robust(self)

    @transition(field=state, source=State.EXPIRING, target=State.ACTIVE)
    def enable_autorenew(self):
        self.reason = ""

    @post_transition(enable_autorenew)
    def post_enable_autorenew(self):
        self.save()
        signals.autorenew_enabled.send_robust(self)

    @transition(field=state, source=[State.ACTIVE, State.SUSPENDED], target=State.RENEWING)
    def renew(self):
        self.reason = ""

    @post_transition(renew)
    def post_renew(self):
        self.save()
        signals.subscription_due.send_robust(self)

    @fsm_log_description
    @transition(
        field=state, source=[State.ACTIVE, State.RENEWING, State.ERROR], target=State.ACTIVE
    )
    def renewed(self, new_end_date, new_reference, description=None):
        self.reason = ""
        self.end = new_end_date
        self.reference = new_reference

    @post_transition(renewed)
    def post_renewed(self):
        self.save()
        signals.subscription_renewed.send_robust(self)

    @fsm_log_description
    @transition(field=state, source=[State.RENEWING, State.ERROR], target=State.SUSPENDED)
    def renewal_failed(self, reason="", description=None):
        if description:
            self.reason = description
        else:
            self.reason = reason

    @post_transition(renewal_failed)
    def post_renewal_failed(self):
        self.save()
        signals.renewal_failed.send_robust(self)

    @fsm_log_by
    @fsm_log_description
    @transition(
        field=state,
        source=[State.ACTIVE, State.SUSPENDED, State.EXPIRING, State.ERROR],
        target=State.ENDED,
    )
    def end_subscription(self, reason="", by=None, description=None):
        if description:
            self.reason = description
        else:
            self.reason = reason
        self.end = timezone.now()

    @post_transition(end_subscription)
    def post_end_subscription(self):
        self.save()
        signals.subscription_ended.send_robust(self)

    @fsm_log_description
    @transition(field=state, source=State.RENEWING, target=State.ERROR)
    def state_unknown(self, reason="", description=None):
        """
        An error occurred after the payment was signalled, but before the
        subscription could be updated, so the correct state is unknown.

        Requires manual investigation to determine the correct state from
        here.

        If a record remains in RENEWING state for longer than some timeout, the
        record will be moved to this state.
        """
        if description:
            self.reason = description
        else:
            self.reason = reason

    @post_transition(state_unknown)
    def post_state_unknown(self):
        self.save()
        signals.subscription_error.send_robust(self)
Пример #28
0
class Convention(TimeStampedModel):
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
    )

    STATUS = Choices(
        (
            -10,
            'inactive',
            'Inactive',
        ),
        (
            0,
            'new',
            'New',
        ),
        (
            5,
            'built',
            'Built',
        ),
        (
            10,
            'active',
            'Active',
        ),
    )

    status = FSMIntegerField(
        help_text=
        """DO NOT CHANGE MANUALLY unless correcting a mistake.  Use the buttons to change state.""",
        choices=STATUS,
        default=STATUS.new,
    )

    name = models.CharField(
        max_length=255,
        default='Convention',
    )

    DISTRICT = Choices(
        (110, 'bhs', 'BHS'),
        (200, 'car', 'CAR'),
        (205, 'csd', 'CSD'),
        (210, 'dix', 'DIX'),
        (215, 'evg', 'EVG'),
        (220, 'fwd', 'FWD'),
        (225, 'ill', 'ILL'),
        (230, 'jad', 'JAD'),
        (235, 'lol', 'LOL'),
        (240, 'mad', 'MAD'),
        (345, 'ned', 'NED'),
        (350, 'nsc', 'NSC'),
        (355, 'ont', 'ONT'),
        (360, 'pio', 'PIO'),
        (365, 'rmd', 'RMD'),
        (370, 'sld', 'SLD'),
        (375, 'sun', 'SUN'),
        (380, 'swd', 'SWD'),
    )

    district = models.IntegerField(
        choices=DISTRICT,
        blank=True,
        null=True,
    )

    SEASON = Choices(
        (
            3,
            'fall',
            'Fall',
        ),
        (
            4,
            'spring',
            'Spring',
        ),
    )

    season = models.IntegerField(choices=SEASON, )

    PANEL = Choices(
        (1, 'single', "Single"),
        (2, 'double', "Double"),
        (3, 'triple', "Triple"),
        (4, 'quadruple', "Quadruple"),
        (5, 'quintiple', "Quintiple"),
    )

    panel = models.IntegerField(
        choices=PANEL,
        null=True,
        blank=True,
    )

    YEAR_CHOICES = []
    for r in reversed(range(1939, (datetime.datetime.now().year + 2))):
        YEAR_CHOICES.append((r, r))

    year = models.IntegerField(choices=YEAR_CHOICES, )

    open_date = models.DateField(
        null=True,
        blank=True,
    )

    close_date = models.DateField(
        null=True,
        blank=True,
    )

    start_date = models.DateField(
        null=True,
        blank=True,
    )

    end_date = models.DateField(
        null=True,
        blank=True,
    )

    venue_name = models.CharField(
        help_text="""
            The venue name (when available).""",
        max_length=255,
        default='(TBD)',
    )

    location = models.CharField(
        help_text="""
            The general location in the form "City, State".""",
        max_length=255,
        default='(TBD)',
    )

    timezone = TimeZoneField(
        help_text="""
            The local timezone of the convention.""",
        null=True,
        blank=True,
    )

    description = models.TextField(
        help_text="""
            A general description field; usually used for hotel and venue info.""",
        blank=True,
        max_length=1000,
    )

    DIVISION = Choices(
        ('EVG', [
            (10, 'evgd1', 'EVG Division I'),
            (20, 'evgd2', 'EVG Division II'),
            (30, 'evgd3', 'EVG Division III'),
            (40, 'evgd4', 'EVG Division IV'),
            (50, 'evgd5', 'EVG Division V'),
        ]),
        ('FWD', [
            (60, 'fwdaz', 'FWD Arizona'),
            (70, 'fwdne', 'FWD Northeast'),
            (80, 'fwdnw', 'FWD Northwest'),
            (90, 'fwdse', 'FWD Southeast'),
            (100, 'fwdsw', 'FWD Southwest'),
        ]),
        ('LOL', [
            (110, 'lol10l', 'LOL 10000 Lakes'),
            (120, 'lolone', 'LOL Division One'),
            (130, 'lolnp', 'LOL Northern Plains'),
            (140, 'lolpkr', 'LOL Packerland'),
            (150, 'lolsw', 'LOL Southwest'),
        ]),
        (
            'MAD',
            [
                # (160, 'madatl', 'MAD Atlantic'),
                (170, 'madcen', 'MAD Central'),
                (180, 'madnth', 'MAD Northern'),
                (190, 'madsth', 'MAD Southern'),
                # (200, 'madwst', 'MAD Western'),
            ]),
        ('NED', [
            (210, 'nedgp', 'NED Granite and Pine'),
            (220, 'nedmtn', 'NED Mountain'),
            (230, 'nedpat', 'NED Patriot'),
            (240, 'nedsun', 'NED Sunrise'),
            (250, 'nedyke', 'NED Yankee'),
        ]),
        ('SWD', [
            (260, 'swdne', 'SWD Northeast'),
            (270, 'swdnw', 'SWD Northwest'),
            (280, 'swdse', 'SWD Southeast'),
            (290, 'swdsw', 'SWD Southwest'),
        ]),
    )

    divisions = DivisionsField(
        help_text=
        """Only select divisions if required.  If it is a district-wide convention do not select any.""",
        base_field=models.IntegerField(choices=DIVISION, ),
        default=list,
        blank=True,
    )

    KINDS = Choices(
        (32, 'chorus', "Chorus"),
        (41, 'quartet', "Quartet"),
        (42, 'mixed', "Mixed"),
        (43, 'senior', "Senior"),
        (44, 'youth', "Youth"),
        (45, 'unknown', "Unknown"),
        (46, 'vlq', "VLQ"),
    )

    kinds = DivisionsField(
        help_text="""The session kind(s) created at build time.""",
        base_field=models.IntegerField(choices=KINDS, ),
        default=list,
        blank=True,
    )

    image = models.ImageField(
        upload_to=UploadPath('image'),
        max_length=255,
        null=True,
        blank=True,
    )

    # FKs
    persons = models.ManyToManyField(
        'Person',
        related_name='conventions',
        blank=True,
    )

    owners = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name='conventions',
    )

    # Relations
    statelogs = GenericRelation(
        StateLog,
        related_query_name='conventions',
    )

    @cached_property
    def nomen(self):
        if self.district == self.DISTRICT.bhs:
            return " ".join([
                self.get_district_display(),
                str(self.year),
                self.name,
            ])
        return " ".join([
            self.get_district_display(),
            self.get_season_display(),
            str(self.year),
            self.name,
        ])

    def is_searchable(self):
        return self.district

    @cached_property
    def image_id(self):
        return self.image.name or 'missing_image'

    @cached_property
    def image_url(self):
        try:
            return self.image.url
        except ValueError:
            return 'https://res.cloudinary.com/barberscore/image/upload/v1554830585/missing_image.jpg'

    # Internals
    class Meta:
        constraints = [
            models.UniqueConstraint(name='unique_convention',
                                    fields=[
                                        'year',
                                        'season',
                                        'name',
                                        'district',
                                    ])
        ]

    class JSONAPIMeta:
        resource_name = "convention"

    def __str__(self):
        return self.nomen

    def clean(self):
        return

    def get_owners_emails(self):
        owners = self.owners.order_by(
            'last_name',
            'first_name',
        )
        return ["{0} <{1}>".format(x.name, x.email) for x in owners]

    # Methods
    # Convention Permissions
    @staticmethod
    @allow_staff_or_superuser
    @authenticated_users
    def has_read_permission(request):
        return True

    @allow_staff_or_superuser
    @authenticated_users
    def has_object_read_permission(self, request):
        return True

    @staticmethod
    @allow_staff_or_superuser
    @authenticated_users
    def has_write_permission(request):
        return any([request.user.roles.filter(name__in=[
            'SCJC',
        ])])

    @allow_staff_or_superuser
    @authenticated_users
    def has_object_write_permission(self, request):
        return any([request.user.roles.filter(name__in=[
            'SCJC',
        ])])

    # Convention Transition Conditions
    def can_reset(self):
        if self.status <= self.STATUS.built:
            return True
        return False

    def can_build(self):
        if self.kinds and self.panel:
            return True
        return False

    def can_activate(self):
        try:
            return all([
                self.open_date,
                self.close_date,
                self.start_date,
                self.end_date,
                self.open_date < self.close_date,
                self.close_date < self.start_date,
                self.start_date <= self.end_date,
                self.location,
                self.timezone,
            ])
        except TypeError:
            return False
        return False

    def can_deactivate(self):
        return

    # Convention Transitions
    @fsm_log_by
    @transition(
        field=status,
        source='*',
        target=STATUS.new,
        conditions=[can_reset],
    )
    def reset(self, *args, **kwargs):
        return

    @fsm_log_by
    @transition(
        field=status,
        source=STATUS.new,
        target=STATUS.built,
        conditions=[can_build],
    )
    def build(self, *args, **kwargs):
        """Build convention and related sessions."""

        # Reset for indempodence
        self.reset()
        return

    @fsm_log_by
    @transition(
        field=status,
        source=STATUS.built,
        target=STATUS.active,
        conditions=[can_activate],
    )
    def activate(self, *args, **kwargs):
        """Activate convention."""
        return

    @fsm_log_by
    @transition(
        field=status,
        source=STATUS.active,
        target=STATUS.inactive,
        conditions=[can_deactivate],
    )
    def deactivate(self, *args, **kwargs):
        """Archive convention and related sessions."""
        return
Пример #29
0
class Chart(TimeStampedModel):
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
    )

    STATUS = Choices(
        (
            -20,
            'protected',
            'Protected',
        ),
        (
            -10,
            'inactive',
            'Inactive',
        ),
        (0, 'new', 'New'),
        (10, 'active', 'Active'),
    )

    status = FSMIntegerField(
        help_text=
        """DO NOT CHANGE MANUALLY unless correcting a mistake.  Use the buttons to change state.""",
        choices=STATUS,
        default=STATUS.new,
    )

    title = models.CharField(max_length=255, )

    arrangers = models.CharField(max_length=255, )

    composers = models.CharField(
        max_length=255,
        blank=True,
        default='',
    )

    lyricists = models.CharField(
        max_length=255,
        blank=True,
        default='',
    )

    holders = models.TextField(
        blank=True,
        default='',
    )

    description = models.TextField(
        help_text="""
            Fun or interesting facts to share about the chart (ie, 'from Disney's Lion King, first sung by Elton John'.)""",
        blank=True,
        max_length=1000,
        default='',
    )

    notes = models.TextField(
        help_text="""
            Private Notes (for internal use only).""",
        blank=True,
        default='',
    )

    image = models.ImageField(
        upload_to=ImageUploadPath('image'),
        null=True,
        blank=True,
    )

    # Relations
    statelogs = GenericRelation(
        StateLog,
        related_query_name='charts',
    )

    @cached_property
    def nomen(self):
        return "{0} [{1}]".format(
            self.title,
            self.arrangers,
        )

    def is_searchable(self):
        return self.status == self.STATUS.active

    @cached_property
    def image_id(self):
        return self.image.name or 'missing_image'

    @cached_property
    def image_url(self):
        try:
            return self.image.url
        except ValueError:
            return 'https://res.cloudinary.com/barberscore/image/upload/v1554830585/missing_image.jpg'

    # Internals
    objects = ChartManager()

    class Meta:
        constraints = [
            models.UniqueConstraint(name='unique_chart',
                                    fields=[
                                        'title',
                                        'arrangers',
                                    ])
        ]

    class JSONAPIMeta:
        resource_name = "chart"

    def __str__(self):
        return self.nomen

    # Permissions
    @staticmethod
    @allow_staff_or_superuser
    @authenticated_users
    def has_read_permission(request):
        return True

    @allow_staff_or_superuser
    @authenticated_users
    def has_object_read_permission(self, request):
        return True

    @staticmethod
    @allow_staff_or_superuser
    @authenticated_users
    def has_write_permission(request):
        return any(
            [request.user.roles.filter(name__in=[
                'SCJC',
                'Librarian',
            ], )])

    @allow_staff_or_superuser
    @authenticated_users
    def has_object_write_permission(self, request):
        return any(
            [request.user.roles.filter(name__in=[
                'SCJC',
                'Librarian',
            ], )])

    # Transitions
    @fsm_log_by
    @transition(field=status, source='*', target=STATUS.active)
    def activate(self, *args, **kwargs):
        """Activate the Chart."""
        return

    @fsm_log_by
    @transition(field=status, source='*', target=STATUS.inactive)
    def deactivate(self, *args, **kwargs):
        """Deactivate the Chart."""
        return

    @fsm_log_by
    @transition(field=status, source='*', target=STATUS.protected)
    def protect(self, *args, **kwargs):
        """Protect the Chart."""
        return
Пример #30
0
class Award(TimeStampedModel):
    """
    Award Model.

    The specific award conferred by a Group.
    """

    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
    )

    name = models.CharField(
        help_text="""Award Name.""",
        max_length=255,
    )

    STATUS = Choices(
        (
            -10,
            'inactive',
            'Inactive',
        ),
        (
            0,
            'new',
            'New',
        ),
        (
            10,
            'active',
            'Active',
        ),
    )

    status = FSMIntegerField(
        help_text=
        """DO NOT CHANGE MANUALLY unless correcting a mistake.  Use the buttons to change state.""",
        choices=STATUS,
        default=STATUS.new,
    )

    KIND = Choices(
        (32, 'chorus', "Chorus"),
        (41, 'quartet', "Quartet"),
    )

    kind = models.IntegerField(choices=KIND, )

    GENDER = Choices(
        (10, 'male', "Male"),
        (20, 'female', "Female"),
        (30, 'mixed', "Mixed"),
    )

    gender = models.IntegerField(
        help_text="""
            The gender to which the award is restricted.  If unselected, this award is open to all combinations.
        """,
        choices=GENDER,
        null=True,
        blank=True,
    )

    LEVEL = Choices(
        (10, 'championship', "Championship"),
        (30, 'qualifier', "Qualifier"),
        (45, 'representative', "Representative"),
        (50, 'deferred', "Deferred"),
        (60, 'manual', "Manual"),
        (70, 'raw', "Improved - Raw"),
        (80, 'standard', "Improved - Standard"),
    )

    level = models.IntegerField(choices=LEVEL, )

    SEASON = Choices(
        (
            1,
            'summer',
            'Summer',
        ),
        (
            2,
            'midwinter',
            'Midwinter',
        ),
        (
            3,
            'fall',
            'Fall',
        ),
        (
            4,
            'spring',
            'Spring',
        ),
    )

    season = models.IntegerField(choices=SEASON, )

    DISTRICT = Choices(
        (110, 'bhs', 'BHS'),
        (200, 'car', 'CAR'),
        (205, 'csd', 'CSD'),
        (210, 'dix', 'DIX'),
        (215, 'evg', 'EVG'),
        (220, 'fwd', 'FWD'),
        (225, 'ill', 'ILL'),
        (230, 'jad', 'JAD'),
        (235, 'lol', 'LOL'),
        (240, 'mad', 'MAD'),
        (345, 'ned', 'NED'),
        (350, 'nsc', 'NSC'),
        (355, 'ont', 'ONT'),
        (360, 'pio', 'PIO'),
        (365, 'rmd', 'RMD'),
        (370, 'sld', 'SLD'),
        (375, 'sun', 'SUN'),
        (380, 'swd', 'SWD'),
    )

    district = models.IntegerField(
        choices=DISTRICT,
        null=True,
        blank=True,
    )

    DIVISION = Choices(
        (10, 'evgd1', 'EVG Division I'),
        (20, 'evgd2', 'EVG Division II'),
        (30, 'evgd3', 'EVG Division III'),
        (40, 'evgd4', 'EVG Division IV'),
        (50, 'evgd5', 'EVG Division V'),
        (60, 'fwdaz', 'FWD Arizona'),
        (70, 'fwdne', 'FWD Northeast'),
        (80, 'fwdnw', 'FWD Northwest'),
        (90, 'fwdse', 'FWD Southeast'),
        (100, 'fwdsw', 'FWD Southwest'),
        (110, 'lol10l', 'LOL 10000 Lakes'),
        (120, 'lolone', 'LOL Division One'),
        (130, 'lolnp', 'LOL Northern Plains'),
        (140, 'lolpkr', 'LOL Packerland'),
        (150, 'lolsw', 'LOL Southwest'),
        # (160, 'madatl', 'MAD Atlantic'),
        (170, 'madcen', 'MAD Central'),
        (180, 'madnth', 'MAD Northern'),
        (190, 'madsth', 'MAD Southern'),
        # (200, 'madwst', 'MAD Western'),
        (210, 'nedgp', 'NED Granite and Pine'),
        (220, 'nedmtn', 'NED Mountain'),
        (230, 'nedpat', 'NED Patriot'),
        (240, 'nedsun', 'NED Sunrise'),
        (250, 'nedyke', 'NED Yankee'),
        (260, 'swdne', 'SWD Northeast'),
        (270, 'swdnw', 'SWD Northwest'),
        (280, 'swdse', 'SWD Southeast'),
        (290, 'swdsw', 'SWD Southwest'),
    )

    division = models.IntegerField(
        choices=DIVISION,
        null=True,
        blank=True,
    )

    is_single = models.BooleanField(
        help_text="""Single-round award""",
        default=False,
    )

    threshold = models.FloatField(
        help_text="""
            The score threshold for automatic qualification (if any.)
        """,
        null=True,
        blank=True,
    )

    minimum = models.FloatField(
        help_text="""
            The minimum score required for qualification (if any.)
        """,
        null=True,
        blank=True,
    )

    advance = models.FloatField(
        help_text="""
            The score threshold to advance to next round (if any) in
            multi-round qualification.
        """,
        null=True,
        blank=True,
    )

    spots = models.IntegerField(
        help_text="""Number of top spots which qualify""",
        null=True,
        blank=True,
    )

    description = models.TextField(
        help_text="""
            The Public description of the award.""",
        blank=True,
        max_length=1000,
    )

    notes = models.TextField(
        help_text="""
            Private Notes (for internal use only).""",
        blank=True,
    )

    AGE = Choices(
        (
            10,
            'seniors',
            'Seniors',
        ),
        (
            20,
            'novice',
            'Novice',
        ),
        (
            30,
            'youth',
            'Youth',
        ),
    )

    age = models.IntegerField(
        choices=AGE,
        null=True,
        blank=True,
    )

    is_novice = models.BooleanField(default=False, )

    SIZE = Choices(
        (
            100,
            'p1',
            'Plateau 1',
        ),
        (
            110,
            'p2',
            'Plateau 2',
        ),
        (
            120,
            'p3',
            'Plateau 3',
        ),
        (
            130,
            'p4',
            'Plateau 4',
        ),
        (
            140,
            'pa',
            'Plateau A',
        ),
        (
            150,
            'paa',
            'Plateau AA',
        ),
        (
            160,
            'paaa',
            'Plateau AAA',
        ),
        (
            170,
            'paaaa',
            'Plateau AAAA',
        ),
        (
            180,
            'pb',
            'Plateau B',
        ),
        (
            190,
            'pi',
            'Plateau I',
        ),
        (
            200,
            'pii',
            'Plateau II',
        ),
        (
            210,
            'piii',
            'Plateau III',
        ),
        (
            220,
            'piv',
            'Plateau IV',
        ),
        (
            230,
            'small',
            'Small',
        ),
    )

    size = models.IntegerField(
        choices=SIZE,
        null=True,
        blank=True,
    )

    size_range = IntegerRangeField(
        null=True,
        blank=True,
    )

    SCOPE = Choices(
        (
            100,
            'p1',
            'Plateau 1',
        ),
        (
            110,
            'p2',
            'Plateau 2',
        ),
        (
            120,
            'p3',
            'Plateau 3',
        ),
        (
            130,
            'p4',
            'Plateau 4',
        ),
        (
            140,
            'pa',
            'Plateau A',
        ),
        (
            150,
            'paa',
            'Plateau AA',
        ),
        (
            160,
            'paaa',
            'Plateau AAA',
        ),
        (
            170,
            'paaaa',
            'Plateau AAAA',
        ),
        (
            175,
            'paaaaa',
            'Plateau AAAAA',
        ),
    )

    scope = models.IntegerField(
        choices=SCOPE,
        null=True,
        blank=True,
    )

    scope_range = DecimalRangeField(
        null=True,
        blank=True,
    )

    # Denormalizations
    tree_sort = models.IntegerField(
        unique=True,
        blank=True,
        null=True,
        editable=False,
    )

    # Internals
    objects = AwardManager()

    class Meta:
        pass

    class JSONAPIMeta:
        resource_name = "award"

    def __str__(self):
        return self.name

    def clean(self):
        if self.level == self.LEVEL.qualifier and not self.threshold:
            raise ValidationError({'level': 'Qualifiers must have thresholds'})
        # if self.level != self.LEVEL.qualifier and self.threshold:
        #     raise ValidationError(
        #         {'level': 'Non-Qualifiers must not have thresholds'}
        #     )

    def is_searchable(self):
        return bool(self.status == self.STATUS.active)

    # Award Permissions
    @staticmethod
    @allow_staff_or_superuser
    @authenticated_users
    def has_read_permission(request):
        return True

    @allow_staff_or_superuser
    @authenticated_users
    def has_object_read_permission(self, request):
        return True

    @staticmethod
    @allow_staff_or_superuser
    @authenticated_users
    def has_write_permission(request):
        return request.user.roles.filter(name__in=[
            'SCJC',
        ])

    @allow_staff_or_superuser
    @authenticated_users
    def has_object_write_permission(self, request):
        return request.user.roles.filter(name__in=[
            'SCJC',
        ])

    # Transitions
    @fsm_log_by
    @transition(field=status, source='*', target=STATUS.active)
    def activate(self, *args, **kwargs):
        """Activate the Award."""
        return

    @fsm_log_by
    @transition(field=status, source='*', target=STATUS.inactive)
    def deactivate(self, *args, **kwargs):
        """Deactivate the Award."""
        return