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}"
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
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)
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))
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', )
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
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'])
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}"
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
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
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'])
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
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
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
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)
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')
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 = ''
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
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}"
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}"
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}"
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)
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)
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))
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")
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)]
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)
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
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
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