Skip to content

Commit

Permalink
Merge 9e4128a into 26faa58
Browse files Browse the repository at this point in the history
  • Loading branch information
Sumukh committed Jan 28, 2016
2 parents 26faa58 + 9e4128a commit f6c0c38
Show file tree
Hide file tree
Showing 20 changed files with 283 additions and 128 deletions.
5 changes: 3 additions & 2 deletions manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
from datetime import datetime, timedelta
import json

from flask.ext.script import Manager, Server
from flask.ext.script.commands import ShowUrls, Clean
Expand Down Expand Up @@ -36,8 +37,8 @@ def make_backup(user, assign, time, messages, submit=True):
backup = Backup(client_time=time,
submitter=user,
assignment=assign, submit=submit)
messages = [Message(kind=k, backup=backup,
contents=m) for k, m in messages.items()]
messages = [Message(kind=k, backup=backup.id,
raw_contents=json.dumps(m)) for k, m in messages.items()]
backup.messages = messages
db.session.add_all(messages)
db.session.add(backup)
Expand Down
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ Flask==0.10.1
# Caching
redis==2.10.5

# Job Queue
rq==0.5.6
rq-dashboard==0.3.5

# Forms
WTForms>=2.0,<3.0

Expand All @@ -18,6 +22,7 @@ Flask-Migrate==1.7.0
Flask-OAuthlib==0.9.2
Flask-RESTful==0.3.5
Flask-Testing==0.4.2
Flask-RQ==0.2

# Google
google-api-python-client==1.4.2
Expand Down
4 changes: 4 additions & 0 deletions server/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#! ../env/bin/python

from flask import Flask
from flask.ext.rq import RQ
from webassets.loaders import PythonLoader as PythonAssetsLoader

from server import assets
Expand Down Expand Up @@ -34,6 +35,9 @@ def create_app(object_name):

app.config.from_object(object_name)

# initialize redis task queues
RQ(app)

# initialize the cache
cache.init_app(app)

Expand Down
5 changes: 1 addition & 4 deletions server/controllers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ def name_to_assign_id(name):
if assgn:
return assgn.id


class Resource(restful.Resource):
version = 'v3'
method_decorators = [authenticate, check_version]
Expand Down Expand Up @@ -165,7 +164,6 @@ def get(self):
# Fewer methods/APIs as V1 since the frontend will not use the API
# TODO Permsisions for API actions


def make_backup(user, assignment_id, messages, submit):
"""
Create backup with message objects.
Expand All @@ -189,15 +187,14 @@ def make_backup(user, assignment_id, messages, submit):
backup = models.Backup(client_time=client_time, submitter=user,
assignment_id=assignment_id, submit=submit)
messages = [models.Message(kind=k, backup=backup,
contents=m) for k, m in messages.items()]
raw_contents=json.dumps(m)) for k, m in messages.items()]
backup.messages = messages
models.db.session.add_all(messages)
models.db.session.add(backup)
models.db.session.commit()
return backup



class APISchema():
""" APISchema describes the input and output formats for
resources. The parser deals with arguments to the API.
Expand Down
2 changes: 2 additions & 0 deletions server/controllers/main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from flask import Blueprint, render_template
from server.extensions import cache
from flask.ext.rq import get_queue

main = Blueprint('main', __name__)


@cache.cached(5000)
@main.route('/')
def home():
Expand Down
161 changes: 97 additions & 64 deletions server/controllers/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,33 @@
from server.constants import VALID_ROLES, STAFF_ROLES, STUDENT_ROLE
from server.extensions import cache
from server.models import User, Course, Assignment, Group, Backup, db

from server.utils import assignment_by_name, course_by_name

student = Blueprint('student', __name__)

def get_course(func):
""" A decorator for routes to ensure that user is enrolled in the course.
A user is enrolled if they are participating in the course
with any role. Gets the course offering from the route's COURSE argument.
Then binds the actual course object to the course keyword argument.
Usage:
@get_course # Get the course from the cid param of the routes
def my_route(course): return course.id
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
course = course_by_name(kwargs['course'])
if not course:
print("Course not found", kwargs['course'])
return abort(404)
kwargs['course'] = course
enrolled = current_user.is_enrolled(course.id)
if not enrolled and not current_user.is_admin:
flash("You have not been added to this course on OK", "warning")
return func(*args, **kwargs)
return wrapper


@student.route("/")
@login_required
Expand All @@ -32,30 +55,10 @@ def index(auto_redir=True):
return render_template('student/courses/index.html', **courses)


def is_enrolled(func):
""" A decorator for routes to ensure that user is enrolled in
the course. Gets the course id from the named arg cid of the route.
A user is enrolled if they are participating in the course with any role.
Usage:
@is_enrolled # Get the course id from the cid param of the routes
def my_route(cid): ...
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
course_id = kwargs['cid']
enrolled = current_user.is_enrolled(course_id)
if not enrolled and not current_user.is_admin:
flash("You have not been added to this course on OK", "warning")
return func(*args, **kwargs)
return wrapper


