Exemplo n.º 1
0
class ActivityComponentMark(models.Model):
    """
    Marking of one particular component of an activity for one student  
    Stores the mark the student gets for the component
    """
    activity_mark = models.ForeignKey(ActivityMark, null=False, on_delete=models.PROTECT)
    activity_component = models.ForeignKey(ActivityComponent, null=False, on_delete=models.PROTECT)
    value = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='Mark', null=True, blank=True)
    comment = models.TextField(null = True, max_length=1000, blank=True)
    config = JSONField(null=False, blank=False, default=dict)
        # 'display_raw': Whether the comment should be displayed in a <pre> tag instead of using the
        # linebreaks filter.  Useful for comments with blocks of code.

    defaults = {'display_raw': False}
    display_raw, set_display_raw = getter_setter('display_raw')

    def __str__(self):
        # get the student and the activity
        return "Marking for [%s]" %(self.activity_component,)
    def delete(self, *args, **kwargs):
        raise NotImplementedError("This object cannot be deleted because it is used for marking history")
        
    class Meta:
        unique_together = (('activity_mark', 'activity_component'),)
        ordering = ('activity_component',)
Exemplo n.º 2
0
class FormGroupMember(models.Model):
    """
    Member of a FormGroup. Upgraded for simple ManyToManyField so we have the .config

    Do not use as a foreign key: is deleted when people leave the FormGroup
    """
    person = models.ForeignKey(Person)
    formgroup = models.ForeignKey(FormGroup)
    config = JSONField(null=False, blank=False,
                       default={})  # addition configuration stuff:
    # 'email': should this member receive emails on completed sheets?

    defaults = {'email': True}
    email, set_email = getter_setter('email')

    class Meta:
        db_table = 'onlineforms_formgroup_members'  # to make it Just Work with the FormGroup.members without "through=" that existed previously
        unique_together = (("person", "formgroup"), )

    def __unicode__(self):
        return u"%s in %s" % (self.person.name(), self.formgroup.name)
Exemplo n.º 3
0
class Page(models.Model):
    """
    A page in this courses "web site". Actual data is versioned in PageVersion objects.
    """
    offering = models.ForeignKey(CourseOffering)
    label = models.CharField(max_length=30, db_index=True, help_text="The &ldquo;filename&rdquo; for this page")
    can_read = models.CharField(max_length=4, choices=READ_ACL_CHOICES, default="ALL",
        help_text="Who should be able to view this page?")
    can_write = models.CharField(max_length=4, choices=WRITE_ACL_CHOICES, default="STAF",
        verbose_name="Can change", help_text="Who should be able to edit this page?")
    config = JSONField(null=False, blank=False, default={}) # addition configuration stuff:
        # p.config['releasedate']: date after which is page is visible
        # p.config['editdate']: date after which is page is editable
        # p.config['migrated_to']: if this page was migrated to a new location, the new (offering.slug, page.label)
        # p.config['migrated_from']: if this page was migrated from an old location, the old (offering.slug, page.label)
        # p.config['prevent_redirect']: if True, don't do a redirect, even if migration settings look like it should.

    defaults = {'releasedate': None, 'editdate': None}
    releasedate_txt, set_releasedate_txt = getter_setter('releasedate')
    editdate_txt, set_editdate_txt = getter_setter('editdate')

    class Meta:
        ordering = ['label']
        unique_together = (('offering', 'label'), )
    
    def save(self, *args, **kwargs):
        assert self.label_okay(self.label) is None
        self.expire_offering_cache()
        super(Page, self).save(*args, **kwargs)

    def releasedate(self):
        d = self.releasedate_txt()
        if d is None:
            return None
        else:
            return datetime.datetime.strptime(d, "%Y-%m-%d").date()

    def editdate(self):
        d = self.editdate_txt()
        if d is None:
            return None
        else:
            return datetime.datetime.strptime(d, "%Y-%m-%d").date()

    def set_releasedate(self, val):
        if isinstance(val, datetime.date):
            val = val.strftime("%Y-%m-%d")
        self.set_releasedate_txt(val)

    def set_editdate(self, val):
        if isinstance(val, datetime.date):
            val = val.strftime("%Y-%m-%d")
        self.set_editdate_txt(val)

    def get_absolute_url(self):
        if self.label == 'Index':
            return reverse('offering:pages:index_page', kwargs={'course_slug': self.offering.slug})
        else:
            return reverse('offering:pages:view_page', kwargs={'course_slug': self.offering.slug, 'page_label': self.label})

    def version_cache_key(self):
        return "page-curver-" + str(self.id)

    def macro_cache_key(self):
        return "MACROS-" + str(self.offering_id)

    def expire_offering_cache(self):
        # invalidate cache for all pages in this offering: makes sure current page, and all <<filelist>> are up to date
        for pv in PageVersion.objects.filter(page__offering=self.offering):
            cache.delete(pv.html_cache_key())
            cache.delete(pv.wikitext_cache_key())
        # other cache cleanup
        cache.delete(self.version_cache_key())
        cache.delete(self.macro_cache_key())

    def label_okay(self, label):
        """
        Check to make sure this label is acceptable (okay characters)
        
        Used by both self.save() and model validator.
        """
        m = label_re.match(label)
        if not m:
            return "Labels can contain only letters, numbers, underscores, dashes, and periods."
    
    def __unicode__(self):
        return self.offering.name() + '/' + self.label
    
    def current_version(self):
        """
        The most recent PageVersion object for this Page
        
        Cached to save the frequent lookup.
        """
        key = self.version_cache_key()
        v = cache.get(key)
        if v:
            return v
        else:
            v = PageVersion.objects.filter(page=self).select_related('editor__person').latest('created_at')
            cache.set(key, v, 24*3600) # expired when a PageVersion is saved
            return v

    def XXX_safely_delete(self):
        """
        Delete this page (and by "delete", we mean "don't really delete").

        No longer used since redirects are now left in place of deleted pages.
        """
        with transaction.atomic():
            # mangle name and short-name so instructors can delete and replace
            i = 1
            while True:
                suffix = "__%04i" % (i)
                existing = Page.objects.filter(offering=self.offering, label=self.label+suffix).count()
                if existing == 0:
                    break
                i += 1

            new_label = self.label+suffix
            self.label = new_label
            self.can_read = 'NONE'
            self.can_write = 'NONE'
            self.save()

    @staticmethod
    def adjust_acl_release(acl_value, date):
        """
        Adjust the access control value appropriately, taking the release date into account.
        """
        if not date:
            # no release date, so nothing to do.
            return acl_value
        elif date and datetime.date.today() >= date:
            # release date passed: nothing to do.
            return acl_value
        else:
            # release date hasn't passed: upgrade the security level accordingly.
            if acl_value == 'NONE':
                return 'NONE'
            elif acl_value == 'STAF':
                return 'INST'
            else:
                return 'STAF'

    def release_message(self):
        return self._release_message(self.releasedate(), self.can_read, "viewable")

    def _release_message(self, date, acl_value, attrib):
        today = datetime.date.today()
        if not date:
            return None
        elif date > today:
            return "This page has not yet been released. It will be %s by %s as of %s." % (attrib, ACL_DESC[acl_value], date)
        else:
            #return "This page was made %s automatically on %s." % (attrib, date)
            return None
