Skip to content

Commit

Permalink
CHG split usage visuals by course
Browse files Browse the repository at this point in the history
  • Loading branch information
wabscale committed Sep 19, 2021
1 parent 5ff4e29 commit a2128b6
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 54 deletions.
26 changes: 26 additions & 0 deletions api/anubis/lms/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,4 +437,30 @@ def get_user_permissions(user: User) -> Dict[str, Any]:
}


@cache.memoize(timeout=60, unless=is_debug, source_check=True)
def get_courses_with_visuals() -> List[Dict[str, Any]]:
"""
Get a list of the course data for courses with
usage visuals enabled.
:return: [
Course.data,
...
]
"""

# Query for courses with display_visuals on
query = Course.query.filter(
Course.display_visuals == True
)

# Get the list of courses
courses: List[Course] = query.all()

# Break down course db objects into dictionary
return [
course.data for course in courses
]


course_context: Course = LocalProxy(get_course_context)
7 changes: 1 addition & 6 deletions api/anubis/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class Course(db.Model):
theia_default_options = db.Column(MutableJson, default=lambda: copy.deepcopy(THEIA_DEFAULT_OPTIONS))
github_org = db.Column(db.TEXT, default='os3224')
join_code = db.Column(db.String(256), unique=True)
display_visuals = db.Column(db.Boolean, default=True)

assignments = db.relationship('Assignment', cascade='all,delete', backref='course')
ta_for_course = db.relationship('TAForCourse', cascade='all,delete', backref='course')
Expand Down Expand Up @@ -481,12 +482,6 @@ class Submission(db.Model):
test_results = db.relationship("SubmissionTestResult", cascade="all,delete", backref='submission', lazy=False)
repo = db.relationship(AssignmentRepo, backref='submissions')

@property
def netid(self):
if self.owner is not None:
return self.owner.netid
return "null"

@property
def visible_tests(self):
"""
Expand Down
22 changes: 19 additions & 3 deletions api/anubis/rpc/visualizations.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from typing import List

from datetime import datetime

from anubis.config import config
from anubis.models import Assignment
from anubis.models import Assignment, Course
from anubis.utils.data import with_context
from anubis.utils.visuals.assignments import get_assignment_sundial
from anubis.utils.visuals.usage import get_usage_plot
Expand All @@ -14,12 +16,26 @@ def create_visuals(*_, **__):
:return:
"""
get_usage_plot()

recent_assignments = Assignment.query.filter(
course_with_visuals: List[Course] = Course.query.filter(
Course.display_visuals == True,
).all()

# Iterate over courses with display visuals enabled
for course in course_with_visuals:

# Generate a usage graph for each course. This operation is always run
# and always cached when run in the visuals cronjob.
get_usage_plot(course.id)

# For recent assignments
recent_assignments: List[Assignment] = Assignment.query.filter(
Assignment.release_date > datetime.now(),
Assignment.due_date > datetime.now() - config.STATS_REAP_DURATION,
).all()

# Iterate over all recent assignments
for assignment in recent_assignments:

# Generate new sundial data
get_assignment_sundial(assignment.id)
36 changes: 24 additions & 12 deletions api/anubis/utils/visuals/usage.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
from datetime import datetime
from io import BytesIO
from typing import List, Dict, Any
from typing import List, Dict, Any, Optional

import numpy as np
import pandas as pd

from anubis.models import Assignment, Submission, TheiaSession
from anubis.models import Assignment, Submission, TheiaSession, Course
from anubis.utils.data import is_job
from anubis.utils.cache import cache
from anubis.utils.logging import logger
from anubis.utils.data import is_debug


def get_submissions() -> pd.DataFrame:
def get_submissions(course_id: str) -> pd.DataFrame:
"""
Get all submissions from visible assignments, and put them in a dataframe
:return:
"""
# Get the submission sqlalchemy objects
raw_submissions = Submission.query.join(Assignment).filter(Assignment.hidden == False).all()
raw_submissions = Submission.query.join(Assignment).filter(
Assignment.hidden == False,
Assignment.course_id == course_id,
).all()

# Specify which columns we want
columns = ['id', 'owner_id', 'assignment_id', 'processed', 'created']
Expand All @@ -38,15 +42,15 @@ def get_submissions() -> pd.DataFrame:
return submissions


def get_theia_sessions() -> pd.DataFrame:
def get_theia_sessions(course_id: str) -> pd.DataFrame:
"""
Get all theia session objects, and throw them into a dataframe
:return:
"""

# Get all the theia session sqlalchemy objects
raw_theia_sessions = TheiaSession.query.all()
raw_theia_sessions = TheiaSession.query.join(Assignment).filter(Assignment.course_id == course_id).all()

# Specify which columns we want
columns = ['id', 'owner_id', 'assignment_id', 'created', 'ended']
Expand Down Expand Up @@ -111,19 +115,27 @@ def get_raw_submissions() -> List[Dict[str, Any]]:
return list(response.values())


@cache.memoize(timeout=-1, forced_update=is_job)
def get_usage_plot():
@cache.memoize(timeout=-1, forced_update=is_job, unless=is_debug)
def get_usage_plot(course_id: Optional[str]) -> Optional[bytes]:
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors

logger.info('GENERATING USAGE PLOT PNG')

course: Course = Course.query.filter(
Course.id == course_id
).first()

if course is None:
return None

assignments = Assignment.query.filter(
Assignment.hidden == False,
Assignment.release_date <= datetime.now(),
Assignment.course_id == course_id,
).all()
submissions = get_submissions()
theia_sessions = get_theia_sessions()
submissions = get_submissions(course_id)
theia_sessions = get_theia_sessions(course_id)

fig, axs = plt.subplots(2, 1, figsize=(12, 10))

Expand Down Expand Up @@ -167,7 +179,7 @@ def get_usage_plot():
ha='right', va='center',
)
axs[0].legend(handles=legend_handles0, loc='upper left')
axs[0].set(title='Submissions over time', xlabel='time', ylabel='count')
axs[0].set(title=f'{course.course_code} - Submissions over time', xlabel='time', ylabel='count')
axs[0].grid(True)

