Commit c917725f by Calen Pennington

Merge remote-tracking branch 'origin/master' into feature/cale/cms-master

Conflicts:
	common/lib/xmodule/xmodule/seq_module.py
	common/lib/xmodule/xmodule/template_module.py
	common/lib/xmodule/xmodule/x_module.py
	lms/djangoapps/courseware/tests/tests.py
	lms/djangoapps/courseware/views.py
	lms/static/sass/course.scss
	requirements.txt
parents 1a89c14b 98fefd14
python-software-properties
pkg-config
curl
git
python-virtualenv
build-essential
python-dev
gfortran
liblapack-dev
libfreetype6-dev
libpng12-dev
libxml2-dev
libxslt-dev
yui-compressor
graphviz
graphviz-dev
mysql-server
libmysqlclient-dev
libgeos-dev
libreadline6
libreadline6-dev
mongodb
nodejs
npm
coffeescript
ppa:chris-lea/node.js
ppa:chris-lea/node.js-libs
ppa:chris-lea/libjs-underscore
readline
sqlite
gdbm
pkg-config
gfortran
python
yuicompressor
readline
sqlite
gdbm
pkg-config
gfortran
python
yuicompressor
node
graphviz
mysql
geos
mongodb
......@@ -12,10 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
log = logging.getLogger("mitx." + __name__)
from django.template import Context
from django.http import HttpResponse
......
......@@ -54,5 +54,4 @@ class Template(MakoTemplate):
context_dictionary['MITX_ROOT_URL'] = settings.MITX_ROOT_URL
context_dictionary['django_context'] = context_instance
return super(Template, self).render(**context_dictionary)
return super(Template, self).render_unicode(**context_dictionary)
......@@ -36,10 +36,12 @@ file and check it in at the same time as your model changes. To do that,
3. Add the migration file created in mitx/common/djangoapps/student/migrations/
"""
from datetime import datetime
from hashlib import sha1
import json
import logging
import uuid
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
......@@ -125,9 +127,9 @@ class UserProfile(models.Model):
self.meta = json.dumps(js)
class TestCenterUser(models.Model):
"""This is our representation of the User for in-person testing, and
"""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
......@@ -135,9 +137,9 @@ class TestCenterUser(models.Model):
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
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...
......@@ -148,21 +150,21 @@ class TestCenterUser(models.Model):
# 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)
......@@ -175,7 +177,7 @@ class TestCenterUser(models.Model):
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)
......@@ -183,14 +185,28 @@ class TestCenterUser(models.Model):
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
def unique_id_for_user(user):
"""
Return a unique id for a user, suitable for inserting into
e.g. personalized survey links.
Currently happens to be implemented as a sha1 hash of the username
(and thus assumes that usernames don't change).
"""
# Using the user id as the salt because it's sort of random, and is already
# in the db.
salt = str(user.id)
return sha1(salt + user.username).hexdigest()
## TODO: Should be renamed to generic UserGroup, and possibly
# Given an optional field for type of group
class UserTestGroup(models.Model):
......@@ -363,10 +379,10 @@ def replicate_user_save(sender, **kwargs):
# @receiver(post_save, sender=CourseEnrollment)
def replicate_enrollment_save(sender, **kwargs):
"""This is called when a Student enrolls in a course. It has to do the
"""This is called when a Student enrolls in a course. It has to do the
following:
1. Make sure the User is copied into the Course DB. It may already exist
1. Make sure the User is copied into the Course DB. It may already exist
(someone deleting and re-adding a course). This has to happen first or
the foreign key constraint breaks.
2. Replicate the CourseEnrollment.
......@@ -410,9 +426,9 @@ USER_FIELDS_TO_COPY = ["id", "username", "first_name", "last_name", "email",
def replicate_user(portal_user, course_db_name):
"""Replicate a User to the correct Course DB. This is more complicated than
it should be because Askbot extends the auth_user table and adds its own
it should be because Askbot extends the auth_user table and adds its own
fields. So we need to only push changes to the standard fields and leave
the rest alone so that Askbot changes at the Course DB level don't get
the rest alone so that Askbot changes at the Course DB level don't get
overridden.
"""
try:
......@@ -457,7 +473,7 @@ def is_valid_course_id(course_id):
"""Right now, the only database that's not a course database is 'default'.
I had nicer checking in here originally -- it would scan the courses that
were in the system and only let you choose that. But it was annoying to run
tests with, since we don't have course data for some for our course test
tests with, since we don't have course data for some for our course test
databases. Hence the lazy version.
"""
return course_id != 'default'
......
......@@ -6,11 +6,16 @@ Replace this with more appropriate tests for your application.
"""
import logging
from datetime import datetime
from hashlib import sha1
from django.test import TestCase
from mock import patch, Mock
from nose.plugins.skip import SkipTest
from .models import User, UserProfile, CourseEnrollment, replicate_user, USER_FIELDS_TO_COPY
from .models import (User, UserProfile, CourseEnrollment,
replicate_user, USER_FIELDS_TO_COPY,
unique_id_for_user)
from .views import process_survey_link, _cert_info
COURSE_1 = 'edX/toy/2012_Fall'
COURSE_2 = 'edx/full/6.002_Spring_2012'
......@@ -55,7 +60,7 @@ class ReplicationTest(TestCase):
# This hasattr lameness is here because we don't want this test to be
# triggered when we're being run by CMS tests (Askbot doesn't exist
# there, so the test will fail).
#
#
# seen_response_count isn't a field we care about, so it shouldn't have
# been copied over.
if hasattr(portal_user, 'seen_response_count'):
......@@ -74,7 +79,7 @@ class ReplicationTest(TestCase):
# During this entire time, the user data should never have made it over
# to COURSE_2
self.assertRaises(User.DoesNotExist,
self.assertRaises(User.DoesNotExist,
User.objects.using(COURSE_2).get,
id=portal_user.id)
......@@ -108,19 +113,19 @@ class ReplicationTest(TestCase):
# Grab all the copies we expect
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
self.assertEquals(portal_user, course_user)
self.assertRaises(User.DoesNotExist,
self.assertRaises(User.DoesNotExist,
User.objects.using(COURSE_2).get,
id=portal_user.id)
course_enrollment = CourseEnrollment.objects.using(COURSE_1).get(id=portal_enrollment.id)
self.assertEquals(portal_enrollment, course_enrollment)
self.assertRaises(CourseEnrollment.DoesNotExist,
self.assertRaises(CourseEnrollment.DoesNotExist,
CourseEnrollment.objects.using(COURSE_2).get,
id=portal_enrollment.id)
course_user_profile = UserProfile.objects.using(COURSE_1).get(id=portal_user_profile.id)
self.assertEquals(portal_user_profile, course_user_profile)
self.assertRaises(UserProfile.DoesNotExist,
self.assertRaises(UserProfile.DoesNotExist,
UserProfile.objects.using(COURSE_2).get,
id=portal_user_profile.id)
......@@ -174,30 +179,112 @@ class ReplicationTest(TestCase):
portal_user.save()
portal_user_profile.gender = 'm'
portal_user_profile.save()
# Grab all the copies we expect, and make sure it doesn't end up in
# Grab all the copies we expect, and make sure it doesn't end up in
# places we don't expect.
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
self.assertEquals(portal_user, course_user)
self.assertRaises(User.DoesNotExist,
self.assertRaises(User.DoesNotExist,
User.objects.using(COURSE_2).get,
id=portal_user.id)
course_enrollment = CourseEnrollment.objects.using(COURSE_1).get(id=portal_enrollment.id)
self.assertEquals(portal_enrollment, course_enrollment)
self.assertRaises(CourseEnrollment.DoesNotExist,
self.assertRaises(CourseEnrollment.DoesNotExist,
CourseEnrollment.objects.using(COURSE_2).get,
id=portal_enrollment.id)
course_user_profile = UserProfile.objects.using(COURSE_1).get(id=portal_user_profile.id)
self.assertEquals(portal_user_profile, course_user_profile)
self.assertRaises(UserProfile.DoesNotExist,
self.assertRaises(UserProfile.DoesNotExist,
UserProfile.objects.using(COURSE_2).get,
id=portal_user_profile.id)
class CourseEndingTest(TestCase):
"""Test things related to course endings: certificates, surveys, etc"""
def test_process_survey_link(self):
username = "fred"
user = Mock(username=username)
id = unique_id_for_user(user)
link1 = "http://www.mysurvey.com"
self.assertEqual(process_survey_link(link1, user), link1)
link2 = "http://www.mysurvey.com?unique={UNIQUE_ID}"
link2_expected = "http://www.mysurvey.com?unique={UNIQUE_ID}".format(UNIQUE_ID=id)
self.assertEqual(process_survey_link(link2, user), link2_expected)
def test_cert_info(self):
user = Mock(username="fred")
survey_url = "http://a_survey.com"
course = Mock(end_of_course_survey_url=survey_url)
self.assertEqual(_cert_info(user, course, None),
{'status': 'processing',
'show_disabled_download_button': False,
'show_download_url': False,
'show_survey_button': False,})
cert_status = {'status': 'unavailable'}
self.assertEqual(_cert_info(user, course, cert_status),
{'status': 'processing',
'show_disabled_download_button': False,
'show_download_url': False,
'show_survey_button': False})
cert_status = {'status': 'generating', 'grade': '67'}
self.assertEqual(_cert_info(user, course, cert_status),
{'status': 'generating',
'show_disabled_download_button': True,
'show_download_url': False,
'show_survey_button': True,
'survey_url': survey_url,
'grade': '67'
})
cert_status = {'status': 'regenerating', 'grade': '67'}
self.assertEqual(_cert_info(user, course, cert_status),
{'status': 'generating',
'show_disabled_download_button': True,
'show_download_url': False,
'show_survey_button': True,
'survey_url': survey_url,
'grade': '67'
})
download_url = 'http://s3.edx/cert'
cert_status = {'status': 'downloadable', 'grade': '67',
'download_url': download_url}
self.assertEqual(_cert_info(user, course, cert_status),
{'status': 'ready',
'show_disabled_download_button': False,
'show_download_url': True,
'download_url': download_url,
'show_survey_button': True,
'survey_url': survey_url,
'grade': '67'
})
cert_status = {'status': 'notpassing', 'grade': '67',
'download_url': download_url}
self.assertEqual(_cert_info(user, course, cert_status),
{'status': 'notpassing',
'show_disabled_download_button': False,
'show_download_url': False,
'show_survey_button': True,
'survey_url': survey_url,
'grade': '67'
})
# Test a course that doesn't have a survey specified
course2 = Mock(end_of_course_survey_url=None)
cert_status = {'status': 'notpassing', 'grade': '67',
'download_url': download_url}
self.assertEqual(_cert_info(user, course2, cert_status),
{'status': 'notpassing',
'show_disabled_download_button': False,
'show_download_url': False,
'show_survey_button': False,
'grade': '67'
})
......@@ -28,7 +28,7 @@ from django.core.cache import cache
from django_future.csrf import ensure_csrf_cookie, csrf_exempt
from student.models import (Registration, UserProfile,
PendingNameChange, PendingEmailChange,
CourseEnrollment)
CourseEnrollment, unique_id_for_user)
from certificates.models import CertificateStatuses, certificate_status_for_student
......@@ -39,6 +39,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from datetime import date
from collections import namedtuple
from courseware.courses import get_courses_by_university
from courseware.access import has_access
......@@ -68,20 +69,6 @@ def index(request, extra_context={}, user=None):
extra_context is used to allow immediate display of certain modal windows, eg signup,
as used by external_auth.
'''
feed_data = cache.get("students_index_rss_feed_data")
if feed_data == None:
if hasattr(settings, 'RSS_URL'):
feed_data = urllib.urlopen(settings.RSS_URL).read()
else:
feed_data = render_to_string("feed.rss", None)
cache.set("students_index_rss_feed_data", feed_data, settings.RSS_TIMEOUT)
feed = feedparser.parse(feed_data)
entries = feed['entries'][0:3]
for entry in entries:
soup = BeautifulSoup(entry.description)
entry.image = soup.img['src'] if soup.img else None
entry.summary = soup.getText()
# The course selection work is done in courseware.courses.
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
......@@ -89,7 +76,11 @@ def index(request, extra_context={}, user=None):
domain = request.META.get('HTTP_HOST')
universities = get_courses_by_university(None,
domain=domain)
context = {'universities': universities, 'entries': entries}
# Get the 3 most recent news
top_news = _get_news(top=3)
context = {'universities': universities, 'news': top_news}
context.update(extra_context)
return render_to_response('index.html', context)
......@@ -107,9 +98,9 @@ def get_date_for_press(publish_date):
# strip off extra months, and just use the first:
date = re.sub(multimonth_pattern, ", ", publish_date)
if re.search(day_pattern, date):
date = datetime.datetime.strptime(date, "%B %d, %Y")
else:
date = datetime.datetime.strptime(date, "%B, %Y")
date = datetime.datetime.strptime(date, "%B %d, %Y")
else:
date = datetime.datetime.strptime(date, "%B, %Y")
return date
def press(request):
......@@ -127,6 +118,87 @@ def press(request):
return render_to_response('static_templates/press.html', {'articles': articles})
def process_survey_link(survey_link, user):
"""
If {UNIQUE_ID} appears in the link, replace it with a unique id for the user.
Currently, this is sha1(user.username). Otherwise, return survey_link.
"""
return survey_link.format(UNIQUE_ID=unique_id_for_user(user))
def cert_info(user, course):
"""
Get the certificate info needed to render the dashboard section for the given
student and course. Returns a dictionary with keys:
'status': one of 'generating', 'ready', 'notpassing', 'processing'
'show_download_url': bool
'download_url': url, only present if show_download_url is True
'show_disabled_download_button': bool -- true if state is 'generating'
'show_survey_button': bool
'survey_url': url, only if show_survey_button is True
'grade': if status is not 'processing'
"""
if not course.has_ended():
return {}
return _cert_info(user, course, certificate_status_for_student(user, course.id))
def _cert_info(user, course, cert_status):
"""
Implements the logic for cert_info -- split out for testing.
"""
default_status = 'processing'
default_info = {'status': default_status,
'show_disabled_download_button': False,
'show_download_url': False,
'show_survey_button': False}
if cert_status is None:
return default_info
# simplify the status for the template using this lookup table
template_state = {
CertificateStatuses.generating: 'generating',
CertificateStatuses.regenerating: 'generating',
CertificateStatuses.downloadable: 'ready',
CertificateStatuses.notpassing: 'notpassing',
}
status = template_state.get(cert_status['status'], default_status)
d = {'status': status,
'show_download_url': status == 'ready',
'show_disabled_download_button': status == 'generating',}
if (status in ('generating', 'ready', 'notpassing') and
course.end_of_course_survey_url is not None):
d.update({
'show_survey_button': True,
'survey_url': process_survey_link(course.end_of_course_survey_url, user)})
else:
d['show_survey_button'] = False
if status == 'ready':
if 'download_url' not in cert_status:
log.warning("User %s has a downloadable cert for %s, but no download url",
user.username, course.id)
return default_info
else:
d['download_url'] = cert_status['download_url']
if status in ('generating', 'ready', 'notpassing'):
if 'grade' not in cert_status:
# Note: as of 11/20/2012, we know there are students in this state-- cs169.1x,
# who need to be regraded (we weren't tracking 'notpassing' at first).
# We can add a log.warning here once we think it shouldn't happen.
return default_info
else:
d['grade'] = cert_status['grade']
return d
@login_required
@ensure_csrf_cookie
def dashboard(request):
......@@ -160,12 +232,10 @@ 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 = {}
cert_statuses = { course.id: cert_info(request.user, course) for course in courses}
# Get the 3 most recent news
top_news = _get_news(top=3)
context = {'courses': courses,
'message': message,
......@@ -173,6 +243,7 @@ def dashboard(request):
'errored_courses': errored_courses,
'show_courseware_links_for' : show_courseware_links_for,
'cert_statuses': cert_statuses,
'news': top_news,
}
return render_to_response('dashboard.html', context)
......@@ -820,3 +891,24 @@ def test_center_login(request):
return redirect('/courses/MITx/6.002x/2012_Fall/courseware/Final_Exam/Final_Exam_Fall_2012/')
else:
return HttpResponseForbidden()
def _get_news(top=None):
"Return the n top news items on settings.RSS_URL"
feed_data = cache.get("students_index_rss_feed_data")
if feed_data == None:
if hasattr(settings, 'RSS_URL'):
feed_data = urllib.urlopen(settings.RSS_URL).read()
else:
feed_data = render_to_string("feed.rss", None)
cache.set("students_index_rss_feed_data", feed_data, settings.RSS_TIMEOUT)
feed = feedparser.parse(feed_data)
entries = feed['entries'][0:top] # all entries if top is None
for entry in entries:
soup = BeautifulSoup(entry.description)
entry.image = soup.img['src'] if soup.img else None
entry.summary = soup.getText()
return entries
......@@ -33,6 +33,7 @@ from xml.sax.saxutils import unescape
import chem
import chem.chemcalc
import chem.chemtools
import chem.miller
import calc
from correctmap import CorrectMap
......@@ -67,7 +68,8 @@ global_context = {'random': random,
'calc': calc,
'eia': eia,
'chemcalc': chem.chemcalc,
'chemtools': chem.chemtools}
'chemtools': chem.chemtools,
'miller': chem.miller}
# These should be removed from HTML output, including all subelements
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"]
......
""" Calculation of Miller indices """
import numpy as np
import math
import fractions as fr
import decimal
import json
def lcm(a, b):
"""
Returns least common multiple of a, b
Args:
a, b: floats
Returns:
float
"""
return a * b / fr.gcd(a, b)
def segment_to_fraction(distance):
"""
Converts lengths of which the plane cuts the axes to fraction.
Tries convert distance to closest nicest fraction with denominator less or
equal than 10. It is
purely for simplicity and clearance of learning purposes. Jenny: 'In typical
courses students usually do not encounter indices any higher than 6'.
If distance is not a number (numpy nan), it means that plane is parallel to
axis or contains it. Inverted fraction to nan (nan is 1/0) = 0 / 1 is
returned
Generally (special cases):
a) if distance is smaller than some constant, i.g. 0.01011,
than fraction's denominator usually much greater than 10.
b) Also, if student will set point on 0.66 -> 1/3, so it is 333 plane,
But if he will slightly move the mouse and click on 0.65 -> it will be
(16,15,16) plane. That's why we are doing adjustments for points coordinates,
to the closest tick, tick + tick / 2 value. And now UI sends to server only
values multiple to 0.05 (half of tick). Same rounding is implemented for
unittests.
But if one will want to calculate miller indices with exact coordinates and
with nice fractions (which produce small Miller indices), he may want shift
to new origin if segments are like S = (0.015, > 0.05, >0.05) - close to zero
in one coordinate. He may update S to (0, >0.05, >0.05) and shift origin.
In this way he can recieve nice small fractions. Also there is can be
degenerated case when S = (0.015, 0.012, >0.05) - if update S to (0, 0, >0.05) -
it is a line. This case should be considered separately. Small nice Miller
numbers and possibility to create very small segments can not be implemented
at same time).
Args:
distance: float distance that plane cuts on axis, it must not be 0.
Distance is multiple of 0.05.
Returns:
Inverted fraction.
0 / 1 if distance is nan
"""
if np.isnan(distance):
return fr.Fraction(0, 1)
else:
fract = fr.Fraction(distance).limit_denominator(10)
return fr.Fraction(fract.denominator, fract.numerator)
def sub_miller(segments):
'''
Calculates Miller indices from segments.
Algorithm:
1. Obtain inverted fraction from segments
2. Find common denominator of inverted fractions
3. Lead fractions to common denominator and throws denominator away.
4. Return obtained values.
Args:
List of 3 floats, meaning distances that plane cuts on x, y, z axes.
Any float not equals zero, it means that plane does not intersect origin,
i. e. shift of origin has already been done.
Returns:
String that represents Miller indices, e.g: (-6,3,-6) or (2,2,2)
'''
fracts = [segment_to_fraction(segment) for segment in segments]
common_denominator = reduce(lcm, [fract.denominator for fract in fracts])
miller = ([fract.numerator * math.fabs(common_denominator) /
fract.denominator for fract in fracts])
return'(' + ','.join(map(str, map(decimal.Decimal, miller))) + ')'
def miller(points):
"""
Calculates Miller indices from points.
Algorithm:
1. Calculate normal vector to a plane that goes trough all points.
2. Set origin.
3. Create Cartesian coordinate system (Ccs).
4. Find the lengths of segments of which the plane cuts the axes. Equation
of a line for axes: Origin + (Coordinate_vector - Origin) * parameter.
5. If plane goes trough Origin:
a) Find new random origin: find unit cube vertex, not crossed by a plane.
b) Repeat 2-4.
c) Fix signs of segments after Origin shift. This means to consider
original directions of axes. I.g.: Origin was 0,0,0 and became
new_origin. If new_origin has same Y coordinate as Origin, then segment
does not change its sign. But if new_origin has another Y coordinate than
origin (was 0, became 1), than segment has to change its sign (it now
lies on negative side of Y axis). New Origin 0 value of X or Y or Z
coordinate means that segment does not change sign, 1 value -> does
change. So new sign is (1 - 2 * new_origin): 0 -> 1, 1 -> -1
6. Run function that calculates miller indices from segments.
Args:
List of points. Each point is list of float coordinates. Order of
coordinates in point's list: x, y, z. Points are different!
Returns:
String that represents Miller indices, e.g: (-6,3,-6) or (2,2,2)
"""
N = np.cross(points[1] - points[0], points[2] - points[0])
O = np.array([0, 0, 0])
P = points[0] # point of plane
Ccs = map(np.array, [[1.0, 0, 0], [0, 1.0, 0], [0, 0, 1.0]])
segments = ([np.dot(P - O, N) / np.dot(ort, N) if np.dot(ort, N) != 0 else
np.nan for ort in Ccs])
if any(x == 0 for x in segments): # Plane goes through origin.
vertices = [ # top:
np.array([1.0, 1.0, 1.0]),
np.array([0.0, 0.0, 1.0]),
np.array([1.0, 0.0, 1.0]),
np.array([0.0, 1.0, 1.0]),
# bottom, except 0,0,0:
np.array([1.0, 0.0, 0.0]),
np.array([0.0, 1.0, 0.0]),
np.array([1.0, 1.0, 1.0]),
]
for vertex in vertices:
if np.dot(vertex - O, N) != 0: # vertex not in plane
new_origin = vertex
break
# obtain new axes with center in new origin
X = np.array([1 - new_origin[0], new_origin[1], new_origin[2]])
Y = np.array([new_origin[0], 1 - new_origin[1], new_origin[2]])
Z = np.array([new_origin[0], new_origin[1], 1 - new_origin[2]])
new_Ccs = [X - new_origin, Y - new_origin, Z - new_origin]
segments = ([np.dot(P - new_origin, N) / np.dot(ort, N) if
np.dot(ort, N) != 0 else np.nan for ort in new_Ccs])
# fix signs of indices: 0 -> 1, 1 -> -1 (
segments = (1 - 2 * new_origin) * segments
return sub_miller(segments)
def grade(user_input, correct_answer):
'''
Grade crystallography problem.
Returns true if lattices are the same and Miller indices are same or minus
same. E.g. (2,2,2) = (2, 2, 2) or (-2, -2, -2). Because sign depends only
on student's selection of origin.
Args:
user_input, correct_answer: json. Format:
user_input: {"lattice":"sc","points":[["0.77","0.00","1.00"],
["0.78","1.00","0.00"],["0.00","1.00","0.72"]]}
correct_answer: {'miller': '(00-1)', 'lattice': 'bcc'}
"lattice" is one of: "", "sc", "bcc", "fcc"
Returns:
True or false.
'''
def negative(m):
"""
Change sign of Miller indices.
Args:
m: string with meaning of Miller indices. E.g.:
(-6,3,-6) -> (6, -3, 6)
Returns:
String with changed signs.
"""
output = ''
i = 1
while i in range(1, len(m) - 1):
if m[i] in (',', ' '):
output += m[i]
elif m[i] not in ('-', '0'):
output += '-' + m[i]
elif m[i] == '0':
output += m[i]
else:
i += 1
output += m[i]
i += 1
return '(' + output + ')'
def round0_25(point):
"""
Rounds point coordinates to closest 0.5 value.
Args:
point: list of float coordinates. Order of coordinates: x, y, z.
Returns:
list of coordinates rounded to closes 0.5 value
"""
rounded_points = []
for coord in point:
base = math.floor(coord * 10)
fractional_part = (coord * 10 - base)
aliquot0_25 = math.floor(fractional_part / 0.25)
if aliquot0_25 == 0.0:
rounded_points.append(base / 10)
if aliquot0_25 in (1.0, 2.0):
rounded_points.append(base / 10 + 0.05)
if aliquot0_25 == 3.0:
rounded_points.append(base / 10 + 0.1)
return rounded_points
user_answer = json.loads(user_input)
if user_answer['lattice'] != correct_answer['lattice']:
return False
points = [map(float, p) for p in user_answer['points']]
if len(points) < 3:
return False
# round point to closes 0.05 value
points = [round0_25(point) for point in points]
points = [np.array(point) for point in points]
# print miller(points), (correct_answer['miller'].replace(' ', ''),
# negative(correct_answer['miller']).replace(' ', ''))
if miller(points) in (correct_answer['miller'].replace(' ', ''), negative(correct_answer['miller']).replace(' ', '')):
return True
return False
......@@ -671,18 +671,15 @@ class Crystallography(InputTypeBase):
"""
Note: height, width are required.
"""
return [Attribute('size', None),
Attribute('height'),
return [Attribute('height'),
Attribute('width'),
# can probably be removed (textline should prob be always-hidden)
Attribute('hidden', ''),
]
registry.register(Crystallography)
# -------------------------------------------------------------------------
class VseprInput(InputTypeBase):
"""
Input for molecular geometry--show possible structures, let student
......
......@@ -23,6 +23,7 @@ import abc
import os
import subprocess
import xml.sax.saxutils as saxutils
from shapely.geometry import Point, MultiPoint
# specific library imports
from calc import evaluator, UndefinedVariable
......@@ -1312,8 +1313,6 @@ class CodeResponse(LoncapaResponse):
# Sanity check on returned points
if points < 0:
points = 0
elif points > self.maxpoints[self.answer_id]:
points = self.maxpoints[self.answer_id]
# Queuestate is consumed
oldcmap.set(self.answer_id, npoints=points, correctness=correctness,
msg=msg.replace('&nbsp;', '&#160;'), queuestate=None)
......@@ -1721,15 +1720,38 @@ class ImageResponse(LoncapaResponse):
which produces an [x,y] coordinate pair. The click is correct if it falls
within a region specified. This region is a union of rectangles.
Lon-CAPA requires that each <imageresponse> has a <foilgroup> inside it. That
doesn't make sense to me (Ike). Instead, let's have it such that <imageresponse>
should contain one or more <imageinput> stanzas. Each <imageinput> should specify
a rectangle, given as an attribute, defining the correct answer.
Lon-CAPA requires that each <imageresponse> has a <foilgroup> inside it.
That doesn't make sense to me (Ike). Instead, let's have it such that
<imageresponse> should contain one or more <imageinput> stanzas.
Each <imageinput> should specify a rectangle(s) or region(s), given as an
attribute, defining the correct answer.
<imageinput src="/static/images/Lecture2/S2_p04.png" width="811" height="610"
rectangle="(10,10)-(20,30);(12,12)-(40,60)"
regions="[[[10,10], [20,30], [40, 10]], [[100,100], [120,130], [110,150]]]"/>
Regions is list of lists [region1, region2, region3, ...] where regionN
is disordered list of points: [[1,1], [100,100], [50,50], [20, 70]].
If there is only one region in the list, simpler notation can be used:
regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly
setting outer list)
Returns:
True, if click is inside any region or rectangle. Otherwise False.
"""
snippets = [{'snippet': '''<imageresponse>
<imageinput src="image1.jpg" width="200" height="100" rectangle="(10,10)-(20,30)" />
<imageinput src="image2.jpg" width="210" height="130" rectangle="(12,12)-(40,60)" />
<imageinput src="image2.jpg" width="210" height="130" rectangle="(10,10)-(20,30);(12,12)-(40,60)" />
<imageinput src="image1.jpg" width="200" height="100"
rectangle="(10,10)-(20,30)" />
<imageinput src="image2.jpg" width="210" height="130"
rectangle="(12,12)-(40,60)" />
<imageinput src="image3.jpg" width="210" height="130"
rectangle="(10,10)-(20,30);(12,12)-(40,60)" />
<imageinput src="image4.jpg" width="811" height="610"
rectangle="(10,10)-(20,30);(12,12)-(40,60)"
regions="[[[10,10], [20,30], [40, 10]], [[100,100], [120,130], [110,150]]]"/>
<imageinput src="image5.jpg" width="200" height="200"
regions="[[[10,10], [20,30], [40, 10]], [[100,100], [120,130], [110,150]]]"/>
</imageresponse>'''}]
response_tag = 'imageresponse'
......@@ -1737,19 +1759,17 @@ class ImageResponse(LoncapaResponse):
def setup_response(self):
self.ielements = self.inputfields
self.answer_ids = [ie.get('id') for ie in self.ielements]
self.answer_ids = [ie.get('id') for ie in self.ielements]
def get_score(self, student_answers):
correct_map = CorrectMap()
expectedset = self.get_answers()
for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza
given = student_answers[aid] # this should be a string of the form '[x,y]'
for aid in self.answer_ids: # loop through IDs of <imageinput>
# fields in our stanza
given = student_answers[aid] # this should be a string of the form '[x,y]'
correct_map.set(aid, 'incorrect')
if not given: # No answer to parse. Mark as incorrect and move on
if not given: # No answer to parse. Mark as incorrect and move on
continue
# parse given answer
m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', ''))
if not m:
......@@ -1757,29 +1777,44 @@ class ImageResponse(LoncapaResponse):
'error grading %s (input=%s)' % (aid, given))
(gx, gy) = [int(x) for x in m.groups()]
# Check whether given point lies in any of the solution rectangles
solution_rectangles = expectedset[aid].split(';')
for solution_rectangle in solution_rectangles:
# parse expected answer
# TODO: Compile regexp on file load
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
solution_rectangle.strip().replace(' ', ''))
if not m:
msg = 'Error in problem specification! cannot parse rectangle in %s' % (
etree.tostring(self.ielements[aid], pretty_print=True))
raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg)
(llx, lly, urx, ury) = [int(x) for x in m.groups()]
# answer is correct if (x,y) is within the specified rectangle
if (llx <= gx <= urx) and (lly <= gy <= ury):
correct_map.set(aid, 'correct')
break
rectangles, regions = expectedset
if rectangles[aid]: # rectangles part - for backward compatibility
# Check whether given point lies in any of the solution rectangles
solution_rectangles = rectangles[aid].split(';')
for solution_rectangle in solution_rectangles:
# parse expected answer
# TODO: Compile regexp on file load
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
solution_rectangle.strip().replace(' ', ''))
if not m:
msg = 'Error in problem specification! cannot parse rectangle in %s' % (
etree.tostring(self.ielements[aid], pretty_print=True))
raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg)
(llx, lly, urx, ury) = [int(x) for x in m.groups()]
# answer is correct if (x,y) is within the specified rectangle
if (llx <= gx <= urx) and (lly <= gy <= ury):
correct_map.set(aid, 'correct')
break
if correct_map[aid]['correctness'] != 'correct' and regions[aid]:
parsed_region = json.loads(regions[aid])
if parsed_region:
if type(parsed_region[0][0]) != list:
# we have [[1,2],[3,4],[5,6]] - single region
# instead of [[[1,2],[3,4],[5,6], [[1,2],[3,4],[5,6]]]
# or [[[1,2],[3,4],[5,6]]] - multiple regions syntax
parsed_region = [parsed_region]
for region in parsed_region:
polygon = MultiPoint(region).convex_hull
if (polygon.type == 'Polygon' and
polygon.contains(Point(gx, gy))):
correct_map.set(aid, 'correct')
break
return correct_map
def get_answers(self):
return dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements])
return (dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements]),
dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements]))
#-----------------------------------------------------------------------------
# TEMPORARY: List of all response subclasses
# FIXME: To be replaced by auto-registration
......
<section id="inputtype_${id}" class="capa_inputtype" >
<div id="holder" style="width:${width};height:${height}"></div>
<div class="crystalography_problem" style="width:${width};height:${height}"></div>
<div class="input_lattice">
Lattice: <select></select>
</div>
<div class="script_placeholder" data-src="/static/js/raphael.js"></div>
<div class="script_placeholder" data-src="/static/js/sylvester.js"></div>
<div class="script_placeholder" data-src="/static/js/underscore-min.js"></div>
<div class="script_placeholder" data-src="/static/js/crystallography.js"></div>
% if status == 'unsubmitted':
<div class="unanswered" id="status_${id}">
<div class="unanswered" id="status_${id}">
% elif status == 'correct':
<div class="correct" id="status_${id}">
<div class="correct" id="status_${id}">
% elif status == 'incorrect':
<div class="incorrect" id="status_${id}">
<div class="incorrect" id="status_${id}">
% elif status == 'incomplete':
<div class="incorrect" id="status_${id}">
% endif
% if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
<div class="incorrect" id="status_${id}">
% endif
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}"
% if size:
size="${size}"
% endif
% if hidden:
style="display:none;"
% endif
/>
<p class="status">
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}" style="display:none;"/>
<p class="status">
% if status == 'unsubmitted':
unanswered
% elif status == 'correct':
......@@ -38,14 +32,15 @@
% elif status == 'incomplete':
incomplete
% endif
</p>
</p>
<p id="answer_${id}" class="answer"></p>
<p id="answer_${id}" class="answer"></p>
% if msg:
<span class="message">${msg|n}</span>
% endif
% if msg:
<span class="message">${msg|n}</span>
% endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div>
% endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div>
% endif
</section>
......@@ -18,4 +18,23 @@ Hello</p></text>
<text><p>Use conservation of energy.</p></text>
</hintgroup>
</imageresponse>
<imageresponse max="1" loncapaid="12">
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98)" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"/>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98)" regions='[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]'/>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"/>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"/>
<text>Click on either of the two positions as discussed previously.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[[10,10], [20,10], [20, 30]]]"/>
<text>Click on either of the two positions as discussed previously.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[10,10], [30,30], [15, 15]]"/>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[10,10], [30,30], [10, 30], [30, 10]]"/>
<text>Click on either of the two positions as discussed previously.</text>
<hintgroup showoncorrect="no">
<text><p>Use conservation of energy.</p></text>
</hintgroup>
</imageresponse>
</problem>
......@@ -407,13 +407,11 @@ class CrystallographyTest(unittest.TestCase):
def test_rendering(self):
height = '12'
width = '33'
size = '10'
xml_str = """<crystallography id="prob_1_2"
height="{h}"
width="{w}"
size="{s}"
/>""".format(h=height, w=width, s=size)
/>""".format(h=height, w=width)
element = etree.fromstring(xml_str)
......@@ -428,9 +426,7 @@ class CrystallographyTest(unittest.TestCase):
expected = {'id': 'prob_1_2',
'value': value,
'status': 'unsubmitted',
'size': size,
'msg': '',
'hidden': '',
'width': width,
'height': height,
}
......
......@@ -52,24 +52,57 @@ class ImageResponseTest(unittest.TestCase):
def test_ir_grade(self):
imageresponse_file = os.path.dirname(__file__) + "/test_files/imageresponse.xml"
test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=test_system)
correct_answers = {'1_2_1': '(490,11)-(556,98)',
'1_2_2': '(242,202)-(296,276)',
'1_2_3': '(490,11)-(556,98);(242,202)-(296,276)',
'1_2_4': '(490,11)-(556,98);(242,202)-(296,276)',
'1_2_5': '(490,11)-(556,98);(242,202)-(296,276)',
# testing regions only
correct_answers = {
#regions
'1_2_1': '(490,11)-(556,98)',
'1_2_2': '(242,202)-(296,276)',
'1_2_3': '(490,11)-(556,98);(242,202)-(296,276)',
'1_2_4': '(490,11)-(556,98);(242,202)-(296,276)',
'1_2_5': '(490,11)-(556,98);(242,202)-(296,276)',
#testing regions and rectanges
'1_3_1': 'rectangle="(490,11)-(556,98)" \
regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"',
'1_3_2': 'rectangle="(490,11)-(556,98)" \
regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"',
'1_3_3': 'regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"',
'1_3_4': 'regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"',
'1_3_5': 'regions="[[[10,10], [20,10], [20, 30]]]"',
'1_3_6': 'regions="[[10,10], [30,30], [15, 15]]"',
'1_3_7': 'regions="[[10,10], [30,30], [10, 30], [30, 10]]"',
}
test_answers = {'1_2_1': '[500,20]',
'1_2_2': '[250,300]',
'1_2_3': '[500,20]',
'1_2_4': '[250,250]',
'1_2_5': '[10,10]',
test_answers = {
'1_2_1': '[500,20]',
'1_2_2': '[250,300]',
'1_2_3': '[500,20]',
'1_2_4': '[250,250]',
'1_2_5': '[10,10]',
'1_3_1': '[500,20]',
'1_3_2': '[15,15]',
'1_3_3': '[500,20]',
'1_3_4': '[115,115]',
'1_3_5': '[15,15]',
'1_3_6': '[20,20]',
'1_3_7': '[20,15]',
}
# regions
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_3'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_4'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_5'), 'incorrect')
# regions and rectangles
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_2'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_3'), 'incorrect')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_4'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_5'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_6'), 'incorrect')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_7'), 'correct')
class SymbolicResponseTest(unittest.TestCase):
def test_sr_grade(self):
......
......@@ -45,7 +45,7 @@ def get_logger_config(log_dir,
logging_env=logging_env, hostname=hostname)
handlers = ['console', 'local'] if debug else ['console',
'syslogger-remote', 'local', 'newrelic']
'syslogger-remote', 'local']
logger_config = {
'version': 1,
......
......@@ -260,6 +260,10 @@ class CourseDescriptor(SequenceDescriptor):
return self._grading_policy['GRADE_CUTOFFS']
@property
def lowest_passing_grade(self):
return min(self._grading_policy['GRADE_CUTOFFS'].values())
@property
def tabs(self):
"""
Return the tabs config, as a python object, or None if not specified.
......@@ -406,7 +410,7 @@ class CourseDescriptor(SequenceDescriptor):
return False
except:
log.exception("Error parsing discussion_blackouts for course {0}".format(self.id))
return True
@property
......
......@@ -149,14 +149,14 @@ class ErrorDescriptor(JSONEditingDescriptor):
'''
try:
xml = etree.fromstring(self.definition['data']['contents'])
return etree.tostring(xml)
return etree.tostring(xml, encoding='unicode')
except etree.XMLSyntaxError:
# still not valid.
root = etree.Element('error')
root.text = self.definition['data']['contents']
err_node = etree.SubElement(root, 'error_msg')
err_node.text = self.definition['data']['error_msg']
return etree.tostring(root)
return etree.tostring(root, encoding='unicode')
class NonStaffErrorDescriptor(ErrorDescriptor):
......
......@@ -6,15 +6,14 @@ import sys
from lxml import etree
from path import path
from .x_module import XModule
from pkg_resources import resource_string
from .xml_module import XmlDescriptor, name_to_pathname
from .editing_module import EditingDescriptor
from .stringify import stringify_children
from .html_checker import check_html
from xmodule.modulestore import Location
from xmodule.contentstore.content import XASSET_SRCREF_PREFIX, StaticContent
from xmodule.editing_module import EditingDescriptor
from xmodule.html_checker import check_html
from xmodule.modulestore import Location
from xmodule.stringify import stringify_children
from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor, name_to_pathname
log = logging.getLogger("mitx.courseware")
......@@ -121,7 +120,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
try:
with system.resources_fs.open(filepath) as file:
html = file.read()
html = file.read().decode('utf-8')
# Log a warning if we can't parse the file, but don't error
if not check_html(html):
msg = "Couldn't parse html in {0}.".format(filepath)
......@@ -162,7 +161,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
with resource_fs.open(filepath, 'w') as file:
file.write(self.definition['data'])
file.write(self.definition['data'].encode('utf-8'))
# write out the relative name
relname = path(pathname).basename()
......
......@@ -120,7 +120,7 @@ class @SelfAssessment
if @state == 'done'
$.postWithPrefix "#{@ajax_url}/reset", {}, (response) =>
if response.success
@answer_area.html('')
@answer_area.val('')
@rubric_wrapper.html('')
@hint_wrapper.html('')
@message_wrapper.html('')
......
......@@ -157,7 +157,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
make_name_unique(xml_data)
descriptor = XModuleDescriptor.load_from_xml(
etree.tostring(xml_data), self, self.org,
etree.tostring(xml_data, encoding='unicode'), self, self.org,
self.course, xmlstore.default_class)
except Exception as err:
print err, self.load_error_modules
......@@ -419,7 +419,7 @@ class XMLModuleStore(ModuleStoreBase):
self.load_error_modules,
)
course_descriptor = system.process_xml(etree.tostring(course_data))
course_descriptor = system.process_xml(etree.tostring(course_data, encoding='unicode'))
# NOTE: The descriptors end up loading somewhat bottom up, which
# breaks metadata inheritance via get_children(). Instead
......
......@@ -13,7 +13,7 @@ class RawDescriptor(XmlDescriptor, XMLEditingDescriptor):
"""
@classmethod
def definition_from_xml(cls, xml_object, system):
return {'data': etree.tostring(xml_object, pretty_print=True)}
return {'data': etree.tostring(xml_object, pretty_print=True,encoding='unicode')}
def definition_to_xml(self, resource_fs):
try:
......
......@@ -126,8 +126,8 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
children = []
for child in xml_object:
try:
children.append(system.process_xml(etree.tostring(child)).location.url())
except Exception, e:
children.append(system.process_xml(etree.tostring(child, encoding='unicode')).location.url())
except Exception as e:
log.exception("Unable to load child when parsing Sequence. Continuing...")
if system.error_tracker is not None:
system.error_tracker("ERROR: " + str(e))
......
......@@ -22,7 +22,7 @@ def stringify_children(node):
# next element.
parts = [node.text]
for c in node.getchildren():
parts.append(etree.tostring(c, with_tail=True))
parts.append(etree.tostring(c, with_tail=True, encoding='unicode'))
# filter removes possible Nones in texts and tails
return ''.join(filter(None, parts))
return u''.join(filter(None, parts))
......@@ -4,7 +4,7 @@ unittests for xmodule
Run like this:
rake test_common/lib/xmodule
"""
import unittest
......@@ -19,11 +19,12 @@ import xmodule
from xmodule.x_module import ModuleSystem
from mock import Mock
i4xs = ModuleSystem(
test_system = ModuleSystem(
ajax_url='courses/course_id/modx/a_location',
track_function=Mock(),
get_module=Mock(),
render_template=Mock(),
# "render" to just the context...
render_template=lambda template, context: str(context),
replace_urls=Mock(),
user=Mock(),
filestore=Mock(),
......
......@@ -5,7 +5,7 @@ import unittest
from xmodule.progress import Progress
from xmodule import x_module
from . import i4xs
from . import test_system
class ProgressTest(unittest.TestCase):
''' Test that basic Progress objects work. A Progress represents a
......@@ -133,6 +133,6 @@ class ModuleProgressTest(unittest.TestCase):
'''
def test_xmodule_default(self):
'''Make sure default get_progress exists, returns None'''
xm = x_module.XModule(i4xs, 'a://b/c/d/e', None, {})
xm = x_module.XModule(test_system, 'a://b/c/d/e', None, {})
p = xm.get_progress()
self.assertEqual(p, None)
import json
from mock import Mock
import unittest
from xmodule.self_assessment_module import SelfAssessmentModule
from xmodule.modulestore import Location
from . import test_system
class SelfAssessmentTest(unittest.TestCase):
definition = {'rubric': 'A rubric',
'prompt': 'Who?',
'submitmessage': 'Shall we submit now?',
'hintprompt': 'Consider this...',
}
location = Location(["i4x", "edX", "sa_test", "selfassessment",
"SampleQuestion"])
metadata = {'attempts': '10'}
descriptor = Mock()
def test_import(self):
state = json.dumps({'student_answers': ["Answer 1", "answer 2", "answer 3"],
'scores': [0, 1],
'hints': ['o hai'],
'state': SelfAssessmentModule.ASSESSING,
'attempts': 2})
module = SelfAssessmentModule(test_system, self.location,
self.definition, self.descriptor,
state, {}, metadata=self.metadata)
self.assertEqual(module.get_score()['score'], 0)
self.assertTrue('answer 3' in module.get_html())
self.assertFalse('answer 2' in module.get_html())
module.save_assessment({'assessment': '0'})
self.assertEqual(module.state, module.REQUEST_HINT)
module.save_hint({'hint': 'hint for ans 3'})
self.assertEqual(module.state, module.DONE)
d = module.reset({})
self.assertTrue(d['success'])
self.assertEqual(module.state, module.INITIAL)
# if we now assess as right, skip the REQUEST_HINT state
module.save_answer({'student_answer': 'answer 4'})
module.save_assessment({'assessment': '1'})
self.assertEqual(module.state, module.DONE)
......@@ -247,17 +247,17 @@ class XModule(HTMLSnippet):
self._loaded_children = [c for c in children if c is not None]
return self._loaded_children
def get_children_locations(self):
'''
Returns the locations of each of child modules.
Overriding this changes the behavior of get_children and
anything that uses get_children, such as get_display_items.
This method will not instantiate the modules of the children
unless absolutely necessary, so it is cheaper to call than get_children
These children will be the same children returned by the
descriptor unless descriptor.has_dynamic_children() is true.
'''
......@@ -302,8 +302,20 @@ class XModule(HTMLSnippet):
return '{}'
def get_score(self):
''' Score the student received on the problem.
'''
"""
Score the student received on the problem, or None if there is no
score.
Returns:
dictionary
{'score': integer, from 0 to get_max_score(),
'total': get_max_score()}
NOTE (vshnayder): not sure if this was the intended return value, but
that's what it's doing now. I suspect that we really want it to just
return a number. Would need to change (at least) capa and
modx_dispatch to match if we did that.
"""
return None
def max_score(self):
......@@ -333,6 +345,19 @@ class XModule(HTMLSnippet):
get is a dictionary-like object '''
return ""
# cdodge: added to support dynamic substitutions of
# links for courseware assets (e.g. images). <link> is passed through from lxml.html parser
def rewrite_content_links(self, link):
# see if we start with our format, e.g. 'xasset:<filename>'
if link.startswith(XASSET_SRCREF_PREFIX):
# yes, then parse out the name
name = link[len(XASSET_SRCREF_PREFIX):]
loc = Location(self.location)
# resolve the reference to our internal 'filepath' which
link = StaticContent.compute_location_filename(loc.org, loc.course, name)
return link
def policy_key(location):
"""
......@@ -410,8 +435,13 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
# cdodge: this is a list of metadata names which are 'system' metadata
# and should not be edited by an end-user
<<<<<<< HEAD
system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft']
=======
system_metadata_fields = [ 'data_dir' ]
>>>>>>> origin/master
# A list of descriptor attributes that must be equal for the descriptors to
# be equal
equality_attributes = ('definition', 'metadata', 'location',
......@@ -569,18 +599,18 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
self,
metadata=self.metadata
)
def has_dynamic_children(self):
"""
Returns True if this descriptor has dynamic children for a given
student when the module is created.
Returns False if the children of this descriptor are the same
children that the module will return for any student.
children that the module will return for any student.
"""
return False
# ================================= JSON PARSING ===========================
@staticmethod
......
from xmodule.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location
from lxml import etree
import json
import copy
import logging
import traceback
from collections import namedtuple
from fs.errors import ResourceNotFoundError
import os
import sys
from collections import namedtuple
from lxml import etree
from xmodule.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location
log = logging.getLogger(__name__)
# assume all XML files are persisted as utf-8.
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
remove_comments=True, remove_blank_text=True)
remove_comments=True, remove_blank_text=True,
encoding='utf-8')
def name_to_pathname(name):
"""
......@@ -369,7 +370,7 @@ class XmlDescriptor(XModuleDescriptor):
filepath = self.__class__._format_filepath(self.category, url_path)
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
with resource_fs.open(filepath, 'w') as file:
file.write(etree.tostring(xml_object, pretty_print=True))
file.write(etree.tostring(xml_object, pretty_print=True, encoding='utf-8'))
# And return just a pointer with the category and filename.
record_object = etree.Element(self.category)
......@@ -384,7 +385,7 @@ class XmlDescriptor(XModuleDescriptor):
record_object.set('org', self.location.org)
record_object.set('course', self.location.course)
return etree.tostring(record_object, pretty_print=True)
return etree.tostring(record_object, pretty_print=True, encoding='utf-8')
def definition_to_xml(self, resource_fs):
"""
......
......@@ -33,4 +33,4 @@
<!-- This must appear after all mathjax-config blocks, so it is after the imports from the other templates.
It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of
MathJax extension libraries -->
<script type="text/javascript" src="/static/js/vendor/mathjax-MathJax-c9db6ac/MathJax.js?config=TeX-MML-AM_HTMLorMML-full"></script>
<script type="text/javascript" src="https://edx-static.s3.amazonaws.com/mathjax-MathJax-07669ac/MathJax.js?config=TeX-MML-AM_HTMLorMML-full"></script>
......@@ -6,7 +6,7 @@
<p>No - anyone and everyone is welcome to take this course.</p>
</li>
<li>What textbook should I buy?
<p>Although the lectures are designed to be self-contained, we recommend (but do not require) that students refer to the book Worlds Together, Worlds Apart: A History of the World: From 1000 CE to the Present (W W Norton, 3rd edition) -- Volume II, which was written specifically for this course.</p>
<p>Although the lectures are designed to be self-contained, we recommend (but do not require) that students refer to the book Worlds Together, Worlds Apart: A History of the World: From 1000 CE to the Present (W W Norton, 3rd edition) &mdash; Volume II, which was written specifically for this course.</p>
</li>
<li>Does Harvard award credentials or reports regarding my work in this course?
<p>Princeton does not award credentials or issue reports for student work in this course. However, Coursera could maintain a record of your score on the assessments and, with your permission, verify that score for authorized parties.</p>
......
......@@ -2,7 +2,7 @@
<video url_name="welcome"/>
<sequential filename="System_Usage_Sequence" slug="System_Usage_Sequence" format="Lecture Sequence" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="System Usage Sequence"/>
<vertical slug="Lab0_Using_the_tools" format="Lab" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Lab0: Using the tools">
<html slug="html_19" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab </html>
<html slug="html_19" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab </html>
<html slug="html_5555" filename="html_5555"/>
<problem filename="Lab_0_Using_the_Tools" slug="Lab_0_Using_the_Tools" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="false" name="Lab 0: Using the Tools"/>
</vertical>
......
More information given in <a href="/book/${page}">the text</a>.
More information given in <a href="/book/${page}">the text</a>.
<a href='https://6002x.mitx.mit.edu/discussion/questions/scope:all/sort:activity-desc/tags:${tag}/page:1/'> Discussion: ${tag} </a>
\ No newline at end of file
<a href='https://6002x.mitx.mit.edu/discussion/questions/scope:all/sort:activity-desc/tags:${tag}/page:1/'> Discussion: ${tag}… </a>
\ No newline at end of file
Lecture Slides Handout [<a href="">Clean </a>][<a href="">Annotated</a>]
Lecture Slides Handout [<a href="">Clean… </a>][<a href="">Annotated…</a>]
Hint
Hint
<br/><br/>
Remember that the time evolution of any variable \(x(t)\) governed by
a first-order system with a time-constant \(\tau\) for a time \(t) between an initial
......
......@@ -4,14 +4,14 @@
<section class="tutorials">
<h2> Basic Tutorials </h2>
<ul>
<li><a href="/section/wk13_solder">Soldering</a> -- Steve
<li><a href="/section/wk13_solder">Soldering</a> &mdash; Steve
Finberg, one of the pioneers in from Draper Lab, talks about
soldering. </li>
</ul>
<h2> Bonus Tutorials </h2>
<ul>
<li><a href="/section/wk13_FreqResp">Frequency Response
Curves</a> -- We explain several techniques for understanding
Curves</a> &mdash; We explain several techniques for understanding
and approximating Bode plots. </li>
</ul>
</section>
......
......@@ -41,7 +41,7 @@
<li><a href="/section/problem_1_3">OCW Problem 1-3 </a> - Reverse engineer a black-box resistor network</li>
</ul>
<hr/>
<p> Since the course has students from a diverse set of backgrounds, the first week's tutorials includes several extra segments, worked out with greater detail, to help bring everyone up to speed. </p>
<p> Since the course has students from a diverse set of backgrounds, the first week's tutorials includes several extra segments, worked out with greater detail, to help bring everyone up to speed. Gratuitous &ge; entity.</p>
</section>
</body>
</html>
<html slug="html_5555" filename="html_5555> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab </html>
<html slug="html_5555" filename="html_5555> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab. </html>
......@@ -34,6 +34,6 @@
the Thevenin or Norton theorems to summarize the behavior at
a pair of exposed terminals.
</p><p>
Sorry for the confusion of words -- natural language is like
Sorry for the confusion of words &mdash; natural language is like
that!
</p>
......@@ -34,6 +34,6 @@
the Thevenin or Norton theorems to summarize the behavior at
a pair of exposed terminals.
</p><p>
Sorry for the confusion of words -- natural language is like
Sorry for the confusion of words &mdash; natural language is like
that!
</p>
......@@ -9,14 +9,14 @@ the right of the diagram area) and drag it onto the diagram. Release
the mouse when the component is in the correct position.
</td>
</tr>
<!-- note that entities like &mdash; may be used. -->
<tr>
<td>Move a component</td>
<td>Click to select a component in the diagram (it will turn green)
and then drag it to its new location. You can use shift-click to add
a component to the current selection. Or you can click somewhere in
the diagram that is not on top of a component and drag out a selection
rectangle -- components intersecting the rectangle will be added to
rectangle &mdash; components intersecting the rectangle will be added to
the current selection.
</td>
</tr>
......@@ -63,7 +63,7 @@ engineeering notation:
<td>Add a wire</td>
<td>Wires start at connection points, the open circles that
appear at the terminals of components or the ends of wires.
Click on a connection point to start a wire -- a green wire
Click on a connection point to start a wire &mdash; a green wire
will appear with one end anchored at the starting point.
Drag the mouse and release the mouse button when the other
end of the wire is positioned as you wish. Once a wire has
......
Hint
Hint
<br/><br/>
Be careful of units here. Make sure you notice multipliers such
as u, k, m, M.
as u (or &mu;), k, m, M.
......@@ -9,8 +9,9 @@
<li> <h2>May 2 </h2>
<section class="update-description">
<ul>
<li> We have opened the show-answer button on the midterm. </li>
<li> There was a four hour outage in posting ability on the discussion board Monday night. It has been fixed. We apologise for the inconvenience.</li>
<!-- utf-8 characters are acceptable… as are HTML entities -->
<li> We have opened the show-answer button on the midterm… </li>
<li> There was a four hour outage in posting ability on the discussion board Monday night&hellip; It has been fixed. We apologise for the inconvenience.</li>
</ul>
</li>
<li> <h2>April 30 </h2>
......
<problem><startouttext/><p/>Here's a sandbox where you can experiment with all the components
<problem><!-- include ellipses to test non-ascii characters --><startouttext/><p/>Here's a sandbox where you can experiment with all the components
we'll discuss in 6.002x. If you click on CHECK below, your diagram
will be saved on the server and you can return at some later time.
will be saved on the server and you can return at some later time
<endouttext/><schematicresponse><p/><center><schematic name="work" value="" width="800" height="600"/></center><answer type="loncapa/python">
correct = ['correct']
</answer></schematicresponse></problem>
......@@ -78,7 +78,8 @@ So the total heating power in Joe's shop was:
<numericalresponse answer="$Pbad"><responseparam type="tolerance" default="5%" name="tol" description="Numerical Tolerance"/><textline/></numericalresponse>
<startouttext/>
<br/>
No wonder Joe was cold.
<!-- add non-ascii utf-8 character here -->
No wonder Joe was cold…
<endouttext/>
</problem>
......@@ -94,7 +94,7 @@ scope probes to nodes A, B and C and edit their properties so that the
plots will be different colors. Now run a transient analysis for 5ms.
Move the mouse over the plot until the marker (a vertical dashed line
that follows the mouse when it's over the plot) is at approximately
1.25ms. Please report the measured voltages for nodes A, B and C.
1.25ms. Please report the measured voltages for nodes A, B and C
<br/>
<div style="margin-left: 4em;">
......
......@@ -6,7 +6,7 @@ z = "A*x^2 + sqrt(y)"
Enter the algebraic expression \(A x^2 + \sqrt{y}\) in the box below. The
entry is case sensitive. The product must be indicated with an
asterisk, and the exponentation with a caret, so you must write
"A*x^2 + sqrt(y)".
"A*x^2 + sqrt(y)"
<endouttext/>
<formularesponse type="cs" samples="A,x,y@1,1,1:3,3,3#10" answer="$z"><responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/><textline size="40"/></formularesponse>
......
<problem><startouttext/>
Enter the numerical value of the expression \(x + y\) where
\(x = 3\) and \(y = 5\).
\(x = 3\) and \(y = 5\)
<endouttext/>
<numericalresponse answer="8"><responseparam type="tolerance" default="5%" name="tol" description="Numerical Tolerance"/><textline/></numericalresponse>
......
<problem display_name="S3E2: Lorentz Force">
<startouttext/>
<p>Consider a hypothetical magnetic field pointing out of your computer screen. Now imagine an electron traveling from right to leftin the plane of your screen. A diagram of this situation is show below.</p>
<p>Consider a hypothetical magnetic field pointing out of your computer screen. Now imagine an electron traveling from right to left in the plane of your screen. A diagram of this situation is show below…</p>
<center><img width="400" src="/static/images/LSQimages/LSQ_W01_8.png"/></center>
<p>a. The magnitude of the force experienced by the electron is proportional the product of which of the following? (Select all that apply.)</p>
<endouttext/>
<choiceresponse>
<checkboxgroup>
<choice correct="true"><text>Magnetic field strength</text></choice>
<choice correct="false"><text>Electric field strength</text></choice>
<choice correct="true"><text>Electric charge of the electron</text></choice>
<choice correct="false"><text>Radius of the electron</text></choice>
<choice correct="false"><text>Mass of the electron</text></choice>
<choice correct="true"><text>Velocity of the electron</text></choice>
<!-- include ellipses to test non-ascii characters -->
<choice correct="true"><text>Magnetic field strength…</text></choice>
<choice correct="false"><text>Electric field strength…</text></choice>
<choice correct="true"><text>Electric charge of the electron…</text></choice>
<choice correct="false"><text>Radius of the electron…</text></choice>
<choice correct="false"><text>Mass of the electron…</text></choice>
<choice correct="true"><text>Velocity of the electron…</text></choice>
</checkboxgroup>
</choiceresponse>
......
......@@ -2,7 +2,8 @@
<problem display_name="L4 Problem 1">
<text>
<p>
<b class="bfseries">Part 1: Function Types</b>
<!-- include ellipses to test non-ascii characters -->
<b class="bfseries">Part 1: Function Types…</b>
</p>
<p>
For each of the following functions, specify the type of its <b class="bfseries">output</b>. You can assume each function is called with an appropriate argument, as specified by its docstring. </p>
......
......@@ -3,12 +3,13 @@
<vertical slug="vertical_66" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never">
<problem filename="S1E3_AC_power" slug="S1E3_AC_power" name="S1E3: AC power"/>
<customtag tag="S1E3" slug="discuss_67" impl="discuss"/>
<html slug="html_68"> S1E4 has been removed. </html>
<!-- utf-8 characters acceptable, but not HTML entities -->
<html slug="html_68"> S1E4 has been removed…</html>
</vertical>
<vertical filename="vertical_89" slug="vertical_89" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"/>
<vertical slug="vertical_94" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never">
<video youtube="0.75:XNh13VZhThQ,1.0:XbDRmF6J0K0,1.25:JDty12WEQWk,1.50:wELKGj-5iyM" slug="What_s_next" name="What's next"/>
<html slug="html_95">Minor correction: Six elements (five resistors)</html>
<html slug="html_95">Minor correction: Six elements (five resistors)</html>
<customtag tag="S1" slug="discuss_96" impl="discuss"/>
</vertical>
</sequential>
<sequential>
<html slug="html_90">
<h1> </h1>
<!-- UTF-8 characters are acceptable… HTML entities are not -->
<h1>Inline content…</h1>
</html>
<video youtube="1.50:vl9xrfxcr38,1.25:qxNX4REGqx4,1.0:BGU1poJDgOY,0.75:8rK9vnpystQ" slug="S1V14_Summary" name="S1V14: Summary"/>
<customtag tag="S1" slug="discuss_91" impl="discuss"/>
......
<video youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" format="Video" display_name="Welcome"/>
<video youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" format="Video" display_name="Welcome"/>
<b>Lab 2A: Superposition Experiment</b>
<<<<<<< Updated upstream
<p>Isn't the toy course great?</p>
<p>Let's add some markup that uses non-ascii characters.
For example, we should be able to write words like encyclop&aelig;dia, or foreign words like fran&ccedil;ais.
Looking beyond latin-1, we should handle math symbols: &pi;r&sup2 &le; &#8734.
And it shouldn't matter if we use entities or numeric codes &mdash; &Omega; &ne; &pi; &equiv; &#937; &#8800; &#960;.
</p>
=======
<p>Isn't the toy course great? — &le;</p>
>>>>>>> Stashed changes
......@@ -98,8 +98,9 @@ RUBY_VER="1.9.3"
NUMPY_VER="1.6.2"
SCIPY_VER="0.10.1"
BREW_FILE="$BASE/mitx/brew-formulas.txt"
APT_REPOS_FILE="$BASE/mitx/apt-repos.txt"
APT_PKGS_FILE="$BASE/mitx/apt-packages.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 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"
......@@ -182,16 +183,22 @@ case `uname -s` in
error "Please install lsb-release."
exit 1
}
distro=`lsb_release -cs`
case $distro in
maya|lisa|natty|oneiric|precise|quantal)
output "Installing ubuntu requirements"
sudo apt-get install python-software-properties
sudo add-apt-repository ppa:chris-lea/node.js
sudo apt-get -y update
# DEBIAN_FRONTEND=noninteractive is required for silent mysql-server installation
sudo DEBIAN_FRONTEND=noninteractive apt-get -y install $APT_PKGS
sudo npm install coffee-script
export DEBIAN_FRONTEND=noninteractive
# add repositories
cat $APT_REPOS_FILE | xargs -n 1 sudo add-apt-repository -y
sudo apt-get -y update
# install packages listed in APT_PKGS_FILE
cat $APT_PKGS_FILE | xargs sudo apt-get -y install
clone_repos
;;
*)
......@@ -272,7 +279,7 @@ output "Installing rvm and ruby"
curl -sL get.rvm.io | bash -s -- --version 1.15.7
source $RUBY_DIR/scripts/rvm
# skip the intro
LESS="-E" rvm install $RUBY_VER
LESS="-E" rvm install $RUBY_VER --with-readline
output "Installing gem bundler"
gem install bundler
output "Installing ruby packages"
......
*******************************************
Capa module
*******************************************
Contents:
.. module:: capa
.. toctree::
:maxdepth: 2
chem.rst
Calc
====
......
*******************************************
Chem module
*******************************************
.. module:: chem
Miller
======
.. automodule:: capa.chem.miller
:members:
:show-inheritance:
UI part and inputtypes
----------------------
Miller module is used in the system in crystallography problems.
Crystallography is a class in :mod:`capa` inputtypes module.
It uses *crystallography.html* for rendering and **crystallography.js**
for UI part.
Documentation from **crystallography.js**::
For a crystallographic problem of the type
Given a plane definition via miller indexes, specify it by plotting points on the edges
of a 3D cube. Additionally, select the correct Bravais cubic lattice type depending on the
physical crystal mentioned in the problem.
we create a graph which contains a cube, and a 3D Cartesian coordinate system. The interface
will allow to plot 3 points anywhere along the edges of the cube, and select which type of
Bravais lattice should be displayed along with the basic cube outline.
When 3 points are successfully plotted, an intersection of the resulting plane (defined by
the 3 plotted points), and the cube, will be automatically displayed for clarity.
After lotting the three points, it is possible to continue plotting additional points. By
doing so, the point that was plotted first (from the three that already exist), will be
removed, and the new point will be added. The intersection of the resulting new plane and
the cube will be redrawn.
The UI has been designed in such a way, that the user is able to determine which point will
be removed next (if adding a new point). This is achieved via filling the to-be-removed point
with a different color.
Chemcalc
========
.. automodule:: capa.chem.chemcalc
:members:
:show-inheritance:
Chemtools
=========
.. automodule:: capa.chem.chemtools
:members:
:show-inheritance:
Tests
=====
.. automodule:: capa.chem.tests
:members:
:show-inheritance:
......@@ -3,6 +3,21 @@
set -e
set -x
function github_status {
gcli status create mitx mitx $GIT_COMMIT \
--params=$1 \
target_url:$BUILD_URL \
description:"Build #$BUILD_NUMBER $2" \
-f csv
}
function github_mark_failed_on_exit {
trap '[ $? == "0" ] || github_status state:failure "failed"' EXIT
}
github_mark_failed_on_exit
github_status state:pending "is running"
# Reset the submodule, in case it changed
git submodule foreach 'git reset --hard HEAD'
......@@ -12,16 +27,22 @@ export PYTHONIOENCODING=UTF-8
GIT_BRANCH=${GIT_BRANCH/HEAD/master}
pip install -q -r pre-requirements.txt
pip install -q -r test-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
# Don't run the studio tests until feature/cale/cms-master is merged in
# 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
# Don't run the studio tests until feature/cale/cms-master is merged in
# rake phantomjs_jasmine_cms || true
rake coverage:xml coverage:html
[ $TESTS_FAILED == '0' ]
rake autodeploy_properties
\ No newline at end of file
rake autodeploy_properties
github_status state:success "passed"
#! /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
# -*- 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 certificates.models import GeneratedCertificate
from courseware import grades, courses
from django.test.client import RequestFactory
from django.core.management.base import BaseCommand
from optparse import make_option
class Command(BaseCommand):
help = """
Find all students that need to be graded
and grade them.
"""
option_list = BaseCommand.option_list + (
make_option('-n', '--noop',
action='store_true',
dest='noop',
default=False,
help="Print but do not update the GeneratedCertificate table"),
make_option('-c', '--course',
metavar='COURSE_ID',
dest='course',
default=False,
help='Grade ungraded users for this course'),
)
def handle(self, *args, **options):
course_id = options['course']
print "Fetching ungraded students for {0}".format(course_id)
ungraded = GeneratedCertificate.objects.filter(
course_id__exact=course_id).filter(grade__exact='')
course = courses.get_course_by_id(course_id)
factory = RequestFactory()
request = factory.get('/')
for cert in ungraded:
# grade the student
grade = grades.grade(cert.user, request, course)
print "grading {0} - {1}".format(cert.user, grade['percent'])
cert.grade = grade['percent']
if not options['noop']:
cert.save()
from django.core.management.base import BaseCommand
from certificates.models import GeneratedCertificate
from django.contrib.auth.models import User
from optparse import make_option
from django.conf import settings
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
from django.db.models import Count
class Command(BaseCommand):
help = """
Generate a certificate status report for all courses that have ended.
This command does not do anything other than report the current
certificate status.
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.
deleted - The certificate has been deleted.
downloadable - The certificate is available for download.
notpassing - The student was graded but is not passing
"""
option_list = BaseCommand.option_list + (
make_option('-c', '--course',
metavar='COURSE_ID',
dest='course',
default=None,
help='Only generate for COURSE_ID'),
)
def _ended_courses(self):
for course_id in [course # all courses in COURSE_LISTINGS
for sub in settings.COURSE_LISTINGS
for course in settings.COURSE_LISTINGS[sub]]:
course_loc = CourseDescriptor.id_to_location(course_id)
course = modulestore().get_instance(course_id, course_loc)
if course.has_ended():
yield course_id
def handle(self, *args, **options):
# Find all courses that have ended
if options['course']:
ended_courses = [options['course']]
else:
ended_courses = self._ended_courses()
cert_data = {}
for course_id in ended_courses:
# find students who are enrolled
print "Looking up certificate states for {0}".format(course_id)
enrolled_students = User.objects.filter(
courseenrollment__course_id=course_id).prefetch_related(
"groups").order_by('username')
unavailable_count = enrolled_students.count() - \
GeneratedCertificate.objects.filter(
course_id__exact=course_id).count()
cert_data[course_id] = {'enrolled': enrolled_students.count()}
cert_data[course_id].update({'unavailable': unavailable_count})
tallies = GeneratedCertificate.objects.filter(
course_id__exact=course_id).values('status').annotate(
dcount=Count('status'))
cert_data[course_id].update(
{status['status']: status['dcount']
for status in tallies})
# all states we have seen far all courses
status_headings = set(
[status for course in cert_data
for status in cert_data[course]])
# print the heading for the report
print "{:>20}".format("course ID"),
print ' '.join(["{:>12}".format(heading)
for heading in status_headings])
# print the report
for course_id in cert_data:
print "{0:>20}".format(course_id[0:18]),
for heading in status_headings:
if heading in cert_data[course_id]:
print "{:>12}".format(cert_data[course_id][heading]),
else:
print " " * 12,
print
......@@ -2,29 +2,92 @@ 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 optparse import make_option
from django.conf import settings
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
from certificates.models import CertificateStatuses
import datetime
class Command(BaseCommand):
help = """
Find all students that have need certificates
and put certificate requests on the queue
Find all students that need certificates
for courses that have finished and
put their cert requests on the queue
This is only for BerkeleyX/CS169.1x/2012_Fall
Use the --noop option to test without actually
putting certificates on the queue to be generated.
"""
option_list = BaseCommand.option_list + (
make_option('-n', '--noop',
action='store_true',
dest='noop',
default=False,
help="Don't add certificate requests to the queue"),
make_option('-c', '--course',
metavar='COURSE_ID',
dest='course',
default=False,
help='Grade and generate certificates for a specific course'),
)
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()
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)
# Will only generate a certificate if the current
# status is in this state
VALID_STATUSES = [
CertificateStatuses.unavailable
]
# Print update after this many students
STATUS_INTERVAL = 500
if options['course']:
ended_courses = [options['course']]
else:
# Find all courses that have ended
ended_courses = []
for course_id in [course # all courses in COURSE_LISTINGS
for sub in settings.COURSE_LISTINGS
for course in settings.COURSE_LISTINGS[sub]]:
course_loc = CourseDescriptor.id_to_location(course_id)
course = modulestore().get_instance(course_id, course_loc)
if course.has_ended():
ended_courses.append(course_id)
for course_id in ended_courses:
print "Fetching enrolled students for {0}".format(course_id)
enrolled_students = User.objects.filter(
courseenrollment__course_id=course_id).prefetch_related(
"groups").order_by('username')
xq = XQueueCertInterface()
total = enrolled_students.count()
count = 0
start = datetime.datetime.now()
for student in enrolled_students:
count += 1
if count % STATUS_INTERVAL == 0:
# Print a status update with an approximation of
# how much time is left based on how long the last
# interval took
diff = datetime.datetime.now() - start
timeleft = diff * (total - count) / STATUS_INTERVAL
hours, remainder = divmod(timeleft.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
print "{0}/{1} completed ~{2:02}:{3:02}m remaining".format(
count, total, hours, minutes)
start = datetime.datetime.now()
if certificate_status_for_student(
student, course_id)['status'] in VALID_STATUSES:
if not options['noop']:
# Add the certificate request to the queue
ret = xq.add_cert(student, course_id)
if ret == 'generating':
print '{0} - {1}'.format(student, ret)
......@@ -75,7 +75,9 @@ def certificate_status_for_student(student, course_id):
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.
unavailable - No entry for this student--if they are actually in
the course, they probably have not been graded for
certificate generation yet.
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,
......@@ -90,7 +92,7 @@ def certificate_status_for_student(student, course_id):
"download_url".
If the student has been graded, the dictionary also contains their
grade for the course.
grade for the course with the key "grade".
'''
try:
......
......@@ -240,7 +240,7 @@ class XQueueCertInterface(object):
cert.save()
else:
cert_status = status.notpassing
cert.grade = grade['percent']
cert.status = cert_status
cert.user = student
cert.course_id = course_id
......
import copy
import logging
log = logging.getLogger("mitx." + __name__)
import json
import os
import sys
import time
from nose import SkipTest
from path import path
from pprint import pprint
from urlparse import urlsplit, urlunsplit
from django.contrib.auth.models import User, Group
from django.core.handlers.wsgi import WSGIRequest
from django.test import TestCase
from django.test.client import Client, RequestFactory
from django.test.client import RequestFactory
from django.conf import settings
from django.core.urlresolvers import reverse
from mock import patch, Mock
from override_settings import override_settings
import xmodule.modulestore.django
......@@ -26,9 +21,11 @@ from courseware.access import _course_staff_group_name
from courseware.models import StudentModuleCache
from student.models import Registration
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.timeparse import stringify_time
def parse_json(response):
......@@ -45,7 +42,6 @@ def registration(email):
'''look up registration object by email'''
return Registration.objects.get(user__email=email)
# A bit of a hack--want mongo modulestore for these tests, until
# jump_to works with the xmlmodulestore or we have an even better solution
# NOTE: this means this test requires mongo to be running.
......@@ -76,14 +72,9 @@ def xml_store_config(data_dir):
}
}
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR)
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
REAL_DATA_DIR = settings.GITHUB_REPO_ROOT
REAL_DATA_MODULESTORE = mongo_store_config(REAL_DATA_DIR)
class ActivateLoginTestCase(TestCase):
'''Check that we can activate and log in'''
......@@ -233,10 +224,17 @@ class PageLoader(ActivateLoginTestCase):
def check_pages_load(self, course_name, data_dir, modstore):
"""Make all locations in course load"""
print "Checking course {0} in {1}".format(course_name, data_dir)
import_from_xml(modstore, data_dir, [course_name])
# enroll in the course before trying to access pages
courses = modstore.get_courses()
default_class='xmodule.hidden_module.HiddenDescriptor'
load_error_modules=True
module_store = XMLModuleStore(
data_dir,
default_class=default_class,
course_dirs=[course_name],
load_error_modules=load_error_modules,
)
# enroll in the course before trying to access pages
courses = module_store.get_courses()
self.assertEqual(len(courses), 1)
course = courses[0]
self.enroll(course)
......@@ -295,18 +293,22 @@ class PageLoader(ActivateLoginTestCase):
self.assertTrue(all_ok) # fail fast
print "{0}/{1} good".format(n - num_bad, n)
log.info( "{0}/{1} good".format(n - num_bad, n))
self.assertTrue(all_ok)
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCoursesLoadTestCase(PageLoader):
'''Check that all pages in test courses load properly'''
def setUp(self):
ActivateLoginTestCase.setUp(self)
xmodule.modulestore.django._MODULESTORES = {}
xmodule.modulestore.django.modulestore().collection.drop()
# xmodule.modulestore.django.modulestore().collection.drop()
# store = xmodule.modulestore.django.modulestore()
# is there a way to empty the store?
def test_toy_course_loads(self):
self.check_pages_load('toy', TEST_DATA_DIR, modulestore())
......@@ -647,35 +649,6 @@ class TestViewAuth(PageLoader):
self.unenroll(self.toy)
self.assertTrue(self.try_enroll(self.toy))
@override_settings(MODULESTORE=REAL_DATA_MODULESTORE)
class RealCoursesLoadTestCase(PageLoader):
'''Check that all pages in real courses load properly'''
def setUp(self):
ActivateLoginTestCase.setUp(self)
xmodule.modulestore.django._MODULESTORES = {}
xmodule.modulestore.django.modulestore().collection.drop()
def test_real_courses_loads(self):
'''See if any real courses are available at the REAL_DATA_DIR.
If they are, check them.'''
# TODO: Disabled test for now.. Fix once things are cleaned up.
raise SkipTest
# TODO: adjust staticfiles_dirs
if not os.path.isdir(REAL_DATA_DIR):
# No data present. Just pass.
return
courses = [course_dir for course_dir in os.listdir(REAL_DATA_DIR)
if os.path.isdir(REAL_DATA_DIR / course_dir)]
for course in courses:
self.check_pages_load(course, REAL_DATA_DIR, modulestore())
# ========= TODO: check ajax interaction here too?
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCourseGrader(PageLoader):
"""Check that a course gets graded properly"""
......@@ -730,7 +703,7 @@ class TestCourseGrader(PageLoader):
def check_grade_percent(self, percent):
grade_summary = self.get_grade_summary()
self.assertEqual(grade_summary['percent'], percent)
self.assertEqual(percent, grade_summary['percent'])
def submit_question_answer(self, problem_url_name, responses):
"""
......
import csv
import json
import logging
import urllib
import itertools
import StringIO
from functools import partial
......@@ -12,7 +8,7 @@ from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.http import Http404, HttpResponse
from django.http import Http404
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
#from django.views.decorators.csrf import ensure_csrf_cookie
......@@ -25,15 +21,11 @@ from courseware.courses import (get_course_with_access, get_courses_by_universit
import courseware.tabs as tabs
from courseware.models import StudentModuleCache
from module_render import toc_for_course, get_module, get_instance_module
from student.models import UserProfile
from multicourse import multicourse_settings
from django_comment_client.utils import get_discussion_title
from student.models import UserTestGroup, CourseEnrollment
from util.cache import cache, cache_if_anonymous
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
......@@ -78,7 +70,7 @@ def courses(request):
'''
universities = get_courses_by_university(request.user,
domain=request.META.get('HTTP_HOST'))
return render_to_response("courses.html", {'universities': universities})
return render_to_response("courseware/courses.html", {'universities': universities})
def render_accordion(request, course, chapter, section):
......@@ -97,7 +89,7 @@ def render_accordion(request, course, chapter, section):
context = dict([('toc', toc),
('course_id', course.id),
('csrf', csrf(request)['csrf_token'])] + template_imports.items())
return render_to_string('accordion.html', context)
return render_to_string('courseware/accordion.html', context)
def get_current_child(xmodule):
......@@ -301,7 +293,6 @@ def index(request, course_id, chapter=None, section=None,
return result
@ensure_csrf_cookie
def jump_to(request, course_id, location):
'''
......@@ -326,18 +317,18 @@ def jump_to(request, course_id, location):
except NoPathToItem:
raise Http404("This location is not in any class: {0}".format(location))
# cdodge: the CAS is generating a link to the LMS for 'subsections' (aka sequentials)
# and there is no associated 'Position' for this. The above Path_to_location is returning None for Position
# however, this ends up producing a 404 on the redirect
if position is None:
position = 0
# choose the appropriate view (and provide the necessary args) based on the
# args provided by the redirect.
# Rely on index to do all error handling and access control.
return redirect('courseware_position',
course_id=course_id,
chapter=chapter,
section=section,
position=position)
if chapter is None:
return redirect('courseware', course_id=course_id)
elif section is None:
return redirect('courseware_chapter', course_id=course_id, chapter=chapter)
elif position is None:
return redirect('courseware_section', course_id=course_id, chapter=chapter, section=section)
else:
return redirect('courseware_position', course_id=course_id, chapter=chapter, section=section, position=position)
@ensure_csrf_cookie
def course_info(request, course_id):
"""
......@@ -413,8 +404,13 @@ def course_about(request, course_id):
show_courseware_link = (has_access(request.user, course, 'load') or
settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'))
<<<<<<< HEAD
return render_to_response('portal/course_about.html',
{ 'course': course,
=======
return render_to_response('courseware/course_about.html',
{'course': course,
>>>>>>> origin/master
'registered': registered,
'course_target': course_target,
'show_courseware_link' : show_courseware_link})
......@@ -455,7 +451,7 @@ def render_notifications(request, course, notifications):
'get_discussion_title': partial(get_discussion_title, request=request, course=course),
'course': course,
}
return render_to_string('notifications.html', context)
return render_to_string('courseware/notifications.html', context)
@login_required
def news(request, course_id):
......@@ -468,7 +464,7 @@ def news(request, course_id):
'content': render_notifications(request, course, notifications),
}
return render_to_response('news.html', context)
return render_to_response('courseware/news.html', context)
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
......
"""
This must be run only after seed_permissions_roles.py!
Creates default roles for all users in the provided course. Just runs through
Enrollments.
"""
from django.core.management.base import BaseCommand, CommandError
from student.models import CourseEnrollment, assign_default_role
class Command(BaseCommand):
args = 'course_id'
help = 'Add roles for all users in a course'
def handle(self, *args, **options):
if len(args) == 0:
raise CommandError("Please provide a course id")
if len(args) > 1:
raise CommandError("Too many arguments")
course_id = args[0]
print "Updated roles for ",
for i, enrollment in enumerate(CourseEnrollment.objects.filter(course_id=course_id), start=1):
assign_default_role(None, enrollment)
if i % 1000 == 0:
print "{0}...".format(i),
print
import logging
from django.db import models
from django.contrib.auth.models import User
import logging
from courseware.courses import get_course_by_id
FORUM_ROLE_ADMINISTRATOR = 'Administrator'
FORUM_ROLE_MODERATOR = 'Moderator'
FORUM_ROLE_COMMUNITY_TA = 'Community TA'
FORUM_ROLE_STUDENT = 'Student'
class Role(models.Model):
name = models.CharField(max_length=30, null=False, blank=False)
users = models.ManyToManyField(User, related_name="roles")
......@@ -15,8 +21,8 @@ class Role(models.Model):
def inherit_permissions(self, role): # TODO the name of this method is a little bit confusing,
# since it's one-off and doesn't handle inheritance later
if role.course_id and role.course_id != self.course_id:
logging.warning("%s cannot inheret permissions from %s due to course_id inconsistency" %
(self, role))
logging.warning("{0} cannot inherit permissions from {1} due to course_id inconsistency", \
self, role)
for per in role.permissions.all():
self.add_permission(per)
......@@ -25,10 +31,10 @@ class Role(models.Model):
def has_permission(self, permission):
course = get_course_by_id(self.course_id)
if self.name == "Student" and \
if self.name == FORUM_ROLE_STUDENT and \
(permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \
(not course.forum_posts_allowed):
return False
return False
return self.permissions.filter(name=permission).exists()
......
import time
from collections import defaultdict
from importlib import import_module
import logging
import time
import urllib
from courseware.models import StudentModuleCache
from courseware.module_render import get_module
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.search import path_to_location
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.db import connection
from django.http import HttpResponse
from django.utils import simplejson
from django.db import connection
from django.conf import settings
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django_comment_client.permissions import check_permissions_by_view
from django_comment_client.models import Role
from django_comment_client.permissions import check_permissions_by_view
from mitxmako import middleware
import logging
import operator
import itertools
import urllib
import pystache_custom as pystache
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.search import path_to_location
# TODO these should be cached via django's caching rather than in-memory globals
_FULLMODULES = None
......@@ -47,9 +41,16 @@ def get_role_ids(course_id):
staff = list(User.objects.filter(is_staff=True).values_list('id', flat=True))
roles_with_ids = {'Staff': staff}
for role in roles:
roles_with_ids[role.name] = list(role.users.values_list('id', flat=True))
roles_with_ids[role.name] = list(role.users.values_list('id', flat=True))
return roles_with_ids
def has_forum_access(uname, course_id, rolename):
try:
role = Role.objects.get(name=rolename, course_id=course_id)
except Role.DoesNotExist:
return False
return role.users.filter(username=uname).exists()
def get_full_modules():
global _FULLMODULES
if not _FULLMODULES:
......@@ -132,8 +133,6 @@ def initialize_discussion_info(course):
return
course_id = course.id
url_course_id = course_id.replace('/', '_').replace('.', '_')
all_modules = get_full_modules()[course_id]
discussion_id_map = {}
......
......@@ -18,5 +18,6 @@ class @Courseware
histg = new Histogram id, $(this).data('histogram')
catch error
histg = error
console.log(error)
if console?
console.log(error)
return histg
lms/static/images/courses/video-thumb.jpg

5.81 KB | W: 0px | H: 0px

lms/static/images/courses/video-thumb.jpg

4.43 KB | W: 0px | H: 0px

  • 2-up
  • Swipe
  • Onion skin
6718f0c6e851376b5478baff94e1f1f4449bd938
\ No newline at end of file
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