Exemplo n.º 4
0
class PageVersion(models.Model):
    """
    A particular revision of a Page's contents. Could be either a wiki page or a file attachment.
    """
    page = models.ForeignKey(Page, blank=True, null=True)
    title = models.CharField(max_length=60, help_text="The title for the page")
    wikitext = models.TextField(help_text='WikiCreole-formatted content of the page')
    diff = models.TextField(null=True, blank=True)
    diff_from = models.ForeignKey('PageVersion', null=True)
    file_attachment = models.FileField(storage=PageFilesStorage, null=False, upload_to=attachment_upload_to, blank=False, max_length=500)
    file_mediatype = models.CharField(null=False, blank=False, max_length=200)
    file_name = models.CharField(null=False, blank=False, max_length=200)
    redirect = models.CharField(null=True, blank=True, max_length=500) # URL to redirect to: may be an absolute URL or relative from the location of self.page

    created_at = models.DateTimeField(auto_now_add=True)
    editor = models.ForeignKey(Member)
    comment = models.TextField()

    config = JSONField(null=False, blank=False, default={}) # addition configuration stuff:
        # p.config['math']: page uses MathJax? (boolean)
        # p.config['syntax']: page uses SyntaxHighlighter? (boolean)
        # p.config['brushes']: used SyntaxHighlighter brushes (list of strings)
        # p.config['depth']: max depth of diff pages below me (to keep it within reason)
        # p.config['redirect_reason']: if present, how this redirect got here: 'rename' or 'delete'.

    defaults = {'math': False, 'syntax': False, 'brushes': [], 'depth': 0, 'redirect_reason': None}
    math, set_math = getter_setter('math')
    syntax, set_syntax = getter_setter('syntax')
    brushes, set_brushes = getter_setter('brushes')
    depth, set_depth = getter_setter('depth')
    redirect_reason, set_redirect_reason = getter_setter('redirect_reason')

    def html_cache_key(self):
        return "page-html-" + str(self.id)
    def wikitext_cache_key(self):
        return "page-wikitext-" + str(self.id)

    def get_wikitext(self):
        """
        Return this version's wikitext (reconstructing from diffs if necessary).
        
        Caches when reconstructing from diffs
        """
        if self.diff_from:
            key = self.wikitext_cache_key()
            wikitext = cache.get(key)
            if wikitext:
                return unicode(wikitext)
            else:
                src = self.diff_from
                diff = json.loads(self.diff)
                wikitext = src.apply_changes(diff)
                cache.set(key, wikitext, 24*3600) # no need to expire: shouldn't change for a version
                return unicode(wikitext)

        return unicode(self.wikitext)

    def __init__(self, *args, **kwargs):
        super(PageVersion, self).__init__(*args, **kwargs)
        self.Creole = None
    
    def get_creole(self, offering=None):
        if not self.Creole:
            if offering:
                self.Creole = ParserFor(offering, self)
            else:
                self.Creole = ParserFor(self.page.offering, self)
    
    def previous_version(self):
        """
        Return the version before this one, or None
        """
        try:
            prev = PageVersion.objects.filter(page=self.page,
                   created_at__lt=self.created_at).latest('created_at')
            return prev
        except PageVersion.DoesNotExist:
            return None

    def changes(self, other):
        """
        Changes to get from the get_wikitext() of self to other.
        
        List of changes that can be insertions, deletions, or replacements. Each
        is a tuple containing:
          (type flag, position of change, [other info need to reconstrut original])
        """
        lines1 = self.get_wikitext().split("\n")
        lines2 = other.get_wikitext().split("\n")
        
        matcher = difflib.SequenceMatcher()
        matcher.set_seqs(lines1, lines2)
        
        changes = []
        for tag, i1, i2, j1, j2 in matcher.get_opcodes():
            if tag == 'equal':
                # ignore no-change blocks
                pass
            elif tag == 'insert':
                changes.append(("I", i1, lines2[j1:j2]))
            elif tag == 'delete':
                changes.append(("D", i1, i2))
            elif tag == 'replace':
                changes.append(("R", i1, i2, lines2[j1:j2]))
            else:
                raise ValueError
        
        return changes

    def apply_changes(self, changes):
        """
        Apply changes to this wikitext
        """
        lines = self.get_wikitext().split("\n")
        # sort by reverse linenumber: make sure we make changes in the right place
        changes.sort(cmp=lambda x,y: cmp(y[1], x[1]))
        
        for change in changes:
            c = change[0]
            if c=="I":
                _, pos, ls = change
                lines[pos:pos] = ls
            elif c=="D":
                _, pos1, pos2 = change
                del lines[pos1:pos2]
            elif c=="R":
                _, pos1, pos2, ls = change
                lines[pos1:pos2] = ls
            else:
                raise ValueError

        return u"\n".join(lines)

    def diff_to(self, other):
        """
        Turn this version into a diff based on the other version (if apprpriate).
        """
        if not self.wikitext or self.diff_from:
            # must already be a diff: don't repeat ourselves
            return
        if self.depth() > 10:
            # don't let the chain of diffs get too long
            return
                
        oldw = self.wikitext

        diff = json.dumps(other.changes(self), separators=(',',':'))
        if len(diff) > len(oldw):
            # if it's a big change, don't bother.
            return

        self.diff = diff
        self.diff_from = other
        self.wikitext = ''
        self.save(check_diff=False) # save but don't go back for more diffing

        other.set_depth(max(self.depth()+1, other.depth()))
        other.save(minor_change=True)

        neww = self.get_wikitext()
        assert oldw==neww

    def save(self, check_diff=True, minor_change=False, *args, **kwargs):
        # check coherence of the data model: exactly one of full text, diff text, file, redirect.
        if not minor_change:
            # minor_change flag set when .diff_to has changed the .config only
            has_wikitext = bool(self.wikitext)
            has_difffrom = bool(self.diff_from)
            has_diff = bool(self.diff)
            has_file = bool(self.file_attachment)
            has_redirect = bool(self.redirect)
            assert (has_wikitext and not has_difffrom and not has_diff and not has_file and not has_redirect) \
                or (not has_wikitext and has_difffrom and has_diff and not has_file and not has_redirect) \
                or (not has_wikitext and not has_difffrom and not has_diff and has_file and not has_redirect) \
                or (not has_wikitext and not has_difffrom and not has_diff and not has_file and has_redirect)
        
            # normalize newlines so our diffs are consistent later
            self.wikitext = normalize_newlines(self.wikitext)
        
            # set the SyntaxHighlighter brushes used on this page.
            self.set_brushes([])
            wikitext = self.get_wikitext()
            if wikitext:
                self.get_creole()
                brushes = brushes_used(self.Creole.parser.parse(wikitext))
                self.set_brushes(list(brushes))
                self.set_syntax(bool(brushes))

        super(PageVersion, self).save(*args, **kwargs)
        # update the *previous* PageVersion so it's a diff instead of storing full text
        if check_diff and not minor_change:
            prev = self.previous_version()
            if prev:
                prev.diff_to(self)

        self.page.expire_offering_cache()

    def __unicode__(self):
        return unicode(self.page) + '@' + unicode(self.created_at)

    def is_filepage(self):
        """
        Is this PageVersion a file attachment (as opposed to a Wiki page)?
        """
        return bool(self.file_attachment)

    @staticmethod
    def _offering_macros(offering):
        """
        Do the actual work of constructing the macro dict for this offering
        """
        try:
            pv = PageVersion.objects.filter(page__offering=offering, page__label=MACRO_LABEL).latest('created_at')
        except PageVersion.DoesNotExist:
            return {}

        macros = {}
        for line in pv.get_wikitext().splitlines(True):
            m = macroline_re.match(line)
            if m:
                macros[m.group('key')] = m.group('value')
        return macros

    def offering_macros(self):
        """
        Return a dict of macros for this page's offering (caches _offering_macros).
        """
        if not self.page:
            return {}
        offering = self.Creole.offering
        key = self.page.macro_cache_key()
        macros = cache.get(key)
        if macros is not None:
            return macros
        else:
            macros = PageVersion._offering_macros(offering)
            cache.set(key, macros, 24*3600) # expired when a page is saved
            return macros

    def substitute_macros(self, wikitext):
        """
        Substitute our macros into the wikitext.
        """
        macros = self.offering_macros()
        if macros:
            for macro, replacement in macros.iteritems():
                wikitext = wikitext.replace('+' + macro + '+', replacement)
        return wikitext

    def html_contents(self, offering=None):
        """
        Return the HTML version of this version's wikitext (with macros substituted if available)

        offering argument only required if self.page isn't set: used when doing a speculative conversion of unsaved content.
        
        Cached to save frequent conversion.
        """
        key = self.html_cache_key()
        html = cache.get(key)
        if html:
            return mark_safe(html)
        else:
            self.get_creole(offering=offering)
            wikitext = self.get_wikitext()
            html = self.Creole.text2html(self.substitute_macros(wikitext))
            cache.set(key, html, 24*3600) # expired if activities are changed (in signal below), or by saving a PageVersion in this offering
            return mark_safe(html)
Exemplo n.º 5
0
class SheetSubmission(models.Model):
    form_submission = models.ForeignKey(FormSubmission)
    sheet = models.ForeignKey(Sheet)
    filler = models.ForeignKey(FormFiller)
    status = models.CharField(max_length=4,
                              choices=SUBMISSION_STATUS,
                              default="WAIT")
    given_at = models.DateTimeField(auto_now_add=True)
    completed_at = models.DateTimeField(null=True)

    # key = models.CharField()
    def autoslug(self):
        return self.filler.identifier()

    slug = AutoSlugField(populate_from=autoslug,
                         null=False,
                         editable=False,
                         unique_with='form_submission')
    config = JSONField(null=False, blank=False,
                       default={})  # addition configuration stuff:

    # 'assigner': the user who assigned this sheet to the filler (Person.id value)
    # 'assign_note': optional note provided for asignee when sheet was assigned by admin
    # 'assign_comment': optional comment provided by admin about the formsubmission
    # 'reject_reason': reason given for rejecting the sheet
    # 'return_reason': reason given for returning the sheet to the filler

    def save(self, *args, **kwargs):
        with django.db.transaction.atomic():
            self.completed_at = datetime.datetime.now()
            super(SheetSubmission, self).save(*args, **kwargs)
            self.form_submission.update_status()

    def __unicode__(self):
        return u"%s by %s" % (self.sheet, self.filler.identifier())

    defaults = {
        'assigner': None,
        'assign_comment': None,
        'assign_note': None,
        'reject_reason': None,
        'return_reason': None
    }
    assigner_id, set_assigner_id = getter_setter('assigner')
    assign_note, set_assign_note = getter_setter('assign_note')
    assign_comment, set_assign_comment = getter_setter('assign_comment')
    reject_reason, set_reject_reason = getter_setter('reject_reason')
    return_reason, set_return_reason = getter_setter('return_reason')

    cached_fields = None

    def get_field_submissions(self, refetch=False):
        if refetch or not (self.cached_fields):
            self.cached_fields = FieldSubmission.objects.filter(
                sheet_submission=self)
        return self.cached_fields

    field_submissions = property(get_field_submissions)

    def assigner(self):
        assigner_id = self.assigner_id()
        if assigner_id:
            return Person.objects.get(id=assigner_id)
        else:
            return None

    def set_assigner(self, assigner):
        self.set_assigner_id(assigner.id)

    def get_secret(self):
        try:
            return SheetSubmissionSecretUrl.objects.get(sheet_submission=self)
        except SheetSubmissionSecretUrl.DoesNotExist:
            return None

    def get_submission_url(self):
        """
        Creates a URL for a sheet submission.
        If a secret URL has been generated it will use that,
        otherwise it will create a standard URL.
        """
        secret_urls = SheetSubmissionSecretUrl.objects.filter(
            sheet_submission=self)
        if secret_urls:
            return reverse('onlineforms.views.sheet_submission_via_url',
                           kwargs={'secret_url': secret_urls[0].key})
        else:
            return reverse('onlineforms.views.sheet_submission_subsequent',
                           kwargs={
                               'form_slug': self.form_submission.form.slug,
                               'formsubmit_slug': self.form_submission.slug,
                               'sheet_slug': self.sheet.slug,
                               'sheetsubmit_slug': self.slug
                           })

    @classmethod
    def sheet_maintenance(cls):
        """
        Do all of the stuff we need to update on a regular basis.
        """
        cls.reject_dormant_initial()
        cls.email_waiting_sheets()

    @classmethod
    def reject_dormant_initial(cls):
        """
        Close any initial sheets that have been hanging around for too long.
        """
        days = 14
        min_age = datetime.datetime.now() - datetime.timedelta(days=days)
        sheetsubs = SheetSubmission.objects.filter(sheet__is_initial=True,
                                                   status='WAIT',
                                                   given_at__lt=min_age)
        for ss in sheetsubs:
            ss.status = 'REJE'
            ss.set_reject_reason(
                'Automatically closed by system after being dormant %i days.' %
                (days))
            ss.save()

            fs = ss.form_submission
            fs.status = 'DONE'
            fs.set_summary(
                'Automatically closed by system after being dormant %i days.' %
                (days))
            fs.save()

    @classmethod
    def waiting_sheets_by_user(cls):
        min_age = datetime.datetime.now() - datetime.timedelta(hours=24)
        sheet_subs = SheetSubmission.objects.exclude(status='DONE').exclude(status='REJE') \
                .exclude(given_at__gt=min_age) \
                .order_by('filler__id') \
                .select_related('filler__sfuFormFiller', 'filler__nonSFUFormFiller', 'form_submission__form__initiator', 'sheet')
        return itertools.groupby(sheet_subs, lambda ss: ss.filler)

    @classmethod
    def email_waiting_sheets(cls):
        """
        Email those with sheets waiting for their attention.
        """
        full_url = settings.BASE_ABS_URL + reverse('onlineforms.views.index')
        subject = 'Waiting form reminder'
        from_email = "*****@*****.**"

        filler_ss = cls.waiting_sheets_by_user()
        template = get_template('onlineforms/emails/reminder.txt')

        for filler, sheets in filler_ss:
            # annotate with secret URL, so we can remind of that.
            sheets = list(sheets)
            for s in sheets:
                secrets = SheetSubmissionSecretUrl.objects.filter(
                    sheet_submission=s)
                if secrets:
                    s.secret = secrets[0]
                else:
                    s.secret = None
            context = Context({
                'full_url': full_url,
                'filler': filler,
                'sheets': list(sheets),
                'BASE_ABS_URL': settings.BASE_ABS_URL
            })
            msg = EmailMultiAlternatives(
                subject,
                template.render(context),
                from_email, [filler.email()],
                headers={'X-coursys-topic': 'onlineforms'})
            msg.send()

    def _send_email(self, request, template_name, subject, mail_from, mail_to,
                    context):
        """
        Send email to user as required in various places below.
        """
        plaintext = get_template('onlineforms/emails/' + template_name +
                                 '.txt')
        html = get_template('onlineforms/emails/' + template_name + '.html')

        sheeturl = request.build_absolute_uri(self.get_submission_url())
        context['sheeturl'] = sheeturl
        email_context = Context(context)
        msg = EmailMultiAlternatives(
            subject,
            plaintext.render(email_context),
            mail_from,
            mail_to,
            headers={'X-coursys-topic': 'onlineforms'})
        msg.attach_alternative(html.render(email_context), "text/html")
        msg.send()

    def email_assigned(self, request, admin, assignee):
        full_url = request.build_absolute_uri(self.get_submission_url())
        context = {
            'username': admin.name(),
            'assignee': assignee.name(),
            'sheeturl': full_url,
            'sheetsub': self
        }
        subject = 'CourSys: You have been assigned a sheet.'
        self._send_email(request, 'sheet_assigned', subject,
                         FormFiller.form_full_email(admin),
                         [assignee.full_email()], context)

    def email_started(self, request):
        full_url = request.build_absolute_uri(self.get_submission_url())
        context = {
            'initiator': self.filler.name(),
            'sheeturl': full_url,
            'sheetsub': self
        }
        subject = u'%s submission incomplete' % (self.sheet.form.title)
        self._send_email(request, 'nonsfu_sheet_started', subject,
                         settings.DEFAULT_FROM_EMAIL,
                         [self.filler.full_email()], context)

    def email_submitted(self, request, rejected=False):
        full_url = request.build_absolute_uri(
            reverse('onlineforms.views.view_submission',
                    kwargs={
                        'form_slug': self.sheet.form.slug,
                        'formsubmit_slug': self.form_submission.slug
                    }))
        context = {
            'initiator': self.filler.name(),
            'adminurl': full_url,
            'form': self.sheet.form,
            'rejected': rejected
        }
        subject = u'%s submission' % (self.sheet.form.title)
        self._send_email(request, 'sheet_submitted', subject,
                         settings.DEFAULT_FROM_EMAIL,
                         self.sheet.form.owner.notify_emails(), context)

    def email_returned(self, request, admin):
        context = {'admin': admin, 'sheetsub': self}
        self._send_email(request, 'sheet_returned',
                         u'%s submission returned' % (self.sheet.title),
                         FormFiller.form_full_email(admin),
                         [self.filler.full_email()], context)