axs[1].text(
Expand All @@ -176,7 +188,7 @@ def get_usage_plot():
ha='right', va='center',
)
axs[1].legend(handles=legend_handles1, loc='upper left')
axs[1].set(title='Cloud IDEs over time', xlabel='time', ylabel='count')
axs[1].set(title=f'{course.course_code} - Cloud IDEs over time', xlabel='time', ylabel='count')
axs[1].grid(True)

file_bytes = BytesIO()
Expand Down
32 changes: 29 additions & 3 deletions api/anubis/views/public/courses.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import List, Dict, Any

from flask import Blueprint

from anubis.models import db, InCourse, Course
Expand All @@ -7,7 +9,7 @@
from anubis.utils.http.decorators import json_response
from anubis.utils.http import error_response, success_response
from anubis.lms.assignments import get_assignments
from anubis.lms.courses import valid_join_code, get_courses
from anubis.lms.courses import valid_join_code, get_courses, get_courses_with_visuals
from anubis.utils.cache import cache

courses_ = Blueprint("public-courses", __name__, url_prefix="/public/courses")
Expand All @@ -17,9 +19,10 @@
@courses_.route("/list")
@require_user()
@json_response
def public_classes():
def public_courses_list():
"""
Get class data for current user
Get courses that the student is currently enrolled in.
This requires authentication.
:return:
"""
Expand Down Expand Up @@ -89,3 +92,26 @@ def public_courses_join(join_code):
return success_response({
"status": f"Joined {course.course_code}"
})


@courses_.get("/visuals-list")
@json_response
def public_courses_visuals_list():
"""
Get a list of the courses that allow for usage visuals
to be displayed for them. This is pulled by the frontend
to determine which visuals are "gettable".
* Possibly slightly cached *
:return:
"""

# Get (possibly) slightly cached course data for courses with
# visuals enabled.
courses_data: List[Dict[str, Any]] = get_courses_with_visuals()

# Pass back the cached response
return success_response({
'courses': courses_data
})
37 changes: 15 additions & 22 deletions api/anubis/views/public/visuals.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,38 @@
from flask import Blueprint, make_response

from anubis.models import Course
from anubis.utils.data import is_debug
from anubis.utils.http import success_response
from anubis.utils.cache import cache
from anubis.utils.http import req_assert
from anubis.utils.visuals.usage import get_usage_plot, get_raw_submissions

visuals = Blueprint('public-visuals', __name__, url_prefix='/public/visuals')


@visuals.route('/usage')
@visuals.route('/usage/<string:course_id>')
@cache.cached(timeout=360, unless=is_debug)
def public_visuals_usage():
def public_visuals_usage(course_id: str):
"""
Get the usage png graph. This endpoint is heavily
cached.
:param course_id:
:return:
"""

# Get the course
course: Course = Course.query.filter(Course.id == course_id).first()

# Confirm that the course exists
req_assert(course is not None, message="Course does not exist")

# Confirm that the course has visuals enabled
req_assert(course.display_visuals, message="Course does not support usage visuals")

# Get the png blob of the usage graph.
# The get_usage_plot is itself a cached function.
blob = get_usage_plot()
blob = get_usage_plot(course.id)

# Take the png bytes, and make a flask response
response = make_response(blob)
Expand All @@ -30,22 +42,3 @@ def public_visuals_usage():

# Pass back the image response
return response


@visuals.route('/raw-usage')
@cache.cached(timeout=360, unless=is_debug)
def public_visuals_raw_usage():
"""
Get the raw usage data for generating a react-vis
graph in the frontend of the usage stats.
:return:
"""

# Get the raw usage stats
usage = get_raw_submissions()

# Pass back the visual data
return success_response({
'usage': usage
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""ADD course display visuals option
Revision ID: c87807debbda
Revises: 86296424818b
Create Date: 2021-09-19 11:42:56.766233
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "c87807debbda"
down_revision = "86296424818b"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"course", sa.Column("display_visuals", sa.Boolean(), nullable=True)
)
conn = op.get_bind()
with conn.begin():
conn.execute("update course set display_visuals = 0;")
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("course", "display_visuals")
# ### end Alembic commands ###
22 changes: 19 additions & 3 deletions api/tests/test_visuals_public.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,23 @@

def test_visuals_public():
student = Session('student')
usage = student.get('/public/visuals/usage', return_request=True, skip_verify=True)
assert usage.status_code == 200

student.get('/public/visuals/raw-usage')
# Get all course data with visuals enabled
courses_with_visuals = student.get('/public/courses/visuals-list')['courses']

# Iterate over courses, generating visuals for each
for course in courses_with_visuals:

# Pull course id
course_id = course['id']

# Get the course visual
usage = student.get(
f'/public/visuals/usage/{course_id}',
return_request=True,
skip_verify=True,
)

# Just make sure it didn't 500
assert usage.status_code == 200

1 change: 1 addition & 0 deletions web/src/Pages/Admin/Course.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const editableFields = [
{field: 'theia_default_image', label: 'IDE Default Image'},
{field: 'theia_default_options', label: 'IDE Default Options'},
{field: 'github_repo_required', label: 'Github Repos Required', type: 'boolean'},
{field: 'display_visuals', label: 'Display Public Usage Visuals', type: 'boolean'},
];

export default function Course() {
Expand Down

0 comments on commit a2128b6

Please sign in to comment.