@student.route("/course/<int:cid>")
@student.route("/<path:course>/")
@login_required
@is_enrolled
def course(cid):
course = Course.query.get(cid)
@get_course
def course(course):
def assignment_info(assignment):
# TODO does this make O(n) db queries?
# TODO need group info too
Expand All @@ -71,48 +74,78 @@ def assignment_info(assignment):
return render_template('student/course/index.html', course=course,
**assignments)

@student.route("/course/<int:cid>/assignment/<int:aid>")

@student.route("/<path:course>/assignments/")
@login_required
@is_enrolled
def assignment(cid, aid):
assgn = Assignment.query.filter_by(id=aid, course_id=cid).one_or_none()
if assgn:
course = assgn.course
user_ids = assgn.active_user_ids(current_user.id)
backups = assgn.backups(user_ids).limit(5).all()
subms = assgn.submissions(user_ids).limit(5).all()
final_submission = assgn.final_submission(user_ids)
flagged = final_submission and final_submission.flagged
return render_template('student/assignment/index.html', course=course,
assignment=assgn, backups=backups, subms=subms, flagged=flagged)
else:
# flash("That assignment does not exist", "warning")
abort(404)
def assignments(course):
return redirect(url_for(".course", course=course))

# CLEANUP : Really long route, used variable to keep lines under 80 chars.
ASSIGNMENT_DETAIL = "/<path:course>/assignments/<string:assign>/"

@student.route(ASSIGNMENT_DETAIL)
@login_required
@get_course
def assignment(course, assign):
assign = assignment_by_name(assign, course.offering)
if not assign:
return abort(404)
user_ids = assign.active_user_ids(current_user.id)
backups = assign.backups(user_ids).limit(5).all()
subms = assign.submissions(user_ids).limit(5).all()
final_submission = assign.final_submission(user_ids)
flagged = final_submission and final_submission.flagged
return render_template('student/assignment/index.html', course=course,
assignment=assign, backups=backups, subms=subms, flagged=flagged)

@student.route("/course/<int:cid>/assignment/<int:aid>/<int:bid>")
# TODO : Consolidate subm/backup list into one route? So many decorators ...
@student.route(ASSIGNMENT_DETAIL + "backups/", defaults={'submit': False})
@student.route(ASSIGNMENT_DETAIL + "submissions/", defaults={'submit': True})
@login_required
@is_enrolled
def code(cid, aid, bid):
assgn = Assignment.query.filter_by(id=aid, course_id=cid).one_or_none()
if assgn:
course = assgn.course
user_ids = assgn.active_user_ids(current_user.id)
backup = Backup.query.get(bid)
if backup and backup.can_view(current_user, user_ids, course):
submitter = User.query.get(backup.submitter_id)
file_contents = [m for m in backup.messages if
m.kind == "file_contents"]
if file_contents:
files = file_contents[0].contents
return render_template('student/assignment/code.html', course=course,
assignment=assgn, backup=backup, submitter=submitter,
files=files)
else:
flash("That code submission doesn't contain any code")
@get_course
def list_backups(course, assign, submit):
assign = assignment_by_name(assign, course.offering)
if not assign:
return abort(404)
page = request.args.get('page', 1, type=int)
user_ids = assign.active_user_ids(current_user.id)

final_submission = assign.final_submission(user_ids)
flagged = final_submission and final_submission.flagged

if submit :
# Submissions should take a flag for backups
subms = assign.submissions(user_ids).paginate(page=page, per_page=10)
return render_template('student/assignment/list.html', course=course,
assignment=assign, subms=subms, flagged=flagged)

backups = assign.backups(user_ids).paginate(page=page, per_page=10)
return render_template('student/assignment/list.html', course=course,
assignment=assign, backups=backups, flagged=flagged)


@student.route(ASSIGNMENT_DETAIL + "code/<int:bid>/")
@login_required
@get_course
def code(course, assign, bid):
assign = assignment_by_name(assign, course.offering)
if not assign:
return abort(404)
user_ids = assign.active_user_ids(current_user.id)
backup = Backup.query.get(bid)
if backup and backup.can_view(current_user, user_ids, course):
submitter = User.query.get(backup.submitter_id)
file_contents = [m for m in backup.messages if
m.kind == "file_contents"]
files = {}
if file_contents:
files = file_contents[0].contents
else:
flash("That code doesn't exist (or you don't have permission)", "danger")
abort(403)
else:
flash("That assignment does not exist", "danger")
flash("That code submission doesn't contain any code")

