diff --git a/Dockerfile b/Dockerfile index 4e3692956..6777e1d5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/migrations/versions/9bc6f244678d_.py b/migrations/versions/9bc6f244678d_.py new file mode 100644 index 000000000..151e478a6 --- /dev/null +++ b/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 ### diff --git a/server/autograder.py b/server/autograder.py index 387d3e713..7888edffb 100644 --- a/server/autograder.py +++ b/server/autograder.py @@ -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: @@ -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: @@ -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 @@ -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( @@ -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)]) diff --git a/server/constants.py b/server/constants.py index 9d2b26a0e..119a349c2 100644 --- a/server/constants.py +++ b/server/constants.py @@ -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") diff --git a/server/controllers/admin.py b/server/controllers/admin.py index c766d78bb..3eb8e6627 100644 --- a/server/controllers/admin.py +++ b/server/controllers/admin.py @@ -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 @@ -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//assignments//stats") @@ -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) diff --git a/server/forms.py b/server/forms.py index 9492d0413..ce8d4f7c7 100644 --- a/server/forms.py +++ b/server/forms.py @@ -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 @@ -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( diff --git a/server/models.py b/server/models.py index d74f99ec3..94811938c 100644 --- a/server/models.py +++ b/server/models.py @@ -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, @@ -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): diff --git a/server/templates/staff/course/course.edit.html b/server/templates/staff/course/course.edit.html index d31b69242..f3641bee3 100644 --- a/server/templates/staff/course/course.edit.html +++ b/server/templates/staff/course/course.edit.html @@ -33,6 +33,7 @@

Edit {{ current_course.display_name_with_semester }}

{{ 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.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 %}