Exemplo n.º 6
0
class FormSubmission(models.Model):
    form = models.ForeignKey(Form)
    initiator = models.ForeignKey(FormFiller)
    owner = models.ForeignKey(FormGroup)
    status = models.CharField(max_length=4,
                              choices=FORM_SUBMISSION_STATUS,
                              default="PEND")

    def autoslug(self):
        return self.initiator.identifier()

    slug = AutoSlugField(populate_from=autoslug,
                         null=False,
                         editable=False,
                         unique_with='form')
    config = JSONField(null=False, blank=False,
                       default={})  # addition configuration stuff:
    # 'summary': summary of the form entered when closing it
    # 'emailed': True if the initiator was emailed when the form was closed
    # 'closer': coredata.Person.id of the person that marked the formsub as DONE

    defaults = {'summary': '', 'emailed': False, 'closer': None}
    summary, set_summary = getter_setter('summary')
    emailed, set_emailed = getter_setter('emailed')
    closer_id, set_closer = getter_setter('closer')

    def update_status(self):
        sheet_submissions = SheetSubmission.objects.filter(
            form_submission=self)
        if all(sheet_sub.status in ['DONE', 'REJE']
               for sheet_sub in sheet_submissions):
            self.status = 'PEND'
        else:
            self.status = 'WAIT'
        self.save()

    def __unicode__(self):
        return u"%s for %s" % (self.form, self.initiator)

    def get_absolute_url(self):
        return reverse('onlineforms.views.view_submission',
                       kwargs={
                           'form_slug': self.form.slug,
                           'formsubmit_slug': self.slug
                       })

    def closer(self):
        try:
            return Person.objects.get(id=self.closer_id())
        except Person.DoesNotExist:
            return None

    def last_sheet_completion(self):
        return self.sheetsubmission_set.all().aggregate(
            Max('completed_at'))['completed_at__max']

    def email_notify_completed(self, request, admin):
        plaintext = get_template('onlineforms/emails/notify_completed.txt')
        html = get_template('onlineforms/emails/notify_completed.html')

        email_context = Context({'formsub': self, 'admin': admin})
        subject = '%s submission complete' % (self.form.title)
        from_email = FormFiller.form_full_email(admin)
        to = self.initiator.full_email()
        msg = EmailMultiAlternatives(
            subject=subject,
            body=plaintext.render(email_context),
            from_email=from_email,
            to=[to],
            bcc=[admin.full_email()],
            headers={'X-coursys-topic': 'onlineforms'})
        msg.attach_alternative(html.render(email_context), "text/html")
        msg.send()

    def email_notify_new_owner(self, request, admin):
        plaintext = get_template('onlineforms/emails/notify_new_owner.txt')
        html = get_template('onlineforms/emails/notify_new_owner.html')

        full_url = request.build_absolute_uri(
            reverse('onlineforms.views.view_submission',
                    kwargs={
                        'form_slug': self.form.slug,
                        'formsubmit_slug': self.slug
                    }))
        email_context = Context({
            'formsub': self,
            'admin': admin,
            'adminurl': full_url
        })
        subject = u'%s submission transferred' % (self.form.title)
        from_email = FormFiller.form_full_email(admin)
        to = self.owner.notify_emails()
        msg = EmailMultiAlternatives(
            subject=subject,
            body=plaintext.render(email_context),
            from_email=from_email,
            to=to,
            bcc=[admin.full_email()],
            headers={'X-coursys-topic': 'onlineforms'})
        msg.attach_alternative(html.render(email_context), "text/html")
        msg.send()
Exemplo n.º 7
0
class Form(models.Model, _FormCoherenceMixin):
    title = models.CharField(max_length=60,
                             null=False,
                             blank=False,
                             help_text='The name of this form.')
    owner = models.ForeignKey(
        FormGroup,
        help_text='The group of users who own/administrate this form.')
    description = models.CharField(
        max_length=500,
        null=False,
        blank=False,
        help_text=
        'A brief description of the form that can be displayed to users.')
    initiators = models.CharField(
        max_length=3,
        choices=INITIATOR_CHOICES,
        default="NON",
        help_text=
        'Who is allowed to fill out the initial sheet? That is, who can initiate a new instance of this form?'
    )
    unit = models.ForeignKey(Unit)
    active = models.BooleanField(default=True)
    original = models.ForeignKey('self', null=True, blank=True)
    created_date = models.DateTimeField(auto_now_add=True)
    last_modified = models.DateTimeField(auto_now=True)
    advisor_visible = models.BooleanField(
        default=False,
        help_text="Should submissions be visible to advisors in this unit?")

    def autoslug(self):
        return make_slug(self.unit.label + ' ' + self.title)

    slug = AutoSlugField(populate_from=autoslug,
                         null=False,
                         editable=False,
                         unique=True)
    config = JSONField(null=False, blank=False,
                       default={})  # addition configuration stuff:
    # 'loginprompt': should the "log in with your account" prompt be displayed for non-logged-in? (default True)

    defaults = {'loginprompt': True}
    loginprompt, set_loginprompt = getter_setter('loginprompt')

    def __unicode__(self):
        return u"%s [%s]" % (self.title, self.id)

    def delete(self, *args, **kwargs):
        self.active = False
        self.save()

    def save(self, *args, **kwargs):
        with django.db.transaction.atomic():
            instance = super(Form, self).save(*args, **kwargs)
            self.cleanup_fields()
            return instance

    @property
    def initial_sheet(self):
        sheets = Sheet.objects.filter(form=self, active=True, is_initial=True)
        if len(sheets) > 0:
            return sheets[0]
        else:
            return None

    cached_sheets = None

    def get_sheets(self, refetch=False):
        if refetch or not (self.cached_sheets):
            self.cached_sheets = Sheet.objects.filter(
                form=self, active=True).order_by('order')
        return self.cached_sheets

    sheets = property(get_sheets)

    def get_initiators_display_short(self):
        return INITIATOR_SHORT[self.initiators]

    def duplicate(self):
        """
        Make a independent duplicate of this form.

        Not called from the UI anywhere, but can be used to duplicate a form for another unit, without the pain of
        re-creating everything:
            newform = oldform.duplicate()
            newform.owner = ...
            newform.unit = ...
            newform.initiators = ...
            newform.slug = None
            newform.save()
        """
        with django.db.transaction.atomic():
            newform = self.clone()
            newform.original = None
            newform.slug = None
            newform.active = True
            newform.initiators = 'NON'
            newform.save()

            sheets = Sheet.objects.filter(form=self)
            for s in sheets:
                newsheet = s.clone()
                newsheet.form = newform
                newsheet.original = None
                newsheet.slug = None
                newsheet.save()

                fields = Field.objects.filter(sheet=s)
                for f in fields:
                    newfield = f.clone()
                    newfield.sheet = newsheet
                    newfield.original = None
                    newfield.slug = None
                    newfield.save()

            return newform

    def all_submission_summary(self):
        """
        Generate summary data of each submission for CSV output
        """
        DATETIME_FMT = "%Y-%m-%d"
        headers = []
        data = []

        # find all sheets (in a sensible order: deleted last)
        sheets = Sheet.objects.filter(
            form__original_id=self.original_id).order_by(
                'order', '-created_date')
        active_sheets = [s for s in sheets if s.active]
        inactive_sheets = [s for s in sheets if not s.active]
        sheet_info = collections.OrderedDict()
        for s in itertools.chain(active_sheets, inactive_sheets):
            if s.original_id not in sheet_info:
                sheet_info[s.original_id] = {
                    'title': s.title,
                    'fields': collections.OrderedDict(),
                    'is_initial': s.is_initial,
                }

        # find all fields in each of those sheets (in a equally-sensible order)
        fields = Field.objects.filter(
            sheet__form__original_id=self.original_id).select_related(
                'sheet').order_by('order', '-created_date')
        active_fields = [f for f in fields if f.active]
        inactive_fields = [f for f in fields if not f.active]
        for f in itertools.chain(active_fields, inactive_fields):
            if not FIELD_TYPE_MODELS[f.fieldtype].in_summary:
                continue
            info = sheet_info[f.sheet.original_id]
            if f.original_id not in info['fields']:
                info['fields'][f.original_id] = {
                    'label': f.label,
                }

        # build header row
        for sid, info in sheet_info.iteritems():
            headers.append(info['title'].upper())
            headers.append(None)
            if info['is_initial']:
                headers.append('Initiated')
            for fid, finfo in info['fields'].iteritems():
                headers.append(finfo['label'])
        headers.append('Last Sheet Completed')
        headers.append('Link')

        # go through FormSubmissions and create a row for each
        formsubs = FormSubmission.objects.filter(form__original_id=self.original_id, status='DONE') \
                .select_related('initiator__sfuFormFiller', 'initiator__nonSFUFormFiller', 'form')
        # selecting only fully completed forms: does it make sense to be more liberal and report status?

        # choose a winning SheetSubmission: there may be multiples of each sheet but we're only outputting one
        sheetsubs = SheetSubmission.objects.filter(form_submission__form__original_id=self.original_id, status='DONE') \
                .order_by('given_at').select_related('sheet', 'filler__sfuFormFiller', 'filler__nonSFUFormFiller')
        # Docs for the dict constructor: "If a key occurs more than once, the last value for that key becomes the corresponding value in the new dictionary."
        # Result is that the sheetsub with most recent given_at wins.
        winning_sheetsub = dict(
            ((ss.form_submission_id, ss.sheet.original_id), ss)
            for ss in sheetsubs)

        # collect fieldsubs to output
        fieldsubs = FieldSubmission.objects.filter(sheet_submission__form_submission__form__original_id=self.original_id) \
                .order_by('sheet_submission__given_at') \
                .select_related('sheet_submission', 'field')
        fieldsub_lookup = dict(
            ((fs.sheet_submission_id, fs.field.original_id), fs)
            for fs in fieldsubs)

        for formsub in formsubs:
            row = []
            found_anything = False
            last_completed = None
            for sid, info in sheet_info.iteritems():
                if (formsub.id, sid) in winning_sheetsub:
                    ss = winning_sheetsub[(formsub.id, sid)]
                    row.append(ss.filler.name())
                    row.append(ss.filler.email())
                    if not last_completed or ss.completed_at > last_completed:
                        last_completed = ss.completed_at
                else:
                    ss = None
                    row.append(None)
                    row.append(None)

                if info['is_initial']:
                    if ss:
                        row.append(ss.given_at.strftime(DATETIME_FMT))
                    else:
                        row.append(None)

                for fid, finfo in info['fields'].iteritems():
                    if ss and (ss.id, fid) in fieldsub_lookup:
                        fs = fieldsub_lookup[(ss.id, fid)]
                        handler = FIELD_TYPE_MODELS[fs.field.fieldtype](
                            fs.field.config)
                        row.append(handler.to_text(fs))
                        found_anything = True
                    else:
                        row.append(None)

            if last_completed:
                row.append(last_completed.strftime(DATETIME_FMT))
            else:
                row.append(None)

            row.append(settings.BASE_ABS_URL + formsub.get_absolute_url())

            if found_anything:
                data.append(row)

        return headers, data
