Skip to content

Commit

Permalink
Expose autograder_url as configurable parameter (#1347)
Browse files Browse the repository at this point in the history
* Expose `autograder_url` as configurable parameter

* Address Comments

* Remove ag_url entry from new course form.

* Re-trigger travis

* Fix build issue

* Address Comment

* Use https

* Set correct value field for ag_url
  • Loading branch information
simon-mo authored and colinschoen committed Jan 30, 2019
1 parent e5eb36e commit 6f8c0f7
Show file tree
Hide file tree
Showing 8 changed files with 55 additions and 13 deletions.
5 changes: 4 additions & 1 deletion Dockerfile
Expand Up @@ -16,7 +16,10 @@ RUN mkdir /code/
WORKDIR /code/

ADD requirements.txt .
RUN pip3 --timeout=60 install --no-cache-dir -r requirements.txt

# Adding --no-use-pep51 due to build error with pip 19.0.1
# https://gist.github.com/dmulter/38330962002d28533d7dd7c1a70ee4f5
RUN pip3 --timeout=60 install --no-cache-dir --no-use-pep51 -r requirements.txt

RUN ln -sf /dev/stdout /var/log/nginx/access.log && \
ln -sf /dev/stderr /var/log/nginx/error.log
Expand Down
27 changes: 27 additions & 0 deletions migrations/versions/9bc6f244678d_.py
@@ -0,0 +1,27 @@
"""empty message
Revision ID: 9bc6f244678d
Revises: 1266914000cd
Create Date: 2019-01-23 21:16:14.715499
"""

# revision identifiers, used by Alembic.
revision = '9bc6f244678d'
down_revision = '1266914000cd'

from alembic import op
import sqlalchemy as sa
import server


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('course', sa.Column('autograder_url', sa.String(length=255), server_default='https://autograder.cs61a.org', nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('course', 'autograder_url')
# ### end Alembic commands ###
20 changes: 12 additions & 8 deletions server/autograder.py
Expand Up @@ -14,14 +14,14 @@
import requests

from server import constants, jobs, utils
from server.models import User, Assignment, Backup, Client, Score, Token, db
from server.models import User, Assignment, Backup, Client, Score, Token, Course, db

logger = logging.getLogger(__name__)

def send_autograder(endpoint, data):
def send_autograder(endpoint, data, autograder_url):
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}

r = requests.post(constants.AUTOGRADER_URL + endpoint,
r = requests.post(autograder_url + endpoint,
data=json.dumps(data), headers=headers, timeout=8)

if r.status_code == requests.codes.ok:
Expand Down Expand Up @@ -74,7 +74,7 @@ def send_batch(token, assignment, backup_ids, priority='default'):
'access_token': token.access_token,
'priority': priority,
'ok-server-version': 'v3',
})
}, autograder_url=assignment.course.autograder_url)
if response_json:
return dict(zip(backup_ids, response_json['jobs']))
else:
Expand Down Expand Up @@ -103,18 +103,20 @@ def submit_continuous(backup):
'emails': [User.email_by_id(oid) for oid in backup.owners()]
}

autograder_url = assignment.course.autograder_url

if not backup.submitter.is_enrolled(assignment.course_id):
raise ValueError("User is not enrolled and cannot be autograded")

return send_autograder('/api/file/grade/continous', data)
return send_autograder('/api/file/grade/continous', data, autograder_url=autograder_url)

def check_job_results(job_ids):
def check_job_results(job_ids, autograder_url):
"""Given a list of autograder job IDs, return a dict mapping job IDs to
either null (if the job does not exist) of a dict with keys
status: one of 'queued', 'finished', 'failed', 'started', 'deferred'
result: string
"""
return send_autograder('/results', job_ids)
return send_autograder('/results', job_ids, autograder_url)

