Merge remote-tracking branch 'origin/master' into feature/cale/cms-master
Conflicts: lms/static/sass/course/courseware/_courseware.scss
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,3 +28,4 @@ cms/static/sass/*.css
|
||||
lms/lib/comment_client/python
|
||||
nosetests.xml
|
||||
cover_html/
|
||||
.idea/
|
||||
|
||||
@@ -29,6 +29,9 @@ from django_future.csrf import ensure_csrf_cookie, csrf_exempt
|
||||
from student.models import (Registration, UserProfile,
|
||||
PendingNameChange, PendingEmailChange,
|
||||
CourseEnrollment)
|
||||
|
||||
from certificates.models import CertificateStatuses, certificate_status_for_student
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -143,11 +146,20 @@ def dashboard(request):
|
||||
show_courseware_links_for = frozenset(course.id for course in courses
|
||||
if has_access(request.user, course, 'load'))
|
||||
|
||||
# TODO: workaround to not have to zip courses and certificates in the template
|
||||
# since before there is a migration to certificates
|
||||
if settings.MITX_FEATURES.get('CERTIFICATES_ENABLED'):
|
||||
cert_statuses = { course.id: certificate_status_for_student(request.user, course.id) for course in courses}
|
||||
else:
|
||||
cert_statuses = {}
|
||||
|
||||
context = {'courses': courses,
|
||||
'message': message,
|
||||
'staff_access': staff_access,
|
||||
'errored_courses': errored_courses,
|
||||
'show_courseware_links_for' : show_courseware_links_for}
|
||||
'show_courseware_links_for' : show_courseware_links_for,
|
||||
'cert_statuses': cert_statuses,
|
||||
}
|
||||
|
||||
return render_to_response('dashboard.html', context)
|
||||
|
||||
@@ -206,13 +218,13 @@ def change_enrollment(request):
|
||||
return {'success': False,
|
||||
'error': 'enrollment in {} not allowed at this time'
|
||||
.format(course.display_name)}
|
||||
|
||||
org, course_num, run=course_id.split("/")
|
||||
|
||||
org, course_num, run=course_id.split("/")
|
||||
statsd.increment("common.student.enrollment",
|
||||
tags=["org:{0}".format(org),
|
||||
"course:{0}".format(course_num),
|
||||
"run:{0}".format(run)])
|
||||
|
||||
|
||||
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
|
||||
return {'success': True}
|
||||
|
||||
@@ -220,13 +232,13 @@ def change_enrollment(request):
|
||||
try:
|
||||
enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id)
|
||||
enrollment.delete()
|
||||
|
||||
org, course_num, run=course_id.split("/")
|
||||
|
||||
org, course_num, run=course_id.split("/")
|
||||
statsd.increment("common.student.unenrollment",
|
||||
tags=["org:{0}".format(org),
|
||||
"course:{0}".format(course_num),
|
||||
"run:{0}".format(run)])
|
||||
|
||||
|
||||
return {'success': True}
|
||||
except CourseEnrollment.DoesNotExist:
|
||||
return {'success': False, 'error': 'You are not enrolled for this course.'}
|
||||
@@ -275,13 +287,13 @@ def login_user(request, error=""):
|
||||
log.info("Login success - {0} ({1})".format(username, email))
|
||||
|
||||
try_change_enrollment(request)
|
||||
|
||||
|
||||
statsd.increment("common.student.successful_login")
|
||||
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
|
||||
log.warning("Login failed - Account not active for user {0}, resending activation".format(username))
|
||||
|
||||
|
||||
reactivation_email_for_user(user)
|
||||
not_activated_msg = "This account has not been activated. We have " + \
|
||||
"sent another activation message. Please check your " + \
|
||||
@@ -483,9 +495,9 @@ def create_account(request, post_override=None):
|
||||
log.debug('bypassing activation email')
|
||||
login_user.is_active = True
|
||||
login_user.save()
|
||||
|
||||
|
||||
statsd.increment("common.student.account_created")
|
||||
|
||||
|
||||
js = {'success': True}
|
||||
return HttpResponse(json.dumps(js), mimetype="application/json")
|
||||
|
||||
@@ -541,9 +553,9 @@ def password_reset(request):
|
||||
''' Attempts to send a password reset e-mail. '''
|
||||
if request.method != "POST":
|
||||
raise Http404
|
||||
|
||||
|
||||
# By default, Django doesn't allow Users with is_active = False to reset their passwords,
|
||||
# but this bites people who signed up a long time ago, never activated, and forgot their
|
||||
# but this bites people who signed up a long time ago, never activated, and forgot their
|
||||
# password. So for their sake, we'll auto-activate a user for whome password_reset is called.
|
||||
try:
|
||||
user = User.objects.get(email=request.POST['email'])
|
||||
@@ -551,7 +563,7 @@ def password_reset(request):
|
||||
user.save()
|
||||
except:
|
||||
log.exception("Tried to auto-activate user to enable password reset, but failed.")
|
||||
|
||||
|
||||
form = PasswordResetForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save(use_https = request.is_secure(),
|
||||
@@ -589,7 +601,7 @@ def reactivation_email_for_user(user):
|
||||
res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def change_email_request(request):
|
||||
@@ -764,8 +776,8 @@ def accept_name_change_by_id(id):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def accept_name_change(request):
|
||||
''' JSON: Name change process. Course staff clicks 'accept' on a given name change
|
||||
|
||||
''' JSON: Name change process. Course staff clicks 'accept' on a given name change
|
||||
|
||||
We used this during the prototype but now we simply record name changes instead
|
||||
of manually approving them. Still keeping this around in case we want to go
|
||||
back to this approval method.
|
||||
@@ -794,6 +806,3 @@ def test_center_login(request):
|
||||
return redirect('/courses/MITx/6.002x/2012_Fall/courseware/Final_Exam/Final_Exam_Fall_2012/')
|
||||
else:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ setup(
|
||||
"problem = xmodule.capa_module:CapaDescriptor",
|
||||
"problemset = xmodule.seq_module:SequenceDescriptor",
|
||||
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
|
||||
"selfassessment = xmodule.self_assessment_module:SelfAssessmentDescriptor",
|
||||
"sequential = xmodule.seq_module:SequenceDescriptor",
|
||||
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
"vertical = xmodule.vertical_module:VerticalDescriptor",
|
||||
|
||||
@@ -29,15 +29,17 @@ TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?)
|
||||
def only_one(lst, default="", process=lambda x: x):
|
||||
"""
|
||||
If lst is empty, returns default
|
||||
If lst has a single element, applies process to that element and returns it
|
||||
Otherwise, raises an exeception
|
||||
|
||||
If lst has a single element, applies process to that element and returns it.
|
||||
|
||||
Otherwise, raises an exception.
|
||||
"""
|
||||
if len(lst) == 0:
|
||||
return default
|
||||
elif len(lst) == 1:
|
||||
return process(lst[0])
|
||||
else:
|
||||
raise Exception('Malformed XML')
|
||||
raise Exception('Malformed XML: expected at most one element in list.')
|
||||
|
||||
|
||||
def parse_timedelta(time_str):
|
||||
@@ -291,11 +293,11 @@ class CapaModule(XModule):
|
||||
# check button is context-specific.
|
||||
|
||||
# Put a "Check" button if unlimited attempts or still some left
|
||||
if self.max_attempts is None or self.attempts < self.max_attempts-1:
|
||||
if self.max_attempts is None or self.attempts < self.max_attempts-1:
|
||||
check_button = "Check"
|
||||
else:
|
||||
# Will be final check so let user know that
|
||||
check_button = "Final Check"
|
||||
check_button = "Final Check"
|
||||
|
||||
reset_button = True
|
||||
save_button = True
|
||||
@@ -518,11 +520,11 @@ class CapaModule(XModule):
|
||||
# Problem queued. Students must wait a specified waittime before they are allowed to submit
|
||||
if self.lcp.is_queued():
|
||||
current_time = datetime.datetime.now()
|
||||
prev_submit_time = self.lcp.get_recentmost_queuetime()
|
||||
prev_submit_time = self.lcp.get_recentmost_queuetime()
|
||||
waittime_between_requests = self.system.xqueue['waittime']
|
||||
if (current_time-prev_submit_time).total_seconds() < waittime_between_requests:
|
||||
msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests
|
||||
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
|
||||
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
|
||||
|
||||
try:
|
||||
old_state = self.lcp.get_state()
|
||||
@@ -663,10 +665,10 @@ class CapaDescriptor(RawDescriptor):
|
||||
'problems/' + path[8:],
|
||||
path[8:],
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CapaDescriptor, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
weight_string = self.metadata.get('weight', None)
|
||||
if weight_string:
|
||||
self.weight = float(weight_string)
|
||||
|
||||
@@ -243,7 +243,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
Returns True if the current time is after the specified course end date.
|
||||
Returns False if there is no end date specified.
|
||||
"""
|
||||
if self.end_date is None:
|
||||
if self.end is None:
|
||||
return False
|
||||
|
||||
return time.gmtime() > self.end
|
||||
@@ -364,6 +364,10 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
displayed_start = self._try_parse_time('advertised_start') or self.start
|
||||
return time.strftime("%b %d, %Y", displayed_start)
|
||||
|
||||
@property
|
||||
def end_date_text(self):
|
||||
return time.strftime("%b %d, %Y", self.end)
|
||||
|
||||
# An extra property is used rather than the wiki_slug/number because
|
||||
# there are courses that change the number for different runs. This allows
|
||||
# courses to share the same css_class across runs even if they have
|
||||
@@ -412,6 +416,16 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
# Explicit comparison to True because we always want to return a bool.
|
||||
return self.metadata.get('hide_progress_tab') == True
|
||||
|
||||
@property
|
||||
def end_of_course_survey_url(self):
|
||||
"""
|
||||
Pull from policy. Once we have our own survey module set up, can change this to point to an automatically
|
||||
created survey for each class.
|
||||
|
||||
Returns None if no url specified.
|
||||
"""
|
||||
return self.metadata.get('end_of_course_survey_url')
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.display_name
|
||||
|
||||
133
common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee
Normal file
133
common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee
Normal file
@@ -0,0 +1,133 @@
|
||||
class @SelfAssessment
|
||||
constructor: (element) ->
|
||||
@el = $(element).find('section.self-assessment')
|
||||
@id = @el.data('id')
|
||||
@ajax_url = @el.data('ajax-url')
|
||||
@state = @el.data('state')
|
||||
@allow_reset = @el.data('allow_reset')
|
||||
# valid states: 'initial', 'assessing', 'request_hint', 'done'
|
||||
|
||||
# Where to put the rubric once we load it
|
||||
@errors_area = @$('.error')
|
||||
@answer_area = @$('textarea.answer')
|
||||
|
||||
@rubric_wrapper = @$('.rubric-wrapper')
|
||||
@hint_wrapper = @$('.hint-wrapper')
|
||||
@message_wrapper = @$('.message-wrapper')
|
||||
@submit_button = @$('.submit-button')
|
||||
@reset_button = @$('.reset-button')
|
||||
@reset_button.click @reset
|
||||
|
||||
@find_assessment_elements()
|
||||
@find_hint_elements()
|
||||
|
||||
@rebind()
|
||||
|
||||
# locally scoped jquery.
|
||||
$: (selector) ->
|
||||
$(selector, @el)
|
||||
|
||||
rebind: () =>
|
||||
# rebind to the appropriate function for the current state
|
||||
@submit_button.unbind('click')
|
||||
@submit_button.show()
|
||||
@reset_button.hide()
|
||||
@hint_area.attr('disabled', false)
|
||||
if @state == 'initial'
|
||||
@answer_area.attr("disabled", false)
|
||||
@submit_button.prop('value', 'Submit')
|
||||
@submit_button.click @save_answer
|
||||
else if @state == 'assessing'
|
||||
@answer_area.attr("disabled", true)
|
||||
@submit_button.prop('value', 'Submit assessment')
|
||||
@submit_button.click @save_assessment
|
||||
else if @state == 'request_hint'
|
||||
@answer_area.attr("disabled", true)
|
||||
@submit_button.prop('value', 'Submit hint')
|
||||
@submit_button.click @save_hint
|
||||
else if @state == 'done'
|
||||
@answer_area.attr("disabled", true)
|
||||
@hint_area.attr('disabled', true)
|
||||
@submit_button.hide()
|
||||
if @allow_reset
|
||||
@reset_button.show()
|
||||
else
|
||||
@reset_button.hide()
|
||||
|
||||
|
||||
find_assessment_elements: ->
|
||||
@assessment = @$('select.assessment')
|
||||
|
||||
find_hint_elements: ->
|
||||
@hint_area = @$('textarea.hint')
|
||||
|
||||
save_answer: (event) =>
|
||||
event.preventDefault()
|
||||
if @state == 'initial'
|
||||
data = {'student_answer' : @answer_area.val()}
|
||||
$.postWithPrefix "#{@ajax_url}/save_answer", data, (response) =>
|
||||
if response.success
|
||||
@rubric_wrapper.html(response.rubric_html)
|
||||
@state = 'assessing'
|
||||
@find_assessment_elements()
|
||||
@rebind()
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html('Problem state got out of sync. Try reloading the page.')
|
||||
|
||||
save_assessment: (event) =>
|
||||
event.preventDefault()
|
||||
if @state == 'assessing'
|
||||
data = {'assessment' : @assessment.find(':selected').text()}
|
||||
$.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) =>
|
||||
if response.success
|
||||
@state = response.state
|
||||
|
||||
if @state == 'request_hint'
|
||||
@hint_wrapper.html(response.hint_html)
|
||||
@find_hint_elements()
|
||||
else if @state == 'done'
|
||||
@message_wrapper.html(response.message_html)
|
||||
@allow_reset = response.allow_reset
|
||||
|
||||
@rebind()
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html('Problem state got out of sync. Try reloading the page.')
|
||||
|
||||
|
||||
save_hint: (event) =>
|
||||
event.preventDefault()
|
||||
if @state == 'request_hint'
|
||||
data = {'hint' : @hint_area.val()}
|
||||
|
||||
$.postWithPrefix "#{@ajax_url}/save_hint", data, (response) =>
|
||||
if response.success
|
||||
@message_wrapper.html(response.message_html)
|
||||
@state = 'done'
|
||||
@allow_reset = response.allow_reset
|
||||
@rebind()
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html('Problem state got out of sync. Try reloading the page.')
|
||||
|
||||
|
||||
reset: (event) =>
|
||||
event.preventDefault()
|
||||
if @state == 'done'
|
||||
$.postWithPrefix "#{@ajax_url}/reset", {}, (response) =>
|
||||
if response.success
|
||||
@answer_area.html('')
|
||||
@rubric_wrapper.html('')
|
||||
@hint_wrapper.html('')
|
||||
@message_wrapper.html('')
|
||||
@state = 'initial'
|
||||
@rebind()
|
||||
@reset_button.hide()
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html('Problem state got out of sync. Try reloading the page.')
|
||||
476
common/lib/xmodule/xmodule/self_assessment_module.py
Normal file
476
common/lib/xmodule/xmodule/self_assessment_module.py
Normal file
@@ -0,0 +1,476 @@
|
||||
"""
|
||||
A Self Assessment module that allows students to write open-ended responses,
|
||||
submit, then see a rubric and rate themselves. Persists student supplied
|
||||
hints, answers, and assessment judgment (currently only correct/incorrect).
|
||||
Parses xml definition file--see below for exact format.
|
||||
"""
|
||||
|
||||
import copy
|
||||
from fs.errors import ResourceNotFoundError
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from lxml import etree
|
||||
from lxml.html import rewrite_links
|
||||
from path import path
|
||||
import json
|
||||
from progress import Progress
|
||||
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from .capa_module import only_one, ComplexEncoder
|
||||
from .editing_module import EditingDescriptor
|
||||
from .html_checker import check_html
|
||||
from .stringify import stringify_children
|
||||
from .x_module import XModule
|
||||
from .xml_module import XmlDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
# Set the default number of max attempts. Should be 1 for production
|
||||
# Set higher for debugging/testing
|
||||
# attempts specified in xml definition overrides this.
|
||||
MAX_ATTEMPTS = 1
|
||||
|
||||
# Set maximum available number of points.
|
||||
# Overriden by max_score specified in xml.
|
||||
MAX_SCORE = 1
|
||||
|
||||
class SelfAssessmentModule(XModule):
|
||||
"""
|
||||
States:
|
||||
|
||||
initial (prompt, textbox shown)
|
||||
|
|
||||
assessing (read-only textbox, rubric + assessment input shown)
|
||||
|
|
||||
request_hint (read-only textbox, read-only rubric and assessment, hint input box shown)
|
||||
|
|
||||
done (submitted msg, green checkmark, everything else read-only. If attempts < max, shows
|
||||
a reset button that goes back to initial state. Saves previous
|
||||
submissions too.)
|
||||
"""
|
||||
|
||||
# states
|
||||
INITIAL = 'initial'
|
||||
ASSESSING = 'assessing'
|
||||
REQUEST_HINT = 'request_hint'
|
||||
DONE = 'done'
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]}
|
||||
js_module_name = "SelfAssessment"
|
||||
|
||||
def __init__(self, system, location, definition, descriptor,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, descriptor,
|
||||
instance_state, shared_state, **kwargs)
|
||||
|
||||
"""
|
||||
Definition file should have 4 blocks -- prompt, rubric, submitmessage, hintprompt,
|
||||
and two optional attributes:
|
||||
attempts, which should be an integer that defaults to 1.
|
||||
If it's > 1, the student will be able to re-submit after they see
|
||||
the rubric.
|
||||
max_score, which should be an integer that defaults to 1.
|
||||
It defines the maximum number of points a student can get. Assumed to be integer scale
|
||||
from 0 to max_score, with an interval of 1.
|
||||
|
||||
Note: all the submissions are stored.
|
||||
|
||||
Sample file:
|
||||
|
||||
<selfassessment attempts="1" max_score="1">
|
||||
<prompt>
|
||||
Insert prompt text here. (arbitrary html)
|
||||
</prompt>
|
||||
<rubric>
|
||||
Insert grading rubric here. (arbitrary html)
|
||||
</rubric>
|
||||
<hintprompt>
|
||||
Please enter a hint below: (arbitrary html)
|
||||
</hintprompt>
|
||||
<submitmessage>
|
||||
Thanks for submitting! (arbitrary html)
|
||||
</submitmessage>
|
||||
</selfassessment>
|
||||
"""
|
||||
|
||||
# Load instance state
|
||||
if instance_state is not None:
|
||||
instance_state = json.loads(instance_state)
|
||||
else:
|
||||
instance_state = {}
|
||||
|
||||
# Note: score responses are on scale from 0 to max_score
|
||||
self.student_answers = instance_state.get('student_answers', [])
|
||||
self.scores = instance_state.get('scores', [])
|
||||
self.hints = instance_state.get('hints', [])
|
||||
|
||||
self.state = instance_state.get('state', 'initial')
|
||||
|
||||
# Used for progress / grading. Currently get credit just for
|
||||
# completion (doesn't matter if you self-assessed correct/incorrect).
|
||||
|
||||
self._max_score = int(self.metadata.get('max_score', MAX_SCORE))
|
||||
|
||||
self.attempts = instance_state.get('attempts', 0)
|
||||
|
||||
self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS))
|
||||
|
||||
self.rubric = definition['rubric']
|
||||
self.prompt = definition['prompt']
|
||||
self.submit_message = definition['submitmessage']
|
||||
self.hint_prompt = definition['hintprompt']
|
||||
|
||||
def _allow_reset(self):
|
||||
"""Can the module be reset?"""
|
||||
return self.state == self.DONE and self.attempts < self.max_attempts
|
||||
|
||||
def get_html(self):
|
||||
#set context variables and render template
|
||||
if self.state != self.INITIAL and self.student_answers:
|
||||
previous_answer = self.student_answers[-1]
|
||||
else:
|
||||
previous_answer = ''
|
||||
|
||||
context = {
|
||||
'prompt': self.prompt,
|
||||
'previous_answer': previous_answer,
|
||||
'ajax_url': self.system.ajax_url,
|
||||
'initial_rubric': self.get_rubric_html(),
|
||||
'initial_hint': self.get_hint_html(),
|
||||
'initial_message': self.get_message_html(),
|
||||
'state': self.state,
|
||||
'allow_reset': self._allow_reset(),
|
||||
}
|
||||
html = self.system.render_template('self_assessment_prompt.html', context)
|
||||
|
||||
# cdodge: perform link substitutions for any references to course static content (e.g. images)
|
||||
return rewrite_links(html, self.rewrite_content_links)
|
||||
|
||||
def get_score(self):
|
||||
"""
|
||||
Returns dict with 'score' key
|
||||
"""
|
||||
return {'score': self.get_last_score()}
|
||||
|
||||
def max_score(self):
|
||||
"""
|
||||
Return max_score
|
||||
"""
|
||||
return self._max_score
|
||||
|
||||
def get_last_score(self):
|
||||
"""
|
||||
Returns the last score in the list
|
||||
"""
|
||||
last_score=0
|
||||
if(len(self.scores)>0):
|
||||
last_score=self.scores[len(self.scores)-1]
|
||||
return last_score
|
||||
|
||||
def get_progress(self):
|
||||
'''
|
||||
For now, just return last score / max_score
|
||||
'''
|
||||
if self._max_score > 0:
|
||||
try:
|
||||
return Progress(self.get_last_score(), self._max_score)
|
||||
except Exception as err:
|
||||
log.exception("Got bad progress")
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
"""
|
||||
This is called by courseware.module_render, to handle an AJAX call.
|
||||
"get" is request.POST.
|
||||
|
||||
Returns a json dictionary:
|
||||
{ 'progress_changed' : True/False,
|
||||
'progress': 'none'/'in_progress'/'done',
|
||||
<other request-specific values here > }
|
||||
"""
|
||||
|
||||
handlers = {
|
||||
'save_answer': self.save_answer,
|
||||
'save_assessment': self.save_assessment,
|
||||
'save_hint': self.save_hint,
|
||||
'reset': self.reset,
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
return 'Error'
|
||||
|
||||
before = self.get_progress()
|
||||
d = handlers[dispatch](get)
|
||||
after = self.get_progress()
|
||||
d.update({
|
||||
'progress_changed': after != before,
|
||||
'progress_status': Progress.to_js_status_str(after),
|
||||
})
|
||||
return json.dumps(d, cls=ComplexEncoder)
|
||||
|
||||
def out_of_sync_error(self, get, msg=''):
|
||||
"""
|
||||
return dict out-of-sync error message, and also log.
|
||||
"""
|
||||
log.warning("Assessment module state out sync. state: %r, get: %r. %s",
|
||||
self.state, get, msg)
|
||||
return {'success': False,
|
||||
'error': 'The problem state got out-of-sync'}
|
||||
|
||||
def get_rubric_html(self):
|
||||
"""
|
||||
Return the appropriate version of the rubric, based on the state.
|
||||
"""
|
||||
if self.state == self.INITIAL:
|
||||
return ''
|
||||
|
||||
# we'll render it
|
||||
context = {'rubric': self.rubric,
|
||||
'max_score' : self._max_score,
|
||||
}
|
||||
|
||||
if self.state == self.ASSESSING:
|
||||
context['read_only'] = False
|
||||
elif self.state in (self.REQUEST_HINT, self.DONE):
|
||||
context['read_only'] = True
|
||||
else:
|
||||
raise ValueError("Illegal state '%r'" % self.state)
|
||||
|
||||
return self.system.render_template('self_assessment_rubric.html', context)
|
||||
|
||||
def get_hint_html(self):
|
||||
"""
|
||||
Return the appropriate version of the hint view, based on state.
|
||||
"""
|
||||
if self.state in (self.INITIAL, self.ASSESSING):
|
||||
return ''
|
||||
|
||||
if self.state == self.DONE and len(self.hints) > 0:
|
||||
# display the previous hint
|
||||
hint = self.hints[-1]
|
||||
else:
|
||||
hint = ''
|
||||
|
||||
context = {'hint_prompt': self.hint_prompt,
|
||||
'hint': hint}
|
||||
|
||||
if self.state == self.REQUEST_HINT:
|
||||
context['read_only'] = False
|
||||
elif self.state == self.DONE:
|
||||
context['read_only'] = True
|
||||
else:
|
||||
raise ValueError("Illegal state '%r'" % self.state)
|
||||
|
||||
return self.system.render_template('self_assessment_hint.html', context)
|
||||
|
||||
def get_message_html(self):
|
||||
"""
|
||||
Return the appropriate version of the message view, based on state.
|
||||
"""
|
||||
if self.state != self.DONE:
|
||||
return ""
|
||||
|
||||
return """<div class="save_message">{0}</div>""".format(self.submit_message)
|
||||
|
||||
|
||||
def save_answer(self, get):
|
||||
"""
|
||||
After the answer is submitted, show the rubric.
|
||||
"""
|
||||
# Check to see if attempts are less than max
|
||||
if self.attempts > self.max_attempts:
|
||||
# If too many attempts, prevent student from saving answer and
|
||||
# seeing rubric. In normal use, students shouldn't see this because
|
||||
# they won't see the reset button once they're out of attempts.
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Too many attempts.'
|
||||
}
|
||||
|
||||
if self.state != self.INITIAL:
|
||||
return self.out_of_sync_error(get)
|
||||
|
||||
self.student_answers.append(get['student_answer'])
|
||||
self.state = self.ASSESSING
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'rubric_html': self.get_rubric_html()
|
||||
}
|
||||
|
||||
def save_assessment(self, get):
|
||||
"""
|
||||
Save the assessment. If the student said they're right, don't ask for a
|
||||
hint, and go straight to the done state. Otherwise, do ask for a hint.
|
||||
|
||||
Returns a dict { 'success': bool, 'state': state,
|
||||
|
||||
'hint_html': hint_html OR 'message_html': html and 'allow_reset',
|
||||
|
||||
'error': error-msg},
|
||||
|
||||
with 'error' only present if 'success' is False, and 'hint_html' or
|
||||
'message_html' only if success is true
|
||||
"""
|
||||
|
||||
n_answers = len(self.student_answers)
|
||||
n_scores = len(self.scores)
|
||||
if (self.state != self.ASSESSING or n_answers != n_scores + 1):
|
||||
msg = "%d answers, %d scores" % (n_answers, n_scores)
|
||||
return self.out_of_sync_error(get, msg)
|
||||
|
||||
try:
|
||||
score = int(get['assessment'])
|
||||
except:
|
||||
return {'success': False, 'error': "Non-integer score value"}
|
||||
|
||||
self.scores.append(score)
|
||||
|
||||
d = {'success': True,}
|
||||
|
||||
if score == self.max_score():
|
||||
self.state = self.DONE
|
||||
d['message_html'] = self.get_message_html()
|
||||
d['allow_reset'] = self._allow_reset()
|
||||
else:
|
||||
self.state = self.REQUEST_HINT
|
||||
d['hint_html'] = self.get_hint_html()
|
||||
|
||||
d['state'] = self.state
|
||||
return d
|
||||
|
||||
|
||||
def save_hint(self, get):
|
||||
'''
|
||||
Save the hint.
|
||||
Returns a dict { 'success': bool,
|
||||
'message_html': message_html,
|
||||
'error': error-msg,
|
||||
'allow_reset': bool},
|
||||
with the error key only present if success is False and message_html
|
||||
only if True.
|
||||
'''
|
||||
if self.state != self.REQUEST_HINT:
|
||||
# Note: because we only ask for hints on wrong answers, may not have
|
||||
# the same number of hints and answers.
|
||||
return self.out_of_sync_error(get)
|
||||
|
||||
self.hints.append(get['hint'].lower())
|
||||
self.state = self.DONE
|
||||
|
||||
# increment attempts
|
||||
self.attempts = self.attempts + 1
|
||||
|
||||
# To the tracking logs!
|
||||
event_info = {
|
||||
'selfassessment_id': self.location.url(),
|
||||
'state': {
|
||||
'student_answers': self.student_answers,
|
||||
'score': self.scores,
|
||||
'hints': self.hints,
|
||||
}
|
||||
}
|
||||
self.system.track_function('save_hint', event_info)
|
||||
|
||||
return {'success': True,
|
||||
'message_html': self.get_message_html(),
|
||||
'allow_reset': self._allow_reset()}
|
||||
|
||||
|
||||
def reset(self, get):
|
||||
"""
|
||||
If resetting is allowed, reset the state.
|
||||
|
||||
Returns {'success': bool, 'error': msg}
|
||||
(error only present if not success)
|
||||
"""
|
||||
if self.state != self.DONE:
|
||||
return self.out_of_sync_error(get)
|
||||
|
||||
if self.attempts > self.max_attempts:
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Too many attempts.'
|
||||
}
|
||||
self.state = self.INITIAL
|
||||
return {'success': True}
|
||||
|
||||
|
||||
def get_instance_state(self):
|
||||
"""
|
||||
Get the current score and state
|
||||
"""
|
||||
|
||||
state = {
|
||||
'student_answers': self.student_answers,
|
||||
'hints': self.hints,
|
||||
'state': self.state,
|
||||
'scores': self.scores,
|
||||
'max_score': self._max_score,
|
||||
'attempts': self.attempts
|
||||
}
|
||||
return json.dumps(state)
|
||||
|
||||
|
||||
class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module for adding self assessment questions to courses
|
||||
"""
|
||||
mako_template = "widgets/html-edit.html"
|
||||
module_class = SelfAssessmentModule
|
||||
filename_extension = "xml"
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
template_dir_name = "selfassessment"
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
|
||||
js_module_name = "HTMLEditingDescriptor"
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
"""
|
||||
Pull out the rubric, prompt, and submitmessage into a dictionary.
|
||||
|
||||
Returns:
|
||||
{
|
||||
'rubric': 'some-html',
|
||||
'prompt': 'some-html',
|
||||
'submitmessage': 'some-html'
|
||||
'hintprompt': 'some-html'
|
||||
}
|
||||
"""
|
||||
expected_children = ['rubric', 'prompt', 'submitmessage', 'hintprompt']
|
||||
for child in expected_children:
|
||||
if len(xml_object.xpath(child)) != 1:
|
||||
raise ValueError("Self assessment definition must include exactly one '{0}' tag".format(child))
|
||||
|
||||
def parse(k):
|
||||
"""Assumes that xml_object has child k"""
|
||||
return stringify_children(xml_object.xpath(k)[0])
|
||||
|
||||
return {'rubric': parse('rubric'),
|
||||
'prompt': parse('prompt'),
|
||||
'submitmessage': parse('submitmessage'),
|
||||
'hintprompt': parse('hintprompt'),
|
||||
}
|
||||
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
'''Return an xml element representing this definition.'''
|
||||
elt = etree.Element('selfassessment')
|
||||
|
||||
def add_child(k):
|
||||
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
|
||||
child_node = etree.fromstring(child_str)
|
||||
elt.append(child_node)
|
||||
|
||||
for child in ['rubric', 'prompt', 'submitmessage', 'hintprompt']:
|
||||
add_child(child)
|
||||
|
||||
return elt
|
||||
@@ -113,3 +113,7 @@ class RoundTripTestCase(unittest.TestCase):
|
||||
|
||||
def test_full_roundtrip(self):
|
||||
self.check_export_roundtrip(DATA_DIR, "full")
|
||||
|
||||
def test_selfassessment_roundtrip(self):
|
||||
#Test selfassessment xmodule to see if it exports correctly
|
||||
self.check_export_roundtrip(DATA_DIR,"self_assessment")
|
||||
|
||||
@@ -339,4 +339,16 @@ class ImportTestCase(unittest.TestCase):
|
||||
|
||||
self.assertRaises(etree.XMLSyntaxError, system.process_xml, bad_xml)
|
||||
|
||||
def test_selfassessment_import(self):
|
||||
'''
|
||||
Check to see if definition_from_xml in self_assessment_module.py
|
||||
works properly. Pulls data from the self_assessment directory in the test data directory.
|
||||
'''
|
||||
|
||||
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['self_assessment'])
|
||||
|
||||
sa_id = "edX/sa_test/2012_Fall"
|
||||
location = Location(["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"])
|
||||
sa_sample = modulestore.get_instance(sa_id, location)
|
||||
#10 attempts is hard coded into SampleQuestion, which is the url_name of a selfassessment xml tag
|
||||
self.assertEqual(sa_sample.metadata['attempts'], '10')
|
||||
|
||||
1
common/test/data/self_assessment/README.md
Normal file
1
common/test/data/self_assessment/README.md
Normal file
@@ -0,0 +1 @@
|
||||
This is a very very simple course, useful for debugging self assessment code.
|
||||
1
common/test/data/self_assessment/course.xml
Symbolic link
1
common/test/data/self_assessment/course.xml
Symbolic link
@@ -0,0 +1 @@
|
||||
roots/2012_Fall.xml
|
||||
5
common/test/data/self_assessment/course/2012_Fall.xml
Normal file
5
common/test/data/self_assessment/course/2012_Fall.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<course>
|
||||
<chapter url_name="Overview">
|
||||
<selfassessment url_name="SampleQuestion"/>
|
||||
</chapter>
|
||||
</course>
|
||||
14
common/test/data/self_assessment/policies/2012_Fall.json
Normal file
14
common/test/data/self_assessment/policies/2012_Fall.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"course/2012_Fall": {
|
||||
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
|
||||
"start": "2015-07-17T12:00",
|
||||
"display_name": "Self Assessment Test",
|
||||
"graded": "true"
|
||||
},
|
||||
"chapter/Overview": {
|
||||
"display_name": "Overview"
|
||||
},
|
||||
"selfassessment/SampleQuestion": {
|
||||
"display_name": "Sample Question",
|
||||
},
|
||||
}
|
||||
1
common/test/data/self_assessment/roots/2012_Fall.xml
Normal file
1
common/test/data/self_assessment/roots/2012_Fall.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course org="edX" course="sa_test" url_name="2012_Fall"/>
|
||||
@@ -0,0 +1,14 @@
|
||||
<selfassessment attempts='10'>
|
||||
<prompt>
|
||||
What is the meaning of life?
|
||||
</prompt>
|
||||
<rubric>
|
||||
This is a rubric.
|
||||
</rubric>
|
||||
<submitmessage>
|
||||
Thanks for your submission!
|
||||
</submitmessage>
|
||||
<hintprompt>
|
||||
Enter a hint below:
|
||||
</hintprompt>
|
||||
</selfassessment>
|
||||
@@ -105,7 +105,7 @@ NUMPY_VER="1.6.2"
|
||||
SCIPY_VER="0.10.1"
|
||||
BREW_FILE="$BASE/mitx/brew-formulas.txt"
|
||||
LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log"
|
||||
APT_PKGS="pkg-config curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript graphviz graphviz-dev mysql-server libmysqlclient-dev"
|
||||
APT_PKGS="pkg-config curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor nodejs npm graphviz graphviz-dev mysql-server libmysqlclient-dev"
|
||||
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
error "This script should not be run using sudo or as the root user"
|
||||
@@ -192,9 +192,12 @@ case `uname -s` in
|
||||
case $distro in
|
||||
maya|lisa|natty|oneiric|precise|quantal)
|
||||
output "Installing ubuntu requirements"
|
||||
sudo apt-get install python-software-properties
|
||||
sudo add-apt-repository ppa:chris-lea/node.js
|
||||
sudo apt-get -y update
|
||||
# DEBIAN_FRONTEND=noninteractive is required for silent mysql-server installation
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get -y install $APT_PKGS
|
||||
sudo npm install coffee-script
|
||||
clone_repos
|
||||
;;
|
||||
*)
|
||||
|
||||
@@ -253,8 +253,10 @@ Supported fields at the course level:
|
||||
* "advertised_start" -- specify what you want displayed as the start date of the course in the course listing and course about pages. This can be useful if you want to let people in early before the formal start. Format-by-example: "2012-09-05T12:00".
|
||||
* "enrollment_start", "enrollment_end" -- when can students enroll? (if not specified, can enroll anytime). Same format as "start".
|
||||
* "end" -- specify the end date for the course. Format-by-example: "2012-11-05T12:00".
|
||||
* "end_of_course_survey_url" -- a url for an end of course survey -- shown after course is over, next to certificate download links.
|
||||
* "tabs" -- have custom tabs in the courseware. See below for details on config.
|
||||
* "discussion_blackouts" -- An array of time intervals during which you want to disable a student's ability to create or edit posts in the forum. Moderators, Community TAs, and Admins are unaffected. You might use this during exam periods, but please be aware that the forum is often a very good place to catch mistakes and clarify points to students. The better long term solution would be to have better flagging/moderation mechanisms, but this is the hammer we have today. Format by example: [["2012-10-29T04:00", "2012-11-03T04:00"], ["2012-12-30T04:00", "2013-01-02T04:00"]]
|
||||
* "show_calculator" (value "Yes" if desired)
|
||||
* TODO: there are others
|
||||
|
||||
### Grading policy file contents
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from certificates.models import certificate_status_for_student
|
||||
from certificates.queue import XQueueCertInterface
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import UserProfile
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
help = """
|
||||
Looks for names that have unicode characters
|
||||
and queues them up for a certificate request
|
||||
"""
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
# TODO this is only temporary for CS169 certs
|
||||
|
||||
course_id = 'BerkeleyX/CS169.1x/2012_Fall'
|
||||
|
||||
enrolled_students = User.objects.filter(
|
||||
courseenrollment__course_id=course_id).prefetch_related(
|
||||
"groups").order_by('username')
|
||||
xq = XQueueCertInterface()
|
||||
print "Looking for unusual names.."
|
||||
for student in enrolled_students:
|
||||
if certificate_status_for_student(
|
||||
student, course_id)['status'] == 'unavailable':
|
||||
continue
|
||||
name = UserProfile.objects.get(user=student).name
|
||||
for c in name:
|
||||
if ord(c) >= 0x200:
|
||||
ret = xq.add_cert(student, course_id)
|
||||
if ret == 'generating':
|
||||
print 'generating for {0}'.format(student)
|
||||
break
|
||||
@@ -1,22 +1,30 @@
|
||||
from django.utils.simplejson import dumps
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from certificates.models import GeneratedCertificate
|
||||
from django.core.management.base import BaseCommand
|
||||
from certificates.models import certificate_status_for_student
|
||||
from certificates.queue import XQueueCertInterface
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
help = """
|
||||
This command finds all GeneratedCertificate objects that do not have a
|
||||
certificate generated. These come into being when a user requests a
|
||||
certificate, or when grade_all_students is called (for pre-generating
|
||||
certificates).
|
||||
Find all students that have need certificates
|
||||
and put certificate requests on the queue
|
||||
|
||||
It returns a json formatted list of users and their user ids
|
||||
This is only for BerkeleyX/CS169.1x/2012_Fall
|
||||
"""
|
||||
|
||||
def handle(self, *args, **options):
|
||||
users = GeneratedCertificate.objects.filter(
|
||||
download_url=None)
|
||||
user_output = [{'user_id':user.user_id, 'name':user.name}
|
||||
for user in users]
|
||||
self.stdout.write(dumps(user_output) + "\n")
|
||||
|
||||
# TODO This is only temporary for CS169 certs
|
||||
|
||||
course_id = 'BerkeleyX/CS169.1x/2012_Fall'
|
||||
enrolled_students = User.objects.filter(
|
||||
courseenrollment__course_id=course_id).prefetch_related(
|
||||
"groups").order_by('username')
|
||||
xq = XQueueCertInterface()
|
||||
for student in enrolled_students:
|
||||
if certificate_status_for_student(
|
||||
student, course_id)['status'] == 'unavailable':
|
||||
ret = xq.add_cert(student, course_id)
|
||||
if ret == 'generating':
|
||||
print 'generating for {0}'.format(student)
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Deleting model 'RevokedCertificate'
|
||||
db.delete_table('certificates_revokedcertificate')
|
||||
|
||||
# Deleting field 'GeneratedCertificate.name'
|
||||
db.delete_column('certificates_generatedcertificate', 'name')
|
||||
|
||||
# Adding field 'GeneratedCertificate.course_id'
|
||||
db.add_column('certificates_generatedcertificate', 'course_id',
|
||||
self.gf('django.db.models.fields.CharField')(default=False, max_length=255),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'GeneratedCertificate.key'
|
||||
db.add_column('certificates_generatedcertificate', 'key',
|
||||
self.gf('django.db.models.fields.CharField')(default=False, max_length=32),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
# Changing field 'GeneratedCertificate.grade'
|
||||
db.alter_column('certificates_generatedcertificate', 'grade', self.gf('django.db.models.fields.CharField')(max_length=5))
|
||||
|
||||
# Changing field 'GeneratedCertificate.certificate_id'
|
||||
db.alter_column('certificates_generatedcertificate', 'certificate_id', self.gf('django.db.models.fields.CharField')(max_length=32))
|
||||
|
||||
# Changing field 'GeneratedCertificate.download_url'
|
||||
db.alter_column('certificates_generatedcertificate', 'download_url', self.gf('django.db.models.fields.CharField')(max_length=128))
|
||||
|
||||
# Changing field 'GeneratedCertificate.graded_download_url'
|
||||
db.alter_column('certificates_generatedcertificate', 'graded_download_url', self.gf('django.db.models.fields.CharField')(max_length=128))
|
||||
|
||||
# Changing field 'GeneratedCertificate.graded_certificate_id'
|
||||
db.alter_column('certificates_generatedcertificate', 'graded_certificate_id', self.gf('django.db.models.fields.CharField')(max_length=32))
|
||||
|
||||
def backwards(self, orm):
|
||||
# Adding model 'RevokedCertificate'
|
||||
db.create_table('certificates_revokedcertificate', (
|
||||
('grade', self.gf('django.db.models.fields.CharField')(max_length=5, null=True)),
|
||||
('certificate_id', self.gf('django.db.models.fields.CharField')(default=None, max_length=32, null=True)),
|
||||
('explanation', self.gf('django.db.models.fields.TextField')(blank=True)),
|
||||
('graded_download_url', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('name', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
|
||||
('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)),
|
||||
('download_url', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)),
|
||||
('graded_certificate_id', self.gf('django.db.models.fields.CharField')(default=None, max_length=32, null=True)),
|
||||
))
|
||||
db.send_create_signal('certificates', ['RevokedCertificate'])
|
||||
|
||||
# Adding field 'GeneratedCertificate.name'
|
||||
db.add_column('certificates_generatedcertificate', 'name',
|
||||
self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Deleting field 'GeneratedCertificate.course_id'
|
||||
db.delete_column('certificates_generatedcertificate', 'course_id')
|
||||
|
||||
# Deleting field 'GeneratedCertificate.key'
|
||||
db.delete_column('certificates_generatedcertificate', 'key')
|
||||
|
||||
|
||||
# Changing field 'GeneratedCertificate.grade'
|
||||
db.alter_column('certificates_generatedcertificate', 'grade', self.gf('django.db.models.fields.CharField')(max_length=5, null=True))
|
||||
|
||||
# Changing field 'GeneratedCertificate.certificate_id'
|
||||
db.alter_column('certificates_generatedcertificate', 'certificate_id', self.gf('django.db.models.fields.CharField')(max_length=32, null=True))
|
||||
|
||||
# Changing field 'GeneratedCertificate.download_url'
|
||||
db.alter_column('certificates_generatedcertificate', 'download_url', self.gf('django.db.models.fields.CharField')(max_length=128, null=True))
|
||||
|
||||
# Changing field 'GeneratedCertificate.graded_download_url'
|
||||
db.alter_column('certificates_generatedcertificate', 'graded_download_url', self.gf('django.db.models.fields.CharField')(max_length=128, null=True))
|
||||
|
||||
# Changing field 'GeneratedCertificate.graded_certificate_id'
|
||||
db.alter_column('certificates_generatedcertificate', 'graded_certificate_id', self.gf('django.db.models.fields.CharField')(max_length=32, null=True))
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
|
||||
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
|
||||
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
|
||||
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
|
||||
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
|
||||
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
|
||||
},
|
||||
'certificates.generatedcertificate': {
|
||||
'Meta': {'object_name': 'GeneratedCertificate'},
|
||||
'certificate_id': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '32'}),
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '255'}),
|
||||
'download_url': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '128'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'grade': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '5'}),
|
||||
'graded_certificate_id': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '32'}),
|
||||
'graded_download_url': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '128'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'key': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '32'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['certificates']
|
||||
@@ -0,0 +1,95 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Deleting field 'GeneratedCertificate.graded_download_url'
|
||||
db.delete_column('certificates_generatedcertificate', 'graded_download_url')
|
||||
|
||||
# Deleting field 'GeneratedCertificate.graded_certificate_id'
|
||||
db.delete_column('certificates_generatedcertificate', 'graded_certificate_id')
|
||||
|
||||
# Adding field 'GeneratedCertificate.distinction'
|
||||
db.add_column('certificates_generatedcertificate', 'distinction',
|
||||
self.gf('django.db.models.fields.BooleanField')(default=False),
|
||||
keep_default=False)
|
||||
|
||||
# Adding unique constraint on 'GeneratedCertificate', fields ['course_id', 'user']
|
||||
db.create_unique('certificates_generatedcertificate', ['course_id', 'user_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing unique constraint on 'GeneratedCertificate', fields ['course_id', 'user']
|
||||
db.delete_unique('certificates_generatedcertificate', ['course_id', 'user_id'])
|
||||
|
||||
# Adding field 'GeneratedCertificate.graded_download_url'
|
||||
db.add_column('certificates_generatedcertificate', 'graded_download_url',
|
||||
self.gf('django.db.models.fields.CharField')(default=False, max_length=128),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'GeneratedCertificate.graded_certificate_id'
|
||||
db.add_column('certificates_generatedcertificate', 'graded_certificate_id',
|
||||
self.gf('django.db.models.fields.CharField')(default=False, max_length=32),
|
||||
keep_default=False)
|
||||
|
||||
# Deleting field 'GeneratedCertificate.distinction'
|
||||
db.delete_column('certificates_generatedcertificate', 'distinction')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'certificates.generatedcertificate': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'GeneratedCertificate'},
|
||||
'certificate_id': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '32'}),
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '255'}),
|
||||
'distinction': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'download_url': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '128'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'grade': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '5'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'key': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '32'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['certificates']
|
||||
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Deleting field 'GeneratedCertificate.enabled'
|
||||
db.delete_column('certificates_generatedcertificate', 'enabled')
|
||||
|
||||
# Adding field 'GeneratedCertificate.status'
|
||||
db.add_column('certificates_generatedcertificate', 'status',
|
||||
self.gf('django.db.models.fields.CharField')(default='unavailable', max_length=32),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Adding field 'GeneratedCertificate.enabled'
|
||||
db.add_column('certificates_generatedcertificate', 'enabled',
|
||||
self.gf('django.db.models.fields.BooleanField')(default=False),
|
||||
keep_default=False)
|
||||
|
||||
# Deleting field 'GeneratedCertificate.status'
|
||||
db.delete_column('certificates_generatedcertificate', 'status')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'certificates.generatedcertificate': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'GeneratedCertificate'},
|
||||
'certificate_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
|
||||
'distinction': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'download_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}),
|
||||
'grade': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '5', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'unavailable'", 'max_length': '32'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['certificates']
|
||||
@@ -0,0 +1,90 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Deleting field 'GeneratedCertificate.certificate_id'
|
||||
db.delete_column('certificates_generatedcertificate', 'certificate_id')
|
||||
|
||||
# Adding field 'GeneratedCertificate.verify_uuid'
|
||||
db.add_column('certificates_generatedcertificate', 'verify_uuid',
|
||||
self.gf('django.db.models.fields.CharField')(default='', max_length=32, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'GeneratedCertificate.download_uuid'
|
||||
db.add_column('certificates_generatedcertificate', 'download_uuid',
|
||||
self.gf('django.db.models.fields.CharField')(default='', max_length=32, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Adding field 'GeneratedCertificate.certificate_id'
|
||||
db.add_column('certificates_generatedcertificate', 'certificate_id',
|
||||
self.gf('django.db.models.fields.CharField')(default='', max_length=32, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Deleting field 'GeneratedCertificate.verify_uuid'
|
||||
db.delete_column('certificates_generatedcertificate', 'verify_uuid')
|
||||
|
||||
# Deleting field 'GeneratedCertificate.download_uuid'
|
||||
db.delete_column('certificates_generatedcertificate', 'download_uuid')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'certificates.generatedcertificate': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'GeneratedCertificate'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
|
||||
'distinction': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'download_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}),
|
||||
'download_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
|
||||
'grade': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '5', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'unavailable'", 'max_length': '32'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'verify_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['certificates']
|
||||
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'GeneratedCertificate.name'
|
||||
db.add_column('certificates_generatedcertificate', 'name',
|
||||
self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'GeneratedCertificate.created_date'
|
||||
db.add_column('certificates_generatedcertificate', 'created_date',
|
||||
self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, auto_now_add=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'GeneratedCertificate.modified_date'
|
||||
db.add_column('certificates_generatedcertificate', 'modified_date',
|
||||
self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, auto_now=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'GeneratedCertificate.name'
|
||||
db.delete_column('certificates_generatedcertificate', 'name')
|
||||
|
||||
# Deleting field 'GeneratedCertificate.created_date'
|
||||
db.delete_column('certificates_generatedcertificate', 'created_date')
|
||||
|
||||
# Deleting field 'GeneratedCertificate.modified_date'
|
||||
db.delete_column('certificates_generatedcertificate', 'modified_date')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'certificates.generatedcertificate': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'GeneratedCertificate'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
|
||||
'created_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'distinction': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'download_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}),
|
||||
'download_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
|
||||
'grade': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '5', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
|
||||
'modified_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'unavailable'", 'max_length': '32'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'verify_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['certificates']
|
||||
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'GeneratedCertificate.error_reason'
|
||||
db.add_column('certificates_generatedcertificate', 'error_reason',
|
||||
self.gf('django.db.models.fields.CharField')(default='', max_length=512, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'GeneratedCertificate.error_reason'
|
||||
db.delete_column('certificates_generatedcertificate', 'error_reason')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'certificates.generatedcertificate': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'GeneratedCertificate'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
|
||||
'created_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'distinction': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'download_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}),
|
||||
'download_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
|
||||
'error_reason': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
|
||||
'grade': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '5', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
|
||||
'modified_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'unavailable'", 'max_length': '32'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'verify_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['certificates']
|
||||
@@ -1,144 +1,108 @@
|
||||
from django.conf import settings as settings
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
'''
|
||||
"""
|
||||
Certificates are created for a student and an offering of a course.
|
||||
|
||||
When a certificate is generated, a unique ID is generated so that
|
||||
the certificate can be verified later. The ID is a UUID4, so that
|
||||
it can't be easily guessed and so that it is unique. Even though
|
||||
we save these generated certificates (for later verification), we
|
||||
also record the UUID so that if we regenerate the certificate it
|
||||
will have the same UUID.
|
||||
it can't be easily guessed and so that it is unique.
|
||||
|
||||
If certificates are being generated on the fly, a GeneratedCertificate
|
||||
should be created with the user, certificate_id, and enabled set
|
||||
when a student requests a certificate. When the certificate has been
|
||||
generated, the download_url should be set.
|
||||
Certificates are generated in batches by a cron job, when a
|
||||
certificate is available for download the GeneratedCertificate
|
||||
table is updated with information that will be displayed
|
||||
on the course overview page.
|
||||
|
||||
Certificates can also be pre-generated. In this case, the user,
|
||||
certificate_id, and download_url are all set before the user does
|
||||
anything. When the user requests the certificate, only enabled
|
||||
needs to be set to true.
|
||||
|
||||
'''
|
||||
State diagram:
|
||||
|
||||
[deleted,error,unavailable] [error,downloadable]
|
||||
+ + +
|
||||
| | |
|
||||
| | |
|
||||
add_cert regen_cert del_cert
|
||||
| | |
|
||||
v v v
|
||||
[generating] [regenerating] [deleting]
|
||||
+ + +
|
||||
| | |
|
||||
certificate certificate certificate
|
||||
created removed,created deleted
|
||||
+----------------+-------------+------->[error]
|
||||
| | |
|
||||
| | |
|
||||
v v v
|
||||
[downloadable] [downloadable] [deleted]
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class CertificateStatuses(object):
|
||||
unavailable = 'unavailable'
|
||||
generating = 'generating'
|
||||
regenerating = 'regenerating'
|
||||
deleting = 'deleting'
|
||||
deleted = 'deleted'
|
||||
downloadable = 'downloadable'
|
||||
notpassing = 'notpassing'
|
||||
error = 'error'
|
||||
|
||||
|
||||
class GeneratedCertificate(models.Model):
|
||||
user = models.ForeignKey(User, db_index=True)
|
||||
# This is the name at the time of request
|
||||
user = models.ForeignKey(User)
|
||||
course_id = models.CharField(max_length=255, blank=True, default='')
|
||||
verify_uuid = models.CharField(max_length=32, blank=True, default='')
|
||||
download_uuid = models.CharField(max_length=32, blank=True, default='')
|
||||
download_url = models.CharField(max_length=128, blank=True, default='')
|
||||
grade = models.CharField(max_length=5, blank=True, default='')
|
||||
key = models.CharField(max_length=32, blank=True, default='')
|
||||
distinction = models.BooleanField(default=False)
|
||||
status = models.CharField(max_length=32, default='unavailable')
|
||||
name = models.CharField(blank=True, max_length=255)
|
||||
created_date = models.DateTimeField(
|
||||
auto_now_add=True, default=datetime.now)
|
||||
modified_date = models.DateTimeField(
|
||||
auto_now=True, default=datetime.now)
|
||||
error_reason = models.CharField(max_length=512, blank=True, default='')
|
||||
|
||||
certificate_id = models.CharField(max_length=32, null=True, default=None)
|
||||
graded_certificate_id = models.CharField(max_length=32, null=True, default=None)
|
||||
|
||||
download_url = models.CharField(max_length=128, null=True)
|
||||
graded_download_url = models.CharField(max_length=128, null=True)
|
||||
|
||||
grade = models.CharField(max_length=5, null=True)
|
||||
|
||||
# enabled should only be true if the student has earned a grade in the course
|
||||
# The student must have a grade and request a certificate for enabled to be True
|
||||
enabled = models.BooleanField(default=False)
|
||||
class Meta:
|
||||
unique_together = (('user', 'course_id'),)
|
||||
|
||||
|
||||
class RevokedCertificate(models.Model):
|
||||
"""
|
||||
This model is for when a GeneratedCertificate must be regenerated. This model
|
||||
contains all the same fields, to store a record of what the GeneratedCertificate
|
||||
was before it was revoked (at which time all of it's information can change when
|
||||
it is regenerated).
|
||||
|
||||
GeneratedCertificate may be deleted once they are revoked, and then created again.
|
||||
For this reason, the only link between a GeneratedCertificate and RevokedCertificate
|
||||
is that they share the same user.
|
||||
"""
|
||||
####-------------------New Fields--------------------####
|
||||
explanation = models.TextField(blank=True)
|
||||
|
||||
####---------Fields from GeneratedCertificate---------####
|
||||
user = models.ForeignKey(User, db_index=True)
|
||||
# This is the name at the time of request
|
||||
name = models.CharField(blank=True, max_length=255)
|
||||
|
||||
certificate_id = models.CharField(max_length=32, null=True, default=None)
|
||||
graded_certificate_id = models.CharField(max_length=32, null=True, default=None)
|
||||
|
||||
download_url = models.CharField(max_length=128, null=True)
|
||||
graded_download_url = models.CharField(max_length=128, null=True)
|
||||
|
||||
grade = models.CharField(max_length=5, null=True)
|
||||
|
||||
enabled = models.BooleanField(default=False)
|
||||
|
||||
|
||||
def revoke_certificate(certificate, explanation):
|
||||
"""
|
||||
This method takes a GeneratedCertificate. It records its information from the certificate
|
||||
into a RevokedCertificate, and then marks the certificate as needing regenerating.
|
||||
When the new certificiate is regenerated it will have new IDs and download URLS.
|
||||
|
||||
Once this method has been called, it is safe to delete the certificate, or modify the
|
||||
certificate's name or grade until it has been generated again.
|
||||
"""
|
||||
revoked = RevokedCertificate(user=certificate.user,
|
||||
name=certificate.name,
|
||||
certificate_id=certificate.certificate_id,
|
||||
graded_certificate_id=certificate.graded_certificate_id,
|
||||
download_url=certificate.download_url,
|
||||
graded_download_url=certificate.graded_download_url,
|
||||
grade=certificate.grade,
|
||||
enabled=certificate.enabled)
|
||||
|
||||
revoked.explanation = explanation
|
||||
|
||||
certificate.certificate_id = None
|
||||
certificate.graded_certificate_id = None
|
||||
certificate.download_url = None
|
||||
certificate.graded_download_url = None
|
||||
|
||||
certificate.save()
|
||||
revoked.save()
|
||||
|
||||
|
||||
def certificate_state_for_student(student, grade):
|
||||
def certificate_status_for_student(student, course_id):
|
||||
'''
|
||||
This returns a dictionary with a key for state, and other information. The state is one of the
|
||||
following:
|
||||
This returns a dictionary with a key for status, and other information.
|
||||
The status is one of the following:
|
||||
|
||||
unavailable - A student is not eligible for a certificate.
|
||||
requestable - A student is eligible to request a certificate
|
||||
generating - A student has requested a certificate, but it is not generated yet.
|
||||
downloadable - The certificate has been requested and is available for download.
|
||||
unavailable - A student is not eligible for a certificate.
|
||||
generating - A request has been made to generate a certificate,
|
||||
but it has not been generated yet.
|
||||
regenerating - A request has been made to regenerate a certificate,
|
||||
but it has not been generated yet.
|
||||
deleting - A request has been made to delete a certificate.
|
||||
|
||||
If the state is "downloadable", the dictionary also contains "download_url" and "graded_download_url".
|
||||
deleted - The certificate has been deleted.
|
||||
downloadable - The certificate is available for download.
|
||||
notpassing - The student was graded but is not passing
|
||||
|
||||
If the status is "downloadable", the dictionary also contains
|
||||
"download_url".
|
||||
|
||||
If the student has been graded, the dictionary also contains their
|
||||
grade for the course.
|
||||
'''
|
||||
|
||||
if grade:
|
||||
#TODO: Remove the following after debugging
|
||||
if settings.DEBUG_SURVEY:
|
||||
return {'state': 'requestable'}
|
||||
try:
|
||||
generated_certificate = GeneratedCertificate.objects.get(
|
||||
user=student, course_id=course_id)
|
||||
d = {'status': generated_certificate.status}
|
||||
if generated_certificate.grade:
|
||||
d['grade'] = generated_certificate.grade
|
||||
if generated_certificate.status == CertificateStatuses.downloadable:
|
||||
d['download_url'] = generated_certificate.download_url
|
||||
|
||||
try:
|
||||
generated_certificate = GeneratedCertificate.objects.get(user=student)
|
||||
if generated_certificate.enabled:
|
||||
if generated_certificate.download_url:
|
||||
return {'state': 'downloadable',
|
||||
'download_url': generated_certificate.download_url,
|
||||
'graded_download_url': generated_certificate.graded_download_url}
|
||||
else:
|
||||
return {'state': 'generating'}
|
||||
else:
|
||||
# If enabled=False, it may have been pre-generated but not yet requested
|
||||
# Our output will be the same as if the GeneratedCertificate did not exist
|
||||
pass
|
||||
except GeneratedCertificate.DoesNotExist:
|
||||
pass
|
||||
return {'state': 'requestable'}
|
||||
else:
|
||||
# No grade, no certificate. No exceptions
|
||||
return {'state': 'unavailable'}
|
||||
return d
|
||||
except GeneratedCertificate.DoesNotExist:
|
||||
pass
|
||||
return {'status': CertificateStatuses.unavailable}
|
||||
|
||||
262
lms/djangoapps/certificates/queue.py
Normal file
262
lms/djangoapps/certificates/queue.py
Normal file
@@ -0,0 +1,262 @@
|
||||
from certificates.models import GeneratedCertificate
|
||||
from certificates.models import certificate_status_for_student
|
||||
from certificates.models import CertificateStatuses as status
|
||||
|
||||
from courseware import grades, courses
|
||||
from django.test.client import RequestFactory
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
from capa.xqueue_interface import make_xheader, make_hashkey
|
||||
from django.conf import settings
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from student.models import UserProfile
|
||||
|
||||
import json
|
||||
import random
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XQueueCertInterface(object):
|
||||
"""
|
||||
XQueueCertificateInterface provides an
|
||||
interface to the xqueue server for
|
||||
managing student certificates.
|
||||
|
||||
Instantiating an object will create a new
|
||||
connection to the queue server.
|
||||
|
||||
See models.py for valid state transitions,
|
||||
summary of methods:
|
||||
|
||||
add_cert: Add a new certificate. Puts a single
|
||||
request on the queue for the student/course.
|
||||
Once the certificate is generated a post
|
||||
will be made to the update_certificate
|
||||
view which will save the certificate
|
||||
download URL.
|
||||
|
||||
regen_cert: Regenerate an existing certificate.
|
||||
For a user that already has a certificate
|
||||
this will delete the existing one and
|
||||
generate a new cert.
|
||||
|
||||
|
||||
del_cert: Delete an existing certificate
|
||||
For a user that already has a certificate
|
||||
this will delete his cert.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, request=None):
|
||||
|
||||
# Get basic auth (username/password) for
|
||||
# xqueue connection if it's in the settings
|
||||
|
||||
if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
|
||||
requests_auth = HTTPBasicAuth(
|
||||
*settings.XQUEUE_INTERFACE['basic_auth'])
|
||||
else:
|
||||
requests_auth = None
|
||||
|
||||
if request is None:
|
||||
factory = RequestFactory()
|
||||
self.request = factory.get('/')
|
||||
else:
|
||||
self.request = request
|
||||
|
||||
self.xqueue_interface = XQueueInterface(
|
||||
settings.XQUEUE_INTERFACE['url'],
|
||||
settings.XQUEUE_INTERFACE['django_auth'],
|
||||
requests_auth,
|
||||
)
|
||||
|
||||
def regen_cert(self, student, course_id):
|
||||
"""
|
||||
Arguments:
|
||||
student - User.object
|
||||
course_id - courseenrollment.course_id (string)
|
||||
|
||||
Removes certificate for a student, will change
|
||||
the certificate status to 'regenerating'.
|
||||
|
||||
Certificate must be in the 'error' or 'downloadable' state
|
||||
|
||||
If the student has a passing grade a certificate
|
||||
request will be put on the queue
|
||||
|
||||
If the student is not passing his state will change
|
||||
to status.notpassing
|
||||
|
||||
otherwise it will return the current state
|
||||
|
||||
"""
|
||||
|
||||
VALID_STATUSES = [status.error, status.downloadable]
|
||||
|
||||
cert_status = certificate_status_for_student(
|
||||
student, course_id)['status']
|
||||
|
||||
if cert_status in VALID_STATUSES:
|
||||
# grade the student
|
||||
course = courses.get_course_by_id(course_id)
|
||||
grade = grades.grade(student, self.request, course)
|
||||
|
||||
profile = UserProfile.objects.get(user=student)
|
||||
try:
|
||||
cert = GeneratedCertificate.objects.get(
|
||||
user=student, course_id=course_id)
|
||||
except GeneratedCertificate.DoesNotExist:
|
||||
logger.critical("Attempting to regenerate a certificate"
|
||||
"for a user that doesn't have one")
|
||||
raise
|
||||
|
||||
if grade['grade'] is not None:
|
||||
|
||||
cert.status = status.regenerating
|
||||
cert.name = profile.name
|
||||
|
||||
contents = {
|
||||
'action': 'regen',
|
||||
'delete_verify_uuid': cert.verify_uuid,
|
||||
'delete_download_uuid': cert.download_uuid,
|
||||
'username': cert.user.username,
|
||||
'course_id': cert.course_id,
|
||||
'name': profile.name,
|
||||
}
|
||||
|
||||
key = cert.key
|
||||
self._send_to_xqueue(contents, key)
|
||||
cert.save()
|
||||
|
||||
else:
|
||||
cert.status = status.notpassing
|
||||
cert.name = profile.name
|
||||
cert.save()
|
||||
|
||||
return cert_status
|
||||
|
||||
def del_cert(self, student, course_id):
|
||||
|
||||
"""
|
||||
Arguments:
|
||||
student - User.object
|
||||
course_id - courseenrollment.course_id (string)
|
||||
|
||||
Removes certificate for a student, will change
|
||||
the certificate status to 'deleting'.
|
||||
|
||||
Certificate must be in the 'error' or 'downloadable' state
|
||||
otherwise it will return the current state
|
||||
|
||||
"""
|
||||
|
||||
VALID_STATUSES = [status.error, status.downloadable]
|
||||
|
||||
cert_status = certificate_status_for_student(
|
||||
student, course_id)['status']
|
||||
|
||||
if cert_status in VALID_STATUSES:
|
||||
|
||||
try:
|
||||
cert = GeneratedCertificate.objects.get(
|
||||
user=student, course_id=course_id)
|
||||
except GeneratedCertificate.DoesNotExist:
|
||||
logger.warning("Attempting to delete a certificate"
|
||||
"for a user that doesn't have one")
|
||||
raise
|
||||
|
||||
cert.status = status.deleting
|
||||
|
||||
contents = {
|
||||
'action': 'delete',
|
||||
'delete_verify_uuid': cert.verify_uuid,
|
||||
'delete_download_uuid': cert.download_uuid,
|
||||
'username': cert.user.username,
|
||||
}
|
||||
|
||||
key = cert.key
|
||||
self._send_to_xqueue(contents, key)
|
||||
cert.save()
|
||||
return cert_status
|
||||
|
||||
def add_cert(self, student, course_id):
|
||||
"""
|
||||
|
||||
Arguments:
|
||||
student - User.object
|
||||
course_id - courseenrollment.course_id (string)
|
||||
|
||||
Request a new certificate for a student.
|
||||
Will change the certificate status to 'deleting'.
|
||||
|
||||
Certificate must be in the 'unavailable', 'error',
|
||||
or 'deleted' state.
|
||||
|
||||
If a student has a passing grade a request will made
|
||||
for a new cert
|
||||
|
||||
If a student does not have a passing grade the status
|
||||
will change to status.notpassing
|
||||
|
||||
Returns the student's status
|
||||
|
||||
"""
|
||||
|
||||
VALID_STATUSES = [status.unavailable, status.deleted, status.error,
|
||||
status.notpassing]
|
||||
|
||||
cert_status = certificate_status_for_student(
|
||||
student, course_id)['status']
|
||||
|
||||
if cert_status in VALID_STATUSES:
|
||||
# grade the student
|
||||
course = courses.get_course_by_id(course_id)
|
||||
grade = grades.grade(student, self.request, course)
|
||||
profile = UserProfile.objects.get(user=student)
|
||||
cert, created = GeneratedCertificate.objects.get_or_create(
|
||||
user=student, course_id=course_id)
|
||||
|
||||
if grade['grade'] is not None:
|
||||
cert_status = status.generating
|
||||
key = make_hashkey(random.random())
|
||||
|
||||
cert.status = cert_status
|
||||
cert.grade = grade['percent']
|
||||
cert.user = student
|
||||
cert.course_id = course_id
|
||||
cert.key = key
|
||||
cert.name = profile.name
|
||||
|
||||
contents = {
|
||||
'action': 'create',
|
||||
'username': student.username,
|
||||
'course_id': course_id,
|
||||
'name': profile.name,
|
||||
}
|
||||
|
||||
self._send_to_xqueue(contents, key)
|
||||
cert.save()
|
||||
else:
|
||||
cert_status = status.notpassing
|
||||
|
||||
cert.status = cert_status
|
||||
cert.user = student
|
||||
cert.course_id = course_id
|
||||
cert.name = profile.name
|
||||
cert.save()
|
||||
|
||||
return cert_status
|
||||
|
||||
def _send_to_xqueue(self, contents, key):
|
||||
|
||||
xheader = make_xheader(
|
||||
'https://{0}/update_certificate?{1}'.format(
|
||||
settings.SITE_NAME, key), key, settings.CERT_QUEUE)
|
||||
|
||||
(error, msg) = self.xqueue_interface.send_to_queue(
|
||||
header=xheader, body=json.dumps(contents))
|
||||
if error:
|
||||
logger.critical('Unable to add a request to the queue')
|
||||
raise Exception('Unable to send queue message')
|
||||
@@ -1,146 +1,77 @@
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from certificates.models import GeneratedCertificate
|
||||
from certificates.models import CertificateStatuses as status
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.http import HttpResponse
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.mail import send_mail
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
|
||||
import courseware.grades as grades
|
||||
from certificates.models import GeneratedCertificate, certificate_state_for_student, revoke_certificate
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from student.models import UserProfile
|
||||
#TODO: Finish migrating these changes from stable
|
||||
# from student.survey_questions import exit_survey_list_for_student
|
||||
# from student.views import student_took_survey, record_exit_survey
|
||||
|
||||
log = logging.getLogger("mitx.certificates")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
def certificate_request(request):
|
||||
''' Attempt to send a certificate. '''
|
||||
if not settings.END_COURSE_ENABLED:
|
||||
raise Http404
|
||||
@csrf_exempt
|
||||
def update_certificate(request):
|
||||
"""
|
||||
Will update GeneratedCertificate for a new certificate or
|
||||
modify an existing certificate entry.
|
||||
|
||||
See models.py for a state diagram of certificate states
|
||||
|
||||
This view should only ever be accessed by the xqueue server
|
||||
"""
|
||||
|
||||
if request.method == "POST":
|
||||
honor_code_verify = request.POST.get('cert_request_honor_code_verify', 'false')
|
||||
name_verify = request.POST.get('cert_request_name_verify', 'false')
|
||||
id_verify = request.POST.get('cert_request_id_verify', 'false')
|
||||
error = ''
|
||||
|
||||
def return_error(error):
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': error}))
|
||||
|
||||
if honor_code_verify != 'true':
|
||||
error += 'Please verify that you have followed the honor code to receive a certificate. '
|
||||
|
||||
if name_verify != 'true':
|
||||
error += 'Please verify that your name is correct to receive a certificate. '
|
||||
|
||||
if id_verify != 'true':
|
||||
error += 'Please certify that you understand the unique ID on the certificate. '
|
||||
|
||||
if len(error) > 0:
|
||||
return return_error(error)
|
||||
|
||||
survey_response = record_exit_survey(request, internal_request=True)
|
||||
if not survey_response['success']:
|
||||
return return_error(survey_response['error'])
|
||||
|
||||
grade = None
|
||||
student_gradesheet = grades.grade(request.user, request, course)
|
||||
grade = student_gradesheet['grade']
|
||||
|
||||
if not grade:
|
||||
return return_error('You have not earned a grade in this course. ')
|
||||
|
||||
generate_certificate(request.user, grade)
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
else:
|
||||
#This is not a POST, we should render the page with the form
|
||||
|
||||
student_gradesheet = grades.grade(request.user, request, course)
|
||||
certificate_state = certificate_state_for_student(request.user, grade_sheet['grade'])
|
||||
|
||||
if certificate_state['state'] != "requestable":
|
||||
return redirect("/profile")
|
||||
|
||||
user_info = UserProfile.objects.get(user=request.user)
|
||||
|
||||
took_survey = student_took_survey(user_info)
|
||||
if settings.DEBUG_SURVEY:
|
||||
took_survey = False
|
||||
survey_list = []
|
||||
if not took_survey:
|
||||
survey_list = exit_survey_list_for_student(request.user)
|
||||
|
||||
context = {'certificate_state': certificate_state,
|
||||
'took_survey': took_survey,
|
||||
'survey_list': survey_list,
|
||||
'name': user_info.name}
|
||||
|
||||
return render_to_response('cert_request.html', context)
|
||||
|
||||
|
||||
# This method should only be called if the user has a grade and has requested a certificate
|
||||
def generate_certificate(user, grade):
|
||||
# Make sure to see the comments in models.GeneratedCertificate to read about the valid
|
||||
# states for a GeneratedCertificate object
|
||||
if grade and user.is_active:
|
||||
generated_certificate = None
|
||||
xqueue_body = json.loads(request.POST.get('xqueue_body'))
|
||||
xqueue_header = json.loads(request.POST.get('xqueue_header'))
|
||||
|
||||
try:
|
||||
generated_certificate = GeneratedCertificate.objects.get(user=user)
|
||||
cert = GeneratedCertificate.objects.get(
|
||||
user__username=xqueue_body['username'],
|
||||
course_id=xqueue_body['course_id'],
|
||||
key=xqueue_header['lms_key'])
|
||||
|
||||
except GeneratedCertificate.DoesNotExist:
|
||||
generated_certificate = GeneratedCertificate(user=user)
|
||||
logger.critical('Unable to lookup certificate\n'
|
||||
'xqueue_body: {0}\n'
|
||||
'xqueue_header: {1}'.format(
|
||||
xqueue_body, xqueue_header))
|
||||
|
||||
generated_certificate.enabled = True
|
||||
if generated_certificate.graded_download_url and (generated_certificate.grade != grade):
|
||||
log.critical(u"A graded certificate has been pre-generated with the grade "
|
||||
"of {gen_grade} but requested by user id {userid} with grade "
|
||||
"{req_grade}! The download URLs were {graded_dl_url} and "
|
||||
"{ungraded_dl_url}".format(
|
||||
gen_grade=generated_certificate.grade,
|
||||
req_grade=grade,
|
||||
graded_dl_url=generated_certificate.graded_download_url,
|
||||
ungraded_dl_url=generated_certificate.download_url,
|
||||
userid=user.id))
|
||||
revoke_certificate(generated_certificate, "The grade on this certificate may be inaccurate.")
|
||||
return HttpResponse(json.dumps({
|
||||
'return_code': 1,
|
||||
'content': 'unable to lookup key'}),
|
||||
mimetype='application/json')
|
||||
|
||||
user_name = UserProfile.objects.get(user=user).name
|
||||
if generated_certificate.download_url and (generated_certificate.name != user_name):
|
||||
log.critical(u"A Certificate has been pre-generated with the name of "
|
||||
"{gen_name} but current name is {user_name} (user id is "
|
||||
"{userid})! The download URLs were {graded_dl_url} and "
|
||||
"{ungraded_dl_url}".format(
|
||||
gen_name=generated_certificate.name.encode('utf-8'),
|
||||
user_name=user_name.encode('utf-8'),
|
||||
graded_dl_url=generated_certificate.graded_download_url,
|
||||
ungraded_dl_url=generated_certificate.download_url,
|
||||
userid=user.id))
|
||||
revoke_certificate(generated_certificate, "The name on this certificate may be inaccurate.")
|
||||
if 'error' in xqueue_body:
|
||||
cert.status = status.error
|
||||
if 'error_reason' in xqueue_body:
|
||||
|
||||
generated_certificate.grade = grade
|
||||
generated_certificate.name = user_name
|
||||
generated_certificate.save()
|
||||
# Hopefully we will record a meaningful error
|
||||
# here if something bad happened during the
|
||||
# certificate generation process
|
||||
#
|
||||
# example:
|
||||
# (aamorm BerkeleyX/CS169.1x/2012_Fall)
|
||||
# <class 'simples3.bucket.S3Error'>:
|
||||
# HTTP error (reason=error(32, 'Broken pipe'), filename=None) :
|
||||
# certificate_agent.py:175
|
||||
|
||||
certificate_id = generated_certificate.certificate_id
|
||||
|
||||
log.debug("Generating certificate for " + str(user.username) + " with ID: " + str(certificate_id))
|
||||
|
||||
# TODO: If the certificate was pre-generated, send the email that it is ready to download
|
||||
if certificate_state_for_student(user, grade)['state'] == "downloadable":
|
||||
subject = render_to_string('emails/certificate_ready_subject.txt', {})
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('emails/certificate_ready.txt', {})
|
||||
|
||||
res = send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email, ])
|
||||
|
||||
else:
|
||||
log.warning("Asked to generate a certificate for student " + str(user.username) + " but with a grade of " + str(grade) + " and active status " + str(user.is_active))
|
||||
cert.error_reason = xqueue_body['error_reason']
|
||||
else:
|
||||
if cert.status in [status.generating, status.regenerating]:
|
||||
cert.download_uuid = xqueue_body['download_uuid']
|
||||
cert.verify_uuid = xqueue_body['verify_uuid']
|
||||
cert.download_url = xqueue_body['url']
|
||||
cert.status = status.downloadable
|
||||
elif cert.status in [status.deleting]:
|
||||
cert.status = status.deleted
|
||||
else:
|
||||
logger.critical('Invalid state for cert update: {0}'.format(
|
||||
cert.status))
|
||||
return HttpResponse(json.dumps({
|
||||
'return_code': 1,
|
||||
'content': 'invalid cert status'}),
|
||||
mimetype='application/json')
|
||||
cert.save()
|
||||
return HttpResponse(json.dumps({'return_code': 0}),
|
||||
mimetype='application/json')
|
||||
|
||||
@@ -22,7 +22,7 @@ DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
|
||||
# Disable askbot, enable Berkeley forums
|
||||
MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
|
||||
|
||||
# IMPORTANT: With this enabled, the server must always be behind a proxy that
|
||||
# IMPORTANT: With this enabled, the server must always be behind a proxy that
|
||||
# strips the header HTTP_X_FORWARDED_PROTO from client requests. Otherwise,
|
||||
# a user can fool our server into thinking it was an https connection.
|
||||
# See https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
|
||||
@@ -61,6 +61,7 @@ SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {})
|
||||
VIRTUAL_UNIVERSITIES = ENV_TOKENS.get('VIRTUAL_UNIVERSITIES', [])
|
||||
COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL",'')
|
||||
COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY",'')
|
||||
CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull')
|
||||
|
||||
############################## SECURE AUTH ITEMS ###############################
|
||||
# Secret things: passwords, access keys, etc.
|
||||
|
||||
@@ -26,7 +26,6 @@ $sidebar-color: #f6f6f6;
|
||||
$outer-border-color: #aaa;
|
||||
|
||||
// old variables
|
||||
|
||||
$light-gray: #ddd;
|
||||
$dark-gray: #333;
|
||||
$text-color: $dark-gray;
|
||||
|
||||
@@ -132,7 +132,7 @@ img {
|
||||
}
|
||||
|
||||
.site-status {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.toast-notification {
|
||||
|
||||
@@ -215,3 +215,36 @@ div.course-wrapper {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.xmodule_VideoModule {
|
||||
margin-bottom: 30px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
section.self-assessment {
|
||||
textarea.answer {
|
||||
height: 200px;
|
||||
padding: 5px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
textarea.hint {
|
||||
height: 100px;
|
||||
padding: 5px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
div {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,7 @@
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: flex-grid(12);
|
||||
z-index: 20;
|
||||
@include transition(all, 0.15s, linear);
|
||||
|
||||
&:last-child {
|
||||
@@ -318,6 +319,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.course-status-completed {
|
||||
background: #ccc;
|
||||
color: #fff;
|
||||
|
||||
p {
|
||||
color: #222;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.enter-course {
|
||||
@include button(shiny, $blue);
|
||||
@include box-sizing(border-box);
|
||||
@@ -357,10 +371,113 @@
|
||||
border-color: darken(rgb(200,200,200), 3%);
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
|
||||
}
|
||||
|
||||
.course-status-completed {
|
||||
background: #888;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-status {
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(0 1px 4px 0 rgba(0,0,0, 0.1), inset 0 -1px 0 0 rgba(255,255,255, 0.8), inset 0 1px 0 0 rgba(255,255,255, 0.8));
|
||||
display: none;
|
||||
position: relative;
|
||||
top: -15px;
|
||||
z-index: 10;
|
||||
margin: 0 0 20px 0;
|
||||
padding: 15px 20px;
|
||||
font-family: "Open Sans", Verdana, Geneva, sans-serif;
|
||||
background: #fffcf0;
|
||||
border: 1px solid #ccc;
|
||||
|
||||
.message-copy {
|
||||
margin: 0;
|
||||
|
||||
.grade-value {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
@include clearfix;
|
||||
list-style: none;
|
||||
margin: 15px 0 0 0;
|
||||
padding: 0;
|
||||
|
||||
.action {
|
||||
float: left;
|
||||
margin:0 15px 10px 0;
|
||||
|
||||
.btn, .cta {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@include button(shiny, $blue);
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(3px);
|
||||
float: left;
|
||||
font: normal 0.8rem/1.2rem $sans-serif;
|
||||
letter-spacing: 1px;
|
||||
padding: 6px 12px;
|
||||
text-align: center;
|
||||
|
||||
&.disabled {
|
||||
@include button(shiny, #eee);
|
||||
cursor: default !important;
|
||||
|
||||
&:hover {
|
||||
background: #eee;
|
||||
background-image: -webkit-linear-gradient(top, #EEE 0%, #C2C2C2 50%, #ABABAB 50%, #B0B0B0 100%);
|
||||
background-image: -moz-linear-gradient(top, #EEE 0%, #C2C2C2 50%, #ABABAB 50%, #B0B0B0 100%);
|
||||
background-image: -ms-linear-gradient(top, #EEE 0%, #C2C2C2 50%, #ABABAB 50%, #B0B0B0 100%);
|
||||
background-image: -o-linear-gradient(top, #EEE 0%, #C2C2C2 50%, #ABABAB 50%, #B0B0B0 100%);
|
||||
background-image: linear-gradient(top, #EEE 0%, #C2C2C2 50%, #ABABAB 50%, #B0B0B0 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cta {
|
||||
@include button(shiny, #666);
|
||||
float: left;
|
||||
font: normal 0.8rem/1.2rem $sans-serif;
|
||||
letter-spacing: 1px;
|
||||
padding: 6px 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.course-status-processing {
|
||||
|
||||
}
|
||||
|
||||
&.course-status-certnotavailable {
|
||||
// background: #fee8d6;
|
||||
}
|
||||
|
||||
&.course-status-certrendering {
|
||||
// background: #d9e7db;
|
||||
|
||||
.cta {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.course-status-certavailable {
|
||||
// background: #d9e7db;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
a.unenroll {
|
||||
float: right;
|
||||
font-style: italic;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
from courseware.courses import course_image_url, get_course_about_section
|
||||
from courseware.access import has_access
|
||||
from certificates.models import CertificateStatuses
|
||||
%>
|
||||
<%inherit file="main.html" />
|
||||
|
||||
@@ -135,8 +136,16 @@
|
||||
<h2 class="university">${get_course_about_section(course, 'university')}</h2>
|
||||
<h3>${course.number} ${course.title}</h3>
|
||||
</hgroup>
|
||||
<section class="course-status">
|
||||
<p>Class Starts - <span>${course.start_date_text}</span></p>
|
||||
<section class="course-status course-status-completed">
|
||||
<p>
|
||||
% if course.has_ended():
|
||||
Course Completed - <span>${course.end_date_text}</span>
|
||||
% elif course.has_started():
|
||||
Course Started - <span>${course.start_date_text}</span>
|
||||
% else: # hasn't started yet
|
||||
Course Starts - <span>${course.start_date_text}</span>
|
||||
% endif
|
||||
</p>
|
||||
</section>
|
||||
% if course.id in show_courseware_links_for:
|
||||
<p class="enter-course">View Courseware</p>
|
||||
@@ -144,6 +153,68 @@
|
||||
</section>
|
||||
</a>
|
||||
</article>
|
||||
|
||||
<%
|
||||
cert_status = cert_statuses.get(course.id)
|
||||
%>
|
||||
% if course.has_ended() and cert_status:
|
||||
<%
|
||||
passing_grade = False
|
||||
cert_button = False
|
||||
survey_button = False
|
||||
if cert_status['status'] in [CertificateStatuses.generating, CertificateStatuses.regenerating]:
|
||||
status_css_class = 'course-status-certrendering'
|
||||
cert_button = True
|
||||
survey_button = True
|
||||
passing_grade = True
|
||||
elif cert_status['status'] == CertificateStatuses.downloadable:
|
||||
status_css_class = 'course-status-certavailable'
|
||||
cert_button = True
|
||||
survey_button = True
|
||||
passing_grade = True
|
||||
elif cert_status['status'] == CertificateStatuses.notpassing:
|
||||
status_css_class = 'course-status-certnotavailable'
|
||||
survey_button = True
|
||||
else:
|
||||
# This is primarily the 'unavailable' state, but also 'error', 'deleted', etc.
|
||||
status_css_class = 'course-status-processing'
|
||||
|
||||
if survey_button and not course.end_of_course_survey_url:
|
||||
survey_button = False
|
||||
%>
|
||||
<div class="message message-status ${status_css_class} is-shown">
|
||||
|
||||
% if cert_status['status'] == CertificateStatuses.unavailable:
|
||||
<p class="message-copy">Final course details are being wrapped up at this time.
|
||||
Your final standing will be available shortly.</p>
|
||||
% elif passing_grade:
|
||||
<p class="message-copy">You have received a grade of
|
||||
<span class="grade-value">${cert_status['grade']}</span>
|
||||
in this course.</p>
|
||||
% elif cert_status['status'] == CertificateStatuses.notpassing:
|
||||
<p class="message-copy">You did not complete the necessary requirements for completion of this course.
|
||||
</p>
|
||||
% endif
|
||||
% if cert_button or survey_button:
|
||||
<ul class="actions">
|
||||
% if cert_button and cert_status['status'] in [CertificateStatuses.generating, CertificateStatuses.regenerating]:
|
||||
<li class="action"><span class="btn disabled" href="">Your Certificate is Generating</span></li>
|
||||
% elif cert_button and cert_status['status'] == CertificateStatuses.downloadable:
|
||||
<li class="action">
|
||||
<a class="btn" href="${cert_status['download_url']}"
|
||||
title="This link will open/download a PDF document">
|
||||
Download Your PDF Certificate</a></li>
|
||||
% endif
|
||||
% if survey_button:
|
||||
<li class="action"><a class="cta" href="${course.end_of_course_survey_url}">
|
||||
Complete our course feedback survey</a></li>
|
||||
% endif
|
||||
</ul>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
% endif
|
||||
|
||||
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">Unregister</a>
|
||||
|
||||
% endfor
|
||||
@@ -280,4 +351,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<%static:css group='ie-fixes'/>
|
||||
<![endif]-->
|
||||
<meta name="path_prefix" content="${MITX_ROOT_URL}">
|
||||
<meta name="google-site-verification" content="_mipQ4AtZQDNmbtOkwehQDOgCxUUV2fb_C0b6wbiRHY" />
|
||||
|
||||
% if not course:
|
||||
<%include file="google_analytics.html" />
|
||||
|
||||
7
lms/templates/self_assessment_hint.html
Normal file
7
lms/templates/self_assessment_hint.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="hint">
|
||||
<div class="hint-prompt">
|
||||
${hint_prompt}
|
||||
</div>
|
||||
<textarea name="hint" class="hint" cols="70" rows="5"
|
||||
${'readonly="true"' if read_only else ''}>${hint}</textarea>
|
||||
</div>
|
||||
20
lms/templates/self_assessment_prompt.html
Normal file
20
lms/templates/self_assessment_prompt.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<section id="self_assessment_${id}" class="self-assessment" data-ajax-url="${ajax_url}"
|
||||
data-id="${id}" data-state="${state}" data-allow_reset="${allow_reset}">
|
||||
<div class="error"></div>
|
||||
<div class="prompt">
|
||||
${prompt}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<textarea name="answer" class="answer" cols="70" rows="20">${previous_answer|h}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="rubric-wrapper">${initial_rubric}</div>
|
||||
|
||||
<div class="hint-wrapper">${initial_hint}</div>
|
||||
|
||||
<div class="message-wrapper">${initial_message}</div>
|
||||
|
||||
<input type="button" value="Submit" class="submit-button" name="show"/>
|
||||
<input type="button" value="Reset" class="reset-button" name="reset"/>
|
||||
</section>
|
||||
15
lms/templates/self_assessment_rubric.html
Normal file
15
lms/templates/self_assessment_rubric.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<div class="assessment">
|
||||
<div class="rubric">
|
||||
<h3>Self-assess your answer with this rubric:</h3>
|
||||
${rubric}
|
||||
</div>
|
||||
|
||||
% if not read_only:
|
||||
<select name="assessment" class="assessment">
|
||||
%for i in xrange(0,max_score+1):
|
||||
<option value="${i}">${i}</option>
|
||||
%endfor
|
||||
</select>
|
||||
% endif
|
||||
|
||||
</div>
|
||||
@@ -10,6 +10,9 @@ if settings.DEBUG:
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = ('',
|
||||
# certificate view
|
||||
|
||||
url(r'^update_certificate$', 'certificates.views.update_certificate'),
|
||||
url(r'^$', 'branding.views.index', name="root"), # Main marketing page, or redirect to courseware
|
||||
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user