class InvalidModel(models.Model): state = FSMField(default='new') action = FSMField(default='no') @transition(source='new', target='no') def validate(self): pass
class BlogPostWithExplicitState(models.Model): state = FSMField(default='new') approvement = FSMField(default='new') @transition(field=state, source='new', target='published') def publish(self): pass @transition(field=approvement, source='new', target='approved') def approve(self): pass @transition(field=approvement, source='new', target='declined') def decline(self): pass
class BlogPost(models.Model): state = FSMField(default='new') @transition(source='new', target='published') def publish(self): pass @transition(source='published') def notify_all(self): pass @transition(source='published', target='hidden') def hide(self): pass @transition(source='new', target='removed') def remove(self): raise Exception('No rights to delete %s' % self) @transition(source=['published', 'hidden'], target='stolen') def steal(self): pass @transition(source='*', target='moderated') def moderate(self): pass
class UserMessage(models.Model): message_type = models.CharField(max_length=80, verbose_name="Send as", choices=MESSAGE_TYPES, default=MESSAGE_TYPES[0]) message_to = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="to", verbose_name="To", blank=False) message_from = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False, related_name="from+") subject = models.CharField(blank=True, null=True, max_length=200) message = models.TextField(blank=True) state = FSMField(default='pending') last_modified = models.DateTimeField(auto_now=True) class Meta: app_label = "signalbox" permissions = (("can_send_message", "Can send email/SMS to participant."), ) ordering = ['-last_modified'] def __unicode__(self): return "%s from %s to %s" % (self.message_type, self.message_from, self.message_to) def _send_sms(self): number = TwilioNumber.objects.get(is_default_account=True) client = number.client() to_number = self.message_to.userprofile.mobile from_number = number.phone_number sms = client.sms.messages.create(to=to_number, from_=from_number, body=self.message) return sms def _send_email(self): mess = send_mail(self.subject, self.message, self.message_from.email, (self.message_to.email, ), fail_silently=False) return mess @transition(field=state, source='pending', target='sent', save=True) def send(self): f = getattr(self, "_send_" + self.message_type.lower()) # e.g. gets _send_sms() return f()
class BlogPostWithConditions(models.Model): state = FSMField(default='new') def model_condition(self): return True def unmet_condition(self): return False @transition(source='new', target='published', conditions=[condition_func, model_condition]) def publish(self): pass @transition(source='published', target='destroyed', conditions=[condition_func, unmet_condition]) def destroy(self): pass
class Profile(models.Model): user = models.OneToOneField(User) nickname = models.CharField(max_length=100, blank=True, null=True) nickname_slug = models.CharField(max_length=100, blank=True, null=True, db_index=True) nickname_state = FSMField(default='unset', protected=True) task_types = models.ManyToManyField( 'TaskType', verbose_name='Which tasks would you like to help with?') wants_reviews = models.BooleanField( verbose_name="Help review other's tasks.", help_text="Reviewing other's tasks helps finish transcripts faster.", default=False) task_order = models.CharField( choices=TASK_ORDER_CHOICES, verbose_name='Which order would you like to receive tasks?', default='eager', max_length=10) def __unicode__(self): if self.nickname: fmt = '{self.nickname} ({self.user.username})' else: fmt = '{self.user.username}' return fmt.format(**locals()) def get_absolute_url(self): pk = self.user.pk # Line up URL with User pk, not Profile. return reverse('profiles:detail', kwargs=dict(pk=pk)) @property def preferred_task_names(self): return set(v['name'] for v in self.task_types.values('name')) @transition(nickname_state, 'unset', 'set') def set_nickname(self, new_nickname): new_nickname = u' '.join(new_nickname.strip().split()) if new_nickname == u'': raise ValueError('Nickname cannot be empty') new_slug = slugify(new_nickname) if Profile.objects.filter(nickname_slug=new_slug).exclude(pk=self.pk): raise ValueError('Nickname already in use by another user') else: self.nickname = new_nickname self.nickname_slug = new_slug
class vmware_resource_lock(models.Model): state = FSMField(default='request') service_request_id = models.IntegerField() esx = models.ForeignKey(vmware_server, blank=True, null=True) cpu = models.IntegerField(blank=True, null=True) mem = models.IntegerField(blank=True, null=True) storage_type = models.ForeignKey(vmware_datastore_type, blank=True, null=True) disk_space = models.IntegerField(blank=True, null=True) data_store = models.ForeignKey(vmware_datastore, blank=True, null=True) vmware_manage_ip = models.ForeignKey(vmware_manage_ip, blank=True, null=True) vmware_service_ip = models.ForeignKey(vmware_service_ip, blank=True, null=True) status = models.ForeignKey(vmware_resource_lock_status) def __unicode__(self): return 'Service request id: %d, %s' % (self.service_request_id, self.status.status) def save(self, *args, **kwargs): return super(vmware_resource_lock, self).save(*args, **kwargs) #@transition(field=state,source='*',target='freeze',save=True) def FreezeResource(self, ChangeStatus=False): logger.info('freeze resource') try: flag=FreezeVMwareLockedResource(esx=self.esx,cpu=self.cpu,mem=self.mem,\ storage_type=self.storage_type,\ data_store=self.data_store,disk_space=self.disk_space,\ manage_ip=self.vmware_manage_ip,service_ip=self.vmware_service_ip) """ if self.status!=vmware_resource_lock_status.objects.get(status='freeze') \ and ChangeStatus: self.status=vmware_resource_lock_status.objects.get(status='freeze') self.save() """ return flag except Exception, e: logger.error('Error in Freezing resource. %s' % e) return False
class RssFetch(models.Model): """A fetch of the RSS feed of a podcast. @startuml [*] --> not_fetched not_fetched --> fetching fetching --> fetched fetching --> failed not_fetched --> fetched fetched --> [*] failed --> [*] @enduml """ podcast = models.ForeignKey(Podcast, related_name='fetches') created = CreationDateTimeField() fetched = models.DateTimeField(blank=True, null=True) body = models.BinaryField(blank=True, null=True) state = FSMField(default='not_fetched', protected=True) @transition(state, 'not_fetched', 'fetched', save=True) def load_rss(self, body=None, filename=None): if body is not None: self.body = body elif filename is not None: with open(filename, 'rb') as f: self.body = f.read() else: raise TypeError('Specify either `body` or `filename`.') self.fetched = datetime_now() @transition(state, 'not_fetched', 'fetching', save=True) def start(self): self.fetched = datetime_now() @transition(state, 'fetching', 'fetched', save=True) def finish(self, body): self.body = body @transition(state, 'fetching', 'failed', save=True) def fail(self): pass
class ServiceRequest(models.Model): state = FSMField(default='submitted', blank=True, null=True) user = models.ForeignKey(User) submitter = models.CharField(max_length=50) description = models.TextField() request_type = models.ForeignKey(RequestType) request_parameter = models.CharField(max_length=1000) submit_time = models.DateTimeField(editable=False, blank=True, null=True) last_modify_time = models.DateTimeField(blank=True, null=True) request_status = models.ForeignKey(RequestStatus, ) status_message = models.CharField(max_length=500, blank=True, null=True) approver = models.CharField(max_length=50, blank=True, null=True) def __unicode__(self): return "ID: %d,Submitter: %s, description: %s." % ( self.id, self.submitter, self.description) def save(self, *args, **kwargs): if not self.id: self.submit_time = datetime.datetime.today() request_status = RequestStatus.objects.get(request_status_id=0) self.submitter = self.user.username self.last_modify_time = datetime.datetime.today() return super(ServiceRequest, self).save(*args, **kwargs)
class Podcast(models.Model): """A podcast we want to have in our directory. approval_state -------------- @startuml [*] --> not_approved not_approved --> owner_verified not_approved --> user_approved not_approved --> staff_approved user_approved --> staff_approved user_approved --> owner_verified staff_approved --> owner_verified @enduml """ rss_url = models.URLField(max_length=512, unique=True) approval_state = FSMField(default='not_approved', protected=True) title = models.CharField(max_length=512, blank=True, null=True) link_url = models.URLField(max_length=512, blank=True, null=True) image_url = models.URLField(max_length=512, blank=True, null=True) provides_own_transcripts = models.BooleanField( default=False, help_text= "If True, episodes have external transcript set to the episode's link URL" ) class Meta: ordering = ('title', 'rss_url') def __unicode__(self): return self.title or self.rss_url def get_absolute_url(self): if self.title: return reverse('podcasts:detail_slug', kwargs=dict(pk=self.pk, slug=slugify(self.title))) else: return reverse('podcasts:detail', kwargs=dict(pk=self.pk)) # approval_state # -------------- @transition(approval_state, source='not_approved', target='user_approved', save=True) def user_approve(self, user, notes=None): TranscriptionApproval.objects.create( podcast=self, user=user, approval_type='user', notes=notes, ) @transition(approval_state, source=['not_approved', 'user_approved'], target='staff_approved', save=True) def staff_approve(self, user, notes=None): TranscriptionApproval.objects.create( podcast=self, user=user, approval_type='staff', notes=notes, ) @transition(approval_state, source=['not_approved', 'user_approved', 'staff_approved'], target='owner_verified', save=True) def owner_verify(self, user, notes=None): TranscriptionApproval.objects.create( podcast=self, user=user, approval_type='owner', notes=notes, )
class Submission(models.Model): submitter = models.ForeignKey(DjangoUser) #accepted_by = models.ForeignKey(DjangoUser, ... editor = models.ForeignKey( DjangoUser, related_name='editor_for_set', blank=True, null=True, help_text="The editor assigned to this article submission.") article = models.OneToOneField(Article) date = models.DateTimeField(auto_now_add=True) state = FSMField(default=NEW) discussion = models.ForeignKey(Discussion) #discussion = models.TextField(blank=True, null=True) # replace this with a Discussion instance def state_label(self): return STATES[self.state] def state_description(self): # return magic.get(self.state, "does not compute") return "description of the current state here" def __getattr__(self, attr): # if article.submission.is_inreview(): print 'woo' if attr[:3] == 'is_': state = attr[3:] if state in STATE_SLUGS: return self.state == state raise AttributeError( 'Attribute "%s" not found. This could be because __getattr__ has been overriden in models.Submission ...' % attr) def has_editor(self): return self.editor != None def has_reviewer(self): return self.reviewassignment_set.count() > 0 @transition(source=[NEW, PRIVATE], target=SUBMITTED, conditions=[cond_article_has_submission_prerequisites]) def submit(self): """Article has never been submitted, or has been submitted and then withdrawn before being assigned to an editor, and needs to be brought before the editor""" @transition(source=SUBMITTED, target=ACCEPTED) def accept_for_review(self): "Article has been accepted by an editor and is now awaiting reviewers to be assigned." @transition(source=ACCEPTED, target=ASSIGNED, conditions=[has_editor]) def assign_to_editor(self): "Article has been accepted by the assigned editor. Editor must now choose reviewers." @transition(source=ASSIGNED, target=INREVIEW, conditions=[has_reviewer]) def in_review(self): "Editor has chosen reviewers and made the article available to them" ''' @transition(source=REVIEWED, target=INPRODUCTION) def in_production(self): "Editor accepts article for publication based on the reviewers and their own decision and has entered the production process." ''' @transition(source=INREVIEW, target=INCOPYEDIT) def begin_copyedit(self): "Editor accepts article for publication based on the reviewers (and their own decision) and has entered the first part of the production process, copyedit." @transition(source=INCOPYEDIT, target=INREVISION) def article_changes_required(self): "Submission has been returned to the user for specific, minor, changes prior to proofing." @transition(source=INREVISION, target=INCOPYEDIT) def article_changes_submitted(self): "Author has completed the changes asked of them and has returned " @transition(source=INCOPYEDIT, target=INPROOFING) def begin_proofing(self): "Copyedit is complete and the submission is ready to enter proofing." @transition(source=INPROOFING, target=RELEASED) def release(self): "Submission has completed proofing and can now be released." # @transition(source=SUBMITTED, target=PRIVATE) def withdraw(self): "User has submitted an article but wishes to alter it before an accept/decline decision is made by the editor." ''' @transition(source=ASSIGNED, target=PRIVATE) def pre_review_changes_required(self): "Article was accepted and assigned to an editor, however editor believes a major edit is required before being sent to peer review and has returned it to the user." ''' ''' @transition(source=PRIVATE, target=ASSIGNED, conditions=[has_editor]) def resubmit(self): "Article was returned to the user for edits prior to peer review and must now be resubmitted" ''' @transition(source=[SUBMITTED, INREVIEW], target=REFUSED) def refuse(self): """Article has been refused. It has been either: refused outright by editor or failed peer review or past peer review but refused by editor regardless.""" def __unicode__(self): return U'submission %s' % self.id def __repr__(self): return U'<Submission "%s">' % self.article.title
class ProtectedAccessModel(models.Model): status = FSMField(default='new', protected=True) @transition(source='new', target='published') def publish(self): pass
class Document(models.Model): status = FSMField(default='new') @transition(source='new', target='published') def publish(self): pass
class ClientCall(models.Model): 'call from Twilio' created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) sid = models.CharField(max_length=34, db_index=True) bed_count = models.PositiveIntegerField(default=0) shelter = models.ForeignKey(Shelter, null=True) location_name = models.CharField(max_length=300, null=True, blank=True) client_name = models.CharField(max_length=300, null=True, blank=True) call_state = FSMField(default='welcome') # states from which more data can be collected COLLECT_DATA_STATES = [ 'welcome', 'processed_location', 'processed_name', 'processed_bed_count', ] @transition(source='*', target='operator', save=True) def transfer_to_operator(self): '''transfer/connect to an operator, this class does not concern itself with further steps''' @transition(source='*', target='enqueued', save=True) def enqueue(self): '''"enqueue" this call (put on hold)''' @transition(source='enqueued', target='dequeued', save=True) def dequeue(self, url, method='GET'): '''"dequeue" this call (take off hold)''' site = Site.objects.get_current() client.calls.route( sid=self.sid, method=method, url=urljoin('http://' + site.domain, url) ) @transition(source='dequeued', target='confirmed', save=True) def confirm(self): '''confirm shelter placement''' @transition(source='*', target='declined', save=True) def decline(self): '''decline shelter placement (no match found/rejection)''' @transition(source=COLLECT_DATA_STATES, target='requested_location', save=True) def request_location(self): '''request client location, via voice''' @transition(source='requested_location', target='processed_location', save=True) def process_location(self, request): '''process location from a given request''' # TODO: fill stub with appropriate GIS conversion self.location_name = request.POST.get('RecordingUrl', '') @transition(source=COLLECT_DATA_STATES, target='requested_name', save=True) def request_name(self): '''request client name, via voice''' @transition(source='requested_name', target='processed_name', save=True) def process_name(self, request): '''process name from a given request''' self.client_name = request.POST.get('RecordingUrl', '') @transition(source=COLLECT_DATA_STATES, target='requested_bed_count', save=True) def request_bed_count(self): '''request number of beds required''' @transition(source='requested_bed_count', target='processed_bed_count', save=True) def process_bed_count(self, request): '''process beds from a given request''' self.bed_count = int(request.POST['Digits'])
class PostStatus(models.Model): """ Keeps track of the status of posts for moderation purposes. """ UNREVIEWED = 'unreviewed' USER_DELETED = 'user_deleted' FILTERED_SPAM = 'filtered_spam' FILTERED_HAM = 'filtered_ham' MARKED_SPAM = 'marked_spam' MARKED_HAM = 'marked_ham' AKISMET_MAX_SIZE = 1024 * 250 post = models.OneToOneField(Post, db_index=True) state = FSMField(default=UNREVIEWED, db_index=True) topic = models.ForeignKey(Topic) # Original topic forum = models.ForeignKey(Forum) # Original forum user_agent = models.CharField(max_length=200, blank=True, null=True) referrer = models.CharField(max_length=200, blank=True, null=True) permalink = models.CharField(max_length=200, blank=True, null=True) objects = PostStatusManager() spam_category = None spam_forum = None spam_topic = None def _get_spam_dustbin(self): if self.spam_category is None: self.spam_category, _ = Category.objects.get_or_create( name=forum_settings.SPAM_CATEGORY_NAME) if self.spam_forum is None: self.spam_forum, _ = Forum.objects.get_or_create( category=self.spam_category, name=forum_settings.SPAM_FORUM_NAME) if self.spam_topic is None: filterbot = User.objects.get_by_natural_key("filterbot") self.spam_topic, _ = Topic.objects.get_or_create( forum=self.spam_forum, name=forum_settings.SPAM_TOPIC_NAME, user=filterbot) return (self.spam_topic, self.spam_forum) def _undelete_post(self): """ If the post is in the spam dustbin, move it back to its original location. """ spam_topic, spam_forum = self._get_spam_dustbin() post = self.post topic = self.topic head = post.topic.head if post == head: # Move the original topic back to the original forum (either from # the dustbin, or from the spam dustbin) topic.move_to(self.forum) if topic != post.topic: # If the post was moved from its original topic, put it back now that # the topic is in place. post.move_to(topic, delete_topic=False) def _delete_post(self): """ Move the post to the spam dustbin. """ spam_topic, spam_forum = self._get_spam_dustbin() post = self.post topic = self.topic head = topic.head if post == head: topic.move_to(spam_forum) else: post.move_to(spam_topic) def to_akismet_data(self): post = self.post topic = self.topic user = post.user user_ip = post.user_ip comment_author = user.username user_agent = self.user_agent referrer = self.referrer permalink = self.permalink comment_date_gmt = post.created.isoformat(' ') comment_post_modified_gmt = topic.created.isoformat(' ') return { 'user_ip': user_ip, 'user_agent': user_agent, 'comment_author': comment_author, 'referrer': referrer, 'permalink': permalink, 'comment_type': 'comment', 'comment_date_gmt': comment_date_gmt, 'comment_post_modified_gmt': comment_post_modified_gmt } def to_akismet_content(self): """ Truncate the post body to the largest allowed string size. Use size, not length, since the Akismet server checks size, not length. """ return self.post.body.encode('utf-8')[:self.AKISMET_MAX_SIZE].decode( 'utf-8', 'ignore') def _comment_check(self): """ Pass the associated post through Akismet if it's available. If it's not available return None. Otherwise return True or False. """ if akismet_api is None: logger.warning("Skipping akismet check. No api.") return None data = self.to_akismet_data() content = self.to_akismet_content() is_spam = None try: is_spam = akismet_api.comment_check(content, data) except AkismetError as e: try: # try again, in case of timeout is_spam = akismet_api.comment_check(content, data) except Exception as e: logger.error("Error while checking Akismet", exc_info=True, extra={ "post": self.post, "post_id": self.post.id, "content_length": len(content) }) is_spam = None except Exception as e: logger.error("Error while checking Akismet", exc_info=True, extra={ "post": self.post, "post_id": self.post.id, "content_length": len(content) }) is_spam = None return is_spam def _submit_comment(self, report_type): """ Report this post to Akismet as spam or ham. Raises an exception if it fails. report_type is 'spam' or 'ham'. Used by report_spam/report_ham. """ if akismet_api is None: logger.error("Can't submit to Akismet. No API.") return None data = self.to_akismet_data() content = self.to_akismet_content() if report_type == "spam": akismet_api.submit_spam(content, data) elif report_type == "ham": akismet_api.submit_ham(content, data) else: raise NotImplementedError( "You're trying to report an unsupported comment type.") def _submit_spam(self): """ Report this post to Akismet as spam. """ self._submit_comment("spam") def _submit_ham(self): """ Report this post to Akismet as ham. """ self._submit_comment("ham") def is_spam(self): """ Condition used by the FSM. Return True if the Akismet API is available and returns a positive. Otherwise return False or None. """ is_spam = self._comment_check() if is_spam is None: return False else: return is_spam def is_ham(self): """ Inverse of is_spam. """ is_spam = self._comment_check() if is_spam is None: return False else: return not is_spam @transition(field=state, source=UNREVIEWED, target=FILTERED_SPAM, save=True, conditions=[is_spam]) def filter_spam(self): """ Akismet detected this post is spam, move it to the dustbin and report it. """ self._delete_post() @transition(field=state, source=UNREVIEWED, target=FILTERED_HAM, save=True, conditions=[is_ham]) def filter_ham(self): """ Akismet detected this post as ham. Don't do anything (except change state). """ pass @transition(field=state, source=[UNREVIEWED, FILTERED_HAM, MARKED_HAM], target=USER_DELETED, save=True) def filter_user_deleted(self): """ Post is not marked spam by akismet, but user has been globally deleted, putting this into the spam dusbin. """ self._delete_post() @transition(field=state, source=[FILTERED_SPAM, MARKED_SPAM], target=MARKED_HAM, save=True) def mark_ham(self): """ Either Akismet returned a false positive, or a moderator accidentally marked this as spam. Tell Akismet that this is ham, undelete it. """ self._submit_ham() self._undelete_post() @transition(field=state, source=[FILTERED_HAM, MARKED_HAM], target=MARKED_SPAM, save=True) def mark_spam(self): """ Akismet missed this, or a moderator accidentally marked it as ham. Tell Akismet that this is spam. """ self._submit_spam() self._delete_post() @transition(field=state, source=USER_DELETED, target=UNREVIEWED, save=True) def filter_user_undeleted(self): """ Post is not marked spam by akismet, but user has been globally deleted, putting this into the spam dusbin. """ self._undelete_post() def review(self, certainly_spam=False): """ Process this post, used by the manager and the spam-hammer. The ``certainly_spam`` argument is used to force mark as spam/delete the post, no matter what status Akismet returns. """ if can_proceed(self.filter_spam): self.filter_spam() elif can_proceed(self.filter_ham): self.filter_ham() if certainly_spam: self.mark_spam() else: if certainly_spam: self._delete_post() logger.warn("Couldn't filter post.", exc_info=True, extra={ 'post_id': self.post.id, 'content_length': len(self.post.body) })
class Course(models.Model): """Models a single course, e.g. the Math 101 course of 2002.""" __metaclass__ = LocalizeModelBase state = FSMField(default='new', protected=True) semester = models.ForeignKey(Semester, verbose_name=_(u"semester")) name_de = models.CharField(max_length=1024, verbose_name=_(u"name (german)")) name_en = models.CharField(max_length=1024, verbose_name=_(u"name (english)")) name = Translate # type of course: lecture, seminar, project kind = models.CharField(max_length=1024, verbose_name=_(u"type")) # bachelor, master, d-school course degree = models.CharField(max_length=1024, verbose_name=_(u"degree")) # students that are allowed to vote participants = models.ManyToManyField(settings.AUTH_USER_MODEL, verbose_name=_(u"participants"), blank=True) participant_count = models.IntegerField( verbose_name=_(u"participant count"), blank=True, null=True, default=None) # students that already voted voters = models.ManyToManyField(settings.AUTH_USER_MODEL, verbose_name=_(u"voters"), blank=True, related_name='+') voter_count = models.IntegerField(verbose_name=_(u"voter count"), blank=True, null=True, default=None) # when the evaluation takes place vote_start_date = models.DateField(null=True, verbose_name=_(u"first date to vote")) vote_end_date = models.DateField(null=True, verbose_name=_(u"last date to vote")) # who last modified this course, shell be noted last_modified_time = models.DateTimeField(auto_now=True) last_modified_user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="+", null=True, blank=True) class Meta: ordering = ('semester', 'degree', 'name_de') unique_together = ( ('semester', 'degree', 'name_de'), ('semester', 'degree', 'name_en'), ) verbose_name = _(u"course") verbose_name_plural = _(u"courses") def __unicode__(self): return self.name def clean(self): if self.vote_start_date and self.vote_end_date: if not (self.vote_start_date < self.vote_end_date): raise ValidationError( _(u"The vote start date must be before the vote end date.") ) def save(self, *args, **kw): super(Course, self).save(*args, **kw) # make sure there is a general contribution if not self.general_contribution: self.contributions.create(contributor=None) def is_fully_checked(self): """Shortcut for finding out whether all text answers to this course have been checked""" return not self.textanswer_set.filter(checked=False).exists() def can_user_vote(self, user): """Returns whether the user is allowed to vote on this course.""" if (not self.state == "inEvaluation") or (self.vote_end_date < datetime.date.today()): return False return user in self.participants.all() and user not in self.voters.all( ) def can_fsr_edit(self): return self.state in [ 'new', 'prepared', 'lecturerApproved', 'approved', 'inEvaluation' ] def can_fsr_delete(self): return not (self.textanswer_set.exists() or self.gradeanswer_set.exists() or not self.can_fsr_edit()) def can_fsr_review(self): return (not self.is_fully_checked()) and self.state in [ 'inEvaluation', 'evaluated' ] def can_fsr_approve(self): return self.state in ['new', 'prepared', 'lecturerApproved'] @transition(field=state, source=['new', 'lecturerApproved'], target='prepared') def ready_for_contributors(self, send_mail=True): if send_mail: EmailTemplate.get_review_template().send_courses( [self], send_to_editors=True) @transition(field=state, source='prepared', target='lecturerApproved') def contributor_approve(self): pass @transition(field=state, source=['new', 'prepared', 'lecturerApproved'], target='approved') def fsr_approve(self): pass @transition(field=state, source='prepared', target='new') def revert_to_new(self): pass @transition(field=state, source='approved', target='inEvaluation') def evaluation_begin(self): pass @transition(field=state, source='inEvaluation', target='evaluated') def evaluation_end(self): pass @transition(field=state, source='evaluated', target='reviewed', conditions=[is_fully_checked]) def review_finished(self): pass @transition(field=state, source='reviewed', target='published') def publish(self): pass @transition(field=state, source='published', target='reviewed') def revoke(self): pass @property def general_contribution(self): try: return self.contributions.get(contributor=None) except Contribution.DoesNotExist: return None @property def num_participants(self): if self.participants.exists(): return self.participants.count() else: return self.participant_count or 0 @property def num_voters(self): if self.voters.exists(): return self.voters.count() else: return self.voter_count or 0 @property def due_participants(self): for user in self.participants.all(): if not user in self.voters.all(): yield user @property def responsible_contributor(self): for contribution in self.contributions.all(): if contribution.responsible: return contribution.contributor @property def responsible_contributors_name(self): return self.responsible_contributor.userprofile.full_name @property def responsible_contributors_username(self): return self.responsible_contributor.username def has_enough_questionnaires(self): return all(contribution.questionnaires.exists() for contribution in self.contributions.all()) and self.general_contribution def is_user_editor_or_delegate(self, user): if self.contributions.filter(can_edit=True, contributor=user).exists(): return True else: represented_userprofiles = user.represented_users.all() represented_users = [ profile.user for profile in represented_userprofiles ] if self.contributions.filter( can_edit=True, contributor__in=represented_users).exists(): return True return False def is_user_responsible_or_delegate(self, user): if self.contributions.filter(responsible=True, contributor=user).exists(): return True else: represented_userprofiles = user.represented_users.all() represented_users = [ profile.user for profile in represented_userprofiles ] if self.contributions.filter( responsible=True, contributor__in=represented_users).exists(): return True return False def is_user_contributor(self, user): if self.contributions.filter(contributor=user).exists(): return True return False def warnings(self): result = [] if not self.has_enough_questionnaires(): result.append(_(u"Not enough questionnaires assigned")) return result @property def textanswer_set(self): """Pseudo relationship to all text answers for this course""" return TextAnswer.objects.filter( contribution__in=self.contributions.all()) @property def open_textanswer_set(self): """Pseudo relationship to all text answers for this course""" return TextAnswer.objects.filter( contribution__in=self.contributions.all()).filter(checked=False) @property def checked_textanswer_set(self): """Pseudo relationship to all text answers for this course""" return TextAnswer.objects.filter( contribution__in=self.contributions.all()).filter(checked=True) @property def gradeanswer_set(self): """Pseudo relationship to all grade answers for this course""" return GradeAnswer.objects.filter( contribution__in=self.contributions.all())