Exemplo n.º 8
0
class Page(models.Model):
    """
    A page in this courses "web site". Actual data is versioned in PageVersion objects.
    """
    offering = models.ForeignKey(CourseOffering)
    label = models.CharField(
        max_length=30,
        db_index=True,
        help_text="The &ldquo;filename&rdquo; for this page")
    can_read = models.CharField(
        max_length=4,
        choices=READ_ACL_CHOICES,
        default="ALL",
        help_text="Who should be able to view this page?")
    can_write = models.CharField(
        max_length=4,
        choices=WRITE_ACL_CHOICES,
        default="STAF",
        verbose_name="Can change",
        help_text="Who should be able to edit this page?")
    config = JSONField(null=False, blank=False,
                       default={})  # addition configuration stuff:
    # p.config['releasedate']: date after which is page is visible
    # p.config['editdate']: date after which is page is editable

    defaults = {'releasedate': None, 'editdate': None}
    releasedate_txt, set_releasedate_txt = getter_setter('releasedate')
    editdate_txt, set_editdate_txt = getter_setter('editdate')

    class Meta:
        ordering = ['label']
        unique_together = (('offering', 'label'), )

    def save(self, *args, **kwargs):
        assert self.label_okay(self.label) is None
        super(Page, self).save(*args, **kwargs)

    def releasedate(self):
        d = self.releasedate_txt()
        if d is None:
            return None
        else:
            return datetime.datetime.strptime(d, "%Y-%m-%d").date()

    def editdate(self):
        d = self.editdate_txt()
        if d is None:
            return None
        else:
            return datetime.datetime.strptime(d, "%Y-%m-%d").date()

    def set_releasedate(self, val):
        if isinstance(val, datetime.date):
            val = val.strftime("%Y-%m-%d")
        self.set_releasedate_txt(val)

    def set_editdate(self, val):
        if isinstance(val, datetime.date):
            val = val.strftime("%Y-%m-%d")
        self.set_editdate_txt(val)

    def get_absolute_url(self):
        if self.label == 'Index':
            return reverse('pages.views.index_page',
                           kwargs={'course_slug': self.offering.slug})
        else:
            return reverse('pages.views.view_page',
                           kwargs={
                               'course_slug': self.offering.slug,
                               'page_label': self.label
                           })

    def version_cache_key(self):
        return "page-curver-" + str(self.id)

    def label_okay(self, label):
        """
        Check to make sure this label is acceptable (okay characters)
        
        Used by both self.save() and model validator.
        """
        m = label_re.match(label)
        if not m:
            return "Labels can contain only letters, numbers, underscores, dashes, and periods."

    def __unicode__(self):
        return self.offering.name() + '/' + self.label

    def current_version(self):
        """
        The most recent PageVersion object for this Page
        
        Cached to save the frequent lookup.
        """
        key = self.version_cache_key()
        v = cache.get(key)
        if v:
            return v
        else:
            v = PageVersion.objects.filter(page=self).select_related(
                'editor__person').latest('created_at')
            cache.set(key, v, 3600)  # expired when a PageVersion is saved
            return v

    @classmethod
    def adjust_acl_release(cls, acl_value, date):
        """
        Adjust the access control value appropriately, taking the release date into account.
        """
        if not date:
            # no release date, so nothing to do.
            return acl_value
        elif date and datetime.date.today() >= date:
            # release date passed: nothing to do.
            return acl_value
        else:
            # release date hasn't passed: upgrade the security level accordingly.
            if acl_value == 'NONE':
                return 'NONE'
            elif acl_value == 'STAF':
                return 'INST'
            else:
                return 'STAF'

    def release_message(self):
        return self._release_message(self.releasedate(), self.can_read,
                                     "viewable")

    def _release_message(self, date, acl_value, attrib):
        today = datetime.date.today()
        if not date:
            return None
        elif date > today:
            return "This page has not yet been released. It will be %s by %s as of %s." % (
                attrib, ACL_DESC[acl_value], date)
        else:
            #return "This page was made %s automatically on %s." % (attrib, date)
            return None
Exemplo n.º 9
0
class DiscussionMessage(models.Model):
    """
    A message (post) associated with a Discussion Topic
    """
    topic = models.ForeignKey(DiscussionTopic, on_delete=models.CASCADE)
    content = models.TextField(blank=False)
    created_at = models.DateTimeField(auto_now_add=True)
    modified_at = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=3,
                              choices=MESSAGE_STATUSES,
                              default='VIS')
    author = models.ForeignKey(Member, on_delete=models.PROTECT)

    def autoslug(self):
        return make_slug(self.author.person.userid)

    slug = AutoSlugField(populate_from='autoslug',
                         null=False,
                         editable=False,
                         unique_with=['topic'])
    config = JSONField(null=False, blank=False, default=dict)
    # p.config['markup']:  markup language used: see courselib/markup.py
    # p.config['math']: content uses MathJax? (boolean)
    # p.config['brushes']: used SyntaxHighlighter brushes (list of strings) -- no longer used with highlight.js

    defaults = {'math': False, 'markup': 'creole'}
    math, set_math = getter_setter('math')
    markup, set_markup = getter_setter('markup')

    #brushes, set_brushes = getter_setter('brushes')

    def save(self, *args, **kwargs):
        if self.status not in [status[0] for status in MESSAGE_STATUSES]:
            raise ValueError('Invalid topic status')
        if not self.pk:
            self.topic.new_message_update()

        self.content = ensure_sanitary_markup(self.content,
                                              self.markup(),
                                              restricted=True)

        new_message = self.id is None
        super(DiscussionMessage, self).save(*args, **kwargs)

        # handle subscriptions
        if new_message:
            subs = DiscussionSubscription.objects.filter(
                member__offering=self.topic.offering).select_related(
                    'member__person')
            for s in subs:
                s.notify_message(self)

    def html_content(self):
        "Convert self.content to HTML"
        return markup_to_html(self.content,
                              self.markup(),
                              offering=self.topic.offering,
                              html_already_safe=True,
                              restricted=True)

    def get_absolute_url(self):
        return self.topic.get_absolute_url() + '#reply-' + str(self.id)

    def create_at_delta(self):
        return _time_delta_to_string(self.created_at)

    def still_editable(self):
        td = datetime.datetime.now() - self.created_at
        seconds = td.days * 86400 + td.seconds
        return seconds <= 120

    def editable_time_left(self):
        td = datetime.datetime.now() - self.created_at
        seconds = td.days * 86400 + td.seconds
        if seconds > 120:
            return 'none'
        minutes, seconds = divmod(120 - seconds, 60)
        return "%dm:%ds" % (minutes, seconds)

    def was_edited(self):
        td = self.modified_at - self.created_at
        return self.modified_at > self.created_at and td > datetime.timedelta(
            seconds=3) and self.status != 'HID'

    def exportable(self):
        """
        Create JSON-serializable representation of message, for export.
        """
        data = {
            'body': self.content,
            'created_at': self.created_at.isoformat(),
            'author': self.author.person.userid,
            'status': self.status
        }
        return data

    def to_dict(self):
        return {
            "author": self.author.person.userid,
            "topic": self.topic.title,
            "content": self.content,
            "visibility": self.get_status_display(),
            "created": str(self.created_at),
            "modified": str(self.modified_at)
        }
Exemplo n.º 10
0
class TUG(models.Model):
    """
    Time use guideline filled out by instructors
    
    Based on form in Appendix C (p. 73) of the collective agreement:
    http://www.tssu.ca/wp-content/uploads/2010/01/CA-2004-2010.pdf
    """
    member = models.OneToOneField(Member, null=False, on_delete=models.PROTECT)
    base_units = models.DecimalField(max_digits=4,
                                     decimal_places=2,
                                     blank=False,
                                     null=False)
    last_update = models.DateField(auto_now=True)
    config = JSONField(null=False, blank=False,
                       default=dict)  # addition configuration stuff:
    # t.config['prep']: Preparation for labs/tutorials
    # t.config['meetings']: Attendance at planning meetings with instructor
    # t.config['lectures']: Attendance at lectures
    # t.config['tutorials']: Attendance at labs/tutorials
    # t.config['office_hours']: Office hours/student consultation
    # t.config['grading']
    # t.config['test_prep']: Quiz/exam preparation and invigilation
    # t.config['holiday']: Holiday compensation
    # Each of the above is a dictionary like:
    #     {
    #     'weekly': 2.0,
    #     'total': 26.0,
    #     'note': 'if more is required, we can revisit',
    #     }
    # t.config['other1']
    # t.config['other2']
    # As the other fields, but adding 'label'.

    prep = property(*getter_setter('prep'))
    meetings = property(*getter_setter('meetings'))
    lectures = property(*getter_setter('lectures'))
    tutorials = property(*getter_setter('tutorials'))
    office_hours = property(*getter_setter('office_hours'))
    grading = property(*getter_setter('grading'))
    test_prep = property(*getter_setter('test_prep'))
    holiday = property(*getter_setter('holiday'))
    other1 = property(*getter_setter('other1'))
    other2 = property(*getter_setter('other2'))

    def iterothers(self):
        return (other for key, other in self.config.items()
                if key.startswith('other') and isinstance(
                    other.get('total'), float) and other.get('total', 0) > 0)

    others = lambda self: list(self.iterothers())

    def iterfielditems(self):
        return ((field, self.config[field]) for field in self.all_fields
                if field in self.config)

    regular_fields = [
        'prep', 'meetings', 'lectures', 'tutorials', 'office_hours', 'grading',
        'test_prep', 'holiday'
    ]
    other_fields = ['other1', 'other2']
    all_fields = regular_fields + other_fields

    defaults = dict([(field, {
        'weekly': 0,
        'total': 0,
        'comment': ''
    }) for field in regular_fields] + [(field, {
        'label': '',
        'weekly': 0,
        'total': 0,
        'comment': ''
    }) for field in other_fields])

    # depicts the above comment in code
    config_meta = {
        'prep': {
            'label': 'Preparation',
            'help': '1. Preparation for labs/tutorials'
        },
        'meetings': {
            'label':
            'Attendance at planning meetings',
            'help':
            '2. Attendance at planning/coordinating meetings with instructor'
        },
        'lectures': {
            'label': 'Attendance at lectures',
            'help': '3. Attendance at lectures'
        },
        'tutorials': {
            'label': 'Attendance at labs/tutorials',
            'help': '4. Attendance at labs/tutorials'
        },
        'office_hours': {
            'label': 'Office hours',
            'help':
            '5. Office hours/student consultation/electronic communication'
        },
        'grading': {
            'label':
            'Grading',
            'help':
            '6. Grading\u2020',
            'extra':
            '\u2020Includes grading of all assignments, reports and examinations.'
        },
        'test_prep': {
            'label':
            'Quiz/exam preparation and invigilation',
            'help':
            '7. Quiz preparation/assist in exam preparation/Invigilation of exams'
        },
        'holiday': {
            'label':
            'Holiday compensation',
            'help':
            '8. Statutory Holiday Compensation\u2021',
            'extra':
            '''\u2021To compensate for all statutory holidays which  
may occur in a semester, the total workload required will be reduced by %s
hour(s) for each base unit assigned excluding the additional %s B.U. for
preparation, e.g. %s hours reduction for %s B.U. appointment.''' %
            (HOLIDAY_HOURS_PER_BU, LAB_BONUS, 4.4, 4 + LAB_BONUS)
        }
    }

    def __str__(self):
        return "TA: %s  Base Units: %s" % (self.member.person.userid,
                                           self.base_units)

    def save(self, newsitem=True, newsitem_author=None, *args, **kwargs):
        for f in self.config:
            # if 'weekly' in False is invalid, so we have to check if self.config[f] is iterable
            # before we check for 'weekly' or 'total'
            if hasattr(self.config[f], '__iter__'):
                if 'weekly' in self.config[f]:
                    self.config[f]['weekly'] = _round_hours(
                        self.config[f]['weekly'])
                if 'total' in self.config[f]:
                    self.config[f]['total'] = _round_hours(
                        self.config[f]['total'])

        super(TUG, self).save(*args, **kwargs)
        if newsitem:
            n = NewsItem(
                user=self.member.person,
                author=newsitem_author,
                course=self.member.offering,
                source_app='ta',
                title='%s Time Use Guideline Changed' %
                (self.member.offering.name()),
                content=
                'Your Time Use Guideline for %s has been changed. If you have not already, please review it with the instructor.'
                % (self.member.offering.name()),
                url=self.get_absolute_url())
            n.save()

    def get_absolute_url(self):
        return reverse('offering:view_tug',
                       kwargs={
                           'course_slug': self.member.offering.slug,
                           'userid': self.member.person.userid
                       })

    def max_hours(self):
        return self.base_units * HOURS_PER_BU

    def total_hours(self):
        """
        Total number of hours assigned
        """
        return round(
            sum((decimal.Decimal(data['total'])
                 for _, data in self.iterfielditems() if data['total'])), 2)