abort(404)
return render_template('student/assignment/code.html', course=course,
assignment=assign, backup=backup, submitter=submitter,
files=files)
else:
flash("File doesn't exist (or you don't have permission)", "danger")
abort(403)
29 changes: 18 additions & 11 deletions server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ def wrapper(*args, **kwds):
raise
return wrapper

class DictMixin(object):

def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}


class TimestampMixin(object):
created = db.Column(db.DateTime, server_default=db.func.now())

Expand Down Expand Up @@ -71,7 +77,7 @@ def lookup(email):
return User.query.filter_by(email=email).one_or_none()


class Course(db.Model, TimestampMixin):
class Course(db.Model, TimestampMixin, DictMixin):
id = db.Column(db.Integer(), primary_key=True)
offering = db.Column(db.String(), unique=True)
# offering - E.g., 'cal/cs61a/fa14
Expand All @@ -90,7 +96,7 @@ def is_enrolled(self, user):
).count() > 0


class Assignment(db.Model, TimestampMixin):
class Assignment(db.Model, TimestampMixin, DictMixin):
"""Assignments are particular to courses and have unique names.
name - cal/cs61a/fa14/proj1
display_name - Hog
Expand Down Expand Up @@ -163,6 +169,13 @@ def final_submission(self, user_ids):
Backup.submit == True
).order_by(Backup.flagged.desc(), Backup.created.desc()).first()

def offering_name(self):
""" Returns the assignment name without the course offering.
"""
return self.name.replace(self.course.offering + '/', '')



class Enrollment(db.Model, TimestampMixin):
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.ForeignKey("user.id"), index=True, nullable=False)
Expand Down Expand Up @@ -237,25 +250,22 @@ def create(cid, usr_ids=[], role=STUDENT_ROLE):
db.session.add_all(new_records)


class Message(db.Model, TimestampMixin):
class Message(db.Model, TimestampMixin, DictMixin):
id = db.Column(db.Integer(), primary_key=True)
backup = db.Column(db.ForeignKey("backup.id"), index=True)
raw_contents = db.Column(db.String())
kind = db.Column(db.String(), index=True)

@hybrid_property
def contents(self):
return json.dumps(str(self.raw_contents))
return json.loads(str(self.raw_contents))

@contents.setter
def contents(self, value):
self.raw_contents = str(json.dumps(value))

def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}


class Backup(db.Model, TimestampMixin):
class Backup(db.Model, TimestampMixin, DictMixin):
id = db.Column(db.Integer(), primary_key=True)
messages = db.relationship("Message")
scores = db.relationship("Score")
Expand All @@ -276,9 +286,6 @@ class Backup(db.Model, TimestampMixin):
db.Index('idx_submittedBacks', 'assignment', 'submit')
db.Index('idx_flaggedBacks', 'assignment', 'flagged')

def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}

def can_view(self, user, member_ids, course):
if user.is_admin:
return True
Expand Down
2 changes: 2 additions & 0 deletions server/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class Config:
'consumer_key': os.getenv('GOOGLE_ID', ''),
'consumer_secret': os.getenv('GOOGLE_SECRET', '')
}
TESTING = False
CACHE_KEY_PREFIX = 'ok-cache'

class LocalConfig(Config):
DEBUG = True
Expand Down
3 changes: 3 additions & 0 deletions server/settings/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ class DevConfig(LocalConfig):
ENV = 'dev'
DEBUG_TB_INTERCEPT_REDIRECTS = False
SQLALCHEMY_DATABASE_URI = 'sqlite:///../development.db'
RQ_LOW_URL = "redis://localhost:6379/1"

CACHE_REDIS_URL = "redis://localhost:6379/0"
CACHE_TYPE = 'simple'

ASSETS_DEBUG = True
2 changes: 2 additions & 0 deletions server/settings/prod.py.sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ class ProdConfig(Config):
SECRET_KEY = 'samplekey'
SQLALCHEMY_DATABASE_URI = 'postgresql://user:@localhost:5432/okprod'
CACHE_TYPE = 'simple'
RQ_LOW_URL = "redis://localhost:6379/1"
CACHE_REDIS_URL = "redis://localhost:6379/0"

0 comments on commit f6c0c38

Please sign in to comment.