Commit 0ad2154b by Victor Shnayder

merge in origin/master

parents 2b55c590 1e956d44
......@@ -7,3 +7,4 @@ python
yuicompressor
node
graphviz
mysql
# .coveragerc for cms
[run]
data_file = reports/cms/.coverage
source = cms
[report]
ignore_errors = True
[html]
directory = reports/cms/cover
[xml]
output = reports/cms/coverage.xml
......@@ -14,9 +14,7 @@ from path import path
# Nose Test Runner
INSTALLED_APPS += ('django_nose',)
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html', '--cover-inclusive']
for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
NOSE_ARGS += ['--cover-package', app]
NOSE_ARGS = ['--with-xunit']
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
TEST_ROOT = path('test_root')
......
......@@ -3,8 +3,7 @@ from staticfiles.storage import staticfiles_storage
from pipeline_mako import compressed_css, compressed_js
%>
<%def name='url(file)'>
<%
<%def name='url(file)'><%
try:
url = staticfiles_storage.url(file)
except:
......
import csv
import uuid
from collections import defaultdict, OrderedDict
from datetime import datetime
from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterUser
class Command(BaseCommand):
CSV_TO_MODEL_FIELDS = OrderedDict([
("ClientCandidateID", "client_candidate_id"),
("FirstName", "first_name"),
("LastName", "last_name"),
("MiddleName", "middle_name"),
("Suffix", "suffix"),
("Salutation", "salutation"),
("Email", "email"),
# Skipping optional fields Username and Password
("Address1", "address_1"),
("Address2", "address_2"),
("Address3", "address_3"),
("City", "city"),
("State", "state"),
("PostalCode", "postal_code"),
("Country", "country"),
("Phone", "phone"),
("Extension", "extension"),
("PhoneCountryCode", "phone_country_code"),
("FAX", "fax"),
("FAXCountryCode", "fax_country_code"),
("CompanyName", "company_name"),
# Skipping optional field CustomQuestion
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
])
args = '<output_file>'
help = """
Export user information from TestCenterUser model into a tab delimited
text file with a format that Pearson expects.
"""
def handle(self, *args, **kwargs):
if len(args) < 1:
print Command.help
return
self.reset_sample_data()
with open(args[0], "wb") as outfile:
writer = csv.DictWriter(outfile,
Command.CSV_TO_MODEL_FIELDS,
delimiter="\t",
quoting=csv.QUOTE_MINIMAL,
extrasaction='ignore')
writer.writeheader()
for tcu in TestCenterUser.objects.order_by('id'):
record = dict((csv_field, getattr(tcu, model_field))
for csv_field, model_field
in Command.CSV_TO_MODEL_FIELDS.items())
record["LastUpdate"] = record["LastUpdate"].strftime("%Y/%m/%d %H:%M:%S")
writer.writerow(record)
def reset_sample_data(self):
def make_sample(**kwargs):
data = dict((model_field, kwargs.get(model_field, ""))
for model_field in Command.CSV_TO_MODEL_FIELDS.values())
return TestCenterUser(**data)
def generate_id():
return "edX{:012}".format(uuid.uuid4().int % (10**12))
# TestCenterUser.objects.all().delete()
samples = [
make_sample(
client_candidate_id=generate_id(),
first_name="Jack",
last_name="Doe",
middle_name="C",
address_1="11 Cambridge Center",
address_2="Suite 101",
city="Cambridge",
state="MA",
postal_code="02140",
country="USA",
phone="(617)555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Clyde",
last_name="Smith",
middle_name="J",
suffix="Jr.",
salutation="Mr.",
address_1="1 Penny Lane",
city="Honolulu",
state="HI",
postal_code="96792",
country="USA",
phone="555-555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Patty",
last_name="Lee",
salutation="Dr.",
address_1="P.O. Box 555",
city="Honolulu",
state="HI",
postal_code="96792",
country="USA",
phone="808-555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Jimmy",
last_name="James",
address_1="2020 Palmer Blvd.",
city="Springfield",
state="MA",
postal_code="96792",
country="USA",
phone="917-555-5555",
phone_country_code="1",
extension="2039",
fax="917-555-5556",
fax_country_code="1",
company_name="ACME Traps",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Yeong-Un",
last_name="Seo",
address_1="Duryu, Lotte 101",
address_2="Apt 55",
city="Daegu",
country="KOR",
phone="917-555-5555",
phone_country_code="011",
user_updated_at=datetime.utcnow()
),
]
for tcu in samples:
tcu.save()
\ No newline at end of file
import csv
import uuid
from collections import defaultdict, OrderedDict
from datetime import datetime
from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterUser
def generate_id():
return "{:012}".format(uuid.uuid4().int % (10**12))
class Command(BaseCommand):
args = '<output_file>'
help = """
Export user information from TestCenterUser model into a tab delimited
text file with a format that Pearson expects.
"""
FIELDS = [
'AuthorizationTransactionType',
'AuthorizationID',
'ClientAuthorizationID',
'ClientCandidateID',
'ExamAuthorizationCount',
'ExamSeriesCode',
'EligibilityApptDateFirst',
'EligibilityApptDateLast',
'LastUpdate',
]
def handle(self, *args, **kwargs):
if len(args) < 1:
print Command.help
return
# self.reset_sample_data()
with open(args[0], "wb") as outfile:
writer = csv.DictWriter(outfile,
Command.FIELDS,
delimiter="\t",
quoting=csv.QUOTE_MINIMAL,
extrasaction='ignore')
writer.writeheader()
for tcu in TestCenterUser.objects.order_by('id')[:5]:
record = defaultdict(
lambda: "",
AuthorizationTransactionType="Add",
ClientAuthorizationID=generate_id(),
ClientCandidateID=tcu.client_candidate_id,
ExamAuthorizationCount="1",
ExamSeriesCode="6002x001",
EligibilityApptDateFirst="2012/12/15",
EligibilityApptDateLast="2012/12/30",
LastUpdate=datetime.utcnow().strftime("%Y/%m/%d %H:%M:%S")
)
writer.writerow(record)
def reset_sample_data(self):
def make_sample(**kwargs):
data = dict((model_field, kwargs.get(model_field, ""))
for model_field in Command.CSV_TO_MODEL_FIELDS.values())
return TestCenterUser(**data)
# TestCenterUser.objects.all().delete()
samples = [
make_sample(
client_candidate_id=generate_id(),
first_name="Jack",
last_name="Doe",
middle_name="C",
address_1="11 Cambridge Center",
address_2="Suite 101",
city="Cambridge",
state="MA",
postal_code="02140",
country="USA",
phone="(617)555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Clyde",
last_name="Smith",
middle_name="J",
suffix="Jr.",
salutation="Mr.",
address_1="1 Penny Lane",
city="Honolulu",
state="HI",
postal_code="96792",
country="USA",
phone="555-555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Patty",
last_name="Lee",
salutation="Dr.",
address_1="P.O. Box 555",
city="Honolulu",
state="HI",
postal_code="96792",
country="USA",
phone="808-555-5555",
phone_country_code="1",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Jimmy",
last_name="James",
address_1="2020 Palmer Blvd.",
city="Springfield",
state="MA",
postal_code="96792",
country="USA",
phone="917-555-5555",
phone_country_code="1",
extension="2039",
fax="917-555-5556",
fax_country_code="1",
company_name="ACME Traps",
user_updated_at=datetime.utcnow()
),
make_sample(
client_candidate_id=generate_id(),
first_name="Yeong-Un",
last_name="Seo",
address_1="Duryu, Lotte 101",
address_2="Apt 55",
city="Daegu",
country="KOR",
phone="917-555-5555",
phone_country_code="011",
user_updated_at=datetime.utcnow()
),
]
for tcu in samples:
tcu.save()
\ No newline at end of file
import uuid
from datetime import datetime
from optparse import make_option
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterUser
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option(
'--client_candidate_id',
action='store',
dest='client_candidate_id',
help='ID we assign a user to identify them to Pearson'
),
make_option(
'--first_name',
action='store',
dest='first_name',
),
make_option(
'--last_name',
action='store',
dest='last_name',
),
make_option(
'--address_1',
action='store',
dest='address_1',
),
make_option(
'--city',
action='store',
dest='city',
),
make_option(
'--state',
action='store',
dest='state',
help='Two letter code (e.g. MA)'
),
make_option(
'--postal_code',
action='store',
dest='postal_code',
),
make_option(
'--country',
action='store',
dest='country',
help='Three letter country code (ISO 3166-1 alpha-3), like USA'
),
make_option(
'--phone',
action='store',
dest='phone',
help='Pretty free-form (parens, spaces, dashes), but no country code'
),
make_option(
'--phone_country_code',
action='store',
dest='phone_country_code',
help='Phone country code, just "1" for the USA'
),
)
args = "<student_username>"
help = "Create a TestCenterUser entry for a given Student"
@staticmethod
def is_valid_option(option_name):
base_options = set(option.dest for option in BaseCommand.option_list)
return option_name not in base_options
def handle(self, *args, **options):
username = args[0]
print username
our_options = dict((k, v) for k, v in options.items()
if Command.is_valid_option(k))
student = User.objects.get(username=username)
student.test_center_user = TestCenterUser(**our_options)
student.test_center_user.save()
......@@ -8,7 +8,7 @@ Portal servers that hold the canoncial user information and that user
information is replicated to slave Course server pools. Each Course has a set of
servers that serves only its content and has users that are relevant only to it.
We replicate the following tables into the Course DBs where the user is
We replicate the following tables into the Course DBs where the user is
enrolled. Only the Portal servers should ever write to these models.
* UserProfile
* CourseEnrollment
......@@ -41,33 +41,22 @@ import uuid
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django_countries import CountryField
from django.db.models.signals import post_save
from django.dispatch import receiver
from functools import partial
import comment_client as cc
from django_comment_client.models import Role, Permission
from django_comment_client.models import Role
import logging
from xmodule.modulestore.django import modulestore
#from cache_toolbox import cache_model, cache_relation
log = logging.getLogger(__name__)
class UserProfile(models.Model):
"""This is where we store all the user demographic fields. We have a
"""This is where we store all the user demographic fields. We have a
separate table for this rather than extending the built-in Django auth_user.
Notes:
* Some fields are legacy ones from the first run of 6.002, from which
* Some fields are legacy ones from the first run of 6.002, from which
we imported many users.
* Fields like name and address are intentionally open ended, to account
for international variations. An unfortunate side-effect is that we
......@@ -133,6 +122,72 @@ class UserProfile(models.Model):
def set_meta(self, js):
self.meta = json.dumps(js)
class TestCenterUser(models.Model):
"""This is our representation of the User for in-person testing, and
specifically for Pearson at this point. A few things to note:
* Pearson only supports Latin-1, so we have to make sure that the data we
capture here will work with that encoding.
* While we have a lot of this demographic data in UserProfile, it's much
more free-structured there. We'll try to pre-pop the form with data from
UserProfile, but we'll need to have a step where people who are signing
up re-enter their demographic data into the fields we specify.
* Users are only created here if they register to take an exam in person.
The field names and lengths are modeled on the conventions and constraints
of Pearson's data import system, including oddities such as suffix having
a limit of 255 while last_name only gets 50.
"""
# Our own record keeping...
user = models.ForeignKey(User, unique=True, default=None)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True, db_index=True)
# user_updated_at happens only when the user makes a change to their data,
# and is something Pearson needs to know to manage updates. Unlike
# updated_at, this will not get incremented when we do a batch data import.
user_updated_at = models.DateTimeField(db_index=True)
# Unique ID given to us for this User by the Testing Center. It's null when
# we first create the User entry, and is assigned by Pearson later.
candidate_id = models.IntegerField(null=True, db_index=True)
# Unique ID we assign our user for a the Test Center.
client_candidate_id = models.CharField(max_length=50, db_index=True)
# Name
first_name = models.CharField(max_length=30, db_index=True)
last_name = models.CharField(max_length=50, db_index=True)
middle_name = models.CharField(max_length=30, blank=True)
suffix = models.CharField(max_length=255, blank=True)
salutation = models.CharField(max_length=50, blank=True)
# Address
address_1 = models.CharField(max_length=40)
address_2 = models.CharField(max_length=40, blank=True)
address_3 = models.CharField(max_length=40, blank=True)
city = models.CharField(max_length=32, db_index=True)
# state example: HI -- they have an acceptable list that we'll just plug in
# state is required if you're in the US or Canada, but otherwise not.
state = models.CharField(max_length=20, blank=True, db_index=True)
# postal_code required if you're in the US or Canada
postal_code = models.CharField(max_length=16, blank=True, db_index=True)
# country is a ISO 3166-1 alpha-3 country code (e.g. "USA", "CAN", "MNG")
country = models.CharField(max_length=3, db_index=True)
# Phone
phone = models.CharField(max_length=35)
extension = models.CharField(max_length=8, blank=True, db_index=True)
phone_country_code = models.CharField(max_length=3, db_index=True)
fax = models.CharField(max_length=35, blank=True)
# fax_country_code required *if* fax is present.
fax_country_code = models.CharField(max_length=3, blank=True)
# Company
company_name = models.CharField(max_length=50, blank=True)
@property
def email(self):
return self.user.email
## TODO: Should be renamed to generic UserGroup, and possibly
# Given an optional field for type of group
......
......@@ -19,16 +19,19 @@ from django.core.context_processors import csrf
from django.core.mail import send_mail
from django.core.validators import validate_email, validate_slug, ValidationError
from django.db import IntegrityError
from django.http import HttpResponse, Http404
from django.http import HttpResponse, HttpResponseForbidden, Http404
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
from bs4 import BeautifulSoup
from django.core.cache import cache
from django_future.csrf import ensure_csrf_cookie
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.
......@@ -774,3 +786,23 @@ def accept_name_change(request):
raise Http404
return accept_name_change_by_id(int(request.POST['id']))
# TODO: This is a giant kludge to give Pearson something to test against ASAP.
# Will need to get replaced by something that actually ties into TestCenterUser
@csrf_exempt
def test_center_login(request):
if not settings.MITX_FEATURES.get('ENABLE_PEARSON_HACK_TEST'):
raise Http404
client_candidate_id = request.POST.get("clientCandidateID")
# registration_id = request.POST.get("registrationID")
exit_url = request.POST.get("exitURL")
error_url = request.POST.get("errorURL")
if client_candidate_id == "edX003671291147":
user = authenticate(username=settings.PEARSON_TEST_USER,
password=settings.PEARSON_TEST_PASSWORD)
login(request, user)
return redirect('/courses/MITx/6.002x/2012_Fall/courseware/Final_Exam/Final_Exam_Fall_2012/')
else:
return HttpResponseForbidden()
# .coveragerc for common/lib/capa
[run]
data_file = reports/common/lib/capa/.coverage
source = common/lib/capa
[report]
ignore_errors = True
[html]
directory = reports/common/lib/capa/cover
[xml]
output = reports/common/lib/capa/coverage.xml
......@@ -68,7 +68,7 @@ class CorrectMap(object):
correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This
means that when the definition of CorrectMap (e.g. its properties) are altered,
an existing correct_map dict not coincide with the newest CorrectMap format as
an existing correct_map dict will not coincide with the newest CorrectMap format as
defined by self.set.
For graceful migration, feed the contents of each correct map to self.set, rather than
......
......@@ -81,7 +81,7 @@ class LoncapaResponse(object):
by __init__
- check_hint_condition : check to see if the student's answers satisfy a particular
condition for a hint to be displayed
condition for a hint to be displayed
- render_html : render this Response as HTML (must return XHTML-compliant string)
- __unicode__ : unicode representation of this Response
......@@ -149,6 +149,7 @@ class LoncapaResponse(object):
# for convenience
self.answer_id = self.answer_ids[0]
# map input_id -> maxpoints
self.maxpoints = dict()
for inputfield in self.inputfields:
# By default, each answerfield is worth 1 point
......@@ -280,17 +281,14 @@ class LoncapaResponse(object):
(correctness, npoints, msg) for each answer_id.
Arguments:
- student_answers : dict of (answer_id,answer) where answer = student input (string)
- old_cmap : previous CorrectMap (may be empty); useful for analyzing or
recording history of responses
- student_answers : dict of (answer_id, answer) where answer = student input (string)
'''
pass
@abc.abstractmethod
def get_answers(self):
'''
Return a dict of (answer_id,answer_text) for each answer for this question.
Return a dict of (answer_id, answer_text) for each answer for this question.
'''
pass
......
......@@ -19,7 +19,7 @@
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}"
% if size:
size="${size}"
% endif
......
......@@ -12,7 +12,7 @@
% endif
<p class="debug">${status}</p>
<input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" data-required_files="${required_files}" data-allowed_files="${allowed_files}"/>
<input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" data-required_files="${required_files|h}" data-allowed_files="${allowed_files|h}"/>
</div>
<div class="message">${msg|n}</div>
</section>
......@@ -2,7 +2,7 @@
<input type="hidden" name="input_${id}" id="input_${id}" class="javascriptinput_input"/>
<div class="javascriptinput_data" data-display_class="${display_class}"
data-problem_state="${problem_state}" data-params="${params}"
data-submission="${value}" data-evaluation="${evaluation}">
data-submission="${value|h}" data-evaluation="${msg|h}">
</div>
<div class="script_placeholder" data-src="/static/js/${display_file}"></div>
<div class="javascriptinput_container"></div>
......
......@@ -20,7 +20,7 @@
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}"
% if do_math:
class="math"
% endif
......
......@@ -21,7 +21,7 @@
<div class="incorrect" id="status_${id}">
% endif
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}"
style="display:none;"
/>
......
......@@ -2,9 +2,18 @@
Tests of input types.
TODO:
- refactor: so much repetive code (have factory methods that build xml elements directly, etc)
- test error cases
- check rendering -- e.g. msg should appear in the rendered output. If possible, test that
templates are escaping things properly.
- test unicode in values, parameters, etc.
- test various html escapes
- test funny xml chars -- should never get xml parse error if things are escaped properly.
"""
from lxml import etree
......@@ -46,6 +55,19 @@ class OptionInputTest(unittest.TestCase):
self.assertEqual(context, expected)
def test_option_parsing(self):
f = inputtypes.OptionInput.parse_options
def check(input, options):
"""Take list of options, confirm that output is in the silly doubled format"""
expected = [(o, o) for o in options]
self.assertEqual(f(input), expected)
check("('a','b')", ['a', 'b'])
check("('a', 'b')", ['a', 'b'])
check("('a b','b')", ['a b', 'b'])
check("('My \"quoted\"place','b')", ['My \"quoted\"place', 'b'])
class ChoiceGroupTest(unittest.TestCase):
'''
Test choice groups, radio groups, and checkbox groups
......@@ -73,6 +95,7 @@ class ChoiceGroupTest(unittest.TestCase):
expected = {'id': 'sky_input',
'value': 'foil3',
'status': 'answered',
'msg': '',
'input_type': expected_input_type,
'choices': [('foil1', '<text>This is foil One.</text>'),
('foil2', '<text>This is foil Two.</text>'),
......@@ -119,12 +142,13 @@ class JavascriptInputTest(unittest.TestCase):
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'status': 'unanswered',
'msg': '',
'value': '3',
'params': params,
'display_file': display_file,
'display_class': display_class,
'problem_state': problem_state,
'value': '3',
'evaluation': '',}
'problem_state': problem_state,}
self.assertEqual(context, expected)
......@@ -204,9 +228,6 @@ class FileSubmissionTest(unittest.TestCase):
element = etree.fromstring(xml_str)
escapedict = {'"': '&quot;'}
esc = lambda s: saxutils.escape(s, escapedict)
state = {'value': 'BumbleBee.py',
'status': 'incomplete',
'feedback' : {'message': '3'}, }
......@@ -220,8 +241,8 @@ class FileSubmissionTest(unittest.TestCase):
'msg': input_class.submitted_msg,
'value': 'BumbleBee.py',
'queue_len': '3',
'allowed_files': esc('["runme.py", "nooooo.rb", "ohai.java"]'),
'required_files': esc('["cookies.py"]')}
'allowed_files': '["runme.py", "nooooo.rb", "ohai.java"]',
'required_files': '["cookies.py"]'}
self.assertEqual(context, expected)
......@@ -255,14 +276,15 @@ class CodeInputTest(unittest.TestCase):
'status': 'incomplete',
'feedback' : {'message': '3'}, }
the_input = lookup_tag('codeinput')(test_system, element, state)
input_class = lookup_tag('codeinput')
the_input = input_class(test_system, element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
'status': 'queued',
'msg': 'Submitted to grader.',
'msg': input_class.submitted_msg,
'mode': mode,
'linenumbers': linenumbers,
'rows': rows,
......@@ -311,8 +333,9 @@ class SchematicTest(unittest.TestCase):
expected = {'id': 'prob_1_2',
'value': value,
'initial_value': initial_value,
'status': 'unsubmitted',
'msg': '',
'initial_value': initial_value,
'width': width,
'height': height,
'parts': parts,
......@@ -476,6 +499,7 @@ class ChemicalEquationTest(unittest.TestCase):
expected = {'id': 'prob_1_2',
'value': 'H2OYeah',
'status': 'unanswered',
'msg': '',
'size': size,
'previewer': '/static/js/capa/chemical_equation_preview.js',
}
......
# .coveragerc for common/lib/xmodule
[run]
data_file = reports/common/lib/xmodule/.coverage
source = common/lib/xmodule
[report]
ignore_errors = True
[html]
directory = reports/common/lib/xmodule/cover
[xml]
output = reports/common/lib/xmodule/coverage.xml
......@@ -540,15 +540,9 @@ class CapaModule(XModule):
lcp_id = self.lcp.problem_id
correct_map = self.lcp.grade_answers(answers)
except StudentInputError as inst:
# TODO (vshnayder): why is this line here?
#self.lcp = LoncapaProblem(self.definition['data'],
# id=lcp_id, state=old_state, system=self.system)
log.exception("StudentInputError in capa_module:problem_check")
return {'success': inst.message}
except Exception, err:
# TODO: why is this line here?
#self.lcp = LoncapaProblem(self.definition['data'],
# id=lcp_id, state=old_state, system=self.system)
if self.system.DEBUG:
msg = "Error checking problem: " + str(err)
msg += '\nTraceback:\n' + traceback.format_exc()
......
......@@ -133,7 +133,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
......@@ -250,6 +250,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
......@@ -277,6 +281,21 @@ class CourseDescriptor(SequenceDescriptor):
return self.metadata.get('discussion_link', None)
@property
def forum_posts_allowed(self):
try:
blackout_periods = [(parse_time(start), parse_time(end))
for start, end
in self.metadata.get('discussion_blackouts', [])]
now = time.gmtime()
for start, end in blackout_periods:
if start <= now <= end:
return False
except:
log.exception("Error parsing discussion_blackouts for course {0}".format(self.id))
return True
@property
def hide_progress_tab(self):
"""TODO: same as above, intended to let internal CS50 hide the progress tab
until we get grade integration set up."""
......@@ -284,6 +303,16 @@ class CourseDescriptor(SequenceDescriptor):
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
......
......@@ -355,6 +355,34 @@ div.video {
}
}
a.quality_control {
background: url(../images/hd.png) center no-repeat;
border-right: 1px solid #000;
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
color: #797979;
display: block;
float: left;
line-height: 46px; //height of play pause buttons
margin-left: 0;
padding: 0 lh(.5);
text-indent: -9999px;
@include transition();
width: 30px;
&:hover {
background-color: #444;
color: #fff;
text-decoration: none;
}
&.active {
background-color: #F44;
color: #0ff;
text-decoration: none;
}
}
a.hide-subtitles {
background: url('../images/cc.png') center no-repeat;
color: #797979;
......
......@@ -1995,7 +1995,7 @@ cktsim = (function() {
// set up each schematic entry widget
function update_schematics() {
// set up each schematic on the page
var schematics = document.getElementsByClassName('schematic');
var schematics = $('.schematic');
for (var i = 0; i < schematics.length; ++i)
if (schematics[i].getAttribute("loaded") != "true") {
try {
......@@ -2036,7 +2036,7 @@ function add_schematic_handler(other_onload) {
// ask each schematic input widget to update its value field for submission
function prepare_schematics() {
var schematics = document.getElementsByClassName('schematic');
var schematics = $('.schematic');
for (var i = schematics.length - 1; i >= 0; i--)
schematics[i].schematic.update_value();
}
......@@ -3339,23 +3339,28 @@ schematic = (function() {
}
// add method to canvas to compute relative coords for event
HTMLCanvasElement.prototype.relMouseCoords = function(event){
// run up the DOM tree to figure out coords for top,left of canvas
var totalOffsetX = 0;
var totalOffsetY = 0;
var currentElement = this;
do {
totalOffsetX += currentElement.offsetLeft;
totalOffsetY += currentElement.offsetTop;
}
while (currentElement = currentElement.offsetParent);
// now compute relative position of click within the canvas
this.mouse_x = event.pageX - totalOffsetX;
this.mouse_y = event.pageY - totalOffsetY;
this.page_x = event.pageX;
this.page_y = event.pageY;
try {
if (HTMLCanvasElement)
HTMLCanvasElement.prototype.relMouseCoords = function(event){
// run up the DOM tree to figure out coords for top,left of canvas
var totalOffsetX = 0;
var totalOffsetY = 0;
var currentElement = this;
do {
totalOffsetX += currentElement.offsetLeft;
totalOffsetY += currentElement.offsetTop;
}
while (currentElement = currentElement.offsetParent);
// now compute relative position of click within the canvas
this.mouse_x = event.pageX - totalOffsetX;
this.mouse_y = event.pageY - totalOffsetY;
this.page_x = event.pageX;
this.page_y = event.pageY;
}
}
catch (err) { // ignore
}
///////////////////////////////////////////////////////////////////////////////
......@@ -4091,48 +4096,52 @@ schematic = (function() {
// add dashed lines!
// from http://davidowens.wordpress.com/2010/09/07/html-5-canvas-and-dashed-lines/
CanvasRenderingContext2D.prototype.dashedLineTo = function(fromX, fromY, toX, toY, pattern) {
// Our growth rate for our line can be one of the following:
// (+,+), (+,-), (-,+), (-,-)
// Because of this, our algorithm needs to understand if the x-coord and
// y-coord should be getting smaller or larger and properly cap the values
// based on (x,y).
var lt = function (a, b) { return a <= b; };
var gt = function (a, b) { return a >= b; };
var capmin = function (a, b) { return Math.min(a, b); };
var capmax = function (a, b) { return Math.max(a, b); };
var checkX = { thereYet: gt, cap: capmin };
var checkY = { thereYet: gt, cap: capmin };
if (fromY - toY > 0) {
checkY.thereYet = lt;
checkY.cap = capmax;
}
if (fromX - toX > 0) {
checkX.thereYet = lt;
checkX.cap = capmax;
}
this.moveTo(fromX, fromY);
var offsetX = fromX;
var offsetY = fromY;
var idx = 0, dash = true;
while (!(checkX.thereYet(offsetX, toX) && checkY.thereYet(offsetY, toY))) {
var ang = Math.atan2(toY - fromY, toX - fromX);
var len = pattern[idx];
offsetX = checkX.cap(toX, offsetX + (Math.cos(ang) * len));
offsetY = checkY.cap(toY, offsetY + (Math.sin(ang) * len));
if (dash) this.lineTo(offsetX, offsetY);
else this.moveTo(offsetX, offsetY);
idx = (idx + 1) % pattern.length;
dash = !dash;
}
};
try {
if (CanvasRenderingContext2D)
CanvasRenderingContext2D.prototype.dashedLineTo = function(fromX, fromY, toX, toY, pattern) {
// Our growth rate for our line can be one of the following:
// (+,+), (+,-), (-,+), (-,-)
// Because of this, our algorithm needs to understand if the x-coord and
// y-coord should be getting smaller or larger and properly cap the values
// based on (x,y).
var lt = function (a, b) { return a <= b; };
var gt = function (a, b) { return a >= b; };
var capmin = function (a, b) { return Math.min(a, b); };
var capmax = function (a, b) { return Math.max(a, b); };
var checkX = { thereYet: gt, cap: capmin };
var checkY = { thereYet: gt, cap: capmin };
if (fromY - toY > 0) {
checkY.thereYet = lt;
checkY.cap = capmax;
}
if (fromX - toX > 0) {
checkX.thereYet = lt;
checkX.cap = capmax;
}
this.moveTo(fromX, fromY);
var offsetX = fromX;
var offsetY = fromY;
var idx = 0, dash = true;
while (!(checkX.thereYet(offsetX, toX) && checkY.thereYet(offsetY, toY))) {
var ang = Math.atan2(toY - fromY, toX - fromX);
var len = pattern[idx];
offsetX = checkX.cap(toX, offsetX + (Math.cos(ang) * len));
offsetY = checkY.cap(toY, offsetY + (Math.sin(ang) * len));
if (dash) this.lineTo(offsetX, offsetY);
else this.moveTo(offsetX, offsetY);
idx = (idx + 1) % pattern.length;
dash = !dash;
}
};
}
catch (err) { //noop
}
// given a range of values, return a new range [vmin',vmax'] where the limits
// have been chosen "nicely". Taken from matplotlib.ticker.LinearLocator
function view_limits(vmin,vmax) {
......
......@@ -22,7 +22,7 @@ class @VideoCaption extends Subview
"""
@$('.video-controls .secondary-controls').append """
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
"""
"""#"
@$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5
@fetchCaption()
......@@ -144,7 +144,7 @@ class @VideoCaption extends Subview
@el.removeClass('closed')
@scrollCaption()
$.cookie('hide_captions', hide_captions, expires: 3650, path: '/')
captionHeight: ->
if @el.hasClass('fullscreen')
$(window).height() - @$('.video-controls').height()
......
......@@ -16,7 +16,7 @@ class @VideoControl extends Subview
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
</div>
</div>
"""
"""#"
unless onTouchBasedDevice()
@$('.video_control').addClass('play').html('Play')
......
......@@ -9,6 +9,7 @@ class @VideoPlayer extends Subview
bind: ->
$(@control).bind('play', @play)
.bind('pause', @pause)
$(@qualityControl).bind('changeQuality', @handlePlaybackQualityChange)
$(@caption).bind('seek', @onSeek)
$(@speedControl).bind('speedChange', @onSpeedChange)
$(@progressSlider).bind('seek', @onSeek)
......@@ -25,6 +26,7 @@ class @VideoPlayer extends Subview
render: ->
@control = new VideoControl el: @$('.video-controls')
@qualityControl = new VideoQualityControl el: @$('.secondary-controls')
@caption = new VideoCaption
el: @el
youtubeId: @video.youtubeId('1.0')
......@@ -41,10 +43,12 @@ class @VideoPlayer extends Subview
rel: 0
showinfo: 0
enablejsapi: 1
modestbranding: 1
videoId: @video.youtubeId()
events:
onReady: @onReady
onStateChange: @onStateChange
onPlaybackQualityChange: @onPlaybackQualityChange
@caption.hideCaptions(@['video'].hide_captions)
addToolTip: ->
......@@ -53,7 +57,7 @@ class @VideoPlayer extends Subview
my: 'top right'
at: 'top center'
onReady: =>
onReady: (event) =>
unless onTouchBasedDevice()
$('.video-load-complete:first').data('video').player.play()
......@@ -68,6 +72,13 @@ class @VideoPlayer extends Subview
when YT.PlayerState.ENDED
@onEnded()
onPlaybackQualityChange: (event, value) =>
quality = @player.getPlaybackQuality()
@qualityControl.onQualityChange(quality)
handlePlaybackQualityChange: (event, value) =>
@player.setPlaybackQuality(value)
onUnstarted: =>
@control.pause()
@caption.pause()
......
class @VideoQualityControl extends Subview
initialize: ->
@quality = null;
bind: ->
@$('.quality_control').click @toggleQuality
render: ->
@el.append """
<a href="#" class="quality_control" title="HD">HD</a>
"""#"
onQualityChange: (value) ->
@quality = value
if @quality in ['hd720', 'hd1080', 'highres']
@el.addClass('active')
else
@el.removeClass('active')
toggleQuality: (event) =>
event.preventDefault()
if @quality in ['hd720', 'hd1080', 'highres']
newQuality = 'large'
else
newQuality = 'hd720'
$(@).trigger('changeQuality', newQuality)
\ No newline at end of file
......@@ -17,7 +17,7 @@ class @VideoVolumeControl extends Subview
<div class="volume-slider"></div>
</div>
</div>
"""
"""#"
@slider = @$('.volume-slider').slider
orientation: "vertical"
range: "min"
......
......@@ -176,6 +176,33 @@ class ImportTestCase(unittest.TestCase):
self.assertEqual(chapter_xml.tag, 'chapter')
self.assertFalse('graceperiod' in chapter_xml.attrib)
def test_is_pointer_tag(self):
"""
Check that is_pointer_tag works properly.
"""
yes = ["""<html url_name="blah"/>""",
"""<html url_name="blah"></html>""",
"""<html url_name="blah"> </html>""",
"""<problem url_name="blah"/>""",
"""<course org="HogwartsX" course="Mathemagics" url_name="3.14159"/>"""]
no = ["""<html url_name="blah" also="this"/>""",
"""<html url_name="blah">some text</html>""",
"""<problem url_name="blah"><sub>tree</sub></problem>""",
"""<course org="HogwartsX" course="Mathemagics" url_name="3.14159">
<chapter>3</chapter>
</course>
"""]
for xml_str in yes:
print "should be True for {0}".format(xml_str)
self.assertTrue(is_pointer_tag(etree.fromstring(xml_str)))
for xml_str in no:
print "should be False for {0}".format(xml_str)
self.assertFalse(is_pointer_tag(etree.fromstring(xml_str)))
def test_metadata_inherit(self):
"""Make sure that metadata is inherited properly"""
......@@ -324,4 +351,4 @@ class ImportTestCase(unittest.TestCase):
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')
\ No newline at end of file
self.assertEqual(sa_sample.metadata['attempts'], '10')
......@@ -32,6 +32,7 @@ class VideoModule(XModule):
self.position = 0
self.show_captions = xmltree.get('show_captions', 'true')
self.source = self._get_source(xmltree)
self.track = self._get_track(xmltree)
if instance_state is not None:
state = json.loads(instance_state)
......@@ -40,13 +41,25 @@ class VideoModule(XModule):
def _get_source(self, xmltree):
# find the first valid source
source = None
for element in xmltree.findall('source'):
return self._get_first_external(xmltree, 'source')
def _get_track(self, xmltree):
# find the first valid track
return self._get_first_external(xmltree, 'track')
def _get_first_external(self, xmltree, tag):
"""
Will return the first valid element
of the given tag.
'valid' means has a non-empty 'src' attribute
"""
result = None
for element in xmltree.findall(tag):
src = element.get('src')
if src:
source = src
result = src
break
return source
return result
def handle_ajax(self, dispatch, get):
'''
......@@ -85,6 +98,7 @@ class VideoModule(XModule):
'id': self.location.html_id(),
'position': self.position,
'source': self.source,
'track' : self.track,
'display_name': self.display_name,
# TODO (cpennington): This won't work when we move to data that isn't on the filesystem
'data_dir': self.metadata['data_dir'],
......
......@@ -25,7 +25,7 @@ def name_to_pathname(name):
def is_pointer_tag(xml_obj):
"""
Check if xml_obj is a pointer tag: <blah url_name="something" />.
No children, one attribute named url_name.
No children, one attribute named url_name, no text.
Special case for course roots: the pointer is
<course url_name="something" org="myorg" course="course">
......@@ -40,7 +40,10 @@ def is_pointer_tag(xml_obj):
expected_attr = set(['url_name', 'course', 'org'])
actual_attr = set(xml_obj.attrib.keys())
return len(xml_obj) == 0 and actual_attr == expected_attr
has_text = xml_obj.text is not None and len(xml_obj.text.strip()) > 0
return len(xml_obj) == 0 and actual_attr == expected_attr and not has_text
def get_metadata_from_xml(xml_object, remove=True):
meta = xml_object.find('meta')
......
......@@ -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"
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,8 +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
sudo apt-get -y install $APT_PKGS
# 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
;;
*)
......
# Notes on using mongodb backed LMS and CMS
These are some random notes for developers, on how things are stored in mongodb, and how to debug mongodb data.
## Databases
Two mongodb databases are used:
- xmodule: stores module definitions and metadata (modulestore)
- xcontent: stores filesystem content, like PDF files
modulestore documents are stored with an _id which has fields like this:
{"_id": {"tag":"i4x","org":"HarvardX","course":"CS50x","category":"chapter","name":"Week_1","revision":null}}
## Document fields
### Problems
Here is an example showing the fields available in problem documents:
{
"_id" : {
"tag" : "i4x",
"org" : "MITx",
"course" : "6.00x",
"category" : "problem",
"name" : "ps03:ps03-Hangman_part_2_The_Game",
"revision" : null
},
"definition" : {
"data" : " ..."
},
"metadata" : {
"display_name" : "Hangman Part 2: The Game",
"attempts" : "30",
"title" : "Hangman, Part 2",
"data_dir" : "6.00x",
"type" : "lecture"
}
}
## Sample interaction with mongodb
1. "mongo"
2. "use xmodule"
3. "show collections" should give "modulestore" and "system.indexes"
4. 'db.modulestore.find( {"_id.org": "MITx"} )' will produce a list of all MITx course documents
5. 'db.modulestore.find( {"_id.org": "MITx", "_id.category": "problem"} )' will produce a list of all problems in MITx courses
Example query for finding all files with "image" in the filename:
- use xcontent
- db.fs.files.find({'filename': /image/ } )
- db.fs.files.find({'filename': /image/ } ).count()
## Debugging the mongodb contents
A convenient tool is http://phpmoadmin.com/ (needs php)
Under ubuntu, do:
- apt-get install php5-fpm php-pear
- pecl install mongo
- edit /etc/php5/fpm/php.ini to add "extension=mongo.so"
- /etc/init.d/php5-fpm restart
and also setup nginx to run php through fastcgi.
## Backing up mongodb
- mogodump (dumps all dbs)
- mongodump --collection modulestore --db xmodule (dumps just xmodule/modulestore)
- mongodump -d xmodule -q '{"_id.org": "MITx"}' (dumps just MITx documents in xmodule)
- mongodump -q '{"_id.org": "MITx"}' (dumps all MITx documents)
## Deleting course content
Use "remove" instead of "find":
- db.modulestore.remove( {"_id.course": "8.01greytak"})
## Finding useful information from the mongodb modulestore
- Organizations
> db.modulestore.distinct( "_id.org")
[ "HarvardX", "MITx", "edX", "edx" ]
- Courses
> db.modulestore.distinct( "_id.course")
[
"CS50x",
"PH207x",
"3.091x",
"6.002x",
"6.00x",
"8.01esg",
"8.01rq_MW",
"8.02teal",
"8.02x",
"edx4edx",
"toy",
"templates"
]
- Find a problem which has the word "quantum" in its definition
db.modulestore.findOne( {"definition.data":/quantum/})n
- Find Location for all problems with the word "quantum" in its definition
db.modulestore.find( {"definition.data":/quantum/}, {'_id':1})
- Number of problems in each course
db.runCommand({
mapreduce: "modulestore",
query: { '_id.category': 'problem' },
map: function(){ emit(this._id.course, {count:1}); },
reduce: function(key, values){
var result = {count:0};
values.forEach(function(value) {
result.count += value.count;
});
return result;
},
out: 'pbyc',
verbose: true
});
produces:
> db.pbyc.find()
{ "_id" : "3.091x", "value" : { "count" : 184 } }
{ "_id" : "6.002x", "value" : { "count" : 176 } }
{ "_id" : "6.00x", "value" : { "count" : 147 } }
{ "_id" : "8.01esg", "value" : { "count" : 184 } }
{ "_id" : "8.01rq_MW", "value" : { "count" : 73 } }
{ "_id" : "8.02teal", "value" : { "count" : 5 } }
{ "_id" : "8.02x", "value" : { "count" : 99 } }
{ "_id" : "PH207x", "value" : { "count" : 25 } }
{ "_id" : "edx4edx", "value" : { "count" : 50 } }
{ "_id" : "templates", "value" : { "count" : 11 } }
......@@ -250,9 +250,13 @@ Values are dictionaries of the form {"metadata-key" : "metadata-value"}.
Supported fields at the course level:
* "start" -- specify the start date for the course. Format-by-example: "2012-09-05T12:00".
* "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
......
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/MITx.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/MITx.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/MITx"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/MITx"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
{% extends "!layout.html" %}
{% block header %}
<img src="{{ pathto("_static/homepage-bg.jpg", 1) }}" alt="Edx logo" width="100%;"/>
{% endblock %}
\ No newline at end of file
*******************************************
Capa module
*******************************************
.. module:: capa
Calc
====
.. automodule:: capa.calc
:members:
:show-inheritance:
Capa_problem
============
.. automodule:: capa.capa_problem
:members:
:show-inheritance:
Checker
=======
.. automodule:: capa.checker
:members:
:show-inheritance:
Correctmap
==========
.. automodule:: capa.correctmap
:members:
:show-inheritance:
Customrender
============
.. automodule:: capa.customrender
:members:
:show-inheritance:
Inputtypes
==========
.. automodule:: capa.inputtypes
:members:
:show-inheritance:
Resposetypes
============
.. automodule:: capa.responsetypes
:members:
:show-inheritance:
*******************************************
CMS module
*******************************************
.. module:: cms
Auth
====
.. automodule:: auth
:members:
:show-inheritance:
Authz
-----
.. automodule:: auth.authz
:members:
:show-inheritance:
Content store
=============
.. .. automodule:: contentstore
.. :members:
.. :show-inheritance:
.. Utils
.. -----
.. .. automodule:: contentstore.untils
.. :members:
.. :show-inheritance:
.. Views
.. -----
.. .. automodule:: contentstore.views
.. :members:
.. :show-inheritance:
.. Management
.. ----------
.. .. automodule:: contentstore.management
.. :members:
.. :show-inheritance:
.. Tests
.. -----
.. .. automodule:: contentstore.tests
.. :members:
.. :show-inheritance:
Github sync
===========
.. automodule:: github_sync
:members:
:show-inheritance:
Exceptions
----------
.. automodule:: github_sync.exceptions
:members:
:show-inheritance:
Views
-----
.. automodule:: github_sync.views
:members:
:show-inheritance:
Management
----------
.. automodule:: github_sync.management
:members:
:show-inheritance:
Tests
-----
.. .. automodule:: github_sync.tests
.. :members:
.. :show-inheritance:
\ No newline at end of file
Common / lib
===============================
Contents:
.. toctree::
:maxdepth: 2
xmodule.rst
capa.rst
\ No newline at end of file
*******************************************
Common
*******************************************
.. module:: common.djangoapps
Student
=======
.. automodule:: student
:members:
:show-inheritance:
Models
------
.. automodule:: student.models
:members:
:show-inheritance:
Views
-----
.. automodule:: student.views
:members:
:show-inheritance:
Admin
-----
.. automodule:: student.admin
:members:
:show-inheritance:
Tests
-----
.. automodule:: student.tests
:members:
:show-inheritance:
Management
----------
.. automodule:: student.management
:members:
:show-inheritance:
Migrations
----------
.. automodule:: student.migrations
:members:
:show-inheritance:
\ No newline at end of file
Django applications
===============================
Contents:
.. toctree::
:maxdepth: 2
lms.rst
cms.rst
djangoapps-common.rst
\ No newline at end of file
.. MITx documentation master file, created by
sphinx-quickstart on Fri Nov 2 15:43:00 2012.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to MITx's documentation!
================================
Contents:
.. toctree::
:maxdepth: 2
overview.rst
common-lib.rst
djangoapps.rst
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
*******************************************
LMS module
*******************************************
.. module:: lms
Branding
========
.. automodule:: branding
:members:
:show-inheritance:
Views
-----
.. automodule:: branding.views
:members:
:show-inheritance:
Certificates
============
.. automodule:: certificates
:members:
:show-inheritance:
Models
------
.. automodule:: certificates.models
:members:
:show-inheritance:
Views
-----
.. automodule:: certificates.views
:members:
:show-inheritance:
Tests
-----
.. automodule:: certificates.tests
:members:
:show-inheritance:
Circuit
=======
.. automodule:: circuit
:members:
:show-inheritance:
Models
------
.. automodule:: circuit.models
:members:
:show-inheritance:
Views
-----
.. automodule:: circuit.views
:members:
:show-inheritance:
Tests
-----
.. automodule:: circuit.tests
:members:
:show-inheritance:
Course_wiki
===========
.. automodule:: course_wiki
:members:
:show-inheritance:
Course Nav
----------
.. .. automodule:: course_wiki.course_nav
.. :members:
.. :show-inheritance:
Views
-----
.. automodule:: course_wiki.views
:members:
:show-inheritance:
Editors
-------
.. automodule:: course_wiki.editors
:members:
:show-inheritance:
Courseware
==========
.. automodule:: courseware
:members:
:show-inheritance:
Access
------
.. automodule:: courseware.access
:members:
:show-inheritance:
Admin
-----
.. automodule:: courseware.admin
:members:
:show-inheritance:
Courses
-------
.. automodule:: courseware.courses
:members:
:show-inheritance:
Grades
------
.. automodule:: courseware.grades
:members:
:show-inheritance:
Models
------
.. automodule:: courseware.models
:members:
:show-inheritance:
Progress
--------
.. automodule:: courseware.progress
:members:
:show-inheritance:
Tabs
----
.. automodule:: courseware.tabs
:members:
:show-inheritance:
Dashboard
=========
.. automodule:: dashboard
:members:
:show-inheritance:
Models
------
.. automodule:: dashboard.models
:members:
:show-inheritance:
Views
-----
.. automodule:: dashboard.views
:members:
:show-inheritance:
Tests
-----
.. automodule:: dashboard.tests
:members:
:show-inheritance:
Django comment client
=====================
.. automodule:: django_comment_client
:members:
:show-inheritance:
Models
------
.. automodule:: django_comment_client.models
:members:
:show-inheritance:
Tests
-----
.. automodule:: django_comment_client.tests
:members:
:show-inheritance:
Heartbeat
=========
.. automodule:: heartbeat
:members:
:show-inheritance:
Instructor
==========
.. automodule:: instructor
:members:
:show-inheritance:
Views
-----
.. automodule:: instructor.views
:members:
:show-inheritance:
Tests
-----
.. .. automodule:: instructor.tests
.. :members:
.. :show-inheritance:
Lisenses
========
.. automodule:: licenses
:members:
:show-inheritance:
Models
------
.. automodule:: licenses.models
:members:
:show-inheritance:
Views
-----
.. automodule:: licenses.views
:members:
:show-inheritance:
Tests
-----
.. automodule:: licenses.tests
:members:
:show-inheritance:
LMS migration
=============
.. automodule:: lms_migration
:members:
:show-inheritance:
Migration
---------
.. automodule:: lms_migration.migrate
:members:
:show-inheritance:
Multicourse
===========
.. automodule:: multicourse
:members:
:show-inheritance:
Psychometrics
=============
.. automodule:: psychometrics
:members:
:show-inheritance:
Models
------
.. automodule:: psychometrics.models
:members:
:show-inheritance:
Admin
-----
.. automodule:: psychometrics.admin
:members:
:show-inheritance:
Psychoanalyze
-------------
.. automodule:: psychometrics.psychoanalyze
:members:
:show-inheritance:
Simple wiki
===========
.. automodule:: simplewiki
:members:
:show-inheritance:
Models
------
.. automodule:: simplewiki.models
:members:
:show-inheritance:
Views
-----
.. automodule:: simplewiki.views
:members:
:show-inheritance:
Tests
-----
.. automodule:: simplewiki.tests
:members:
:show-inheritance:
Static template view
====================
.. automodule:: static_template_view
:members:
:show-inheritance:
Models
------
.. automodule:: static_template_view.models
:members:
:show-inheritance:
Views
-----
.. automodule:: static_template_view.views
:members:
:show-inheritance:
Tests
-----
.. automodule:: static_template_view.tests
:members:
:show-inheritance:
Static book
===========
.. automodule:: staticbook
:members:
:show-inheritance:
Models
------
.. automodule:: staticbook.models
:members:
:show-inheritance:
Views
-----
.. automodule:: staticbook.views
:members:
:show-inheritance:
Tests
-----
.. automodule:: staticbook.tests
:members:
:show-inheritance:
*******************************************
What the pieces are?
*******************************************
What
====
...
How
===
...
Who
===
...
\ No newline at end of file
*******************************************
Xmodule
*******************************************
.. module:: xmodule
Abtest
======
.. automodule:: xmodule.abtest_module
:members:
:show-inheritance:
Back compatibility
==================
.. automodule:: xmodule.backcompat_module
:members:
:show-inheritance:
Capa
====
.. automodule:: xmodule.capa_module
:members:
:show-inheritance:
Course
======
.. automodule:: xmodule.course_module
:members:
:show-inheritance:
Discussion
==========
.. automodule:: xmodule.discussion_module
:members:
:show-inheritance:
Editing
=======
.. automodule:: xmodule.editing_module
:members:
:show-inheritance:
Error
=====
.. automodule:: xmodule.error_module
:members:
:show-inheritance:
Error tracker
=============
.. automodule:: xmodule.errortracker
:members:
:show-inheritance:
Exceptions
==========
.. automodule:: xmodule.exceptions
:members:
:show-inheritance:
Graders
=======
.. automodule:: xmodule.graders
:members:
:show-inheritance:
Hidden
======
.. automodule:: xmodule.hidden_module
:members:
:show-inheritance:
Html checker
============
.. automodule:: xmodule.html_checker
:members:
:show-inheritance:
Html
====
.. automodule:: xmodule.html_module
:members:
:show-inheritance:
Mako
====
.. automodule:: xmodule.mako_module
:members:
:show-inheritance:
Progress
========
.. automodule:: xmodule.progress
:members:
:show-inheritance:
Schematic
=========
.. automodule:: xmodule.schematic_module
:members:
:show-inheritance:
Sequence
========
.. automodule:: xmodule.seq_module
:members:
:show-inheritance:
Stringify
=========
.. automodule:: xmodule.stringify
:members:
:show-inheritance:
Template
========
.. automodule:: xmodule.template_module
:members:
:show-inheritance:
Templates
=========
.. automodule:: xmodule.templates
:members:
:show-inheritance:
Time parse
==========
.. automodule:: xmodule.timeparse
:members:
:show-inheritance:
Vertical
========
.. automodule:: xmodule.vertical_module
:members:
:show-inheritance:
Video
=====
.. automodule:: xmodule.video_module
:members:
:show-inheritance:
X
=
.. automodule:: xmodule.x_module
:members:
:show-inheritance:
Xml
===
.. automodule:: xmodule.xml_module
:members:
:show-inheritance:
# Python libraries to install directly from github
-e git://github.com/MITx/django-staticfiles.git@6d2504e5c8#egg=django-staticfiles
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
-e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
#! /bin/bash
set -e
set -x
# Reset the submodule, in case it changed
git submodule foreach 'git reset --hard HEAD'
# Set the IO encoding to UTF-8 so that askbot will start
export PYTHONIOENCODING=UTF-8
rake clobber
rake pep8 || echo "pep8 failed, continuing"
rake pylint || echo "pylint failed, continuing"
#! /bin/bash
set -e
set -x
# Reset the submodule, in case it changed
git submodule foreach 'git reset --hard HEAD'
# Set the IO encoding to UTF-8 so that askbot will start
export PYTHONIOENCODING=UTF-8
GIT_BRANCH=${GIT_BRANCH/HEAD/master}
pip install -q -r pre-requirements.txt
yes w | pip install -q -r requirements.txt
[ ! -d askbot ] || pip install -q -r askbot/askbot_requirements.txt
rake clobber
TESTS_FAILED=0
rake test_cms[false] || TESTS_FAILED=1
rake test_lms[false] || TESTS_FAILED=1
rake test_common/lib/capa || TESTS_FAILED=1
rake test_common/lib/xmodule || TESTS_FAILED=1
rake phantomjs_jasmine_lms || true
rake phantomjs_jasmine_cms || true
rake coverage:xml coverage:html
[ $TESTS_FAILED == '0' ]
rake autodeploy_properties
\ No newline at end of file
#! /bin/bash
set -e
set -x
# Reset the submodule, in case it changed
git submodule foreach 'git reset --hard HEAD'
# Set the IO encoding to UTF-8 so that askbot will start
export PYTHONIOENCODING=UTF-8
GIT_BRANCH=${GIT_BRANCH/HEAD/master}
pip install -q -r pre-requirements.txt
yes w | pip install -q -r requirements.txt
[ ! -d askbot ] || pip install -q -r askbot/askbot_requirements.txt
rake clobber
TESTS_FAILED=0
rake test_lms[false] || TESTS_FAILED=1
rake test_common/lib/capa || TESTS_FAILED=1
rake test_common/lib/xmodule || TESTS_FAILED=1
rake phantomjs_jasmine_lms || true
rake coverage:xml coverage:html
[ $TESTS_FAILED == '0' ]
rake autodeploy_properties
\ No newline at end of file
# .coveragerc for lms
[run]
data_file = reports/lms/.coverage
source = lms
[report]
ignore_errors = True
[html]
directory = reports/lms/cover
[xml]
output = reports/lms/coverage.xml
# -*- 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
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)
# -*- 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']
\ No newline at end of file
# -*- 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']
\ No newline at end of file
# -*- 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']
\ No newline at end of file
# -*- 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']
\ No newline at end of file
# -*- 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']
\ No newline at end of file
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.
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 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.
'''
it can't be easily guessed and so that it is unique.
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.
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
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 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 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
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 = models.BooleanField(default=False)
class Meta:
unique_together = (('user', 'course_id'),)
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.
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.
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.
deleted - The certificate has been deleted.
downloadable - The certificate is available for download.
notpassing - The student was graded but is not passing
If the state is "downloadable", the dictionary also contains "download_url" and "graded_download_url".
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)
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'}
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
return d
except GeneratedCertificate.DoesNotExist:
pass
return {'status': CertificateStatuses.unavailable}
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')
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
logger = logging.getLogger(__name__)
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")
@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
@login_required
def certificate_request(request):
''' Attempt to send a certificate. '''
if not settings.END_COURSE_ENABLED:
raise Http404
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)
except GeneratedCertificate.DoesNotExist:
generated_certificate = GeneratedCertificate(user=user)
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.")
cert = GeneratedCertificate.objects.get(
user__username=xqueue_body['username'],
course_id=xqueue_body['course_id'],
key=xqueue_header['lms_key'])
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.")
generated_certificate.grade = grade
generated_certificate.name = user_name
generated_certificate.save()
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))
except GeneratedCertificate.DoesNotExist:
logger.critical('Unable to lookup certificate\n'
'xqueue_body: {0}\n'
'xqueue_header: {1}'.format(
xqueue_body, xqueue_header))
return HttpResponse(json.dumps({
'return_code': 1,
'content': 'unable to lookup key'}),
mimetype='application/json')
if 'error' in xqueue_body:
cert.status = status.error
if 'error_reason' in xqueue_body:
# 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
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')
......@@ -108,7 +108,6 @@ class StudentModuleCache(object):
else:
self.cache = []
@classmethod
def cache_for_descriptor_descendents(cls, course_id, user, descriptor, depth=None,
descriptor_filter=lambda descriptor: True,
......@@ -138,7 +137,6 @@ class StudentModuleCache(object):
return descriptors
descriptors = get_child_descriptors(descriptor, depth, descriptor_filter)
return StudentModuleCache(course_id, user, descriptors, select_for_update)
......
......@@ -337,7 +337,7 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
'''
# Test xqueue package, which we expect to be:
# xpackage = {'xqueue_header': json.dumps({'lms_key':'secretkey',...}),
# 'xqueue_body' : 'Message from grader}
# 'xqueue_body' : 'Message from grader'}
get = request.POST.copy()
for key in ['xqueue_header', 'xqueue_body']:
if not get.has_key(key):
......@@ -372,7 +372,8 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
# We go through the "AJAX" path
# So far, the only dispatch from xqueue will be 'score_update'
try:
ajax_return = instance.handle_ajax(dispatch, get) # Can ignore the "ajax" return in 'xqueue_callback'
# Can ignore the return value--not used for xqueue_callback
instance.handle_ajax(dispatch, get)
except:
log.exception("error processing ajax call")
raise
......@@ -386,12 +387,12 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
#Bin score into range and increment stats
score_bucket=get_score_bucket(instance_module.grade, instance_module.max_grade)
org, course_num, run=course_id.split("/")
org, course_num, run=course_id.split("/")
statsd.increment("lms.courseware.question_answered",
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
"run:{0}".format(run),
"score_bucket:{0}".format(score_bucket),
"run:{0}".format(run),
"score_bucket:{0}".format(score_bucket),
"type:xqueue"])
return HttpResponse("")
......@@ -479,12 +480,12 @@ def modx_dispatch(request, dispatch, location, course_id):
#Bin score into range and increment stats
score_bucket=get_score_bucket(instance_module.grade, instance_module.max_grade)
org, course_num, run=course_id.split("/")
org, course_num, run=course_id.split("/")
statsd.increment("lms.courseware.question_answered",
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
"run:{0}".format(run),
"score_bucket:{0}".format(score_bucket),
"run:{0}".format(run),
"score_bucket:{0}".format(score_bucket),
"type:ajax"])
......
......@@ -217,6 +217,7 @@ def index(request, course_id, chapter=None, section=None,
'init': '',
'content': '',
'staff_access': staff_access,
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa')
}
chapter_descriptor = course.get_child_by_url_name(chapter)
......
......@@ -2,6 +2,7 @@ from django.db import models
from django.contrib.auth.models import User
import logging
from courseware.courses import get_course_by_id
class Role(models.Model):
name = models.CharField(max_length=30, null=False, blank=False)
......@@ -23,6 +24,12 @@ class Role(models.Model):
self.permissions.add(Permission.objects.get_or_create(name=permission)[0])
def has_permission(self, permission):
course = get_course_by_id(self.course_id)
if self.name == "Student" and \
(permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \
(not course.forum_posts_allowed):
return False
return self.permissions.filter(name=permission).exists()
......
......@@ -19,7 +19,7 @@ class PermissionsTestCase(TestCase):
def setUp(self):
self.course_id = "MITx/6.002x/2012_Fall"
self.course_id = "edX/toy/2012_Fall"
self.moderator_role = Role.objects.get_or_create(name="Moderator", course_id=self.course_id)[0]
self.student_role = Role.objects.get_or_create(name="Student", course_id=self.course_id)[0]
......
......@@ -23,7 +23,7 @@ DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
MITX_FEATURES['ENABLE_DISCUSSION'] = False
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
......@@ -60,6 +60,7 @@ COURSE_LISTINGS = ENV_TOKENS.get('COURSE_LISTINGS', {})
SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {})
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.
......@@ -79,3 +80,5 @@ XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE']
if 'COURSE_ID' in ENV_TOKENS:
ASKBOT_URL = "courses/{0}/discussions/".format(ENV_TOKENS['COURSE_ID'])
PEARSON_TEST_USER = "pearsontest"
PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD")
......@@ -173,3 +173,8 @@ FILE_UPLOAD_HANDLERS = (
########################### PIPELINE #################################
PIPELINE_SASS_ARGUMENTS = '-r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
########################## PEARSON TESTING ###########################
MITX_FEATURES['ENABLE_PEARSON_HACK_TEST'] = True
PEARSON_TEST_USER = "pearsontest"
PEARSON_TEST_PASSWORD = "12345"
......@@ -27,18 +27,11 @@ SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
# Nose Test Runner
INSTALLED_APPS += ('django_nose',)
NOSE_ARGS = []
# Turning off coverage speeds up tests dramatically... until we have better config,
# leave it here for manual fiddling.
_coverage = True
if _coverage:
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html',
# '-v', '--pdb', # When really stuck, uncomment to start debugger on error
'--cover-inclusive', '--cover-html-dir',
os.environ.get('NOSE_COVER_HTML_DIR', 'cover_html')]
for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
NOSE_ARGS += ['--cover-package', app]
NOSE_ARGS = [
'--with-xunit',
# '-v', '--pdb', # When really stuck, uncomment to start debugger on error
]
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
# Local Directories
......
......@@ -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 {
......
......@@ -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;
......
......@@ -25,7 +25,9 @@
<%static:js group='discussion'/>
<%include file="../discussion/_js_body_dependencies.html" />
% if staff_access:
<%include file="xqa_interface.html"/>
% endif
<!-- TODO: http://docs.jquery.com/Plugins/Validation -->
<script type="text/javascript">
......
<script type="text/javascript" src="/static/js/vendor/jquery.leanModal.min.js"></script>
<script type="text/javascript">
function setup_debug(element_id, edit_link, staff_context){
$('#' + element_id + '_trig').leanModal();
$('#' + element_id + '_xqa_log').leanModal();
$('#' + element_id + '_xqa_form').submit(function () {sendlog(element_id, edit_link, staff_context);});
}
function sendlog(element_id, edit_link, staff_context){
var xqaLog = {
authkey: staff_context.xqa_key,
location: staff_context.location,
category : staff_context.category,
'username' : staff_context.user.username,
return : 'query',
format : 'html',
email : staff_context.user.email,
tag:$('#' + element_id + '_xqa_tag').val(),
entry: $('#' + element_id + '_xqa_entry').val()
};
if (edit_link) xqaLog["giturl"] = edit_link;
$.ajax({
url: '${xqa_server}/log',
type: 'GET',
contentType: 'application/json',
data: JSON.stringify(xqaLog),
crossDomain: true,
dataType: 'jsonp',
beforeSend: function (xhr) {
xhr.setRequestHeader ("Authorization", "Basic eHFhOmFnYXJ3YWw="); },
timeout : 1000,
success: function(result) {
$('#' + element_id + '_xqa_log_data').html(result);
},
error: function() {
alert('Error: cannot connect to XQA server');
console.log('error!');
}
});
return false;
};
function getlog(element_id, staff_context){
var xqaQuery = {
authkey: staff_context.xqa_key,
location: staff_context.location,
format: 'html'
};
$.ajax({
url: '${xqa_server}/query',
type: 'GET',
contentType: 'application/json',
data: JSON.stringify(xqaQuery),
crossDomain: true,
dataType: 'jsonp',
timeout : 1000,
success: function(result) {
$('#' + element_id + '_xqa_log_data').html(result);
},
error: function() {
alert('Error: cannot connect to XQA server');
}
});
};
</script>
\ No newline at end of file
......@@ -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>
<%! from django_comment_client.permissions import has_permission %>
<%inherit file="../courseware/course_navigation.html" />
<%block name="extratabs">
% if has_permission(user, 'create_thread', course.id):
<li class="right"><a href="#" class="new-post-btn"><span class="new-post-icon"></span>New Post</a></li>
% endif
</%block>
\ No newline at end of file
......@@ -3,4 +3,6 @@
<div class="discussion-module" data-discussion-id="${discussion_id | h}">
<a class="discussion-show control-button" href="javascript:void(0)" data-discussion-id="${discussion_id | h}"><span class="show-hide-discussion-icon"></span><span class="button-text">Show Discussion</span></a>
<a href="#" class="new-post-btn"><span class="new-post-icon"></span>New Post</a>
</div>
<%! from django_comment_client.permissions import has_permission %>
<script type="text/template" id="thread-template">
<article class="discussion-article" data-id="${'<%- id %>'}">
<div class="thread-content-wrapper"></div>
......@@ -7,6 +9,7 @@
<div class="post-status-closed bottom-post-status" style="display: none">
This thread is closed.
</div>
% if course is UNDEFINED or has_permission(user, 'create_comment', course.id):
<form class="discussion-reply-new" data-id="${'<%- id %>'}">
<h4>Post a response:</h4>
<ul class="discussion-errors"></ul>
......@@ -15,6 +18,7 @@
<a class="discussion-submit-post control-button" href="#">Submit</a>
</div>
</form>
% endif
</article>
</script>
......@@ -75,6 +79,7 @@
<div class="discussion-response"></div>
<ol class="comments">
<li class="new-comment response-local">
% if course is UNDEFINED or has_permission(user, 'create_sub_comment', course.id):
<form class="comment-form" data-id="${'<%- wmdId %>'}">
<ul class="discussion-errors"></ul>
<div class="comment-body" data-id="${'<%- wmdId %>'}"
......@@ -83,6 +88,7 @@
<a class="discussion-submit-comment control-button" href="#">Submit</a>
</div>
</form>
% endif
</li>
</ol>
</script>
......
${module_content}
%if edit_link:
<div><a href="${edit_link}">Edit</a> / <a href="#${element_id}_xqa-modal" onclick="getlog_${element_id}()" id="${element_id}_xqa_log">QA</a></div>
<div>
<a href="${edit_link}">Edit</a> /
<a href="#${element_id}_xqa-modal" onclick="javascript:getlog('${element_id}', {
'location': '${location}',
'xqa_key': '${xqa_key}',
'category': '${category}',
'user': '${user}'
})" id="${element_id}_xqa_log">QA</a>
</div>
% endif
<div><a href="#${element_id}_debug" id="${element_id}_trig">Staff Debug Info</a></div>
......@@ -50,77 +58,19 @@ category = ${category | h}
<div id="${element_id}_setup"></div>
## leanModal needs to be included here otherwise this breaks when in a <vertical>
<script type="text/javascript" src="/static/js/vendor/jquery.leanModal.min.js"></script>
<script type="text/javascript">
function setup_debug_${element_id}(){
$('#${element_id}_trig').leanModal();
$('#${element_id}_xqa_log').leanModal();
$('#${element_id}_xqa_form').submit(sendlog_${element_id});
}
setup_debug_${element_id}();
function sendlog_${element_id}(){
var xqaLog = {authkey: '${xqa_key}',
location: '${location}',
%if edit_link:
giturl: '${edit_link}',
%endif
category : '${category}',
username : '${user.username}',
return : 'query',
format : 'html',
email : '${user.email}',
tag:$('#${element_id}_xqa_tag').val(),
entry: $('#${element_id}_xqa_entry').val()};
$.ajax({
url: '${xqa_server}/log',
type: 'GET',
contentType: 'application/json',
data: JSON.stringify(xqaLog),
crossDomain: true,
dataType: 'jsonp',
beforeSend: function (xhr) { xhr.setRequestHeader ("Authorization", "Basic eHFhOmFnYXJ3YWw="); },
timeout : 1000,
success: function(result) {
$('#${element_id}_xqa_log_data').html(result);
},
error: function() {
alert('Error: cannot connect to XQA server');
console.log('error!');
}
});
return false;
};
function getlog_${element_id}(){
var xqaQuery = {authkey: '${xqa_key}',
location: '${location}',
format: 'html'};
$.ajax({
url: '${xqa_server}/query',
type: 'GET',
contentType: 'application/json',
data: JSON.stringify(xqaQuery),
crossDomain: true,
dataType: 'jsonp',
timeout : 1000,
success: function(result) {
$('#${element_id}_xqa_log_data').html(result);
},
error: function() {
alert('Error: cannot connect to XQA server');
}
});
};
// assumes courseware.html's loaded this method.
setup_debug('${element_id}',
%if edit_link:
'${edit_link}',
%else:
null,
%endif
{
'location': '${location}',
'xqa_key': '${xqa_key}',
'category': '${category}',
'user': '${user}'
});
</script>
......@@ -30,99 +30,20 @@
<hr class="horizontal-divider">
<section class="jobs-wrapper">
<section class="jobs-listing">
<article id="content-engineer" class="job">
<article id="" class="job">
<div class="inner-wrapper">
<h3>EdX Content Engineer</h3>
<p>Content Engineers support edX Fellows and edX Course Managers in the overall technical development of course content, assessments, and domain-specific online tools. Tasks include developing graders for rich problems, designing automated tools for import of problems from other formats, as well as creating new ways for students to interact with domain-specific problems in the system.</p>
<p>A candidate must have:</p>
<ul>
<li>Python or JavaScript development experience</li>
<li>A deep interest in pedagogy and education</li>
</ul>
<p>Knowledge of GWT or Backbone.js a plus.</p> <p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
</div>
</article>
<article id="platform-developer" class="job">
<div class="inner-wrapper">
<h3>Platform Developer</h3>
<p>Platform Developers build the core learning platform that powers edX, writing both front-end and back-end code. They tackle a wide range of technical challenges, and so the best candidates will have a strong background in one or more of the following areas: machine learning, education, user interaction design, big data, social network analysis, and devops. Specialists are encouraged to apply, but team members often wear many hats. Our ideal candidate would have excellent coding skills, a proven history of delivering projects, and a deep research background.</p>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p>
</div>
</article>
<article id="learning-designer" class="job">
<div class="inner-wrapper">
<h3>Learning Designer/Interaction Learning Designer </h3>
<p>The Learning Designer will work as part of the content and development team to plan, develop and deliver highly engaging and media rich online courses. The learning designer will be a flexible thinker, able to determine and apply sound pedagogical strategies to unique situations and a diverse set of academic disciplines. This is a 6-12 months contract opportunity.</p>
<h4>Specific Responsibilities include: </h4>
<ul>
<li>Work with producers, product developers and course staff on implementing instructional design approaches in the development of media and other course materials. </li>
<li>Articulate learning objectives and align them to content design strategy and assessments. </li>
<li>Write effective instructional text, and audio and video scripts. </li>
<li>Coordinate workflows with video and content development team</li>
<li>Identify best practices and share these with the course staff and faculty as needed. </li>
<li>Create course communication style guides. Train and coach teaching staff on best practices for communication and discussion management. </li>
<li>Develop use case guides as needed on the use of edX courseware and new technologies. </li>
<li>Serve as a liaison to instructional design teams located at X universities. </li>
<li>Design peer review processes to be used by learners in selected courses. </li>
<li>Ability to apply game-based learning theory and design into selected courses as appropriate.</li>
<li>Use learning analytics and metrics to inform course design and revision process. </li>
<li>Work closely with the Content Research Director on articulating best practices for MOOC teaching and learning and course design.</li>
<li>Assist in the development of pilot courses used for sponsored research initiatives. </li>
</ul>
<h4>Qualifications:</h4>
<p>Master's Degree in Educational Technology, Instructional Design or related field. Experience in higher education with additional experience in a start-up or research environment desirable. Excellent interpersonal and communication (written and verbal), project management, problem-solving and time management skills. The ability to be flexible with projects and to work on multiple courses essential. Ability to meet deadlines and manage expectations of constituents. Capacity to develop new and relevant technology skills. &nbsp;Experience using game theory design and learning analytics to inform instructional design decisions and strategy.</p>
<h4>Technical Skills:</h4>
<p>Video and screencasting experience. LMS Platform experience, xml, HTML, CSS, Adobe Design Suite, Camtasia or Captivate experience. Experience with web 2.0 collaboration tools.</p>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p>
</div>
</article>
<article id="production-coordinator" class="job">
<div class="inner-wrapper">
<h3>Production Coordinator</h3>
<p>The Production Coordinator supports video editors and course staff in all video related tasks, such as ingesting footage, transcoding, tracking live dates, transcriptions, organizing project deliverables and archiving completed projects.</p>
<h4>Primary responsibilities:</h4>
<ul>
<li>organize, track, and manage video and associated assets across the video workflow</li>
<li>manage project data and spreadsheets</li>
<li>route incoming source footage, and apply metadata tags</li>
<li>run encoding/transcoding jobs </li>
<li>prepare and process associated video assets, such as slides and image files</li>
<li>manage the transcription process </li>
<ul type="circle">
<li>traffic files among project staff and video transcription services</li>
<li>coordinate transcript reviews with course staff</li>
<li>integrate transcripts in course pages</li>
</ul>
<li>other video-related tasks as assigned.</li>
</ul>
<br/>
<h4>Qualifications</h4>
<p>The ideal candidate for the Production Coordinator position will have</p>
<ul>
<li>relentless attention to detail</li>
<li>ability to communicate and collaborate effectively across the organization</li>
<li>knowledge and understanding of digital media production tools and processes</li>
<li>experience with compression techniques, image processing, and presentation software preferred</li>
<li>proficiency with standard office applications </li>
<ul type="circle">
<li>spreadsheets</li>
<li>word processing</li>
<li>presentation</li>
</ul>
<li>experience with web publishing, e.g., HTML, XML, CSS, a plus</li>
</ul>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p>
</div>
<h3>We're hiring! </h3>
<p>Are you passionate? Want to help change the world? Good, you've found the right company! We're growing and our team needs the best and brightest in creating the next evolution in interactive online education.</p>
<h4>Want to apply to edX?</h4>
<p>Send your resume and cover letter to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
<p><em>Note:</em> We'll review each and every resume but please note you may not get a response due to the volume of inquiries.</p>
</article>
</section>
<section class="jobs-sidebar">
<h2>Positions</h2>
<nav>
<a href="#content-engineer">EdX Content Engineer</a>
<a href="#platform-developer">Platform Developer</a>
<a href="#learning-designer">Learning Designer</a>
<a href="#production-coordinator">Production Coordinator</a>
</nav>
<!-- <h2>Positions</h2> -->
<!-- <nav> -->
<!-- <a href="#content-engineer">EdX Content Engineer</a> -->
<!-- </nav> -->
<h2>How to Apply</h2>
<p>E-mail your resume, coverletter and any other materials to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p>
<h2>Our Location</h2>
......
......@@ -18,3 +18,9 @@
<p>Download video <a href="${source}">here</a>.</p>
</div>
% endif
% if track:
<div class="video-tracks">
<p>Download subtitles <a href="${track}">here</a>.</p>
</div>
% endif
......@@ -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"),
......@@ -26,6 +29,11 @@ urlpatterns = ('',
url(r'^reject_name_change$', 'student.views.reject_name_change'),
url(r'^pending_name_changes$', 'student.views.pending_name_changes'),
url(r'^testcenter/login$', 'student.views.test_center_login'),
# url(r'^testcenter/login$', 'student.test_center_views.login'),
# url(r'^testcenter/logout$', 'student.test_center_views.logout'),
url(r'^event$', 'track.views.user_track'),
url(r'^t/(?P<template>[^/]*)$', 'static_template_view.views.index'), # TODO: Is this used anymore? What is STATIC_GRAB?
......
# Python libraries to install that are local to the mitx repo
-e common/lib/capa
-e common/lib/xmodule
......@@ -26,7 +26,7 @@ PACKAGE_REPO = "packages@gp.mitx.mit.edu:/opt/pkgrepo.incoming"
NORMALIZED_DEPLOY_NAME = DEPLOY_NAME.downcase().gsub(/[_\/]/, '-')
INSTALL_DIR_PATH = File.join(DEPLOY_DIR, NORMALIZED_DEPLOY_NAME)
# Set up the clean and clobber tasks
CLOBBER.include(BUILD_DIR, REPORT_DIR, 'cover*', '.coverage', 'test_root/*_repo', 'test_root/staticfiles')
CLOBBER.include(BUILD_DIR, REPORT_DIR, 'test_root/*_repo', 'test_root/staticfiles')
CLEAN.include("#{BUILD_DIR}/*.deb", "#{BUILD_DIR}/util")
def select_executable(*cmds)
......@@ -38,6 +38,10 @@ def django_admin(system, env, command, *args)
return "#{django_admin} #{command} --settings=#{system}.envs.#{env} --pythonpath=. #{args.join(' ')}"
end
def report_dir_path(dir)
return File.join(REPORT_DIR, dir.to_s)
end
task :default => [:test, :pep8, :pylint]
directory REPORT_DIR
......@@ -49,16 +53,16 @@ default_options = {
task :predjango do
sh("find . -type f -name *.pyc -delete")
sh('pip install -e common/lib/xmodule -e common/lib/capa')
sh('pip install -q --upgrade -r local-requirements.txt')
sh('git submodule update --init')
end
task :clean_test_files do
sh("git clean -fdx test_root")
sh("git clean -fqdx test_root")
end
[:lms, :cms, :common].each do |system|
report_dir = File.join(REPORT_DIR, system.to_s)
report_dir = report_dir_path(system)
directory report_dir
desc "Run pep8 on all #{system} code"
......@@ -84,11 +88,19 @@ end
$failed_tests = 0
def run_under_coverage(cmd, root)
cmd0, cmd_rest = cmd.split(" ", 2)
# We use "python -m coverage" so that the proper python will run the importable coverage
# rather than the coverage that OS path finds.
cmd = "python -m coverage run --rcfile=#{root}/.coveragerc `which #{cmd0}` #{cmd_rest}"
return cmd
end
def run_tests(system, report_dir, stop_on_failure=true)
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
ENV['NOSE_COVER_HTML_DIR'] = File.join(report_dir, "cover")
dirs = Dir["common/djangoapps/*"] + Dir["#{system}/djangoapps/*"]
sh(django_admin(system, :test, 'test', *dirs.each)) do |ok, res|
cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', *dirs.each)
sh(run_under_coverage(cmd, system)) do |ok, res|
if !ok and stop_on_failure
abort "Test failed!"
end
......@@ -96,11 +108,10 @@ def run_tests(system, report_dir, stop_on_failure=true)
end
end
TEST_TASKS = []
TEST_TASK_DIRS = []
[:lms, :cms].each do |system|
report_dir = File.join(REPORT_DIR, system.to_s)
directory report_dir
report_dir = report_dir_path(system)
# Per System tasks
desc "Run all django tests on our djangoapps for the #{system}"
......@@ -113,7 +124,7 @@ TEST_TASKS = []
run_tests(system, report_dir, args.stop_on_failure)
end
TEST_TASKS << "test_#{system}"
TEST_TASK_DIRS << system
desc <<-desc
Start the #{system} locally with the specified environment (defaults to dev).
......@@ -134,35 +145,48 @@ TEST_TASKS = []
desc "Run collectstatic in the specified environment"
task "#{system}:collectstatic:#{env}" => :predjango do
sh("#{django_admin(system, env, 'collectstatic', '--noinput')}")
sh("#{django_admin(system, env, 'collectstatic', '--noinput')} > /tmp/collectstatic.out") do |ok, status|
if !ok
abort "collectstatic failed!"
end
end
end
end
end
Dir["common/lib/*"].each do |lib|
Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib|
task_name = "test_#{lib}"
report_dir = File.join(REPORT_DIR, task_name.gsub('/', '_'))
directory report_dir
report_dir = report_dir_path(lib)
desc "Run tests for common lib #{lib}"
task task_name => report_dir do
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
sh("nosetests #{lib} --cover-erase --with-xunit --with-xcoverage --cover-html --cover-inclusive --cover-package #{File.basename(lib)} --cover-html-dir #{File.join(report_dir, "cover")}")
cmd = "nosetests #{lib} --logging-clear-handlers --with-xunit"
sh(run_under_coverage(cmd, lib)) do |ok, res|
$failed_tests += 1 unless ok
end
end
TEST_TASKS << task_name
TEST_TASK_DIRS << lib
desc "Run tests for common lib #{lib} (without coverage)"
task "fasttest_#{lib}" do
sh("nosetests #{lib}")
end
end
task :report_dirs
TEST_TASK_DIRS.each do |dir|
report_dir = report_dir_path(dir)
directory report_dir
task :report_dirs => [REPORT_DIR, report_dir]
end
task :test do
TEST_TASKS.each do |task|
Rake::Task[task].invoke(false)
TEST_TASK_DIRS.each do |dir|
Rake::Task["test_#{dir}"].invoke(false)
end
if $failed_tests > 0
......@@ -170,6 +194,34 @@ task :test do
end
end
namespace :coverage do
desc "Build the html coverage reports"
task :html => :report_dirs do
TEST_TASK_DIRS.each do |dir|
report_dir = report_dir_path(dir)
if !File.file?("#{report_dir}/.coverage")
next
end
sh("coverage html --rcfile=#{dir}/.coveragerc")
end
end
desc "Build the xml coverage reports"
task :xml => :report_dirs do
TEST_TASK_DIRS.each do |dir|
report_dir = report_dir_path(dir)
if !File.file?("#{report_dir}/.coverage")
next
end
# Why doesn't the rcfile control the xml output file properly??
sh("coverage xml -o #{report_dir}/coverage.xml --rcfile=#{dir}/.coveragerc")
end
end
end
task :runserver => :lms
desc "Run django-admin <action> against the specified system and environment"
......@@ -250,3 +302,24 @@ task :autodeploy_properties do
file.puts("UPSTREAM_REVISION=#{COMMIT}")
end
end
desc "Invoke sphinx 'make build' to generate docs."
task :builddocs do
Dir.chdir('docs') do
sh('make html')
end
end
desc "Show doc in browser (mac only for now) TODO add linux support"
task :showdocs do
Dir.chdir('docs/build/html') do
sh('open index.html')
end
end
desc "Build docs and show them in browser"
task :doc => :builddocs do
Dir.chdir('docs/build/html') do
sh('open index.html')
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment