class Account(Model): OPEN = 'OPEN' CLOSED = 'CLOSED' STATUS_CHOICES = ( (OPEN, _('Open')), (CLOSED, _('Closed')), ) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) created = models.DateTimeField(auto_now_add=True, db_index=True) modified = models.DateTimeField(auto_now=True) owner = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='billing_account', on_delete=PROTECT) currency = CurrencyField(db_index=True) status = FSMField(max_length=20, choices=STATUS_CHOICES, default=OPEN, db_index=True) objects = AccountQuerySet.as_manager() def balance(self, as_of: date = None): charges = Charge.objects.filter(account=self) transactions = Transaction.successful.filter(account=self) if as_of is not None: charges = charges.filter(created__lte=as_of) transactions = transactions.filter(created__lte=as_of) return total_amount(transactions) - total_amount(charges) @transition(field=status, source=OPEN, target=CLOSED) def close(self): pass @transition(field=status, source=CLOSED, target=OPEN) def reopen(self): pass def __str__(self): return str(self.owner)
class Research(models.Model): claim = models.ForeignKey(Claim, related_name="research_set", on_delete=models.CASCADE) truth = models.IntegerField(_("Truth"), null=True) content = models.TextField(blank=True) sources = models.ManyToManyField("Source", verbose_name=_("Sources"), related_name='research') researcher = models.ForeignKey("user.User", related_name="researched", on_delete=models.PROTECT) researched = models.DateTimeField(null=True) verification = models.ForeignKey("Verification", related_name="+", null=True, on_delete=models.SET_NULL) arbitration = models.ForeignKey("Arbitration", related_name="+", null=True, on_delete=models.SET_NULL) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) RESEARCHING = 'researching' RESEARCHED = 'researched' VERIFYING = 'verifying' VERIFIED = 'verified' CONTENTION = 'contention' # a type of Research ARBITRATE = 'arbitrate' ARBITRATING = 'arbitrating' COMPLETED = 'completed' PHASES = ( (RESEARCHING, pgettext_lazy('research.status', 'Researching')), (RESEARCHED, pgettext_lazy('research.status', 'Researched')), (VERIFYING, pgettext_lazy('research.status', 'Verifying')), (VERIFIED, pgettext_lazy('research.status', 'Verified')), (CONTENTION, pgettext_lazy('research.status', 'Contention')), (ARBITRATE, pgettext_lazy('research.status', 'Needs Arbitration')), (ARBITRATING, pgettext_lazy('research.status', 'Arbitrating')), (COMPLETED, pgettext_lazy('research.status', 'Completed')), ) phase = FSMField(choices=PHASES, default=RESEARCHING) objects = ResearchQuerySet.as_manager() def is_completed(self): return self.phase == Research.COMPLETED
class OrderPayment(with_metaclass(WorkflowMixinMetaclass, models.Model)): """ A model to hold received payments for a given order. """ order = deferred.ForeignKey(BaseOrder, verbose_name=_("Order")) status = FSMField(default='new', protected=True, verbose_name=_("Status")) amount = MoneyField( _("Amount paid"), help_text=_("How much was paid with this particular transfer.")) transaction_id = models.CharField( _("Transaction ID"), max_length=255, help_text=_("The transaction processor's reference")) created_at = models.DateTimeField(_("Received at"), auto_now_add=True) payment_method = models.CharField( _("Payment method"), max_length=255, help_text=_("The payment backend used to process the purchase")) class Meta: verbose_name = pgettext_lazy('order_models', "Order payment") verbose_name_plural = pgettext_lazy('order_models', "Order payments")
class Order(BaseModel): """ This Model is aimed to manage customer Orders contain a state field that manager can change any time. """ customer = models.ForeignKey( User, verbose_name="Customer", related_name='orders', on_delete=models.PROTECT, ) state = FSMField( default='waiting', protected=True, verbose_name="Status", ) total_price = models.PositiveIntegerField("Total Price", default=0) @transition(field=state, source="waiting", target="preparation") def prepare(self): # send email to customer pass @transition(field=state, source="preparation", target="ready") def ready(self): # send email to customer pass @transition(field=state, source="ready", target="delivered") def deliver(self): # send email to customer pass @transition(field=state, source="waiting", target="canceled") def cancel(self): # send email to customer pass
class Bloggs(models.Model): blogger = models.ForeignKey(blogger, related_name="bloggs") blog_type = models.ForeignKey(BlogType, related_name="bloggs") title = models.CharField(max_length=200) is_public = models.BooleanField(default=False) description = models.CharField(max_length=1000) state = FSMField(default='new') def __str__(self): return self.title @transition(field=state, source=["new", "draft"], target="draft") def draft(self): pass @transition(field=state, source=["new", "draft"], target="publish") def draft(self): pass @transition(field=state, source=["publish"], target="cancel") def draft(self): pass
class Order(models.Model): customer = models.CharField(max_length=255) address = models.CharField(max_length=1000) item = models.CharField(max_length=255) price = models.DecimalField(max_digits=9, decimal_places=2) STATE_CHOICES = (("ordered", "Ordered", "Order"), ("shipped", "Shipped", "Order"), ("returned", "Returned", "CancelledOrder"), ("cancelled", "Cancelled", "CancelledOrder")) state = FSMField(default="ordered", state_choices=STATE_CHOICES) def refund(self): # return the money pass @transition(field=state, source="ordered", target="shipped") def ship(self): # send notification email pass def not_too_expensive_for_return(self): return self.price <= 200 @transition(field=state, source="shipped", target="returned", conditions=[not_too_expensive_for_return], permission=lambda instance, user: user not in BLACKLIST) def receive_return(self): self.refund() @transition(field=state, source="ordered", target="cancelled") def cancel(self): self.refund() @property def revenue(self): return self.price
class Article(models.Model): STATES = ( ('draft', 'Draft'), ('submitted', 'Article submitted'), ('published', 'Article published'), ('deleted', 'Article deleted'), ) state = FSMField(choices=STATES, default='draft', protected=True) @fsm_log_by @fsm_log_description @transition(field=state, source='draft', target='submitted') def submit(self, description=None, by=None): pass @fsm_log_by @transition(field=state, source='submitted', target='draft') def request_changes(self, by=None): pass @fsm_log_by @transition(field=state, source='submitted', target='published') def publish(self, by=None): pass @fsm_log_by @transition(field=state, source='*', target='deleted') def delete(self, using=None): pass @fsm_log_by @fsm_log_description(allow_inline=True) @transition(field=state, source='draft', target='submitted') def submit_inline_description_change(self, change_to, description=None, by=None): description.set(change_to)
class Insect(models.Model): class STATE: CATERPILLAR = 'CTR' BUTTERFLY = 'BTF' STATE_CHOICES = ((STATE.CATERPILLAR, 'Caterpillar', 'Caterpillar'), (STATE.BUTTERFLY, 'Butterfly', 'Butterfly')) state = FSMField(default=STATE.CATERPILLAR, state_choices=STATE_CHOICES) @transition(field=state, source=STATE.CATERPILLAR, target=STATE.BUTTERFLY) def cocoon(self): pass def fly(self): raise NotImplementedError def crawl(self): raise NotImplementedError class Meta: app_label = 'testapp'
class BlogPostWithConditions(models.Model): state = FSMField(default='new') def model_condition(self): return True def unmet_condition(self): return False @transition(field=state, source='new', target='published', conditions=[condition_func, model_condition]) def publish(self): pass @transition(field=state, source='published', target='destroyed', conditions=[condition_func, unmet_condition]) def destroy(self): pass
class Application(models.Model): class Meta: verbose_name = "Application Form" verbose_name_plural = "Application Forms" ordering = ('created', ) name = models.CharField(max_length=512) description = models.TextField() position = models.ForeignKey('user.Position', related_name='applications', on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) published = models.DateTimeField(null=True) DRAFT = "draft" READY = "ready" LIVE = "live" STATE_CHOICES = ( (DRAFT, "Draft"), (READY, "Ready"), (LIVE, "Live"), ) state = FSMField(default=DRAFT, choices=STATE_CHOICES) objects = ApplicationQuerySet.as_manager() @transition(field=state, source=[DRAFT], target=READY) def approve(self): pass @transition(field=state, source=[READY], target=LIVE) def publish(self): self.published = timezone.now() def __str__(self): return self.name
class MultiResultTest(models.Model): state = FSMField(default='new') @transition( field=state, source='new', target=RETURN_VALUE('for_moderators', 'published')) def publish(self, is_public=False): return 'published' if is_public else 'for_moderators' @transition( field=state, source='for_moderators', target=GET_STATE( lambda self, allowed: 'published' if allowed else 'rejected', states=['published', 'rejected'] ) ) def moderate(self, allowed): pass class Meta: app_label = 'testapp'
class MemberApplication(models.Model): applicant = models.OneToOneField( settings.AUTH_USER_MODEL, related_name='applicant') mobile = models.CharField('手机号码', max_length=100, blank=True) job_title = models.CharField('职位', max_length=100, blank=True) company_title = models.CharField('公司名称', max_length=255, blank=True) company = models.ForeignKey(Company, blank=True, null=True) info = models.TextField('其它信息', blank=True) reason = models.TextField('拒绝理由', blank=True) auditor = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='auditor', null=True, blank=True) state = FSMField(default='new') created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) def __str__(self): return '{0} - {1}'.format(self.company_title, self.applicant) def has_auditor(self): return self.auditor is not None def has_company_assoc(self): return self.company is not None and self.auditor is not None def can_approve(self): return self.has_auditor() and self.has_company_assoc() @transition(field=state, source='new', target='approved', conditions=[can_approve]) def approved(self): notify.send(self.auditor, recipient=self.applicant, verb='审核通过') @transition(field=state, source='new', target='denied', conditions=[has_auditor]) def denied(self): notify.send(self.auditor, recipient=self.applicant, verb='拒绝', description=self.reason)
class AuthResult(core_models.UuidMixin, core_models.ErrorMessageMixin, TimeStampedModel): class States: SCHEDULED = 'Scheduled' PROCESSING = 'Processing' OK = 'OK' CANCELED = 'Canceled' ERRED = 'Erred' CHOICES = ((SCHEDULED, SCHEDULED), (PROCESSING, PROCESSING), (OK, OK), (CANCELED, CANCELED), (ERRED, ERRED)) user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='auth_valimo_results', null=True) phone = models.CharField(max_length=30) message = models.CharField(max_length=4, default=_default_message, help_text='This message will be shown to user.') state = FSMField(choices=States.CHOICES, default=States.SCHEDULED) details = models.CharField(max_length=255, blank=True, help_text='Cancellation details.') backend_transaction_id = models.CharField(max_length=100, blank=True) # for consistency with other models with state @property def human_readable_state(self): return self.state @transition(field=state, source=States.SCHEDULED, target=States.PROCESSING) def begin_processing(self): pass @transition(field=state, source=States.PROCESSING, target=States.OK) def set_ok(self): pass @transition(field=state, source=States.PROCESSING, target=States.CANCELED) def set_canceled(self): pass @transition(field=state, source='*', target=States.ERRED) def set_erred(self): pass
class Take(models.Model): session = models.ForeignKey(Session, related_name="takes", on_delete=CASCADE) name = models.CharField(max_length=255) number = models.IntegerField() started_at = models.DateTimeField(auto_now_add=True) location = models.FloatField() length = models.FloatField(null=True) take_mix_source = models.CharField(max_length=1024, null=True) take_mix_processed = models.CharField(max_length=1024, null=True) state = FSMField(default="started") @transition(field=state, source="started", target="queued") def stop(self): pass @transition(field=state, source="started", target="canceled") def cancel(self): pass @transition(field=state, source="recorded", target="queued") def queue(self): pass @transition(field=state, source="queued", target="recorded") def unqueue(self): pass @transition(field=state, source="queued", target="processing") def processing_started(self): pass @transition(field=state, source="processing", target="uploaded") def upload_finished(self): pass
class StatisticsEntry(ExportModelOperationsMixin("attendance.StatisticsEntry"), models.Model): statistics = models.ForeignKey("Statistics", models.CASCADE, related_name="entries") incoming = models.ForeignKey("Entry", models.CASCADE, related_name="+") outgoing = models.ForeignKey("Entry", models.CASCADE, null=True, blank=True, related_name="+") state = FSMField(default="created") class Meta: unique_together = (("statistics", "incoming"), ) ordering = ("incoming__created", ) get_latest_by = "incoming__created" @transition(field=state, source="created", target="completed") def complete(self, entry=None): self.outgoing = entry def __str__(s): return f"{s.statistics}: {s.incoming}/{s.outgoing}"
class Season(models.Model): """Season model""" class Meta: verbose_name = "Выезжавший на сезон" verbose_name_plural = "Выезжавшие на сезон" boec = models.ForeignKey(Boec, on_delete=models.CASCADE, verbose_name="ФИО", related_name="seasons") class SeasonState(TextChoices): INITIAL = "initial", _("Не подтвержен") ACCEPTED = "accepted", _("Зачтен") NOT_ACCEPTED = "rejected", _("Не зачтен") state = FSMField( default=SeasonState.INITIAL, choices=SeasonState.choices, verbose_name="Зачтенность", ) def __str__(self): return self.boec.full_name
class Product(models.Model): DRAFT = 'draft' PUBLISHED = 'published' STATES = ( (DRAFT, 'Draft'), (PUBLISHED, 'Published'), ) name = models.CharField(max_length=255) price = models.IntegerField() currency = models.CharField(choices=CURRENCIES, default=EUR, max_length=3) status = FSMField(default=DRAFT, protected=True, choices=STATES) objects = ProductManager() @transition(field=status, source=DRAFT, target=PUBLISHED) def publish(self): pass @transition(field=status, source=PUBLISHED, target=DRAFT) def unpublish(self): pass def __str__(self): return self.name
class Text(models.Model): created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) name = models.CharField(max_length=255) text = models.TextField() state = FSMField(default='new', protected=True) @transition(field=state, source='new', target='done') def processed(self): """ Side effects """ @transition(field=state, source='done', target='new') def renew(self): """ Side effects """ def save_model(self, request, obj, form, change): obj.created_by = request.user obj.save() def __str__(self): return '{name} ({state})'.format(name=self.name, state=self.state)
class Organization(core_models.AbstractBaseModel): STATE_CHOICES = ( ("CURRENT", "Current"), ("NAN", "No Approval Necessary"), ("LAPSED", "Lapsed"), ) SUBSCRIPTION_LEVEL_CHOICES = ( ("FREE", "Free"), ("TRIAL", "Trial"), ("PRO", "Professional"), ) name = models.CharField("Name", max_length=96, null=False) notes = models.TextField("Notes", null=True, blank=True) state = FSMField( default="CURRENT", verbose_name="Status", choices=STATE_CHOICES, # protected=True, ) subscription_level = models.CharField("Subscription Level", max_length=24, default="FREE") tenant_id = "id" def __str__(self): return str(self.name) # States: # ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– # CURRENT - tenant is active and current with account # NO APPROVAL NEEDED - tenant exists outside of approval structure (admin/pro bono) # LAPSED - tenant is inactive or account is past due # ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– # Users of tenants that are CURRENT can log in normally # Users of tenants that are NO APPROVAL NEEDED can log in normally # Users of tenants that are LAPSED can log in on a read-only basis @fsm_log_by @transition( field=state, source="*", target="CURRENT", permission="app.change_tenant_status", ) def approve(self, by=None): return @fsm_log_by @transition( field=state, source="*", target="NAN", permission="app.change_tenant_status", ) def no_approval_needed(self, by=None): return @fsm_log_by @transition( field=state, source="*", target="LAPSED", permission="app.change_tenant_status", ) def lapsed(self, by=None): return class Meta: ordering = ["name"] permissions = [ ("change_tenant_status", "Can change status of tenant"), ]
class Candidate(models.Model): SHORTLIST = 'shortlist' TECHNICAL = 'technical' PRACTICAL = 'practical' HR = 'hr' REJECTED = 'rejected' SELECTED = 'selected' STATUS_TAG = [ (SHORTLIST, 'Short Listed'), (TECHNICAL, 'For Technical Round'), (PRACTICAL, 'For Practical Round'), (HR, 'For HR Round'), (REJECTED, 'Rejected'), (SELECTED, 'Selected'), ] EXPERIENCE_CHOICE = [ ('2', '0 - 1'), ('1 - 2', '1 - 2'), ('2 - 3', '2 - 3'), ('3 - Above', '3 - Above'), ] first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) birth_date = models.DateField() mobile = PhoneNumberField() email = models.EmailField(unique=True) token = models.CharField(max_length=30, null=True, blank=True) resume = models.FileField(upload_to='resumes/', null=True, blank=True) applied_for = models.ForeignKey(Vacancies, on_delete=models.CASCADE) status = FSMField(default=SHORTLIST, choices=STATUS_TAG) experience = models.CharField(default='0 - 1', choices=EXPERIENCE_CHOICE, max_length=20) is_verified = models.BooleanField(default=False) def __str__(self): return self.first_name def get_absolute_url(self): return reverse('scheduler:candidate_detail', args=[self.pk]) @transition(field='status', source=SHORTLIST, target=TECHNICAL) def technical_round(self): pass @transition(field='status', source=TECHNICAL, target=PRACTICAL) def practical_round(self): pass @transition(field='status', source=PRACTICAL, target=HR) def hr_round(self): pass @transition(field='status', source=HR, target=SELECTED) def select(self): pass @transition(field='status', source=[SHORTLIST, TECHNICAL, PRACTICAL, HR], target=REJECTED) def reject(self): pass
class Transaction(models.Model): amount = models.DecimalField( decimal_places=2, max_digits=12, validators=[MinValueValidator(Decimal('0.00'))]) currency = models.CharField(choices=currencies, max_length=4, help_text='The currency used for billing.') class States: Initial = 'initial' Pending = 'pending' Settled = 'settled' Failed = 'failed' Canceled = 'canceled' Refunded = 'refunded' @classmethod def as_list(cls): return [ getattr(cls, state) for state in vars(cls).keys() if state[0].isupper() ] @classmethod def as_choices(cls): return ((state, _(state.capitalize())) for state in cls.as_list()) external_reference = models.CharField(max_length=256, null=True, blank=True) data = JSONField(default={}, null=True, blank=True) state = FSMField(max_length=8, choices=States.as_choices(), default=States.Initial) proforma = models.ForeignKey("Proforma", null=True, blank=True) invoice = models.ForeignKey("Invoice", null=True, blank=True) payment_method = models.ForeignKey('PaymentMethod') uuid = models.UUIDField(default=uuid.uuid4) valid_until = models.DateTimeField(null=True, blank=True) last_access = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(default=timezone.now) updated_at = AutoDateTimeField(default=timezone.now) fail_code = models.CharField(choices=[(code, code) for code in FAIL_CODES.keys()], max_length=32, null=True, blank=True) refund_code = models.CharField(choices=[(code, code) for code in REFUND_CODES.keys()], max_length=32, null=True, blank=True) cancel_code = models.CharField(choices=[(code, code) for code in CANCEL_CODES.keys()], max_length=32, null=True, blank=True) @property def final_fields(self): fields = [ 'proforma', 'uuid', 'payment_method', 'amount', 'currency', 'created_at' ] if self.invoice: fields.append('invoice') return fields def __init__(self, *args, **kwargs): self.form_class = kwargs.pop('form_class', None) super(Transaction, self).__init__(*args, **kwargs) @transition(field=state, source=States.Initial, target=States.Pending) def process(self): pass @transition(field=state, source=[States.Initial, States.Pending], target=States.Settled) def settle(self): pass @transition(field=state, source=[States.Initial, States.Pending], target=States.Canceled) def cancel(self, cancel_code='default', cancel_reason='Unknown cancel reason'): self.cancel_code = cancel_code logger.error(str(cancel_reason)) @transition(field=state, source=[States.Initial, States.Pending], target=States.Failed) def fail(self, fail_code='default', fail_reason='Unknown fail reason'): self.fail_code = fail_code logger.error(str(fail_reason)) @transition(field=state, source=States.Settled, target=States.Refunded) def refund(self, refund_code='default', refund_reason='Unknown refund reason'): self.refund_code = refund_code logger.error(str(refund_reason)) def clean(self): document = self.document if not document: raise ValidationError( 'The transaction must have at least one document ' '(invoice or proforma).') if document.state == document.STATES.DRAFT: raise ValidationError( 'The transaction must have a non-draft document ' '(invoice or proforma).') if document.provider != self.provider: raise ValidationError( 'Provider doesn\'t match with the one in documents.') if document.customer != self.customer: raise ValidationError( 'Customer doesn\'t match with the one in documents.') if self.invoice and self.proforma: if self.invoice.proforma != self.proforma: raise ValidationError('Invoice and proforma are not related.') else: if self.invoice: self.proforma = self.invoice.proforma else: self.invoice = self.proforma.invoice # New transaction if not self.pk: if document.state != document.STATES.ISSUED: raise ValidationError( 'Transactions can only be created for issued documents.') if self.currency: if self.currency != self.document.transaction_currency: raise ValidationError( "Transaction currency is different from it's document's" " transaction_currency.") else: self.currency = self.document.transaction_currency if (self.payment_method.allowed_currencies and self.currency not in self.payment_method.allowed_currencies): raise ValidationError( 'Currency {} is not allowed by the payment method. ' 'Allowed currencies are {}.'.format( self.currency, self.payment_method.allowed_currencies)) if self.amount: if self.amount != self.document.transaction_total: raise ValidationError( "Transaction amount is different from it's document's " "transaction_total.") else: self.amount = self.document.transaction_total # We also check for settled because document pay transition might fail if self.document.transactions.filter(state__in=[ Transaction.States.Initial, Transaction.States.Pending, Transaction.States.Settled ]).exists(): raise ValidationError( 'There already are active transactions for the same ' 'billing documents.') def full_clean(self, *args, **kwargs): # 'amount' and 'currency' are handled in our clean method kwargs['exclude'] = kwargs.get('exclude', []) + ['currency', 'amount'] super(Transaction, self).full_clean(*args, **kwargs) # this assumes that nobody calls clean and then modifies this object # without calling clean again self.cleaned = True @property def can_be_consumed(self): if self.valid_until and self.valid_until < timezone.now(): return False if self.state != Transaction.States.Initial: return False return True @property def customer(self): return self.payment_method.customer @property def document(self): return self.invoice or self.proforma @property def provider(self): return self.document.provider @property def payment_processor(self): return self.payment_method.payment_processor def update_document_state(self): if (self.state == Transaction.States.Settled and self.document.state != self.document.STATES.PAID): self.document.pay() self.document.save() def __unicode__(self): return unicode(self.uuid)
class Playlist(TimeStampedModel): table = models.ForeignKey('users.Table', on_delete=models.SET_DEFAULT, null=True, related_name='playlist', default=None, verbose_name=_('Requester')) music = models.ForeignKey(Music, on_delete=models.CASCADE, related_name='playlist', verbose_name=_('Music')) is_active = models.BooleanField(default=True, verbose_name=_('Active?')) state = FSMField(choices=MUSIC_PLAYLIST_STATE_CHOICES, default=MUSIC_PLAYLIST_STATE.default) def __str__(self): return "<%s(%d):%s>" % (_('Playlist'), self.pk, self.music.title) class Meta: ordering = ('created', ) verbose_name = _('Playlist') verbose_name_plural = _('Playlist') def get_customer_name(self): if self.table is None: return None return self.table.name @property def title(self): return self.get_title() def get_title(self): return self.music.title @property def url(self): return self.get_url() def get_url(self): return self.music.music_url @property def pic_url(self): return self.get_pic_url() def get_pic_url(self): return self.music.picture_url @property def artist(self): return self.music.author @property def identifier(self): return self.music.external_id @classmethod def pendings(cls): return cls.objects.filter(state=MUSIC_PLAYLIST_STATE.default, is_active=True, music__provider=MUSIC_PROVIDER.spotify, music__state=MUSIC_STATE.ready)
class Music(TimeStampedModel): title = models.CharField(max_length=64, verbose_name=_('Title')) author = models.CharField(max_length=32, verbose_name=_('Author')) url = models.URLField(verbose_name=_('Music url')) external_id = models.CharField(max_length=32, unique=True, verbose_name=_('External ID')) pic_url = models.URLField(verbose_name=_('Picture URL')) provider = FSMField(choices=MUSIC_PROVIDER_CHOICES, default=MUSIC_PROVIDER.ting, verbose_name=_('Music Provider')) state = FSMField(choices=MUSIC_STATE_CHOICES, default=MUSIC_STATE.default, verbose_name=_('State')) details = JSONField(default={}, verbose_name=_('Details')) class Meta: ordering = ('title', ) verbose_name = _('Music') verbose_name_plural = _('Musics') def __str__(self): return "<%s(%d):%s>" % (_('Music'), self.pk, self.title) @classmethod def external_search(cls, keyword): # response = Ting.search_music(keyword) response = Spotify.search(keyword) return response @classmethod def find_music(cls, external_id, provider=MUSIC_PROVIDER.spotify): exist, instance = cls.exists(external_id, provider=provider) if exist: return True, instance else: instance = cls(external_id=external_id, provider=provider) is_valid = instance.clean() if is_valid: instance.save() return is_valid, instance def clean(self): super(Music, self).clean() external_id = self.external_id if self.provider == MUSIC_PROVIDER.ting: response_data = Ting.retrieve_music(external_id) if not response_data.get('songinfo'): return False else: self.title = response_data['songinfo']['title'] self.author = response_data['songinfo']['author'] self.url = response_data['bitrate']['show_link'] self.pic_url = response_data['songinfo']['pic_big'] return True else: response_data = Spotify.retrieve_music(external_id) self.title = response_data['name'] self.author = response_data['author'] self.url = response_data['url'] self.pic_url = response_data['pic_url'] self.state = MUSIC_STATE.ready return True @classmethod def exists(cls, external_id, provider=MUSIC_PROVIDER.spotify): qs = cls.objects.filter(external_id=external_id, provider=provider) if qs.exists(): return True, qs.first() else: return False, None @property def music_file_extension(self): return self.url.split('?')[0].split('/')[-1].split('.')[-1] @property def picture_file_extension(self): return self.pic_url.split('?')[0].split('/')[-1].split('.')[-1] @property def download_file_path(self): return "%s/%s.%s" % (settings.MUSIC_DOWNLOAD_PATH, self.external_id, self.music_file_extension) @property def picture_download_file_path(self): return "%s/%s.%s" % (settings.MUSIC_DOWNLOAD_PATH, self.external_id, self.picture_file_extension) @property def upload_key(self): return "media/music/%s/%s.%s" % (self.external_id, self.external_id, self.music_file_extension) @property def picture_upload_key(self): return "media/music/%s/%s.%s" % (self.external_id, self.external_id, self.picture_file_extension) @property def music_url(self): return "http://%s/%s" % (settings.AWS_S3_CUSTOM_DOMAIN, self.upload_key) @property def picture_url(self): return "http://%s/%s" % (settings.AWS_S3_CUSTOM_DOMAIN, self.picture_upload_key) @classmethod def get_invalid_musics(cls): return cls.objects.filter( modified__date__lte=datetime.datetime.now().date()) @transition(field='state', source=MUSIC_STATE.default, target=MUSIC_STATE.downloading) def download(self): request.urlretrieve(self.url, self.download_file_path) request.urlretrieve(self.pic_url, self.picture_download_file_path) @transition(field='state', source=MUSIC_STATE.downloading, target=MUSIC_STATE.uploading) def upload(self): s3 = boto3.client('s3', aws_access_key_id=settings.AWS_ACCESS_KEY_ID, aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY) bucket_name = settings.AWS_STORAGE_BUCKET_NAME s3.upload_file(self.download_file_path, bucket_name, self.upload_key) s3.upload_file(self.picture_download_file_path, bucket_name, self.picture_upload_key) @transition(field='state', source=MUSIC_STATE.uploading, target=MUSIC_STATE.ready) def approve(self): if os.path.exists(self.download_file_path): os.remove(self.download_file_path) if os.path.exists(self.picture_download_file_path): os.remove(self.picture_download_file_path) def process(self): self.download() self.upload() self.approve() self.save() @classmethod def spotify_musics(cls): return cls.objects.filter(provider=MUSIC_PROVIDER.spotify)
class PurchaseIndentRequest(BaseModel): """This stores all the information regarding a purchase indent of an employee.""" indenter = models.ForeignKey(Employee, on_delete=models.CASCADE) project_name = models.CharField(max_length=200) budget_head = models.CharField(max_length=50) category = models.CharField(max_length=50) make_or_model_reason = models.TextField(max_length=500, null=True, blank=True) proprietary_owner = models.CharField(max_length=100, null=True, blank=True) proprietary_distributor = models.CharField(max_length=100, null=True, blank=True) state = FSMField(blank=True, protected=not settings.DEBUG, default=STATE.SUBMITTED) budgetary_approval = models.ImageField(upload_to='budgetary_approval', blank=True) directors_approval = models.ImageField(upload_to='directors_approval', blank=True) project_approval = models.ImageField(upload_to='project_approval', blank=True) budget_sanctioned = models.DecimalField(max_digits=10, decimal_places=2, default=0) amount_already_spent = models.DecimalField(max_digits=10, decimal_places=2, default=0) budget_available = models.DecimalField(max_digits=10, decimal_places=2, default=0) expenditure_debitable_to = models.CharField(max_length=100, null=False, blank=False) @transition(field=state, source=STATE.SUBMITTED, target=STATE.APPROVED_BY_HOD) def hod_approve(self): """HOD approves the indent form.""" print "HOD approved this form. Current state:", self.state @transition(field=state, source=STATE.APPROVED_BY_HOD, target=STATE.APPROVED_BY_JAO) def jao_approve(self): """JAO approves the indent form.""" print "JAO approved this form. Current state:", self.state @transition(field=state, source=STATE.APPROVED_BY_JAO, target=STATE.APPROVED_BY_DR) def dr_approve(self): """DR approves the indent form.""" print "DR approved this form. Current state:", self.state @transition( field=state, source=[STATE.SUBMITTED, STATE.APPROVED_BY_HOD, STATE.APPROVED_BY_JAO], target=STATE.REJECT) def reject(self): """Reject the indent form.""" print "This form has been rejected. Current state:", self.state def __str__(self): """Return string representing the form as Name of Indentor[space]Created time.""" return str(self.indenter.user.first_name) + ' ' \ + str(self.indenter.user.last_name) + ' ' \ + str(self.created_at) def __unicode__(self): """Return string representing the form as Name of Indentor[space]Created time.""" return str(self.indenter.user.first_name) + ' ' \ + str(self.indenter.user.last_name) + ' ' \ + str(self.created_at)
class Payment(models.Model): objects = Manager.from_queryset(PaymentQuerySet)() amount = models.DecimalField( decimal_places=2, max_digits=8, validators=[MinValueValidator(Decimal('0.00'))]) due_date = models.DateField(null=True, blank=True, default=None) class Status(object): Unpaid = 'unpaid' Pending = 'pending' Paid = 'paid' Canceled = 'canceled' FinalStatuses = [Paid, Canceled] STATUS_CHOICES = ((Status.Unpaid, _('Unpaid')), (Status.Pending, _('Pending')), (Status.Paid, _('Paid')), (Status.Canceled, _('Canceled'))) status = FSMField(max_length=8, choices=STATUS_CHOICES, default=Status.Unpaid) customer = models.ForeignKey(Customer) provider = models.ForeignKey(Provider) proforma = models.OneToOneField("Proforma", null=True, blank=True, related_name='proforma_payment') invoice = models.OneToOneField("Invoice", null=True, blank=True, related_name='invoice_payment') visible = models.BooleanField(default=True) currency = models.CharField(choices=currencies, max_length=4, default='USD', help_text='The currency used for billing.') currency_rate_date = models.DateField(blank=True, null=True) @transition(field='status', source=[Status.Unpaid, Status.Pending], target=Status.Canceled) def cancel(self): pass @transition(field='status', source=Status.Unpaid, target=Status.Pending) def process(self): pass @transition(field='status', source=(Status.Unpaid, Status.Pending), target=Status.Paid) def succeed(self): pass @transition(field='status', source=Status.Pending, target=Status.Unpaid) def fail(self): pass def clean(self): document = self.invoice or self.proforma if document: if document.provider != self.provider: raise ValidationError( 'Provider doesn\'t match with the one in documents.') if document.customer != self.customer: raise ValidationError( 'Customer doesn\'t match with the one in documents.') if self.invoice and self.proforma: if self.invoice.proforma != self.proforma: raise ValidationError('Invoice and proforma are not related.') def _log_unsuccessful_transition(self, transition_name): logger.warning( '[Models][Payment]: %s', { 'detail': 'Couldn\'t %s payment' % transition_name, 'payment_id': self.id, 'customer_id': self.customer.id }) @property def is_overdue(self): if self.status == Payment.Status.Unpaid and self.days_left <= 0: return True return False @property def days_left(self): return (self.due_date - date.today()).days def __unicode__(self): return '#%0#5d' % self.pk def diff(self, other_payment): changes = {} for attr in [ 'amount', 'due_date', 'status', 'customer', 'provider', 'proforma', 'invoice', 'visible', 'currency', 'currency_rate_date' ]: if not hasattr(other_payment, attr) or not hasattr(self, attr): continue current = getattr(self, attr, None) other = getattr(other_payment, attr, None) if current != other: changes[attr] = {'from': current, 'to': other} return changes
class Relationship(ManageableModel): start_date = models.DateTimeField(default=now) end_date = models.DateTimeField(null=True, blank=True) effective_start_date = models.DateTimeField(null=True, blank=True) effective_end_date = models.DateTimeField(null=True, blank=True) review_date = models.DateTimeField(null=True, blank=True) suspended_until = models.DateTimeField(null=True, blank=True) comment = models.TextField(blank=True) dependent_on = models.ForeignKey('self', null=True, blank=True) state = FSMField(max_length=16, choices=RELATIONSHIP_STATE_CHOICES, db_index=True, protected=True) suspended = FSMBooleanField(db_index=True, default=False, protected=True) delayed_save = GenericRelation(DelayedSave) class Meta: abstract = True def schedule_resave(self): now = timezone.now() dates = [self.start_date, self.end_date, self.effective_start_date, self.effective_end_date, self.review_date, self.suspended_until] dates = sorted(d for d in dates if d and d > now) if dates: try: delayed_save = self.delayed_save.get() except DelayedSave.DoesNotExist: delayed_save = DelayedSave(object=self) delayed_save.when = dates[0] delayed_save.save() elif self.delayed_save.exists(): self.delayed_save.get().delete() @transition(field=suspended, source=True, target=False) def unsuspend(self): self.suspended_until = None if self.state == 'suspended': self._unsuspend_state() @transition(field=suspended, source=False, target=True) def suspend(self, until=None): self.suspended_until = until if self.state == 'active': self._suspend_state() @transition(field=state, source='suspended', target='active') def _unsuspend_state(self): pass @transition(field=state, source='active', target='suspended') def _suspend_state(self): pass @transition(field=state, source='offered', target=RETURN_VALUE(), permission=is_owning_identity) def accept(self): return self._time_has_passed(now_active=True) @transition(field=state, source='offered', target='rejected', permission=is_owning_identity) def reject(self): pass @transition(field=state, source='*', target=RETURN_VALUE()) def _time_has_passed(self, now_active=False): start_date = self.effective_start_date or self.start_date end_date = self.effective_end_date or self.end_date now = timezone.now() if self.state == 'suspended' and self.suspended_until and self.suspended_until < now: self.unsuspend() if now_active or self.state in {'forthcoming', 'active', 'historic', ''}: if start_date <= now and (not end_date or now <= end_date): return 'active' if not self.suspended else 'suspended' elif now < start_date: return 'forthcoming' elif end_date < now: return 'historic' else: return self.state def save(self, *args, **kwargs): self._time_has_passed() super().save(*args, **kwargs) self.schedule_resave()
class AbstractUpload(models.Model): """ Abstract model for managing TUS uploads """ guid = models.UUIDField(_('GUID'), default=uuid.uuid4, unique=True) state = FSMField(default=states.INITIAL) upload_offset = models.BigIntegerField(default=0) upload_length = models.BigIntegerField(default=-1) upload_metadata = JSONField( load_kwargs={'object_pairs_hook': collections.OrderedDict}) filename = models.CharField(max_length=255, blank=True) temporary_file_path = models.CharField(max_length=4096, null=True) expires = models.DateTimeField(null=True, blank=True) class Meta: abstract = True def clean_fields(self, exclude=None): super(AbstractUpload, self).clean_fields(exclude=exclude) if self.upload_offset < 0: raise ValidationError(_('upload_offset should be >= 0.')) def write_data(self, bytes, chunk_size): num_bytes_written = write_bytes_to_file(self.temporary_file_path, self.upload_offset, bytes, makedirs=True) if num_bytes_written > 0: self.upload_offset += num_bytes_written self.save() def delete(self, *args, **kwargs): if self.temporary_file_path and os.path.exists( self.temporary_file_path): os.remove(self.temporary_file_path) super(AbstractUpload, self).delete(*args, **kwargs) def generate_filename(self): return os.path.join('{}.bin'.format(uuid.uuid4())) def save(self, force_insert=False, force_update=False, using=None, update_fields=None): if not self.filename: self.filename = self.generate_filename() return super(AbstractUpload, self).save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) def is_complete(self): return self.upload_offset == self.upload_length def temporary_file_exists(self): return self.temporary_file_path and os.path.isfile( self.temporary_file_path) def _temporary_file_exists(self): return self.temporary_file_exists() def get_or_create_temporary_file(self): if not self.temporary_file_path: fd, path = tempfile.mkstemp(prefix="tus-upload-") os.close(fd) self.temporary_file_path = path self.save() assert os.path.isfile(self.temporary_file_path) return self.temporary_file_path @transition(field=state, source=states.INITIAL, target=states.RECEIVING, conditions=[_temporary_file_exists]) def start_receiving(self): """ State transition to indicate the first file chunk has been received successfully """ # Trigger signal signals.receiving.send(sender=self.__class__, instance=self) @transition(field=state, source=states.RECEIVING, target=states.SAVING, conditions=[is_complete]) def start_saving(self): """ State transition to indicate that the upload is complete, and that the temporary file will be transferred to its final destination. """ # Trigger signal signals.saving.send(sender=self.__class__, instance=self) @transition(field=state, source=states.SAVING, target=states.DONE) def finish(self): """ State transition to indicate the upload is ready and the file is ready for access """ # Trigger signal signals.finished.send(sender=self.__class__, instance=self)
class TPMVisit(SoftDeleteMixin, TimeStampedModel, models.Model): DRAFT = 'draft' ASSIGNED = 'assigned' CANCELLED = 'cancelled' ACCEPTED = 'tpm_accepted' REJECTED = 'tpm_rejected' REPORTED = 'tpm_reported' REPORT_REJECTED = 'tpm_report_rejected' UNICEF_APPROVED = 'unicef_approved' STATUSES = Choices( (DRAFT, _('Draft')), (ASSIGNED, _('Assigned')), (CANCELLED, _('Cancelled')), (ACCEPTED, _('TPM Accepted')), (REJECTED, _('TPM Rejected')), (REPORTED, _('TPM Reported')), (REPORT_REJECTED, _('Sent Back to TPM')), (UNICEF_APPROVED, _('UNICEF Approved')), ) STATUSES_DATES = { STATUSES.draft: 'date_created', STATUSES.assigned: 'date_of_assigned', STATUSES.cancelled: 'date_of_cancelled', STATUSES.tpm_accepted: 'date_of_tpm_accepted', STATUSES.tpm_rejected: 'date_of_tpm_rejected', STATUSES.tpm_reported: 'date_of_tpm_reported', STATUSES.tpm_report_rejected: 'date_of_tpm_report_rejected', STATUSES.unicef_approved: 'date_of_unicef_approved', } author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, blank=True, null=True) tpm_partner = models.ForeignKey( TPMPartner, verbose_name=_('TPM Vendor'), null=True, on_delete=models.CASCADE, ) status = FSMField(verbose_name=_('Status'), max_length=20, choices=STATUSES, default=STATUSES.draft, protected=True) # UNICEF cancelled visit cancel_comment = models.TextField(verbose_name=_('Cancel Comment'), blank=True) # TPM rejected visit reject_comment = models.TextField(verbose_name=_('Reason for Rejection'), blank=True) approval_comment = models.TextField(verbose_name=_('Approval Comments'), blank=True) report_attachments = GenericRelation(Attachment, verbose_name=_('Visit Report'), blank=True) visit_information = models.TextField(verbose_name=_('Visit Information'), blank=True) date_of_assigned = models.DateField(blank=True, null=True, verbose_name=_('Date of Assigned')) date_of_cancelled = models.DateField(blank=True, null=True, verbose_name=_('Date of Cancelled')) date_of_tpm_accepted = models.DateField( blank=True, null=True, verbose_name=_('Date of TPM Accepted')) date_of_tpm_rejected = models.DateField( blank=True, null=True, verbose_name=_('Date of TPM Rejected')) date_of_tpm_reported = models.DateField( blank=True, null=True, verbose_name=_('Date of TPM Reported')) date_of_tpm_report_rejected = models.DateField( blank=True, null=True, verbose_name=_('Date of Sent Back to TPM')) date_of_unicef_approved = models.DateField( blank=True, null=True, verbose_name=_('Date of UNICEF Approved')) tpm_partner_focal_points = models.ManyToManyField( TPMPartnerStaffMember, verbose_name=_('TPM Focal Points'), related_name='tpm_visits', blank=True) tpm_partner_tracker = FieldTracker(fields=[ 'tpm_partner', ]) class Meta: ordering = ('id', ) verbose_name = _('TPM Visit') verbose_name_plural = _('TPM Visits') @property def date_created(self): return self.created.date() @property def status_date(self): return getattr(self, self.STATUSES_DATES[self.status]) @property def reference_number(self): return '{}/{}/{}/TPM'.format( connection.tenant.country_short_code or '', self.created.year, self.id, ) @property def start_date(self): # TODO: Rewrite to reduce number of SQL queries. return self.tpm_activities.aggregate(models.Min('date'))['date__min'] @property def end_date(self): # TODO: Rewrite to reduce number of SQL queries. return self.tpm_activities.aggregate(models.Max('date'))['date__max'] @property def unicef_focal_points(self): return set( itertools.chain(*map(lambda a: a.unicef_focal_points.all(), self.tpm_activities.all()))) @property def unicef_focal_points_with_emails(self): return list( filter(lambda u: u.email and u.is_active, self.unicef_focal_points)) @property def unicef_focal_points_and_pme(self): users = self.unicef_focal_points_with_emails if self.author and self.author.is_active and self.author.email: users += [self.author] return users def __str__(self): return 'Visit ({} to {} at {} - {})'.format( self.tpm_partner, ', '.join( filter( lambda x: x, self.tpm_activities.values_list('partner__name', flat=True))), self.start_date, self.end_date) def get_mail_context(self, user=None, include_token=False, include_activities=True): object_url = self.get_object_url(user=user, include_token=include_token) activities = self.tpm_activities.all() interventions = set(a.intervention.title for a in activities if a.intervention) partner_names = set(a.partner.name for a in activities) context = { 'reference_number': self.reference_number, 'tpm_partner': self.tpm_partner.name if self.tpm_partner else '-', 'multiple_tpm_activities': activities.count() > 1, 'object_url': object_url, 'partners': ', '.join(partner_names), 'interventions': ', '.join(interventions), } if include_activities: context['tpm_activities'] = [ a.get_mail_context(user=user, include_visit=False) for a in activities ] return context def _send_email(self, recipients, template_name, context=None, user=None, include_token=False, **kwargs): context = context or {} base_context = { 'visit': self.get_mail_context(user=user, include_token=include_token), 'environment': get_environment(), } base_context.update(context) context = base_context if isinstance(recipients, str): recipients = [ recipients, ] else: recipients = list(recipients) # assert recipients if recipients: send_notification_with_template( recipients=recipients, template_name=template_name, context=context, ) def _get_unicef_focal_points_as_email_recipients(self): return list( map(lambda u: u.email, self.unicef_focal_points_with_emails)) def _get_unicef_focal_points_and_pme_as_email_recipients(self): return list(map(lambda u: u.email, self.unicef_focal_points_and_pme)) def _get_tpm_focal_points_as_email_recipients(self): return list( self.tpm_partner_focal_points.filter( user__email__isnull=False, user__is_active=True).values_list('user__email', flat=True)) @transition( status, source=[STATUSES.draft, STATUSES.tpm_rejected], target=STATUSES.assigned, conditions=[ TPMVisitAssignRequiredFieldsCheck.as_condition(), ValidateTPMVisitActivities.as_condition(), ], permission=has_action_permission(action='assign'), custom={ 'name': lambda obj: _('Re-assign') if obj.status == TPMVisit.STATUSES.tpm_rejected else _('Assign') }) def assign(self): self.date_of_assigned = timezone.now() if self.tpm_partner.email: self._send_email( self.tpm_partner.email, 'tpm/visit/assign', cc=self._get_unicef_focal_points_and_pme_as_email_recipients()) for staff_member in self.tpm_partner_focal_points.filter( user__email__isnull=False, user__is_active=True): self._send_email( staff_member.user.email, 'tpm/visit/assign_staff_member', context={'recipient': staff_member.user.get_full_name()}, user=staff_member.user, include_token=True) @transition(status, source=[ STATUSES.draft, STATUSES.assigned, STATUSES.tpm_accepted, STATUSES.tpm_rejected, STATUSES.tpm_reported, STATUSES.tpm_report_rejected, ], target=STATUSES.cancelled, permission=has_action_permission(action='cancel'), custom={ 'serializer': TPMVisitCancelSerializer, 'name': _('Cancel Visit') }) def cancel(self, cancel_comment): self.cancel_comment = cancel_comment self.date_of_cancelled = timezone.now() @transition(status, source=[STATUSES.assigned], target=STATUSES.tpm_rejected, permission=has_action_permission(action='reject'), custom={'serializer': TPMVisitRejectSerializer}) def reject(self, reject_comment): self.date_of_tpm_rejected = timezone.now() self.reject_comment = reject_comment for recipient in self.unicef_focal_points_and_pme: self._send_email( recipient.email, 'tpm/visit/reject', cc=self._get_tpm_focal_points_as_email_recipients(), context={'recipient': recipient.get_full_name()}, user=recipient, ) @transition(status, source=[STATUSES.assigned], target=STATUSES.tpm_accepted, permission=has_action_permission(action='accept')) def accept(self): self.date_of_tpm_accepted = timezone.now() @transition(status, source=[STATUSES.tpm_accepted, STATUSES.tpm_report_rejected], target=STATUSES.tpm_reported, conditions=[ TPMVisitReportValidations.as_condition(), ], permission=has_action_permission(action='send_report'), custom={'name': _('Submit Report')}) def send_report(self): self.date_of_tpm_reported = timezone.now() for recipient in self.unicef_focal_points_and_pme: self._send_email( recipient.email, 'tpm/visit/report', cc=self._get_tpm_focal_points_as_email_recipients(), context={'recipient': recipient.get_full_name()}, user=recipient, ) @transition(status, source=[STATUSES.tpm_reported], target=STATUSES.tpm_report_rejected, permission=has_action_permission(action='reject_report'), custom={ 'serializer': TPMVisitRejectSerializer, 'name': _('Send back to TPM') }) def reject_report(self, reject_comment): self.date_of_tpm_report_rejected = timezone.now() TPMVisitReportRejectComment.objects.create( reject_reason=reject_comment, tpm_visit=self) for staff_user in self.tpm_partner_focal_points.filter( user__email__isnull=False, user__is_active=True): self._send_email( [staff_user.user.email], 'tpm/visit/report_rejected', context={'recipient': staff_user.user.get_full_name()}, user=staff_user.user) @transition(status, source=[STATUSES.tpm_reported], target=STATUSES.unicef_approved, custom={'serializer': TPMVisitApproveSerializer}, permission=has_action_permission(action='approve')) def approve(self, mark_as_programmatic_visit=None, approval_comment=None, notify_focal_point=True, notify_tpm_partner=True): mark_as_programmatic_visit = mark_as_programmatic_visit or [] self.tpm_activities.filter(id__in=mark_as_programmatic_visit).update( is_pv=True) self.date_of_unicef_approved = timezone.now() if notify_focal_point: for recipient in self.unicef_focal_points_with_emails: self._send_email( recipient.email, 'tpm/visit/approve_report', context={'recipient': recipient.get_full_name()}, user=recipient) if notify_tpm_partner: # TODO: Generate report as PDF attachment. for staff_user in self.tpm_partner_focal_points.filter( user__email__isnull=False, user__is_active=True): self._send_email( [ staff_user.user.email, ], 'tpm/visit/approve_report_tpm', context={'recipient': staff_user.user.get_full_name()}, user=staff_user.user) if approval_comment: self.approval_comment = approval_comment def get_object_url(self, **kwargs): return build_frontend_url('tpm', 'visits', self.id, 'details', **kwargs)
class DeliveryGroup(models.Model): """Represents a single shipment. A single order can consist of multiple shipment groups. """ status = FSMField(max_length=32, default=GroupStatus.NEW, choices=GroupStatus.CHOICES, protected=True) order = models.ForeignKey(Order, related_name='groups', editable=False, on_delete=models.CASCADE) shipping_method_name = models.CharField(max_length=255, null=True, default=None, blank=True, editable=False) tracking_number = models.CharField(max_length=255, default='', blank=True) last_updated = models.DateTimeField(null=True, auto_now=True) def __str__(self): return pgettext_lazy('Shipment group str', 'Shipment #%s') % self.pk def __repr__(self): return '%s(%r)' % (self.__class__.__name__, list(self)) def __iter__(self): return iter(self.lines.all()) @transition(field=status, source=GroupStatus.NEW, target=GroupStatus.NEW) def process(self, cart_lines, discounts=None): process_delivery_group(self, cart_lines, discounts) @transition(field=status, source=GroupStatus.NEW, target=GroupStatus.SHIPPED) def ship(self, tracking_number=''): ship_delivery_group(self, tracking_number) @transition(field=status, source=[GroupStatus.NEW, GroupStatus.SHIPPED], target=GroupStatus.CANCELLED) def cancel(self): cancel_delivery_group(self) def get_total_quantity(self): return sum([line.quantity for line in self]) def is_shipping_required(self): return any([line.is_shipping_required for line in self.lines.all()]) def can_ship(self): return self.is_shipping_required() and self.status == GroupStatus.NEW def can_cancel(self): return self.status != GroupStatus.CANCELLED def can_edit_lines(self): return self.status not in {GroupStatus.CANCELLED, GroupStatus.SHIPPED} def get_total(self): subtotals = [line.get_total() for line in self] if not subtotals: raise AttributeError( 'Calling get_total() on an empty shipment group') return sum(subtotals[1:], subtotals[0])
class CourseState(TimeStampedModel, ChangedByMixin): """ Publisher Workflow Course State Model. """ name = FSMField(default=CourseStateChoices.Draft, choices=CourseStateChoices.choices) approved_by_role = models.CharField(blank=True, null=True, max_length=63, choices=PublisherUserRole.choices) owner_role = models.CharField(max_length=63, choices=PublisherUserRole.choices) course = models.OneToOneField(Course, related_name='course_state') owner_role_modified = models.DateTimeField(auto_now_add=True, null=True, blank=True) marketing_reviewed = models.BooleanField(default=False) history = HistoricalRecords() # course team status Draft = _('Draft') SubmittedForMarketingReview = _('Submitted for Marketing Review') ApprovedByCourseTeam = _('Approved by Course Team') AwaitingCourseTeamReview = _('Awaiting Course Team Review') # internal user status NotAvailable = _('N/A') AwaitingMarketingReview = _('Awaiting Marketing Review') ApprovedByMarketing = _('Approved by Marketing') def __str__(self): return self.get_name_display() def can_send_for_review(self): """ Validate minimum required fields before sending for review. """ course = self.course return all([ course.title, course.number, course.short_description, course.full_description, course.organizations.first(), course.level_type, course.expected_learnings, course.primary_subject, course.image, course.course_team_admin ]) @transition(field=name, source='*', target=CourseStateChoices.Draft) def draft(self): # TODO: send email etc. pass @transition(field=name, source=CourseStateChoices.Draft, target=CourseStateChoices.Review, conditions=[can_send_for_review]) def review(self): # TODO: send email etc. pass @transition(field=name, source=CourseStateChoices.Review, target=CourseStateChoices.Approved) def approved(self): # TODO: send email etc. pass def change_state(self, state, user, site=None): """ Change course workflow state and ownership also send emails if required. """ is_notifications_enabled = waffle.switch_is_active( 'enable_publisher_email_notifications') if state == CourseStateChoices.Draft: self.draft() elif state == CourseStateChoices.Review: user_role = self.course.course_user_roles.get(user=user) if user_role.role == PublisherUserRole.MarketingReviewer: self.change_owner_role(PublisherUserRole.CourseTeam) self.marketing_reviewed = True elif user_role.role == PublisherUserRole.CourseTeam: self.change_owner_role(PublisherUserRole.MarketingReviewer) if is_notifications_enabled: emails.send_email_for_seo_review(self.course, site) self.review() if is_notifications_enabled: emails.send_email_for_send_for_review(self.course, user, site) elif state == CourseStateChoices.Approved: user_role = self.course.course_user_roles.get(user=user) self.approved_by_role = user_role.role self.marketing_reviewed = True self.approved() if is_notifications_enabled: emails.send_email_for_mark_as_reviewed(self.course, user, site) self.save() @property def is_approved(self): """ Check that course is approved or not.""" return self.name == CourseStateChoices.Approved def change_owner_role(self, role): """ Change ownership role. """ self.owner_role = role self.owner_role_modified = timezone.now() self.save() @property def is_draft(self): """ Check that course is in Draft state or not.""" return self.name == CourseStateChoices.Draft @property def is_in_review(self): """ Check that course is in Review state or not.""" return self.name == CourseStateChoices.Review @property def course_team_status(self): if self.is_draft and self.owner_role == PublisherUserRole.CourseTeam and not self.marketing_reviewed: return self.Draft elif self.owner_role == PublisherUserRole.MarketingReviewer: return self.SubmittedForMarketingReview elif self.owner_role == PublisherUserRole.CourseTeam and self.is_approved: return self.ApprovedByCourseTeam elif self.marketing_reviewed and self.owner_role == PublisherUserRole.CourseTeam: return self.AwaitingCourseTeamReview @property def internal_user_status(self): if self.is_draft and self.owner_role == PublisherUserRole.CourseTeam: return self.NotAvailable elif self.owner_role == PublisherUserRole.MarketingReviewer and ( self.is_in_review or self.is_draft): return self.AwaitingMarketingReview elif self.marketing_reviewed: return self.ApprovedByMarketing