class AdvisorVisitCategory(models.Model): """ Allow each unit to manage the categories which are now included in a visit. """ unit = models.ForeignKey(Unit, null=False, blank=False, on_delete=models.PROTECT) label = models.CharField(null=False, blank=False, max_length=50) description = models.CharField(null=True, blank=True, max_length=500) hidden = models.BooleanField(null=False, blank=False, default=False, editable=False) config = JSONField(null=False, blank=False, default=dict, editable=False) # addition configuration stuff: def autoslug(self): return make_slug(self.unit.slug + '-' + self.label) slug = AutoSlugField(populate_from='autoslug', null=False, editable=False, unique=True) def __str__(self): return self.label objects = AdvisorVisitCategoryQuerySet.as_manager() def delete(self): # As usual, only hide stuff, don't delete it. self.hidden = True self.save()
class Announcement(models.Model): title = models.CharField(max_length=100) message = models.TextField(blank=False, null=False) created_at = models.DateTimeField(default=datetime.datetime.now) author = models.ForeignKey(Person, related_name='posted_by', on_delete=models.PROTECT, help_text='The user who created the news item', editable=False) hidden = models.BooleanField(null=False, db_index=True, default=False) unit = models.ForeignKey(Unit, help_text='Academic unit who owns the note', null=False, blank=False, on_delete=models.PROTECT) config = JSONField(null=False, blank=False, default=dict) markup = config_property('markup', 'plain') math = config_property('math', False) def __str__(self): return "%s" % (self.title) class Meta: ordering = ('-created_at',) def delete(self, *args, **kwargs): raise NotImplementedError("This object cannot be deleted, set the hidden flag instead.") def html_content(self): return markup_to_html(self.message, self.markup, restricted=False)
class FacultyMemberInfo(models.Model): #person = models.ForeignKey(Person, unique=True, related_name='+') person = models.OneToOneField(Person, related_name='+') title = models.CharField(max_length=50) birthday = models.DateField(verbose_name="Birthdate", null=True, blank=True) office_number = models.CharField('Office', max_length=20, null=True, blank=True) phone_number = models.CharField('Local Phone Number', max_length=20, null=True, blank=True) emergency_contact = models.TextField('Emergency Contact Information', blank=True) config = JSONField(blank=True, null=True, default={}) # addition configuration last_updated = models.DateTimeField(auto_now=True) def __unicode__(self): return u'<FacultyMemberInfo({})>'.format(self.person) def get_absolute_url(self): return reverse('faculty.views.faculty_member_info', args=[self.person.userid_or_emplid()])
class AlertType(models.Model): """ An alert code. "GPA < 2.4" """ code = models.CharField( help_text="A short (<30 characters) title for the report.", max_length=30) description = models.TextField( help_text= "A longer, more in-depth explanation of what this alert is for.", null=True, blank=True) unit = models.ForeignKey( Unit, help_text="Only people in this unit can manage this Alert", null=False) hidden = models.BooleanField(null=False, default=False) config = JSONField(null=False, blank=False, default=dict) def autoslug(self): return make_slug(self.code) slug = AutoSlugField(populate_from='autoslug', null=False, editable=False, unique=True)
class AssetChangeRecord(models.Model): asset = models.ForeignKey(Asset, null=False, blank=False, related_name='records', on_delete=models.PROTECT) person = models.ForeignKey(Person, null=False, blank=False, on_delete=models.PROTECT) qty = models.IntegerField( "Quantity adjustment", null=False, blank=False, help_text= "The change in quantity. For removal of item, make it a negative number. " "For adding items, make it a positive. e.g. '-2' if someone removed two of " "this item for something") date = models.DateField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True, editable=False) last_modified = models.DateTimeField(editable=False, blank=False, null=False) hidden = models.BooleanField(default=False, null=False, blank=False, editable=False) saved_by_userid = models.CharField(max_length=8, blank=False, null=False, editable=False) # In case we forgot something, this will make it easier to add something in the future without a migration. config = JSONField(null=False, blank=False, default=dict, editable=False) def autoslug(self): if self.qty > 0: change_string = " added " else: change_string = " removed " return make_slug(self.person.userid_or_emplid() + change_string + str(self.qty) + ' ' + str(self.asset)) slug = AutoSlugField(populate_from='autoslug', null=False, editable=False, unique=True) objects = AssetChangeRecordQuerySet.as_manager() def save(self, user, *args, **kwargs): self.last_modified = timezone.now() self.saved_by_userid = user super(AssetChangeRecord, self).save(*args, **kwargs) def delete(self, user): """ Like most of our objects, never actually delete them, just hide them. """ self.hidden = True self.save(user)
class Artifact(models.Model): name = models.CharField(max_length=140, help_text='The name of the artifact', null=False, blank=False) category = models.CharField(max_length=3, choices=ARTIFACT_CATEGORIES, null=False, blank=False) def autoslug(self): return make_slug(self.unit.label + '-' + self.name) slug = AutoSlugField(populate_from=autoslug, null=False, editable=False, unique=True) unit = models.ForeignKey( Unit, help_text='The academic unit that owns this artifact', null=False, blank=False) config = JSONField(null=False, blank=False, default={}) # addition configuration stuff: class Meta: ordering = ['name'] unique_together = [('name', 'unit')] def __unicode__(self): return unicode(self.name) + ' (' + unicode( self.get_category_display()) + ')'
class Position(models.Model): title = models.CharField(max_length=100) projected_start_date = models.DateField('Projected Start Date', default=timezone_today) unit = models.ForeignKey(Unit, null=False, blank=False, on_delete=models.PROTECT) position_number = models.CharField(max_length=8) rank = models.CharField(choices=RANK_CHOICES, max_length=50, null=True, blank=True) step = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True) percentage = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True, help_text='Percentage of this position in the given unit', default=100) base_salary = models.DecimalField(decimal_places=2, max_digits=10, null=True, blank=True) add_salary = models.DecimalField(decimal_places=2, max_digits=10, null=True, blank=True) add_pay = models.DecimalField(decimal_places=2, max_digits=10, null=True, blank=True) config = JSONField(null=False, blank=False, editable=False, default=dict) # For future fields hidden = models.BooleanField(default=False, editable=False) any_person = models.ForeignKey(AnyPerson, on_delete=models.SET_NULL, null=True, blank=True) degree1 = models.CharField(max_length=12, default='') year1 = models.CharField(max_length=5, default='') institution1 = models.CharField(max_length=25, default='') location1 = models.CharField(max_length=23, default='') degree2 = models.CharField(max_length=12, default='') year2 = models.CharField(max_length=5, default='') institution2 = models.CharField(max_length=25, default='') location2 = models.CharField(max_length=23, default='') degree3 = models.CharField(max_length=12, default='') year3 = models.CharField(max_length=5, default='') institution3 = models.CharField(max_length=25, default='') location3 = models.CharField(max_length=23, default='') teaching_semester_credits = models.DecimalField(decimal_places=0, max_digits=3, null=True, blank=True) objects = PositionManager() def __str__(self): return "%s - %s" % (self.position_number, self.title) def hide(self): self.hidden = True class Meta: ordering = ('projected_start_date', 'title') def get_load_display(self): """ Called if you're going to insert this in another AnnualTeachingCreditField, like when we populate the onboarding wizard with this value. """ if 'teaching_load' in self.config and not self.config['teaching_load'] == 'None': return str(Fraction(self.config['teaching_load'])) else: return 0 def get_load_display_corrected(self): """ Called if you're purely going to display the value, as when displaying the contents of the position. """ if 'teaching_load' in self.config and not self.config['teaching_load'] == 'None': return str(Fraction(self.config['teaching_load'])*3) else: return 0
class FormGroup(models.Model): """ A group that owns forms and form submissions. """ unit = models.ForeignKey(Unit) name = models.CharField(max_length=60, null=False, blank=False) members = models.ManyToManyField(Person, through='FormGroupMember') # def autoslug(self): return make_slug(self.unit.label + ' ' + self.name) slug = AutoSlugField(populate_from=autoslug, null=False, editable=False, unique=True) config = JSONField(null=False, blank=False, default={}) # addition configuration stuff: class Meta: unique_together = (("unit", "name"), ) def __unicode__(self): return u"%s, %s" % (self.name, self.unit.label) def delete(self, *args, **kwargs): raise NotImplementedError, "This object cannot be deleted because it is used as a foreign key." def notify_emails(self): """ Collection of emails to notify about something in this group """ return [ FormFiller.form_full_email(m.person) for m in self.formgroupmember_set.all() if m.email() ]
class HardcodedReport(models.Model): """ Represents a report that exists as a python file in courses/reports/reportlib/reports """ report = models.ForeignKey(Report) file_location = models.CharField(help_text="The location of this report, on disk.", max_length=80, choices=all_reports(), null=False) config = JSONField(null=False, blank=False, default={}) created_at = models.DateTimeField(auto_now_add=True) def run(self, manual=False): """ execute the code in this file """ r = Run(report=self.report, name=self.file_location, manual=manual) r.save() logger = RunLineLogger(r) try: report_object = report_map(self.file_location, logger) report_object.run() for artifact in report_object.artifacts: artifact.convert_to_unicode() try: result = Result(run=r, name=artifact.title, table=artifact.to_dict() ) except AttributeError: result = Result(run=r, table=artifact.to_dict() ) result.save() r.success = True r.save() except Exception as e: logger.log("ERROR: " + str(e) ) type_, value_, traceback_ = sys.exc_info() logger.log( ",".join(traceback.format_tb( traceback_ )) ) return r
class TempGrant(models.Model): label = models.CharField(max_length=150, help_text="for identification from FAST import") initial = models.DecimalField(verbose_name="initial balance", max_digits=12, decimal_places=2) project_code = models.CharField(max_length=32, help_text="The fund and project code, like '13-123456'") import_key = models.CharField(null=True, blank=True, max_length=255, help_text="e.g. 'nserc-43517b4fd422423382baab1e916e7f63'") creator = models.ForeignKey(Person, blank=True, null=True, on_delete=models.PROTECT) created = models.DateTimeField(auto_now_add=True) config = JSONField(default=dict) # addition configuration for within the temp grant objects = TempGrantManager() class Meta: unique_together = (('label', 'creator'),) def get_convert_url(self): return reverse("faculty:convert_grant", args=[self.id]) def get_delete_url(self): return reverse("faculty:delete_grant", args=[self.id]) def grant_dict(self, **kwargs): data = { "label": self.label, "title": self.label, "project_code": self.project_code, "initial": self.initial, "import_key": self.import_key, } data.update(**kwargs) return data
class SemesterPlan(models.Model): """ A semester plan which holds potential planned offerings. """ semester = models.ForeignKey(Semester) name = models.CharField( max_length=70, help_text="A name to help you remeber which plan this is.") visibility = models.CharField(max_length=4, choices=VISIBILITY_CHOICES, default="ADMI", help_text="Who can see this plan?") slug = AutoSlugField(populate_from='name', null=False, editable=False, unique_with='semester') unit = models.ForeignKey( Unit, help_text='The academic unit that owns this course plan') config = JSONField(null=False, blank=False, default=dict) def get_absolute_url(self): return reverse('planning.views.view_plan', kwargs={'semester': self.semester.name}) def save(self, *args, **kwargs): super(SemesterPlan, self).save(*args, **kwargs) class Meta: ordering = ['semester', 'name'] unique_together = (('semester', 'name'), )
class Query(models.Model): """ A custom query developed by the user. """ report = models.ForeignKey(Report, on_delete=models.CASCADE) name = models.CharField(max_length=150, null=False) query = models.TextField() config = JSONField(null=False, blank=False, default=dict) created_at = models.DateTimeField(auto_now_add=True) def run(self, manual=False): r = Run(report=self.report, name=self.name, manual=manual) r.save() logger = RunLineLogger(r) try: DB2_Query.set_logger(logger) DB2_Query.connect() q = DB2_Query() q.query = string.Template(self.query) artifact = q.result() artifact.convert_to_unicode() result = Result(run=r, name=self.name, table=artifact.to_dict()) result.save() r.success = True r.save() except Exception as e: logger.log("ERROR: " + str(e)) type_, value_, traceback_ = sys.exc_info() logger.log(",".join(traceback.format_tb(traceback_))) return r
class CourseDescription(models.Model): """ Description of the work for a TA contract """ unit = models.ForeignKey(Unit, on_delete=models.PROTECT) description = models.CharField( max_length=60, blank=False, null=False, help_text= "Description of the work for a course, as it will appear on the contract. (e.g. 'Office/marking')" ) labtut = models.BooleanField( default=False, verbose_name="Lab/Tutorial?", help_text="Does this description get the %s BU bonus?" % (LAB_BONUS)) hidden = models.BooleanField(default=False) config = JSONField(null=False, blank=False, default=dict) def __str__(self): return self.description def delete(self): """Like most of our objects, we don't want to ever really delete it.""" self.hidden = True self.save()
class AccessRule(models.Model): """ This person can see this report. """ report = models.ForeignKey(Report, on_delete=models.CASCADE) person = models.ForeignKey(Person, on_delete=models.PROTECT) notify = models.BooleanField( null=False, default=False, help_text="Email this person when a report completes.") config = JSONField(null=False, blank=False, default=dict) created_at = models.DateTimeField(auto_now_add=True) def send_notification(self, run): n = NewsItem( user=self.person, source_app='reports', title="Completed Run: " + self.report.name + " : " + run.slug, url=reverse('reports:view_run', kwargs={ 'report': self.report.slug, 'run': run.slug }), content="You have a scheduled report that has completed! \n" + self.report.description) n.save()
class NonSFUFormFiller(models.Model): """ A person without an SFU account that can fill out forms. """ last_name = models.CharField(max_length=32) first_name = models.CharField(max_length=32) email_address = models.EmailField(max_length=254) config = JSONField(null=False, blank=False, default={}) # addition configuration stuff: def __unicode__(self): return u"%s, %s" % (self.last_name, self.first_name) def name(self): return u"%s %s" % (self.first_name, self.last_name) def sortname(self): return u"%s, %s" % (self.last_name, self.first_name) def initials(self): return u"%s%s" % (self.first_name[0], self.last_name[0]) def email(self): return self.email_address def delete(self, *args, **kwargs): raise NotImplementedError( "This object cannot be deleted because it is used as a foreign key." )
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',)
class ScheduleRule(models.Model): """ Run this Report at this time. """ report = models.ForeignKey(Report, on_delete=models.CASCADE) schedule_type = models.CharField(max_length=3, choices=SCHEDULE_TYPE_CHOICES, null=False, default="ONE") last_run = models.DateTimeField( null=True) # the last time this ScheduleRule was run next_run = models.DateTimeField() # the next time to run this ScheduleRule config = JSONField(null=False, blank=False, default=dict) created_at = models.DateTimeField(auto_now_add=True) def set_next_run(self): self.last_run = self.next_run if self.schedule_type == 'DAI': self.next_run = increment_day(self.next_run) if self.schedule_type == 'WEE': self.next_run = increment_week(self.next_run) if self.schedule_type == 'MON': self.next_run = increment_month(self.next_run) if self.schedule_type == 'YEA': self.next_run = increment_year(self.next_run) if self.schedule_type == 'ONE': self.next_run = None # if this doesn't get the run to past the current date, try again. if self.next_run < datetime.datetime.now(): self.set_next_run()
class Grant(models.Model): STATUS_CHOICES = ( ("A", "Active"), ("D", "Deleted"), ) title = models.CharField(max_length=64, help_text='Label for the grant within this system') slug = AutoSlugField(populate_from='title', unique_with=("unit",), null=False, editable=False) label = models.CharField(max_length=150, help_text="for identification from FAST import", db_index=True) owners = models.ManyToManyField(Person, through='GrantOwner', blank=False, help_text='Who owns/controls this grant?') project_code = models.CharField(max_length=32, db_index=True, help_text="The fund and project code, like '13-123456'") start_date = models.DateField(null=False, blank=False) expiry_date = models.DateField(null=True, blank=True) status = models.CharField(max_length=2, choices=STATUS_CHOICES, default='A') initial = models.DecimalField(verbose_name="Initial balance", max_digits=12, decimal_places=2) overhead = models.DecimalField(verbose_name="Annual overhead", max_digits=12, decimal_places=2, help_text="Annual overhead returned to Faculty budget") import_key = models.CharField(null=True, blank=True, max_length=255, help_text="e.g. 'nserc-43517b4fd422423382baab1e916e7f63'") unit = models.ForeignKey(Unit, null=False, blank=False, help_text="Unit who owns the grant", on_delete=models.PROTECT) config = JSONField(blank=True, null=True, default=dict) # addition configuration for within the grant objects = GrantManager() class Meta: unique_together = (('label', 'unit'),) ordering = ['title'] def __str__(self): return "%s" % self.title def get_absolute_url(self): return reverse("faculty:view_grant", kwargs={'unit_slug': self.unit.slug, 'grant_slug': self.slug}) def update_balance(self, balance, spent_this_month, actual, date=datetime.datetime.today()): gb = GrantBalance.objects.create( date=date, grant=self, balance=balance, actual=actual, month=spent_this_month ) return gb def get_owners_display(self, units): """ HTML display of the owners list (some logic required since we want to link to faculty profiles if exists && permitted) """ from django.utils.html import conditional_escape as escape from django.utils.safestring import mark_safe res = [] for o in self.grantowner_set.all(): p = o.person if Role.objects.filter(unit__in=units, role='FAC', person=p).exists(): url = reverse('faculty:summary', kwargs={'userid': p.userid_or_emplid()}) res.append('<a href="%s">%s</a>' %(escape(url), escape(o.person.name()))) else: res.append(escape(o.person.name())) return mark_safe(', '.join(res))
class AdvisorNote(models.Model): """ An academic advisor's note about a student. """ text = models.TextField(blank=False, null=False, verbose_name="Contents", help_text='Note about a student') student = models.ForeignKey(Person, related_name='student', on_delete=models.PROTECT, help_text='The student that the note is about', editable=False, null=True) nonstudent = models.ForeignKey(NonStudent, editable=False, null=True, on_delete=models.PROTECT, help_text='The non-student that the note is about') advisor = models.ForeignKey(Person, related_name='advisor', on_delete=models.PROTECT, help_text='The advisor that created the note', editable=False) created_at = models.DateTimeField(default=datetime.datetime.now) file_attachment = models.FileField(storage=UploadedFileStorage, null=True, upload_to=attachment_upload_to, blank=True, max_length=500) file_mediatype = models.CharField(null=True, blank=True, max_length=200, editable=False) unit = models.ForeignKey(Unit, help_text='The academic unit that owns this note', on_delete=models.PROTECT) # Set this flag if the note is no longer to be accessible. hidden = models.BooleanField(null=False, db_index=True, default=False) emailed = models.BooleanField(null=False, default=False) config = JSONField(null=False, blank=False, default=dict) # addition configuration stuff: # 'markup': markup language used in reminder content: see courselib/markup.py # 'math': page uses MathJax? (boolean) markup = config_property('markup', 'plain') math = config_property('math', False) def __str__(self): return str(self.student) + "@" + str(self.created_at) def delete(self, *args, **kwargs): raise NotImplementedError("This object cannot be deleted, set the hidden flag instead.") class Meta: ordering = ['student', 'created_at'] def save(self, *args, **kwargs): # make sure one of student and nonstudent is there if not self.student and not self.nonstudent: raise ValueError("AdvisorNote must have either student or non-student specified.") super(AdvisorNote, self).save(*args, **kwargs) def attachment_filename(self): """ Return the filename only (no path) for the attachment. """ _, filename = os.path.split(self.file_attachment.name) return filename def unique_tuple(self): return ( make_slug(self.text[0:100]), self.created_at.isoformat() ) def __hash__(self): return self.unique_tuple().__hash__() def html_content(self): return markup_to_html(self.text, self.markup, restricted=False)
class OutreachEvent(models.Model): """ An outreach event. These are different than our other events, as they are to be attended by non-users of the system. """ title = models.CharField(max_length=60, null=False, blank=False) start_date = models.DateTimeField('Start Date and Time', default=timezone_today, help_text='Event start date and time. Use 24h format for the time if needed.') end_date = models.DateTimeField('End Date and Time', blank=False, null=False, help_text='Event end date and time') location = models.CharField(max_length=400, blank=True, null=True) description = models.CharField(max_length=800, blank=True, null=True) score = models.DecimalField(max_digits=2, decimal_places=0, max_length=2, null=True, blank=True, help_text='The score according to the event score matrix') unit = models.ForeignKey(Unit, blank=False, null=False) resources = models.CharField(max_length=400, blank=True, null=True, help_text="Resources needed for this event.") cost = models.DecimalField(blank=True, null=True, max_digits=8, decimal_places=2, help_text="Cost of this event") hidden = models.BooleanField(default=False, null=False, blank=False, editable=False) notes = models.CharField(max_length=400, blank=True, null=True, help_text='Special notes to registrants. These ' '*will* be displayed on the registration ' 'forms.') email = models.EmailField('Contact e-mail', null=True, blank=True, help_text='Contact email. Address that will be given to registrants on the registration ' 'success page in case they have any questions/problems.') closed = models.BooleanField('Close Registration', default=False, help_text='If this box is checked, people will not be able to register for this ' 'event even if it is still current.') config = JSONField(null=False, blank=False, default=dict) # 'extra_questions': additional questions to ask registrants extra_questions = config_property('extra_questions', []) objects = EventQuerySet.as_manager() def autoslug(self): return make_slug(self.unit.slug + '-' + self.title + '-' + str(self.start_date.date())) slug = AutoSlugField(populate_from='autoslug', null=False, editable=False, unique=True) def __unicode__(self): return u"%s - %s - %s" % (self.title, self.unit.label, self.start_date) def delete(self): """Like most of our objects, we don't want to ever really delete it.""" self.hidden = True self.save() def current(self): """ Find out if an event is still current. Otherwise, we shouldn't be able to register for it. """ return self.start_date > timezone_today() or self.end_date >= timezone_today() # TODO add copy method to copy from one event to another def registration_count(self): return OutreachEventRegistration.objects.attended_event(self).count()
class NonStudent(models.Model): """ For a person (prospective student) who isn't part of the university """ last_name = models.CharField(max_length=32) first_name = models.CharField(max_length=32) middle_name = models.CharField(max_length=32, null=True, blank=True) pref_first_name = models.CharField(max_length=32, null=True, blank=True) email_address = models.EmailField( null=True, blank=True, help_text="Needed only if you want to copy the student on notes") high_school = models.CharField(max_length=32, null=True, blank=True) college = models.CharField(max_length=32, null=True, blank=True) start_year = models.IntegerField( null=True, blank=True, help_text="The predicted/potential start year") notes = models.TextField( help_text="Any general information for the student", blank=True) unit = models.ForeignKey( Unit, help_text='The potential academic unit for the student', null=True, blank=True, on_delete=models.PROTECT) def autoslug(self): return make_slug(self.first_name + ' ' + self.last_name) slug = AutoSlugField(populate_from='autoslug', null=False, editable=False, unique=True) config = JSONField(null=False, blank=False, default=dict) # addition configuration stuff: def __str__(self): return "%s, %s" % (self.last_name, self.first_name) def name(self): return "%s %s" % (self.first_name, self.last_name) def sortname(self): return "%s, %s" % (self.last_name, self.first_name) def search_label_value(self): return "%s (Prospective %s)" % (self.name(), self.start_year) def unique_tuple(self): return (self.first_name, self.middle_name, self.last_name, self.pref_first_name, self.high_school) def __hash__(self): return self.unique_tuple().__hash__() def email(self): return self.email_address
class QuestionAnswer(models.Model): class AnswerStatusManager(models.Manager): def get_queryset(self): return super().get_queryset().select_related('question', 'question_version').filter(question__status='V') question = models.ForeignKey(Question, on_delete=models.PROTECT) question_version = models.ForeignKey(QuestionVersion, on_delete=models.PROTECT) # Technically .question is redundant with .question_version.question, but keeping it for convenience # and the unique_together. student = models.ForeignKey(Member, on_delete=models.PROTECT) modified_at = models.DateTimeField(default=datetime.datetime.now, null=False, blank=False) # format of .answer determined by the corresponding QuestionHelper answer = JSONField(null=False, blank=False, default=dict) # .file used for file upload question types; null otherwise file = models.FileField(blank=True, null=True, storage=UploadedFileStorage, upload_to=file_upload_to, max_length=500) class Meta: unique_together = [['question_version', 'student']] objects = AnswerStatusManager() def save(self, *args, **kwargs): assert self.question_id == self.question_version.question_id # ensure denormalized field stays consistent saving_file = False if '_file' in self.answer: if self.answer['_file'] is None: # keep current .file pass elif self.answer['_file'] is False: # user requested "clear" self.file = None else: # actually a file self.file = self.answer['_file'] saving_file = True del self.answer['_file'] if saving_file: # Inject the true save path into the .answer. Requires a double .save() super().save(*args, **kwargs) fn = self.file.name self.answer['filepath'] = fn return super().save(*args, **kwargs) def get_absolute_url(self): return resolve_url('offering:quiz:view_submission', course_slug=self.question.quiz.activity.offering.slug, activity_slug=self.question.quiz.activity.slug, userid=self.student.person.userid_or_emplid()) + '#' + self.question.ident() def answer_html(self) -> SafeText: helper = self.question_version.helper() return helper.to_html(self)
class EventConfig(models.Model): """ A unit's configuration for a particular event type """ unit = models.ForeignKey(Unit, null=False, blank=False, on_delete=models.PROTECT) event_type = models.CharField(max_length=10, null=False, choices=EVENT_TYPE_CHOICES) config = JSONField(default=dict) class Meta: unique_together = ('unit', 'event_type')
class SemesterConfig(models.Model): """ A table for department-specific semester config. """ unit = models.ForeignKey(Unit, null=False, blank=False, on_delete=models.PROTECT) semester = models.ForeignKey(Semester, null=False, blank=False, on_delete=models.PROTECT) config = JSONField(null=False, blank=False, default=dict) # addition configuration stuff defaults = {'start_date': None, 'end_date': None} # 'start_date': default first day of contracts that semester, 'YYYY-MM-DD' # 'end_date': default last day of contracts that semester, 'YYYY-MM-DD' class Meta: unique_together = (('unit', 'semester'), ) @classmethod def get_config(cls, units, semester): """ Either get existing SemesterConfig or return a new one. """ configs = SemesterConfig.objects.filter( unit__in=units, semester=semester).select_related('semester') if configs: return configs[0] else: return SemesterConfig(unit=list(units)[0], semester=semester) def start_date(self): if 'start_date' in self.config: return datetime.datetime.strptime(self.config['start_date'], '%Y-%m-%d').date() else: return self.semester.start def end_date(self): if 'end_date' in self.config: return datetime.datetime.strptime(self.config['end_date'], '%Y-%m-%d').date() else: return self.semester.end def set_start_date(self, date): self.config['start_date'] = date.strftime('%Y-%m-%d') def set_end_date(self, date): self.config['end_date'] = date.strftime('%Y-%m-%d')
class UserConfig(models.Model): """ Simple class to hold user preferences. """ user = models.ForeignKey(Person, null=False, on_delete=models.PROTECT) key = models.CharField(max_length=20, db_index=True, null=False) value = JSONField(null=False, blank=False, default=dict) class Meta: unique_together = (("user", "key"),) def __str__(self): return "%s: %s='%s'" % (self.user.userid, self.key, self.value)
class AlertEmailTemplate(models.Model): """ An automatic e-mail to send. """ alerttype = models.ForeignKey(AlertType, null=False) threshold = models.IntegerField(default=0, null=False, help_text="This email will only be sent to students who have less than this many emails on file already") subject = models.CharField(max_length=50, null=False) content = models.TextField(help_text="I.e. 'This is to confirm {{title}} {{last_name}} ... '") created_at = models.DateTimeField(auto_now_add=True) created_by = models.CharField(max_length=32, null=False, help_text='Email template created by.') hidden = models.BooleanField(default=False) config = JSONField(null=False, blank=False, default={})
class GrantBalance(models.Model): date = models.DateField(default=datetime.date.today) grant = models.ForeignKey(Grant, null=False, blank=False, on_delete=models.PROTECT) balance = models.DecimalField(verbose_name="grant balance", max_digits=12, decimal_places=2) actual = models.DecimalField(verbose_name="YTD actual", max_digits=12, decimal_places=2) month = models.DecimalField(verbose_name="current month", max_digits=12, decimal_places=2) config = JSONField(blank=True, null=True, default=dict) # addition configuration within the memo def __str__(self): return "%s balance as of %s" % (self.grant, self.date) class Meta: ordering = ['date']
class Event(models.Model): contact = models.ForeignKey(Contact, null=False, blank=False, on_delete=models.PROTECT) event_type = models.CharField(max_length=25, choices=EVENT_CHOICES) timestamp = models.DateTimeField(default=datetime.datetime.now, editable=False) last_modified = models.DateTimeField(null=True, blank=True, editable=False) last_modified_by = models.ForeignKey(Person, null=True, blank=True, on_delete=models.PROTECT) deleted = models.BooleanField(default=False) config = JSONField(default=dict) slug = AutoSlugField(populate_from='slug_string', unique_with=('contact', ), slugify=make_slug, null=False, editable=False) objects = IgnoreEventDeleted() @property def slug_string(self): return '%s-%s' % (self.timestamp.year, self.event_type) def save(self, call_from_handler=False, editor=None, *args, **kwargs): assert call_from_handler, "A contact event must be saved through the handler." self.last_modified = datetime.datetime.now() self.last_modified_by = editor return super(Event, self).save(*args, **kwargs) def get_handler(self): # Create and return a handler for ourselves. If we already created it, use the same one again. if not hasattr(self, 'handler_cache'): self.handler_cache = EVENT_TYPES.get(self.event_type, None)(self) return self.handler_cache def get_handler_name(self): return self.get_handler().name def get_config_value(self, field): if field in self.config: return self.config.get(field) else: return None def is_text_based(self): return self.get_handler().text_content
class ActivityComponent(models.Model): """ Markable Component of a numeric activity """ numeric_activity = models.ForeignKey(NumericActivity, null=False, on_delete=models.PROTECT) max_mark = models.DecimalField(max_digits=8, decimal_places=2, null=False) title = models.CharField(max_length=30, null=False) description = models.TextField(max_length=COMMENT_LENGTH, null=True, blank=True) position = models.IntegerField(null=True, default=0, blank=True) # set this flag if it is deleted by the user deleted = models.BooleanField(null=False, db_index=True, default=False) def autoslug(self): return make_slug(self.title) slug = AutoSlugField(populate_from='autoslug', null=False, editable=False, unique_with='numeric_activity') config = JSONField(null=False, blank=False, default=dict) # .config['quiz-question-id']: a Question.id if this ActivityComponent was generated by the quizzes module def __str__(self): return self.title def delete(self, *args, **kwargs): raise NotImplementedError( "This object cannot be deleted because it is used as a foreign key." ) class Meta: verbose_name_plural = "Activity Marking Components" ordering = ['numeric_activity', 'deleted', 'position'] def save(self, *args, **kwargs): self.slug = None # regerate slug so import format stays in sync if self.position == 0: others = ActivityComponent.objects.filter( numeric_activity=self.numeric_activity).exclude(pk=self.pk) maxpos = others.aggregate(models.Max('position'))['position__max'] if maxpos: self.position = maxpos + 1 else: self.position = 1 super(ActivityComponent, self).save(*args, **kwargs)
class PagePermission(models.Model): """ An additional person who has permission to view pages for this offering """ offering = models.ForeignKey(CourseOffering) person = models.ForeignKey(Person) role = models.CharField(max_length=4, choices=PERMISSION_ACL_CHOICES, default="STUD", help_text="What level of access should this person have for the course?") config = JSONField(null=False, blank=False, default={}) # addition configuration stuff: defaults = {} class Meta: unique_together = (('offering', 'person'), )