Exemplo n.º 11
0
class TAPosting(models.Model):
    """
    Posting for one unit in one semester
    """
    semester = models.ForeignKey(Semester, on_delete=models.PROTECT)
    unit = models.ForeignKey(Unit, on_delete=models.PROTECT)
    opens = models.DateField(help_text='Opening date for the posting')
    closes = models.DateField(help_text='Closing date for the posting')

    def autoslug(self):
        return make_slug(self.semester.slugform() + "-" + self.unit.label)

    slug = AutoSlugField(populate_from='autoslug',
                         null=False,
                         editable=False,
                         unique=True)
    config = JSONField(null=False, blank=False,
                       default=dict)  # addition configuration stuff:
    # 'salary': default pay rates per BU for each GTA1, GTA2, UTA, EXT: ['1.00', '2.00', '3.00', '4.00']
    # 'scholarship': default scholarship rates per BU for each GTA1, GTA2, UTA, EXT
    # 'accounts': default accounts for GTA1, GTA2, UTA, EXT (ra.models.Account.id values)
    # 'start': default start date for contracts ('YYYY-MM-DD')
    # 'end': default end date for contracts ('YYYY-MM-DD')
    # 'payroll_start': default payroll start date for contracts ('YYYY-MM-DD')
    # 'payroll_end': default payroll start date for contracts ('YYYY-MM-DD')
    # 'deadline': default deadline to accept contracts ('YYYY-MM-DD')
    # 'excluded': courses to exclude from posting (list of Course.id values)
    # 'payperiods': number of pay periods in the semeseter
    # 'contact': contact person for offer questions (Person.id value)
    # 'max_courses': Maximum number of courses an applicant can select
    # 'min_courses': Minimum number of courses an applicant can select
    # 'offer_text': Text to be displayed when students accept/reject the offer (creole markup)
    # 'export_seq': sequence ID for payroll export (so we can create a unique Batch ID)
    # 'extra_questions': additional questions to ask applicants
    # 'instructions': instructions for completing the TA Application
    # 'hide_campuses': whether or not to prompt for Campus

    defaults = {
        'salary': ['0.00'] * len(CATEGORY_CHOICES),
        'scholarship': ['0.00'] * len(CATEGORY_CHOICES),
        'accounts': [None] * len(CATEGORY_CHOICES),
        'start': '',
        'end': '',
        'payroll_start': '',
        'payroll_end': '',
        'deadline': '',
        'excluded': [],
        'bu_defaults': {},
        'payperiods': 8,
        'max_courses': 10,
        'min_courses': 0,
        'contact': None,
        'offer_text': '',
        'export_seq': 0,
        'extra_questions': [],
        'instructions': '',
        'hide_campuses': False
    }
    salary, set_salary = getter_setter('salary')
    scholarship, set_scholarship = getter_setter('scholarship')
    accounts, set_accounts = getter_setter('accounts')
    start, set_start = getter_setter('start')
    end, set_end = getter_setter('end')
    payroll_start, set_payroll_start = getter_setter('payroll_start')
    payroll_end, set_payroll_end = getter_setter('payroll_end')
    deadline, set_deadline = getter_setter('deadline')
    excluded, set_excluded = getter_setter('excluded')
    bu_defaults, set_bu_defaults = getter_setter('bu_defaults')
    payperiods_str, set_payperiods = getter_setter('payperiods')
    max_courses, set_max_courses = getter_setter('max_courses')
    min_courses, set_min_courses = getter_setter('min_courses')
    offer_text, set_offer_text = getter_setter('offer_text')
    extra_questions, set_extra_questions = getter_setter('extra_questions')
    instructions, set_instructions = getter_setter('instructions')
    hide_campuses, set_hide_campuses = getter_setter('hide_campuses')
    _, set_contact = getter_setter('contact')

    class Meta:
        unique_together = (('unit', 'semester'), )

    def __str__(self):
        return "%s, %s" % (self.unit.name, self.semester)

    def save(self, *args, **kwargs):
        super(TAPosting, self).save(*args, **kwargs)
        key = self.html_cache_key()
        cache.delete(key)

    def short_str(self):
        return "%s %s" % (self.unit.label, self.semester)

    def delete(self, *args, **kwargs):
        raise NotImplementedError(
            "This object cannot be deleted because it is used as a foreign key."
        )

    def contact(self):
        if 'contact' in self.config:
            return Person.objects.get(id=self.config['contact'])
        else:
            return None

    def payperiods(self):
        return decimal.Decimal(self.payperiods_str())

    def selectable_courses(self):
        """
        Course objects that can be selected as possible choices
        """
        excl = set(self.excluded())
        offerings = CourseOffering.objects.filter(
            semester=self.semester, owner=self.unit).select_related('course')
        # remove duplicates and sort nicely
        courses = list(
            set((o.course for o in offerings if o.course_id not in excl)))
        courses.sort()
        return courses

    def selectable_offerings(self):
        """
        CourseOffering objects that can be selected as possible choices
        """
        excl = set(self.excluded())
        offerings = CourseOffering.objects.filter(
            semester=self.semester,
            owner=self.unit).exclude(course__id__in=excl)
        return offerings

    def is_open(self):
        today = datetime.date.today()
        return self.opens <= today <= self.closes

    def next_export_seq(self):
        if 'export_seq' in self.config:
            current = self.config['export_seq']
        else:
            current = 0

        self.config['export_seq'] = current + 1
        self.save()
        return self.config['export_seq']

    def cat_index(self, val):
        indexer = dict((v[0], k) for k, v in enumerate(CATEGORY_CHOICES))
        return indexer.get(val)

    def default_bu(self, offering, count=None):
        """
        Default BUs to assign for this course offering
        """
        strategy = bu_rules.get_bu_strategy(self.semester, self.unit)
        return strategy(self, offering, count)

    def required_bu(self, offering, count=None):
        """
        Actual BUs to assign to this course: default + extra + 0.17*number of TA's
        """
        default = self.default_bu(offering, count=count)
        extra = offering.extra_bu()

        if offering.labtas():
            tacourses = TACourse.objects.filter(
                contract__posting=self,
                course=offering).exclude(contract__status__in=['REJ', 'CAN'])
            return default + extra + decimal.Decimal(
                LAB_BONUS_DECIMAL * len(tacourses))
        else:
            return default + extra

    def required_bu_cap(self, offering):
        """
        Actual BUs to assign to this course at its enrolment cap
        """
        default = self.default_bu(offering, offering.enrl_cap)
        extra = offering.extra_bu()
        return default + extra

    def assigned_bu(self, offering):
        """
        BUs already assigned to this course
        """
        total = decimal.Decimal(0)
        tacourses = TACourse.objects.filter(
            contract__posting=self,
            course=offering).exclude(contract__status__in=['REJ', 'CAN'])
        if (tacourses.count() > 0):
            total = sum([t.total_bu for t in tacourses])
        return decimal.Decimal(total)

    def applicant_count(self, offering):
        """
        Number of people who have applied to TA this offering
        """
        prefs = CoursePreference.objects.filter(
            app__posting=self, app__late=False,
            course=offering.course).exclude(rank=0)
        return prefs.count()

    def ta_count(self, offering):
        """
        Number of people who have assigned to be TA for this offering
        """
        tacourses = TACourse.objects.filter(
            contract__posting=self,
            course=offering).exclude(contract__status__in=['REJ', 'CAN'])
        return tacourses.count()

    def total_pay(self, offering):
        """
        Payments for all tacourses associated with this offering 
        """
        total = 0
        tacourses = TACourse.objects.filter(
            course=offering, contract__posting=self).exclude(
                contract__status__in=['REJ', 'CAN'])
        for course in tacourses:
            total += course.pay()
        return total

    def all_total(self):
        """
        BU's and Payments for all tacourses associated with all offerings 
        """
        pay = 0
        bus = 0
        tac = TAContract.objects.filter(posting=self).exclude(
            status__in=['REJ', 'CAN']).count()
        tacourses = TACourse.objects.filter(contract__posting=self).exclude(
            contract__status__in=['REJ', 'CAN'])
        for course in tacourses:
            pay += course.pay()
            bus += course.total_bu
        return (bus, pay, tac)

    def html_cache_key(self):
        return "taposting-offertext-html-" + str(self.id)

    def html_offer_text(self):
        """
        Return the HTML version of this offer's offer_text
        
        Cached to save frequent conversion.
        """
        key = self.html_cache_key()
        html = cache.get(key)
        if html:
            return mark_safe(html)
        else:
            html = markup_to_html(self.offer_text(), 'creole')
            cache.set(key, html, 24 * 3600)  # expires on self.save() above
            return html