GradingStatus = enum.Enum('GradingStatus', [
'QUEUED', # a job is queued
Expand Down Expand Up @@ -164,6 +166,8 @@ def autograde_backups(assignment, user_id, backup_ids, logger):
]
num_tasks = len(tasks)

autograder_url = assignment.course.autograder_url

def retry_task(task):
if task.retries >= MAX_RETRIES:
logger.error('Did not receive a score for backup {} after {} retries'.format(
Expand All @@ -176,7 +180,7 @@ def retry_task(task):

while True:
time.sleep(POLL_INTERVAL)
results = check_job_results([task.job_id for task in tasks])
results = check_job_results([task.job_id for task in tasks], autograder_url)

graded = len([task for task in tasks
if task.status in (GradingStatus.DONE, GradingStatus.FAILED)])
Expand Down
2 changes: 2 additions & 0 deletions server/constants.py
Expand Up @@ -36,6 +36,8 @@

APPLICATION_ROOT = os.getenv('APPLICATION_ROOT', '/')

# The default autograder url
# Each course can configure their own autograder url in course.edit view
AUTOGRADER_URL = os.getenv('AUTOGRADER_URL', 'https://autograder.cs61a.org')

SENDGRID_KEY = os.getenv("SENDGRID_KEY")
Expand Down
6 changes: 3 additions & 3 deletions server/controllers/admin.py
Expand Up @@ -25,7 +25,7 @@
from server.contrib import analyze

from server.constants import (EMAIL_FORMAT, INSTRUCTOR_ROLE, STAFF_ROLES, STUDENT_ROLE,
LAB_ASSISTANT_ROLE, SCORE_KINDS, AUTOGRADER_URL)
LAB_ASSISTANT_ROLE, SCORE_KINDS)
import server.canvas.api as canvas_api
import server.canvas.jobs
from server.extensions import cache
Expand Down Expand Up @@ -558,7 +558,7 @@ def assignment(cid, aid):
flash("Assignment edited successfully.", "success")

return render_template('staff/course/assignment/assignment.html', assignment=assign,
form=form, courses=courses, autograder_url=AUTOGRADER_URL,
form=form, courses=courses, autograder_url=current_course.autograder_url,
current_course=current_course)

@admin.route("/course/<int:cid>/assignments/<int:aid>/stats")
Expand Down Expand Up @@ -691,7 +691,7 @@ def view_scores(cid, aid):
bar_charts[kind] = bar_chart.render().decode("utf-8")

return render_template('staff/course/assignment/assignment.scores.html',
autograder_url=AUTOGRADER_URL,
autograder_url=current_course.autograder_url,
assignment=assign, current_course=current_course,
courses=courses, scores=all_scores,
score_plots=bar_charts)
Expand Down
4 changes: 4 additions & 0 deletions server/forms.py
Expand Up @@ -648,6 +648,8 @@ class NewCourseForm(BaseForm):
active = BooleanField('Activate Course', default=True)
timezone = SelectField('Course Timezone', choices=[(t, t) for t in pytz.common_timezones],
default=TIMEZONE)
autograder_url = StringField('Autograder Endpoint (Optional)',
validators=[validators.optional(), validators.url()])

def validate(self):
# if our validators do not pass
Expand All @@ -674,6 +676,8 @@ class CourseUpdateForm(BaseForm):
validators=[validators.optional(), validators.url()])
active = BooleanField('Activate Course', default=True)
timezone = SelectField('Course Timezone', choices=[(t, t) for t in pytz.common_timezones])
autograder_url = StringField('Autograder Endpoint (Optional)',
validators=[validators.optional(), validators.url()])

class PublishScores(BaseForm):
published_scores = MultiCheckboxField(
Expand Down
3 changes: 2 additions & 1 deletion server/models.py
Expand Up @@ -30,7 +30,7 @@

from server.constants import (VALID_ROLES, STUDENT_ROLE, STAFF_ROLES, TIMEZONE,
SCORE_KINDS, OAUTH_OUT_OF_BAND_URI,
INSTRUCTOR_ROLE, ROLE_DISPLAY_NAMES)
INSTRUCTOR_ROLE, ROLE_DISPLAY_NAMES, AUTOGRADER_URL)

from server.extensions import cache, storage
from server.utils import (encode_id, chunks, generate_number_table,
Expand Down Expand Up @@ -256,6 +256,7 @@ class Course(Model):
website = db.Column(db.String(255))
active = db.Column(db.Boolean(), nullable=False, default=True)
timezone = db.Column(Timezone, nullable=False, default=pytz.timezone(TIMEZONE))
autograder_url = db.Column(db.String(255), server_default=AUTOGRADER_URL)

@classmethod
def can(cls, obj, user, action):
Expand Down
1 change: 1 addition & 0 deletions server/templates/staff/course/course.edit.html
Expand Up @@ -33,6 +33,7 @@ <h3 class="box-title">Edit {{ current_course.display_name_with_semester }}</h3>
{{ forms.render_field(form.institution, label_visible=true, placeholder='UC Berkeley', type='text') }}
{{ forms.render_field(form.website, label_visible=true, placeholder='http://cs61a.org/', type='text') }}
<!-- {{ forms.render_field(form.timezone, label_visible=true, type='select') }} -->
{{ forms.render_field(form.autograder_url, label_visible=true, value=current_course.autograder_url or 'https://autograder.cs61a.org/', type='text') }}
{{ forms.render_checkbox_field(form.active, label_visible=true) }}
{% endcall %}
</div>
Expand Down

0 comments on commit 6f8c0f7

Please sign in to comment.