Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,5 @@ db.sqlite3
*/migrations/*
static/**/*

data/catalogs/**
data/catalogs/**
media/**
6 changes: 6 additions & 0 deletions catalog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.db import models
from common.models import Student
from catalog_parse.utils.catalog_constants import *
from syllabus.models import Syllabus

class Attribute:
"""
Expand Down Expand Up @@ -120,6 +121,7 @@ class CourseFields:
parent = "parent"
children = "children"
is_half_class = "is_half_class"
syllabi = "syllabi"

# Tools to convert from strings to Course field values
def string_converter(value):
Expand Down Expand Up @@ -427,6 +429,10 @@ def to_json_object(self, full=True):
if self.out_of_class_hours != 0.0:
data[CourseFields.out_of_class_hours] = self.out_of_class_hours

syllabi = Syllabus.objects.filter(subject_id=self.subject_id)
if syllabi.count() > 0:
data[CourseFields.syllabi] = [s.to_json_object() for s in syllabi]

return data


Expand Down
6 changes: 5 additions & 1 deletion common/templates/common/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
<head>
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Inconsolata" rel="stylesheet">
<link href="{% static "requirements/css/nav.css" %}" rel="stylesheet" />
<link href="{% static "common/css/nav.css" %}" rel="stylesheet" />
<link href="{% static "requirements/css/req_preview.css" %}" rel="stylesheet" />
<link href="{% static "requirements/css/editor.css" %}" rel="stylesheet" />
<link href="{% static "syllabus/css/syllabus.css" %}" rel="stylesheet" />
<link type="text/css" rel="stylesheet" href="{% static "common/css/materialize.min.css" %}" media="screen,projection"/>
<link type="text/css" rel="stylesheet" href="{% static "common/css/shared.css" %}" media="screen,projection"/>
<!--Import Google Icon Font-->
Expand Down Expand Up @@ -39,6 +40,7 @@
<ul id="dropdown3" class="dropdown-content">
<li><a class="red-text text-darken-3" href="/admin">Django Admin</a></li>
<li><a class="red-text text-darken-3" href="/requirements/review/">Edit Requests</a></li>
<li><a class="red-text text-darken-3" href="/syllabus/review/">Syllabus Submissions</a></li>
<li><a class="red-text text-darken-3" href="/courseupdater/update_catalog/">Catalog Update</a></li>
<li><a class="red-text text-darken-3" href="/courseupdater/corrections/">Catalog Corrections</a></li>
<li><a class="red-text text-darken-3" href="/courseupdater/download_data/">Download Catalog Data</a></li>
Expand All @@ -52,6 +54,7 @@
<a href="/" class="brand-logo"><img class="logo-img hoverable" src="https://fireroad.mit.edu/catalogs/fireroad_logo.png" width="40" height="40"/>FireRoad</a>
<ul class="right hide-on-med-and-down">
<li><a href="/requirements">Requirements Editor</a></li>
<li><a href="/syllabus">Class Syllabi</a></li>
<li><a href="/reference">API</a></li>
<li><a class="dropdown-trigger" href="#!" data-target="dropdown1">GitHub</a></li>
<li><a href="mailto:base12apps@gmail.com">Contact</a></li>
Expand Down Expand Up @@ -87,6 +90,7 @@

<ul class="sidenav" id="mobile-nav">
<li><a href="/requirements">Requirements Editor</a></li>
<li><a href="/syllabus">Class Syllabi</a></li>
<li><a href="/reference">FireRoad API</a></li>
<li><a class="dropdown-trigger" href="#!" data-target="dropdown2">GitHub<i class="material-icons right">arrow_drop_down</i></a></li>
<li><a href="mailto:base12apps@gmail.com">Contact</a></li>
Expand Down
7 changes: 6 additions & 1 deletion fireroad/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
'requirements',
'courseupdater',
'catalog',
'analytics'
'analytics',
'syllabus'
]

MIDDLEWARE_CLASSES = [
Expand Down Expand Up @@ -175,3 +176,7 @@
}
},
}

MEDIA_URL = '/media/'

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
8 changes: 4 additions & 4 deletions fireroad/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf.urls import url, include
from django.conf.urls.static import static
from django.contrib.admin.views.decorators import staff_member_required
from django.views.generic.base import RedirectView
from django.contrib import admin
Expand All @@ -31,8 +32,9 @@
url(r'sync/', include('sync.urls')),
url(r'analytics/', include('analytics.urls')),
url(r'requirements/', include('requirements.urls')),
url(r'', include('common.urls')),
]
url(r'syllabus/', include('syllabus.urls')),
url(r'', include('common.urls'))
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

# Redirect to the appropriate login page if one is specified in the settings module
if settings.LOGIN_URL:
Expand All @@ -47,5 +49,3 @@
urlpatterns.insert(0, url(r'^login/$', RedirectView.as_view(url=settings.LOGIN_URL,
permanent=True,
query_string=True)))


4 changes: 1 addition & 3 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Installing dependencies
pip install django==1.11.15 pandas nltk==3.4 lxml scipy scikit-learn requests pyjwt==1.6.4
pip install django==1.11.15 pandas nltk==3.4 lxml scipy scikit-learn requests pyjwt==1.6.4 python-magic==0.4.25
echo
echo

Expand Down Expand Up @@ -46,5 +46,3 @@ else
echo -e "${RED}Unrecognized symbol $keepbackend; quitting ${NC}"
exit 1
fi


Empty file added syllabus/__init__.py
Empty file.
8 changes: 8 additions & 0 deletions syllabus/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.contrib import admin

# Register your models here.
from .models import *

admin.site.register(SyllabusSubmission)
admin.site.register(SyllabusDeployment)
admin.site.register(Syllabus)
167 changes: 167 additions & 0 deletions syllabus/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
from django.db import models
from django import forms
from django.core.files.storage import FileSystemStorage
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible
from django.template.defaultfilters import filesizeformat

import os
import magic
import shutil
from uuid import uuid4

SEMESTER_CHOICES = [
('Fall', 'fall'),
('IAP', 'iap'),
('Spring', 'spring'),
('Summer', 'summer')
]

class DeploySyllabusForm(forms.Form):
email_address = forms.CharField(label='Editor Email', max_length=100, widget=forms.TextInput(attrs={'class': 'input-field', 'placeholder': 'Email address'}))
summary = forms.CharField(label='Summary of Uploads', max_length=2000, widget=forms.TextInput(attrs={'class': 'input-field', 'placeholder': 'Summary of uploads...'}))

class SyllabusDeployment(models.Model):
author = models.CharField(max_length=100)
timestamp = models.DateTimeField(auto_now_add=True)
summary = models.CharField(max_length=2000)
date_executed = models.DateTimeField(null=True)

def __unicode__(self):
return u"{}Deployment by {} at {} ({} edits): {}".format("(Pending) " if self.date_executed is None else "", self.author, self.timestamp, self.syllabus_submissions.count(), self.summary)

# Source: https://stackoverflow.com/a/27916582
@deconstructible
class FileValidator(object):
error_messages = {
'max_size': ("File size must not be greater than %(max_size)s."
" Your file size is %(size)s."),
'min_size': ("File size must not be less than %(min_size)s. "
"Your file size is %(size)s."),
'content_type': "Files of type %(content_type)s are not supported.",
}

def __init__(self, max_size=None, min_size=None, content_types=()):
self.max_size = max_size
self.min_size = min_size
self.content_types = content_types

def __call__(self, data):
if self.max_size is not None and data.size > self.max_size:
params = {
'max_size': filesizeformat(self.max_size),
'size': filesizeformat(data.size),
}
raise ValidationError(self.error_messages['max_size'],
'max_size', params)

if self.min_size is not None and data.size < self.min_size:
params = {
'min_size': filesizeformat(self.min_size),
'size': filesizeformat(data.size)
}
raise ValidationError(self.error_messages['min_size'],
'min_size', params)

if self.content_types:
content_type = magic.from_buffer(data.read(), mime=True)
data.seek(0)

if content_type not in self.content_types:
params = { 'content_type': content_type }
raise ValidationError(self.error_messages['content_type'],
'content_type', params)

def __eq__(self, other):
return (
isinstance(other, FileValidator) and
self.max_size == other.max_size and
self.min_size == other.min_size and
self.content_types == other.content_types
)

def validate_subject_id(subject_id):
from catalog.models import Course
if Course.objects.filter(subject_id=subject_id).count() == 0:
raise ValidationError(u'Must provide a valid subject ID')

class SyllabusForm(forms.Form):
is_committing = forms.BooleanField(label='commit')
email_address = forms.CharField(label='Email address', max_length=100, widget=forms.TextInput(attrs={'class': 'input-field', 'placeholder': 'Email address'}))
semester = forms.ChoiceField(label='Semester', choices=SEMESTER_CHOICES, widget=forms.TextInput(attrs={'class': 'input-field', 'placeholder': 'Semester (e.g. Fall)'}))
year = forms.CharField(label='Year', max_length=4, widget=forms.TextInput(attrs={'class': 'input-field', 'placeholder': 'Year (e.g. 2022)'}))
subject_id = forms.CharField(label='Course Number', max_length=20, widget=forms.TextInput(attrs={'class': 'input-field', 'placeholder': 'Course number (e.g. 18.01)'}), validators=[validate_subject_id])
file = forms.FileField(label='Syllabus File', widget=forms.ClearableFileInput(attrs={'class': 'input-field', 'accept': 'application/pdf'}), validators=[FileValidator(max_size=1024*1024*5, content_types=('application/pdf',))])

def rename_file(inst, filename):
upload_to = 'syllabus/'
_, ext = os.path.splitext(filename)
new_name = 'syllabus_' + inst.subject_id.replace('.', '_') + '_' + inst.semester + '_' + inst.year + ext

fss = FileSystemStorage()
filepath = fss.get_available_name(os.path.join(upload_to, new_name))
return filepath

class SyllabusSubmission(models.Model):
email_address = models.CharField(max_length=100)
semester = models.CharField(max_length=6, choices=SEMESTER_CHOICES)
year = models.CharField(max_length=4)
subject_id = models.CharField(max_length=20)
file = models.FileField(upload_to=rename_file)
timestamp = models.DateTimeField(auto_now_add=True)
resolved = models.BooleanField(default=False)
committed = models.BooleanField(default=False)
deployment = models.ForeignKey(SyllabusDeployment, null=True, on_delete=models.SET_NULL, related_name='syllabus_submissions')

def __unicode__(self):
return u"{}{}{} {} {} syllabus by {}".format("(Resolved) " if self.resolved else "", "(Committed) " if self.committed else "", self.subject_id, self.semester, self.year, self.email_address)

def update_file_name(self, copy=True):
current_filepath = self.file.path
current_filename = os.path.basename(current_filepath)
_, ext = os.path.splitext(current_filepath)
upload_to = 'syllabus/'

proper_file_prefix = 'syllabus_' + self.subject_id.replace('.', '_') + '_' + self.semester + '_' + self.year

fss = FileSystemStorage()
filepath = fss.get_available_name(os.path.join(upload_to, proper_file_prefix + ext))
filepath = os.path.join(settings.MEDIA_ROOT, filepath)

if current_filename.startswith(proper_file_prefix):
if copy:
shutil.copyfile(current_filepath, filepath)
self.file = fss.open(filepath)
self.save()
else:
if copy:
shutil.copyfile(current_filepath, filepath)
self.file = fss.open(filepath)
self.save()
else:
shutil.move(current_filepath, filepath)
self.file = fss.open(filepath)
self.save()

def remove_file(self):
os.remove(self.file.path)

class Syllabus(models.Model):
semester = models.CharField(max_length=6, choices=SEMESTER_CHOICES)
year = models.CharField(max_length=4)
subject_id = models.CharField(max_length=20)
file = models.FileField()
timestamp = models.DateTimeField(null=False)

def __unicode__(self):
return u"{} {} {} syllabus".format(self.subject_id, self.semester, self.year)

def to_json_object(self):
return {
'semester': self.semester,
'year': self.year,
'subject_id': self.subject_id,
'file_url': settings.MY_BASE_URL + self.file.url,
'id': self.pk
}
54 changes: 54 additions & 0 deletions syllabus/static/syllabus/css/syllabus.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@media (min-width: 767px) {
.syllabus-head {
margin-top: 12px;
}
.content {
margin-top: -64px;
padding-top: 64px;
}
.content-full-screen {
margin-top: -64px;
padding-top: 64px;
}
}

.syllabus-sub-list-row {
display: flex;
flex-direction: row;
overflow: hidden;
}

.syllabus-sub-list-left {
display: flex;
flex-direction: column;
width: 50%;
margin-right: 12px;
}

.syllabus-sub-list-right {
display: flex;
flex-direction: column;
width: 50%;
}

.syllabus-sub-list {
overflow-y: scroll !important;
}

.syllabus-sub-list > .collection-item:hover {
background-color: #eeeeee;
-webkit-transition: background-color 250ms linear;
-ms-transition: background-color 2500ms linear;
transition: background-color 250ms linear;
}


.searchbar.input-field input[type=search] {
padding-left: 30px;
width: calc(100% - 30px);
}

.searchbar.input-field input[type=search]:focus:not(.browser-default) {
background-color: inherit;
border-bottom: 1px solid rgb(224, 20, 20);
}
Loading