Exemplo n.º 12
0
class DiscussionTopic(models.Model):
    """
    A topic (thread) associated with a CourseOffering
    """
    offering = models.ForeignKey(CourseOffering, null=False)
    title = models.CharField(max_length=140,
                             help_text="A brief description of the topic")
    content = models.TextField(
        help_text=
        'The inital message for the topic, <a href="http://www.wikicreole.org/wiki/Creole1.0">WikiCreole-formatted</a>'
    )
    created_at = models.DateTimeField(auto_now_add=True)
    last_activity_at = models.DateTimeField(auto_now_add=True)
    message_count = models.IntegerField(default=0)
    status = models.CharField(
        max_length=3,
        choices=TOPIC_STATUSES,
        default='OPN',
        help_text=
        "The topic status: Closed: no replies allowed, Hidden: cannot be seen")
    pinned = models.BooleanField(
        default=False,
        help_text="Should this topic be pinned to bring attention?")
    author = models.ForeignKey(Member)

    def autoslug(self):
        return make_slug(self.title)

    slug = AutoSlugField(populate_from='autoslug',
                         null=False,
                         editable=False,
                         unique_with=['offering'])
    config = JSONField(null=False, blank=False, default={})
    # p.config['math']: content uses MathJax? (boolean)
    # p.config['brushes']: used SyntaxHighlighter brushes (list of strings)

    defaults = {'math': False, 'brushes': []}
    math, set_math = getter_setter('math')
    brushes, set_brushes = getter_setter('brushes')

    def save(self, *args, **kwargs):
        if self.status not in [status[0] for status in TOPIC_STATUSES]:
            raise ValueError('Invalid topic status')

        # update the metainfo about creole display
        self.get_creole()
        brushes = brushes_used(self.Creole.parser.parse(self.content))
        self.set_brushes(list(brushes))
        # TODO: nobody sets config.math to True, but it is honoured if it is set magically. UI for that?

        new_topic = self.id is None
        super(DiscussionTopic, self).save(*args, **kwargs)

        # handle subscriptions
        if new_topic:
            subs = DiscussionSubscription.objects.filter(
                member__offering=self.offering).select_related(
                    'member__person')
            for s in subs:
                s.notify(self)

    def get_absolute_url(self):
        return reverse('offering:discussion:view_topic',
                       kwargs={
                           'course_slug': self.offering.slug,
                           'topic_slug': self.slug
                       })

    def new_message_update(self):
        self.last_activity_at = datetime.datetime.now()
        self.message_count = self.message_count + 1
        self.save()

    def last_activity_at_delta(self):
        return _time_delta_to_string(self.last_activity_at)

    def created_at_delta(self):
        return _time_delta_to_string(self.created_at)

    def __unicode___(self):
        return self.title

    def __init__(self, *args, **kwargs):
        super(DiscussionTopic, self).__init__(*args, **kwargs)
        self.Creole = None

    def get_creole(self):
        "Only build the creole parser on-demand."
        if not self.Creole:
            self.Creole = ParserFor(self.offering)
        return self.Creole

    def html_content(self):
        "Convert self.content to HTML"
        creole = self.get_creole()
        html = creole.text2html(self.content)
        return mark_safe(html)

    def still_editable(self):
        td = datetime.datetime.now() - self.created_at
        seconds = td.days * 86400 + td.seconds
        return seconds <= 120

    def editable_time_left(self):
        td = datetime.datetime.now() - self.created_at
        seconds = td.days * 86400 + td.seconds
        if seconds > 120:
            return 'none'
        minutes, seconds = divmod(120 - seconds, 60)
        return "%dm:%ds" % (minutes, seconds)

    def exportable(self):
        """
        Create JSON-serializable representation of topic, for export.
        """
        data = {
            'title': self.title,
            'body': self.content,
            'created_at': self.created_at.isoformat(),
            'author': self.author.person.userid,
            'status': self.status,
            'pinned': self.pinned
        }
        messages = DiscussionMessage.objects.filter(
            topic=self).select_related('author__person')
        data['replies'] = [m.exportable() for m in messages]
        return data
Exemplo n.º 13
0
class DiscussionMessage(models.Model):
    """
    A message (post) associated with a Discussion Topic
    """
    topic = models.ForeignKey(DiscussionTopic)
    content = models.TextField(
        blank=False,
        help_text=mark_safe(
            'Reply to topic, <a href="http://www.wikicreole.org/wiki/Creole1.0">WikiCreole-formatted</a>'
        ))
    created_at = models.DateTimeField(auto_now_add=True)
    modified_at = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=3,
                              choices=MESSAGE_STATUSES,
                              default='VIS')
    author = models.ForeignKey(Member)

    def autoslug(self):
        return make_slug(self.author.person.userid)

    slug = AutoSlugField(populate_from='autoslug',
                         null=False,
                         editable=False,
                         unique_with=['topic'])
    config = JSONField(null=False, blank=False, default={})
    # p.config['math']: content uses MathJax? (boolean)
    # p.config['brushes']: used SyntaxHighlighter brushes (list of strings)

    defaults = {'math': False, 'brushes': []}
    math, set_math = getter_setter('math')
    brushes, set_brushes = getter_setter('brushes')

    def save(self, *args, **kwargs):
        if self.status not in [status[0] for status in MESSAGE_STATUSES]:
            raise ValueError('Invalid topic status')
        if not self.pk:
            self.topic.new_message_update()

        # update the metainfo about creole display
        self.topic.get_creole()
        brushes = brushes_used(self.topic.Creole.parser.parse(self.content))
        self.set_brushes(list(brushes))

        new_message = self.id is None

        super(DiscussionMessage, self).save(*args, **kwargs)

        # handle subscriptions
        if new_message:
            subs = DiscussionSubscription.objects.filter(
                member__offering=self.topic.offering).select_related(
                    'member__person')
            for s in subs:
                s.notify_message(self)
            subs = TopicSubscription.objects.filter(
                member__offering=self.topic.offering,
                topic=self.topic).select_related('member__person')
            for s in subs:
                s.notify(self)

    def html_content(self):
        "Convert self.content to HTML"
        creole = self.topic.get_creole()
        html = creole.text2html(self.content)
        return mark_safe(html)

    def get_absolute_url(self):
        return self.topic.get_absolute_url() + '#reply-' + str(self.id)

    def create_at_delta(self):
        return _time_delta_to_string(self.created_at)

    def still_editable(self):
        td = datetime.datetime.now() - self.created_at
        seconds = td.days * 86400 + td.seconds
        return seconds <= 120

    def editable_time_left(self):
        td = datetime.datetime.now() - self.created_at
        seconds = td.days * 86400 + td.seconds
        if seconds > 120:
            return 'none'
        minutes, seconds = divmod(120 - seconds, 60)
        return "%dm:%ds" % (minutes, seconds)

    def was_edited(self):
        td = self.modified_at - self.created_at
        return self.modified_at > self.created_at and td > datetime.timedelta(
            seconds=3) and self.status != 'HID'

    def exportable(self):
        """
        Create JSON-serializable representation of message, for export.
        """
        data = {
            'body': self.content,
            'created_at': self.created_at.isoformat(),
            'author': self.author.person.userid,
            'status': self.status
        }
        return data
Exemplo n.º 14
0
class RAAppointment(models.Model):
    """
    This stores information about a (Research Assistant)s application and pay.
    """
    person = models.ForeignKey(Person,
                               help_text='The RA who is being appointed.',
                               null=False,
                               blank=False,
                               related_name='ra_person',
                               on_delete=models.PROTECT)
    sin = models.PositiveIntegerField(null=True, blank=True)
    # We want do add some sort of accountability for checking visas.
    visa_verified = models.BooleanField(
        default=False, help_text='I have verified this RA\'s visa information')
    hiring_faculty = models.ForeignKey(
        Person,
        help_text='The manager who is hiring the RA.',
        related_name='ra_hiring_faculty',
        on_delete=models.PROTECT)
    unit = models.ForeignKey(Unit,
                             help_text='The unit that owns the appointment',
                             null=False,
                             blank=False,
                             on_delete=models.PROTECT)
    hiring_category = models.CharField(max_length=4,
                                       choices=HIRING_CATEGORY_CHOICES,
                                       default='GRA')
    scholarship = models.ForeignKey(
        Scholarship,
        null=True,
        blank=True,
        help_text='Scholarship associated with this appointment. Optional.',
        on_delete=models.PROTECT)
    project = models.ForeignKey(Project,
                                null=False,
                                blank=False,
                                on_delete=models.PROTECT)
    account = models.ForeignKey(
        Account,
        null=False,
        blank=False,
        help_text='This is now called "Object" in the new PAF',
        on_delete=models.PROTECT)
    program = models.ForeignKey(
        Program,
        null=True,
        blank=True,
        help_text='If none is provided,  "00000" will be added in the PAF',
        on_delete=models.PROTECT)
    start_date = models.DateField(auto_now=False, auto_now_add=False)
    end_date = models.DateField(auto_now=False, auto_now_add=False)
    pay_frequency = models.CharField(max_length=60,
                                     choices=PAY_FREQUENCY_CHOICES,
                                     default='B')
    lump_sum_pay = models.DecimalField(max_digits=8,
                                       decimal_places=2,
                                       verbose_name="Total Pay")
    lump_sum_hours = models.DecimalField(max_digits=8,
                                         decimal_places=2,
                                         verbose_name="Total Hours",
                                         blank=True,
                                         null=True)
    biweekly_pay = models.DecimalField(max_digits=8, decimal_places=2)
    pay_periods = models.DecimalField(max_digits=6, decimal_places=1)
    hourly_pay = models.DecimalField(max_digits=8, decimal_places=2)
    hours = models.DecimalField(max_digits=5,
                                decimal_places=2,
                                verbose_name="Biweekly Hours")
    reappointment = models.BooleanField(
        default=False, help_text="Are we re-appointing to the same position?")
    medical_benefits = models.BooleanField(
        default=False, help_text="50% of Medical Service Plan")
    dental_benefits = models.BooleanField(default=False,
                                          help_text="50% of Dental Plan")
    #  The two following fields verbose names are reversed for a reason.  They were named incorrectly with regards to
    #  the PAF we generate, so the verbose names are correct.
    notes = models.TextField(
        "Comments",
        blank=True,
        help_text=
        "Biweekly employment earnings rates must include vacation pay, hourly rates will automatically have vacation pay added. The employer cost of statutory benefits will be charged to the amount to the earnings rate."
    )
    comments = models.TextField("Notes",
                                blank=True,
                                help_text="For internal use")
    offer_letter_text = models.TextField(
        null=True,
        help_text=
        "Text of the offer letter to be signed by the RA and supervisor.")

    def autoslug(self):
        if self.person.userid:
            ident = self.person.userid
        else:
            ident = str(self.person.emplid)
        return make_slug(self.unit.label + '-' + str(self.start_date.year) +
                         '-' + ident)

    slug = AutoSlugField(populate_from='autoslug',
                         null=False,
                         editable=False,
                         unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    deleted = models.BooleanField(null=False, default=False)
    config = JSONField(null=False, blank=False,
                       default=dict)  # addition configuration stuff
    defaults = {'use_hourly': False}
    use_hourly, set_use_hourly = getter_setter('use_hourly')

    def __str__(self):
        return str(self.person) + "@" + str(self.created_at)

    class Meta:
        ordering = ['person', 'created_at']

    def save(self, *args, **kwargs):
        # set SIN field on the Person object
        if self.sin and 'sin' not in self.person.config:
            self.person.set_sin(self.sin)
            self.person.save()
        super(RAAppointment, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('ra:view', kwargs={'ra_slug': self.slug})

    def mark_reminded(self):
        self.config['reminded'] = True
        self.save()

    @staticmethod
    def letter_choices(units):
        """
        Return a form choices list for RA letter templates in these units.

        Ignores the units for now: we want to allow configurability later.
        """
        return [(key, label)
                for (key, (label, _, _)) in list(DEFAULT_LETTERS.items())]

    def build_letter_text(self, selection):
        """
        This takes the value passed from the letter selector menu and builds the appropriate
        default letter based on that.
        """
        substitutions = {
            'start_date': self.start_date.strftime("%B %d, %Y"),
            'end_date': self.end_date.strftime("%B %d, %Y"),
            'lump_sum_pay': self.lump_sum_pay,
            'biweekly_pay': self.biweekly_pay,
        }

        _, lumpsum_text, biweekly_text = DEFAULT_LETTERS[selection]

        if self.pay_frequency == 'B':
            text = biweekly_text
        else:
            text = lumpsum_text

        letter_text = text % substitutions
        self.offer_letter_text = letter_text
        self.save()

    def letter_paragraphs(self):
        """
        Return list of paragraphs in the letter (for PDF creation)
        """
        text = self.offer_letter_text
        text = normalize_newlines(text)
        return text.split("\n\n")

    @classmethod
    def semester_guess(cls, date):
        """
        Guess the semester for a date, in the way that financial people do (without regard to class start/end dates)
        """
        mo = date.month
        if mo <= 4:
            se = 1
        elif mo <= 8:
            se = 4
        else:
            se = 7
        semname = str((date.year - 1900) * 10 + se)
        return Semester.objects.get(name=semname)

    @classmethod
    def start_end_dates(cls, semester):
        """
        First and last days of the semester, in the way that financial people do (without regard to class start/end dates)
        """
        return Semester.start_end_dates(semester)
        #yr = int(semester.name[0:3]) + 1900
        #sm = int(semester.name[3])
        #if sm == 1:
        #    start = datetime.date(yr, 1, 1)
        #    end = datetime.date(yr, 4, 30)
        #elif sm == 4:
        #    start = datetime.date(yr, 5, 1)
        #    end = datetime.date(yr, 8, 31)
        #elif sm == 7:
        #    start = datetime.date(yr, 9, 1)
        #    end = datetime.date(yr, 12, 31)
        #return start, end

    def start_semester(self):
        "Guess the starting semester of this appointment"
        start_semester = RAAppointment.semester_guess(self.start_date)
        # We do this to eliminate hang - if you're starting N days before
        # semester 1134, you aren't splitting that payment across 2 semesters.
        start, end = RAAppointment.start_end_dates(start_semester)
        if end - self.start_date < datetime.timedelta(SEMESTER_SLIDE):
            return start_semester.next_semester()
        return start_semester

    def end_semester(self):
        "Guess the ending semester of this appointment"
        end_semester = RAAppointment.semester_guess(self.end_date)
        # We do this to eliminate hang - if you're starting N days after
        # semester 1134, you aren't splitting that payment across 2 semesters.
        start, end = RAAppointment.start_end_dates(end_semester)
        if self.end_date - start < datetime.timedelta(SEMESTER_SLIDE):
            return end_semester.previous_semester()
        return end_semester

    def semester_length(self):
        "The number of semesters this contracts lasts for"
        return self.end_semester() - self.start_semester() + 1

    @classmethod
    def expiring_appointments(cls):
        """
        Get the list of RA Appointments that will expire in the next few weeks so we can send a reminder email
        """
        today = datetime.datetime.now()
        min_age = datetime.datetime.now() + datetime.timedelta(days=14)
        expiring_ras = RAAppointment.objects.filter(end_date__gt=today,
                                                    end_date__lte=min_age,
                                                    deleted=False)
        ras = [
            ra for ra in expiring_ras
            if 'reminded' not in ra.config or not ra.config['reminded']
        ]
        return ras

    @classmethod
    def email_expiring_ras(cls):
        """
        Emails the supervisors of the RAs who have appointments that are about to expire.
        """
        subject = 'RA appointment expiry reminder'
        from_email = settings.DEFAULT_FROM_EMAIL

        expiring_ras = cls.expiring_appointments()
        template = get_template('ra/emails/reminder.txt')

        for raappt in expiring_ras:
            supervisor = raappt.hiring_faculty
            context = {'supervisor': supervisor, 'raappt': raappt}
            # Let's see if we have any Funding CC supervisors that should also get the reminder.
            cc = None
            fund_cc_roles = Role.objects_fresh.filter(unit=raappt.unit,
                                                      role='FDCC')
            # If we do, let's add them to the CC list, but let's also make sure to use their role account email for
            # the given role type if it exists.
            if fund_cc_roles:
                people = []
                for role in fund_cc_roles:
                    people.append(role.person)
                people = list(set(people))
                cc = []
                for person in people:
                    cc.append(person.role_account_email('FDCC'))
            msg = EmailMultiAlternatives(subject,
                                         template.render(context),
                                         from_email, [supervisor.email()],
                                         headers={'X-coursys-topic': 'ra'},
                                         cc=cc)
            msg.send()
            raappt.mark_reminded()

    def get_program_display(self):
        if self.program:
            return self.program.get_program_number_display()
        else:
            return '00000'

    def has_attachments(self):
        return self.attachments.visible().count() > 0
Exemplo n.º 15
0
class DiscussionTopic(models.Model):
    """
    A topic (thread) associated with a CourseOffering
    """
    offering = models.ForeignKey(CourseOffering,
                                 null=False,
                                 on_delete=models.PROTECT)
    title = models.CharField(max_length=140,
                             help_text="A brief description of the topic")
    content = models.TextField(help_text='The inital message for the topic.')
    created_at = models.DateTimeField(auto_now_add=True)
    last_activity_at = models.DateTimeField(auto_now_add=True)
    message_count = models.IntegerField(default=0)
    status = models.CharField(
        max_length=3,
        choices=TOPIC_STATUSES,
        default='OPN',
        help_text=
        "The topic status: Closed: no replies allowed, Hidden: cannot be seen")
    pinned = models.BooleanField(
        default=False,
        help_text="Should this topic be pinned to bring attention?")
    author = models.ForeignKey(Member, on_delete=models.PROTECT)

    def autoslug(self):
        return make_slug(self.title)

    slug = AutoSlugField(populate_from='autoslug',
                         null=False,
                         editable=False,
                         unique_with=['offering'])
    config = JSONField(null=False, blank=False, default=dict)
    # p.config['markup']:  markup language used: see courselib/markup.py
    # p.config['math']: content uses MathJax? (boolean)
    # p.config['brushes']: used SyntaxHighlighter brushes (list of strings) -- no longer used with highlight.js

    defaults = {
        'markup': 'creole',
        'math': False,
    }
    markup, set_markup = getter_setter('markup')
    math, set_math = getter_setter('math')

    def save(self, *args, **kwargs):
        if self.status not in [status[0] for status in TOPIC_STATUSES]:
            raise ValueError('Invalid topic status')

        self.content = ensure_sanitary_markup(self.content,
                                              self.markup(),
                                              restricted=True)

        new_topic = self.id is None
        super(DiscussionTopic, self).save(*args, **kwargs)

        # handle subscriptions
        if new_topic:
            subs = DiscussionSubscription.objects.filter(
                member__offering=self.offering).select_related(
                    'member__person')
            for s in subs:
                s.notify(self)

    def get_absolute_url(self):
        return reverse('offering:discussion:view_topic',
                       kwargs={
                           'course_slug': self.offering.slug,
                           'topic_slug': self.slug
                       })

    def new_message_update(self):
        self.last_activity_at = datetime.datetime.now()
        self.message_count = self.message_count + 1
        self.save()

    def last_activity_at_delta(self):
        return _time_delta_to_string(self.last_activity_at)

    def created_at_delta(self):
        return _time_delta_to_string(self.created_at)

    def __str___(self):
        return self.title

    def html_content(self):
        "Convert self.content to HTML"
        return markup_to_html(self.content,
                              self.markup(),
                              offering=self.offering,
                              html_already_safe=True,
                              restricted=True)

    def still_editable(self):
        td = datetime.datetime.now() - self.created_at
        seconds = td.days * 86400 + td.seconds
        return seconds <= 120

    def editable_time_left(self):
        td = datetime.datetime.now() - self.created_at
        seconds = td.days * 86400 + td.seconds
        if seconds > 120:
            return 'none'
        minutes, seconds = divmod(120 - seconds, 60)
        return "%dm:%ds" % (minutes, seconds)

    def exportable(self):
        """
        Create JSON-serializable representation of topic, for export.
        """
        data = {
            'title': self.title,
            'body': self.content,
            'created_at': self.created_at.isoformat(),
            'author': self.author.person.userid,
            'status': self.status,
            'pinned': self.pinned
        }
        messages = DiscussionMessage.objects.filter(
            topic=self).select_related('author__person')
        data['replies'] = [m.exportable() for m in messages]
        return data
Exemplo n.º 16
0
class Activity(models.Model):
    """
    Generic activity (i.e. column in the gradebook that can have a value assigned for each student).
    This should never be instantiated directly: only its sublcasses should.
    """
    objects = models.Manager()

    name = models.CharField(max_length=30,
                            db_index=True,
                            help_text='Name of the activity.')
    short_name = models.CharField(max_length=15,
                                  db_index=True,
                                  help_text='Short-form name of the activity.')

    def autoslug(self):
        return make_slug(self.short_name)

    slug = AutoSlugField(
        populate_from='autoslug',
        null=False,
        editable=False,
        unique_with='offering',
        manager=objects,
        help_text=
        'String that identifies this activity within the course offering')
    status = models.CharField(max_length=4,
                              null=False,
                              choices=ACTIVITY_STATUS_CHOICES,
                              help_text='Activity status.')
    due_date = models.DateTimeField(null=True, help_text='Activity due date')
    percent = models.DecimalField(max_digits=5,
                                  decimal_places=2,
                                  blank=True,
                                  null=True)
    position = models.PositiveSmallIntegerField(
        help_text="The order of displaying course activities.")
    group = models.BooleanField(null=False, default=False)
    deleted = models.BooleanField(null=False, db_index=True, default=False)
    config = JSONField(null=False, blank=False,
                       default={})  # addition configuration stuff:
    # a.config['url'] (string, default None): URL for more info
    # a.config['showstats'] (boolean, default True): show students summary stats for this activity?
    # a.config['showhisto'] (boolean, default True): show students histogram for this activity?
    # a.config['showformula'] (boolean, default False): show students formula/cutoffs for this activity?
    # TODO: showformula not actually implemented yet

    offering = models.ForeignKey(CourseOffering)

    defaults = {
        'url': '',
        'showstats': True,
        'showhisto': True,
        'showformula': False
    }
    url, set_url = getter_setter('url')
    showstats, set_showstats = getter_setter('showstats')
    showhisto, set_showhisto = getter_setter('showhisto')
    showformula, set_showformula = getter_setter('showformula')

    def __unicode__(self):
        return u"%s - %s" % (self.offering, self.name)

    def short_str(self):
        return self.name

    def __cmp__(self, other):
        return cmp(self.position, other.position)

    def get_absolute_url(self):
        return reverse('grades.views.activity_info',
                       kwargs={
                           'course_slug': self.offering.slug,
                           'activity_slug': self.slug
                       })

    def delete(self, *args, **kwargs):
        raise NotImplementedError, "This object cannot be deleted because it is used as a foreign key."

    def is_numeric(self):
        return False

    class Meta:
        verbose_name_plural = "activities"
        ordering = ['deleted', 'position']
        unique_together = (('offering', 'slug'), )

    def save(self,
             force_insert=False,
             force_update=False,
             newsitem=True,
             entered_by=None,
             *args,
             **kwargs):
        # get old status so we can see if it's newly-released
        try:
            old = Activity.objects.get(id=self.id)
        except Activity.DoesNotExist:
            old = None

        # reset slugs before semester starts
        if self.offering.semester.start > date.today():
            self.slug = None

        super(Activity, self).save(*args, **kwargs)

        if newsitem and old and self.status == 'RLS' and old != None and old.status != 'RLS':
            from grades.tasks import send_grade_released_news, create_grade_released_history

            # newly-released grades: record that grade was released
            assert entered_by
            entered_by = get_entry_person(entered_by)
            create_grade_released_history(self.id, entered_by.id)

            # newly-released grades: create news items
            send_grade_released_news(self.id)

        if old and old.group and not self.group:
            # activity changed group -> individual. Clean out any group memberships
            from groups.models import GroupMember
            GroupMember.objects.filter(activity=self).delete()

    def safely_delete(self):
        """
        Do the actions to safely "delete" the activity.
        """
        with transaction.atomic():
            # mangle name and short-name so instructors can delete and replace
            i = 1
            while True:
                suffix = "__%04i" % (i)
                existing = Activity.objects.filter(offering=self.offering, name=self.name+suffix).count() \
                        + Activity.objects.filter(offering=self.offering, short_name=self.short_name+suffix).count()
                if existing == 0:
                    break
                i += 1

            # update the activity
            self.deleted = True
            self.name = self.name + suffix
            self.short_name = self.short_name + suffix
            self.slug = None
            self.save()

    def display_label(self):
        if self.percent:
            return "%s (%s%%)" % (self.name, self.percent)
        else:
            return "%s" % (self.name)

    def display_grade_student(self, student):
        """
        String representing grade for this student
        """
        return self.display_grade_visible(student, 'STUD')

    def display_grade_staff(self, student):
        """
        String representing grade for this student
        """
        return self.display_grade_visible(student, 'INST')

    def get_status_display(self):
        """
        Override to provide better string for not-yet-due case.
        """
        if self.status == "URLS" and self.due_date and self.due_date > datetime.now(
        ):
            return "no grades: due date not passed"

        return ACTIVITY_STATUS[self.status]

    def get_status_display_staff(self):
        """
        Override to provide better string for not-yet-due case.
        """
        if self.status == "URLS" and self.due_date and self.due_date > datetime.now(
        ):
            return "no grades: due date not passed"
        elif self.status == 'URLS':
            total = Member.objects.filter(offering=self.offering,
                                          role='STUD').count()
            if isinstance(self, NumericActivity):
                GradeClass = NumericGrade
            elif isinstance(self, LetterActivity):
                GradeClass = LetterGrade
            graded = GradeClass.objects.filter(activity=self).exclude(
                flag='NOGR').count()
            # If we've graded everything, might as well change the status back
            if graded == total and total > 0:
                return ACTIVITY_STATUS[self.status]
            # Otherwise, let them know the progress.
            return 'ready to grade (%i/%i graded)' % (graded, total)

        return ACTIVITY_STATUS[self.status]

    def markable(self):
        """
        Returns True if this activity is "markable".  i.e. has any marking components defined.
        """
        return self.activitycomponent_set.all().count() != 0

    def submitable(self):
        """
        Returns True if this activity is "submittable".
        i.e. has any submission components defined and within 30 days after due date
        """
        comp_count = self.submissioncomponent_set.filter(deleted=False).count()
        return comp_count != 0 and not self.too_old()

    def no_submit_too_old(self):
        """
        Returns True if this activity was submittable but is now too old
        """
        comp_count = self.submissioncomponent_set.filter(deleted=False).count()
        return comp_count != 0 and self.too_old()

    def too_old(self):
        """
        Returns True if this activity is not submittable because it is too old
        """
        if not self.due_date:
            return False
        now = datetime.now()
        return now - self.due_date >= timedelta(days=30)

    def SubmissionClass(self):
        from submission.models import StudentSubmission, GroupSubmission
        if self.group:
            return GroupSubmission
        else:
            return StudentSubmission

    def due_in_future(self):
        return self.due_date and self.due_date > datetime.now()

    def due_in_str(self):
        """
        Produce pretty string for "in how long is this due"
        """
        if not self.due_date:
            return u"\u2014"
        due_in = self.due_date - datetime.now()
        seconds = (
            due_in.microseconds +
            (due_in.seconds + due_in.days * 24 * 3600.0) * 10**6) / 10**6
        if due_in < timedelta(seconds=0):
            return u'\u2014'
        elif due_in > timedelta(days=2):
            return "%i days" % (due_in.days)
        elif due_in > timedelta(days=1):
            return "%i day %i hours" % (due_in.days,
                                        int((seconds / 3600) -
                                            (due_in.days * 24)))
        elif due_in > timedelta(hours=2):
            return "%i hours %i minutes" % (int(
                seconds / 3600), int(seconds % 3600 / 60))
        elif due_in > timedelta(hours=1):
            return "%i hour %i minutes" % (int(
                seconds / 3600), int(seconds % 3600 / 60))
        else:
            return "%i minutes" % (int(seconds / 60))

    def due_class(self):
        """
        Produce class name for "heat": how long until it is due?
        """
        if not self.due_date:
            return "due_unknown"
        due_in = self.due_date - datetime.now()
        if due_in < timedelta(seconds=0):
            return "due_overdue"
        elif due_in < timedelta(days=2):
            return "due_verysoon"
        elif due_in < timedelta(days=7):
            return "due_soon"
        else:
            return "due_far"
Exemplo n.º 17
0
class RAAppointment(models.Model):
    """
    This stores information about a (Research Assistant)s application and pay.
    """
    person = models.ForeignKey(Person,
                               help_text='The RA who is being appointed.',
                               null=False,
                               blank=False,
                               related_name='ra_person')
    sin = models.PositiveIntegerField(null=True, blank=True)
    hiring_faculty = models.ForeignKey(
        Person,
        help_text='The manager who is hiring the RA.',
        related_name='ra_hiring_faculty')
    unit = models.ForeignKey(Unit,
                             help_text='The unit that owns the appointment',
                             null=False,
                             blank=False)
    hiring_category = models.CharField(max_length=4,
                                       choices=HIRING_CATEGORY_CHOICES,
                                       default='GRA')
    scholarship = models.ForeignKey(
        Scholarship,
        null=True,
        blank=True,
        help_text='Scholarship associated with this appointment. Optional.')
    project = models.ForeignKey(Project, null=False, blank=False)
    account = models.ForeignKey(Account, null=False, blank=False)
    start_date = models.DateField(auto_now=False, auto_now_add=False)
    end_date = models.DateField(auto_now=False, auto_now_add=False)
    pay_frequency = models.CharField(max_length=60,
                                     choices=PAY_FREQUENCY_CHOICES,
                                     default='B')
    lump_sum_pay = models.DecimalField(max_digits=8,
                                       decimal_places=2,
                                       verbose_name="Total Pay")
    lump_sum_hours = models.DecimalField(max_digits=8,
                                         decimal_places=2,
                                         verbose_name="Total Hours",
                                         blank=True,
                                         null=True)
    biweekly_pay = models.DecimalField(max_digits=8, decimal_places=2)
    pay_periods = models.DecimalField(max_digits=6, decimal_places=1)
    hourly_pay = models.DecimalField(max_digits=8, decimal_places=2)
    hours = models.DecimalField(max_digits=5,
                                decimal_places=2,
                                verbose_name="Biweekly Hours")
    reappointment = models.BooleanField(
        default=False, help_text="Are we re-appointing to the same position?")
    medical_benefits = models.BooleanField(
        default=False, help_text="50% of Medical Service Plan")
    dental_benefits = models.BooleanField(default=False,
                                          help_text="50% of Dental Plan")
    notes = models.TextField(
        blank=True,
        help_text=
        "Biweekly emplyment earnings rates must include vacation pay, hourly rates will automatically have vacation pay added. The employer cost of statutory benefits will be charged to the amount to the earnings rate."
    )
    comments = models.TextField(blank=True, help_text="For internal use")
    offer_letter_text = models.TextField(
        null=True,
        help_text=
        "Text of the offer letter to be signed by the RA and supervisor.")

    def autoslug(self):
        if self.person.userid:
            ident = self.person.userid
        else:
            ident = unicode(self.person.emplid)
        return make_slug(self.unit.label + '-' +
                         unicode(self.start_date.year) + '-' + ident)

    slug = AutoSlugField(populate_from=autoslug,
                         null=False,
                         editable=False,
                         unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    deleted = models.BooleanField(null=False, default=False)
    config = JSONField(null=False, blank=False,
                       default={})  # addition configuration stuff
    defaults = {'use_hourly': False}
    use_hourly, set_use_hourly = getter_setter('use_hourly')

    def __unicode__(self):
        return unicode(self.person) + "@" + unicode(self.created_at)

    class Meta:
        ordering = ['person', 'created_at']

    def save(self, *args, **kwargs):
        # set SIN field on the Person object
        if self.sin and 'sin' not in self.person.config:
            self.person.set_sin(self.sin)
            self.person.save()
        super(RAAppointment, self).save(*args, **kwargs)

    def default_letter_text(self):
        """
        Default text for the letter (for editing, or use if not set)
        """
        substitutions = {
            'start_date': self.start_date.strftime("%B %d, %Y"),
            'end_date': self.end_date.strftime("%B %d, %Y"),
            'lump_sum_pay': self.lump_sum_pay,
            'biweekly_pay': self.biweekly_pay,
        }
        if self.pay_frequency == 'B':
            text = DEFAULT_LETTER_BIWEEKLY
        else:
            text = DEFAULT_LETTER_LUMPSUM
        return '\n\n'.join(text) % substitutions

    def letter_paragraphs(self):
        """
        Return list of paragraphs in the letter (for PDF creation)
        """
        text = self.offer_letter_text or self.default_letter_text()
        text = normalize_newlines(text)
        return text.split("\n\n")

    @classmethod
    def semester_guess(cls, date):
        """
        Guess the semester for a date, in the way that financial people do (without regard to class start/end dates)
        """
        mo = date.month
        if mo <= 4:
            se = 1
        elif mo <= 8:
            se = 4
        else:
            se = 7
        semname = str((date.year - 1900) * 10 + se)
        return Semester.objects.get(name=semname)

    @classmethod
    def start_end_dates(cls, semester):
        """
        First and last days of the semester, in the way that financial people do (without regard to class start/end dates)
        """
        return Semester.start_end_dates(semester)
        #yr = int(semester.name[0:3]) + 1900
        #sm = int(semester.name[3])
        #if sm == 1:
        #    start = datetime.date(yr, 1, 1)
        #    end = datetime.date(yr, 4, 30)
        #elif sm == 4:
        #    start = datetime.date(yr, 5, 1)
        #    end = datetime.date(yr, 8, 31)
        #elif sm == 7:
        #    start = datetime.date(yr, 9, 1)
        #    end = datetime.date(yr, 12, 31)
        #return start, end

    def start_semester(self):
        "Guess the starting semester of this appointment"
        start_semester = RAAppointment.semester_guess(self.start_date)
        # We do this to eliminate hang - if you're starting N days before
        # semester 1134, you aren't splitting that payment across 2 semesters.
        start, end = RAAppointment.start_end_dates(start_semester)
        if end - self.start_date < datetime.timedelta(SEMESTER_SLIDE):
            return start_semester.next_semester()
        return start_semester

    def end_semester(self):
        "Guess the ending semester of this appointment"
        end_semester = RAAppointment.semester_guess(self.end_date)
        # We do this to eliminate hang - if you're starting N days after
        # semester 1134, you aren't splitting that payment across 2 semesters.
        start, end = RAAppointment.start_end_dates(end_semester)
        if self.end_date - start < datetime.timedelta(SEMESTER_SLIDE):
            return end_semester.previous_semester()
        return end_semester

    def semester_length(self):
        "The number of semesters this contracts lasts for"
        return self.end_semester() - self.start_semester() + 1