Commit 87362aa8 by Giulio Gratta

Merge branch 'master' of github.com:edx/mitx into feature/giulio/accessibility

parents f7719a60 729ca900
See doc/ for documentation.
This is edX, a platform for online course delivery. The project is primarily
written in [Python](http://python.org/), using the
[Django](https://www.djangoproject.com/) framework. We also use some
[Ruby](http://www.ruby-lang.org/) and some [NodeJS](http://nodejs.org/).
Installation
============
The installation process is a bit messy at the moment. Here's a high-level
overview of what you should do to get started.
**TLDR:** There is a `create-dev-env.sh` script that will attempt to set all
of this up for you. If you're in a hurry, run that script. Otherwise, I suggest
that you understand what the script is doing, and why, by reading this document.
Directory Hierarchy
-------------------
This code assumes that it is checked out in a directory that has three sibling
directories: `data` (used for XML course data), `db` (used to hold a
[sqlite](https://sqlite.org/) database), and `log` (used to hold logs). If you
clone the repository into a directory called `edx` inside of a directory
called `dev`, here's an example of how the directory hierarchy should look:
* dev
\
* data
* db
* log
* edx
\
README.md
Language Runtimes
-----------------
You'll need to be sure that you have Python 2.7, Ruby 1.9.3, and NodeJS
(latest stable) installed on your system. Some of these you can install
using your system's package manager: [homebrew](http://mxcl.github.io/homebrew/)
for Mac, [apt](http://wiki.debian.org/Apt) for Debian-based systems
(including Ubuntu), [rpm](http://www.rpm.org/) or [yum](http://yum.baseurl.org/)
for Red Hat based systems (including CentOS).
If your system's package manager gives you the wrong version of a language
runtime, then you'll need to use a versioning tool to install the correct version.
Usually, you'll need to do this for Ruby: you can use
[`rbenv`](https://github.com/sstephenson/rbenv) or [`rvm`](https://rvm.io/), but
typically `rbenv` is simpler. For Python, you can use
[`pythonz`](http://saghul.github.io/pythonz/),
and for Node, you can use [`nvm`](https://github.com/creationix/nvm).
Virtual Environments
--------------------
Often, different projects will have conflicting dependencies: for example, two
projects depending on two different, incompatible versions of a library. Clearly,
you can't have both versions installed and used on your machine simultaneously.
Virtual environments were created to solve this problem: by installing libraries
into an isolated environment, only projects that live inside the environment
will be able to see and use those libraries. Got incompatible dependencies? Use
different virtual environments, and your problem is solved.
Remember, each language has a different implementation. Python has
[`virtualenv`](http://www.virtualenv.org/), Ruby has
[`bundler`](http://gembundler.com/), and Node's virtual environment support
is built into [`npm`](https://npmjs.org/), its library management tool.
For each language, decide if you want to use a virtual environment, or if you
want to install all the language dependencies globally (and risk conflicts).
I suggest you start with installing things globally until and unless things
break; you can always switch over to a virtual environment later on.
Language Packages
-----------------
The Python libraries we use are listed in `requirements.txt`. The Ruby libraries
we use are listed in `Gemfile`. The Node libraries we use are listed in
`packages.json`. Python has a library installer called
[`pip`](http://www.pip-installer.org/), Ruby has a library installer called
[`gem`](https://rubygems.org/) (or `bundle` if you're using a virtual
environment), and Node has a library installer called
[`npm`](https://npmjs.org/).
Once you've got your languages and virtual environments set up, install
the libraries like so:
$ pip install -r pre-requirements.txt
$ pip install -r requirements.txt
$ bundle install
$ npm install
Other Dependencies
------------------
You'll also need to install [MongoDB](http://www.mongodb.org/), since our
application uses it in addition to sqlite. You can install it through your
system package manager, and I suggest that you configure it to start
automatically when you boot up your system, so that you never have to worry
about it again. For Mac, use
[`launchd`](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/launchd.8.html)
(running `brew info mongodb` will give you some commands you can copy-paste.)
For Linux, you can use [`upstart`](http://upstart.ubuntu.com/), `chkconfig`,
or any other process management tool.
Configuring Your Project
------------------------
We use [`rake`](http://rake.rubyforge.org/) to execute common tasks in our
project. The `rake` tasks are defined in the `rakefile`, or you can run `rake -T`
to view a summary.
Before you run your project, you need to create a sqlite database, create
tables in that database, run database migrations, and populate templates for
CMS templates. Fortunately, `rake` will do all of this for you! Just run:
$ rake django-admin[syncdb]
$ rake django-admin[migrate]
$ rake django-admin[update_templates]
If you are running these commands using the [`zsh`](http://www.zsh.org/) shell,
zsh will assume that you are doing
[shell globbing](https://en.wikipedia.org/wiki/Glob_(programming)), search for
a file in your directory named `django-adminsyncdb` or `django-adminmigrate`,
and fail. To fix this, just surround the argument with quotation marks, so that
you're running `rake "django-admin[syncdb]"`.
Run Your Project
----------------
edX has two components: Studio, the course authoring system; and the LMS
(learning management system) used by students. These two systems communicate
through the MongoDB database, which stores course information.
To run Studio, run:
$ rake cms
To run the LMS, run:
$ rake lms[cms.dev]
Studio runs on port 8001, while LMS runs on port 8000, so you can run both of
these commands simultaneously, using two different terminal windows. To view
Studio, visit `127.0.0.1:8001` in your web browser; to view the LMS, visit
`127.0.0.1:8000`.
There's also an older version of the LMS that saves its information in XML files
in the `data` directory, instead of in Mongo. To run this older version, run:
$ rake lms
Further Documentation
=====================
Once you've got your project up and running, you can check out the `docs`
directory to see more documentation about how edX is structured.
...@@ -75,11 +75,7 @@ def set_module_info(store, location, post_data): ...@@ -75,11 +75,7 @@ def set_module_info(store, location, post_data):
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it' # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items(): for metadata_key, value in posted_metadata.items():
# let's strip out any metadata fields from the postback which have been identified as system metadata if posted_metadata[metadata_key] is None:
# and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in module.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore # remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module._model_data: if metadata_key in module._model_data:
del module._model_data[metadata_key] del module._model_data[metadata_key]
......
...@@ -676,11 +676,7 @@ def save_item(request): ...@@ -676,11 +676,7 @@ def save_item(request):
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it' # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items(): for metadata_key, value in posted_metadata.items():
# let's strip out any metadata fields from the postback which have been identified as system metadata if posted_metadata[metadata_key] is None:
# and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in existing_item.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore # remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item._model_data: if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key] del existing_item._model_data[metadata_key]
......
...@@ -14,13 +14,14 @@ class CourseMetadata(object): ...@@ -14,13 +14,14 @@ class CourseMetadata(object):
The objects have no predefined attrs but instead are obj encodings of the The objects have no predefined attrs but instead are obj encodings of the
editable metadata. editable metadata.
''' '''
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', FILTERED_LIST = ['xml_attributes',
'end', 'start',
'enrollment_start', 'end',
'enrollment_end', 'enrollment_start',
'tabs', 'enrollment_end',
'graceperiod', 'tabs',
'checklists'] 'graceperiod',
'checklists']
@classmethod @classmethod
def fetch(cls, course_location): def fetch(cls, course_location):
......
...@@ -72,3 +72,14 @@ describe "CMS.Views.ModuleEdit", -> ...@@ -72,3 +72,14 @@ describe "CMS.Views.ModuleEdit", ->
it "loads the .xmodule-display inside the module editor", -> it "loads the .xmodule-display inside the module editor", ->
expect(XModule.loadModule).toHaveBeenCalled() expect(XModule.loadModule).toHaveBeenCalled()
expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display')) expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display'))
describe "changedMetadata", ->
it "returns empty if no metadata loaded", ->
expect(@moduleEdit.changedMetadata()).toEqual({})
it "returns only changed values", ->
@moduleEdit.originalMetadata = {'foo', 'bar'}
spyOn(@moduleEdit, 'metadata').andReturn({'a': '', 'b': 'before', 'c': ''})
@moduleEdit.loadEdit()
@moduleEdit.metadata.andReturn({'a': '', 'b': 'after', 'd': 'only_after'})
expect(@moduleEdit.changedMetadata()).toEqual({'b' : 'after', 'd' : 'only_after'})
...@@ -20,6 +20,7 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -20,6 +20,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
loadEdit: -> loadEdit: ->
if not @module if not @module
@module = XModule.loadModule(@$el.find('.xmodule_edit')) @module = XModule.loadModule(@$el.find('.xmodule_edit'))
@originalMetadata = @metadata()
metadata: -> metadata: ->
# cdodge: package up metadata which is separated into a number of input fields # cdodge: package up metadata which is separated into a number of input fields
...@@ -35,6 +36,14 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -35,6 +36,14 @@ class CMS.Views.ModuleEdit extends Backbone.View
return _metadata return _metadata
changedMetadata: ->
currentMetadata = @metadata()
changedMetadata = {}
for key of currentMetadata
if currentMetadata[key] != @originalMetadata[key]
changedMetadata[key] = currentMetadata[key]
return changedMetadata
cloneTemplate: (parent, template) -> cloneTemplate: (parent, template) ->
$.post("/clone_item", { $.post("/clone_item", {
parent_location: parent parent_location: parent
...@@ -60,7 +69,7 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -60,7 +69,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
course: course_location_analytics course: course_location_analytics
id: _this.model.id id: _this.model.id
data.metadata = _.extend(data.metadata || {}, @metadata()) data.metadata = _.extend(data.metadata || {}, @changedMetadata())
@hideModal() @hideModal()
@model.save(data).done( => @model.save(data).done( =>
# # showToastMessage("Your changes have been saved.", null, 3) # # showToastMessage("Your changes have been saved.", null, 3)
......
<% <%
import hashlib import hashlib
from xmodule.fields import StringyInteger, StringyFloat
hlskey = hashlib.md5(module.location.url()).hexdigest() hlskey = hashlib.md5(module.location.url()).hexdigest()
%> %>
<section class="metadata_edit"> <section class="metadata_edit">
...@@ -7,17 +8,40 @@ ...@@ -7,17 +8,40 @@
% for field_name, field_value in editable_metadata_fields.items(): % for field_name, field_value in editable_metadata_fields.items():
<li> <li>
% if field_name == 'source_code': % if field_name == 'source_code':
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a> % if field_value['is_default'] is False:
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
% endif
% else: % else:
<label>${field_name}:</label> <label>${field_value['field'].display_name}:</label>
<input type='text' data-metadata-name='${field_name}' value='${field_value}' size='60' /> <input type='text' data-metadata-name='${field_value["field"].display_name}'
## This is a hack to keep current behavior for weight and attempts (empty will parse OK as unset).
## This hack will go away with our custom editors.
% if field_value["value"] == None and (isinstance(field_value["field"], StringyFloat) or isinstance(field_value["field"], StringyInteger)):
value = ''
% else:
value='${field_value["field"].to_json(field_value["value"])}'
% endif
size='60' />
## Change to True to see all the information being passed through.
% if False:
<label>Help: ${field_value['field'].help}</label>
<label>Type: ${type(field_value['field']).__name__}</label>
<label>Inherited: ${field_value['is_inherited']}</label>
<label>Default: ${field_value['is_default']}</label>
% if field_value['field'].values:
<label>Possible values:</label>
% for value in field_value['field'].values:
<label>${value}</label>
% endfor
% endif
% endif
% endif % endif
</li> </li>
% endfor % endfor
</ul> </ul>
% if 'source_code' in editable_metadata_fields: % if 'source_code' in editable_metadata_fields and not editable_metadata_fields['source_code']['is_default']:
<%include file="source-edit.html" /> <%include file="source-edit.html" />
% endif % endif
</section> </section>
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
<form id="hls-form" enctype="multipart/form-data"> <form id="hls-form" enctype="multipart/form-data">
<section class="source-edit"> <section class="source-edit">
<textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${editable_metadata_fields['source_code']|h}</textarea> <textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${editable_metadata_fields['source_code']['value']|h}</textarea>
</section> </section>
<div class="submit"> <div class="submit">
<button type="reset" class="hls-compile">Save &amp; Compile to edX XML</button> <button type="reset" class="hls-compile">Save &amp; Compile to edX XML</button>
......
...@@ -6,7 +6,7 @@ from . import one_time_startup ...@@ -6,7 +6,7 @@ from . import one_time_startup
# from django.contrib import admin # from django.contrib import admin
# admin.autodiscover() # admin.autodiscover()
urlpatterns = ('', urlpatterns = ('', # nopep8
url(r'^$', 'contentstore.views.howitworks', name='homepage'), url(r'^$', 'contentstore.views.howitworks', name='homepage'),
url(r'^listing', 'contentstore.views.index', name='index'), url(r'^listing', 'contentstore.views.index', name='index'),
url(r'^edit/(?P<location>.*?)$', 'contentstore.views.edit_unit', name='edit_unit'), url(r'^edit/(?P<location>.*?)$', 'contentstore.views.edit_unit', name='edit_unit'),
...@@ -118,17 +118,17 @@ urlpatterns += ( ...@@ -118,17 +118,17 @@ urlpatterns += (
# static/proof-of-concept views # static/proof-of-concept views
url(r'^ux-alerts$', 'contentstore.views.ux_alerts', name='ux-alerts') url(r'^ux-alerts$', 'contentstore.views.ux_alerts', name='ux-alerts')
) )
js_info_dict = { js_info_dict = {
'domain': 'djangojs', 'domain': 'djangojs',
'packages': ('cms',), 'packages': ('cms',),
} }
urlpatterns += ( urlpatterns += (
# Serve catalog of localized strings to be rendered by Javascript # Serve catalog of localized strings to be rendered by Javascript
url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict), url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict),
) )
if settings.ENABLE_JASMINE: if settings.ENABLE_JASMINE:
...@@ -140,5 +140,3 @@ urlpatterns = patterns(*urlpatterns) ...@@ -140,5 +140,3 @@ urlpatterns = patterns(*urlpatterns)
# Custom error pages # Custom error pages
handler404 = 'contentstore.views.render_404' handler404 = 'contentstore.views.render_404'
handler500 = 'contentstore.views.render_500' handler500 = 'contentstore.views.render_500'
from django.conf.urls import * from django.conf.urls import *
urlpatterns = patterns('', urlpatterns = patterns('', # nopep8
url(r'^$', 'heartbeat.views.heartbeat', name='heartbeat'), url(r'^$', 'heartbeat.views.heartbeat', name='heartbeat'),
) )
...@@ -2,7 +2,7 @@ from student.models import (User, UserProfile, Registration, ...@@ -2,7 +2,7 @@ from student.models import (User, UserProfile, Registration,
CourseEnrollmentAllowed, CourseEnrollment) CourseEnrollmentAllowed, CourseEnrollment)
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from datetime import datetime from datetime import datetime
from factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall from factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall, post_generation
from uuid import uuid4 from uuid import uuid4
...@@ -45,6 +45,16 @@ class UserFactory(DjangoModelFactory): ...@@ -45,6 +45,16 @@ class UserFactory(DjangoModelFactory):
last_login = datetime(2012, 1, 1) last_login = datetime(2012, 1, 1)
date_joined = datetime(2011, 1, 1) date_joined = datetime(2011, 1, 1)
@post_generation
def profile(obj, create, extracted, **kwargs):
if create:
obj.save()
return UserProfileFactory.create(user=obj, **kwargs)
elif kwargs:
raise Exception("Cannot build a user profile without saving the user")
else:
return None
class AdminFactory(UserFactory): class AdminFactory(UserFactory):
is_staff = True is_staff = True
......
import datetime import datetime
import json import json
import logging
import pprint import pprint
import sys import sys
...@@ -7,15 +8,21 @@ from django.conf import settings ...@@ -7,15 +8,21 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.core.mail import send_mail from django.core.mail import send_mail
from django.http import Http404 from django.core.validators import ValidationError, validate_email
from django.http import HttpResponse from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, HttpResponseServerError
from django.shortcuts import redirect from django.shortcuts import redirect
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from urllib import urlencode
import zendesk
import capa.calc import capa.calc
import track.views import track.views
log = logging.getLogger(__name__)
def calculate(request): def calculate(request):
''' Calculator in footer of every page. ''' ''' Calculator in footer of every page. '''
equation = request.GET['equation'] equation = request.GET['equation']
...@@ -29,36 +36,142 @@ def calculate(request): ...@@ -29,36 +36,142 @@ def calculate(request):
return HttpResponse(json.dumps({'result': str(result)})) return HttpResponse(json.dumps({'result': str(result)}))
def send_feedback(request): class _ZendeskApi(object):
''' Feeback mechanism in footer of every page. ''' def __init__(self):
try: """
username = request.user.username Instantiate the Zendesk API.
email = request.user.email
except: All of `ZENDESK_URL`, `ZENDESK_USER`, and `ZENDESK_API_KEY` must be set
username = "anonymous" in `django.conf.settings`.
email = "anonymous" """
self._zendesk_instance = zendesk.Zendesk(
settings.ZENDESK_URL,
settings.ZENDESK_USER,
settings.ZENDESK_API_KEY,
use_api_token=True,
api_version=2
)
def create_ticket(self, ticket):
"""
Create the given `ticket` in Zendesk.
The ticket should have the format specified by the zendesk package.
"""
ticket_url = self._zendesk_instance.create_ticket(data=ticket)
return zendesk.get_id_from_url(ticket_url)
def update_ticket(self, ticket_id, update):
"""
Update the Zendesk ticket with id `ticket_id` using the given `update`.
The update should have the format specified by the zendesk package.
"""
self._zendesk_instance.update_ticket(ticket_id=ticket_id, data=update)
def submit_feedback_via_zendesk(request):
"""
Create a new user-requested Zendesk ticket.
If Zendesk submission is not enabled, any request will raise `Http404`.
If any configuration parameter (`ZENDESK_URL`, `ZENDESK_USER`, or
`ZENDESK_API_KEY`) is missing, any request will raise an `Exception`.
The request must be a POST request specifying `subject` and `details`.
If the user is not authenticated, the request must also specify `name` and
`email`. If the user is authenticated, the `name` and `email` will be
populated from the user's information. If any required parameter is
missing, a 400 error will be returned indicating which field is missing and
providing an error message. If Zendesk returns any error on ticket
creation, a 500 error will be returned with no body. Once created, the
ticket will be updated with a private comment containing additional
information from the browser and server, such as HTTP headers and user
state. Whether or not the update succeeds, if the user's ticket is
successfully created, an empty successful response (200) will be returned.
"""
if not settings.MITX_FEATURES.get('ENABLE_FEEDBACK_SUBMISSION', False):
raise Http404()
if request.method != "POST":
return HttpResponseNotAllowed(["POST"])
if (
not settings.ZENDESK_URL or
not settings.ZENDESK_USER or
not settings.ZENDESK_API_KEY
):
raise Exception("Zendesk enabled but not configured")
def build_error_response(status_code, field, err_msg):
return HttpResponse(json.dumps({"field": field, "error": err_msg}), status=status_code)
additional_info = {}
required_fields = ["subject", "details"]
if not request.user.is_authenticated():
required_fields += ["name", "email"]
required_field_errs = {
"subject": "Please provide a subject.",
"details": "Please provide details.",
"name": "Please provide your name.",
"email": "Please provide a valid e-mail.",
}
for field in required_fields:
if field not in request.POST or not request.POST[field]:
return build_error_response(400, field, required_field_errs[field])
subject = request.POST["subject"]
details = request.POST["details"]
tags = []
if "tag" in request.POST:
tags = [request.POST["tag"]]
if request.user.is_authenticated():
realname = request.user.profile.name
email = request.user.email
additional_info["username"] = request.user.username
else:
realname = request.POST["name"]
email = request.POST["email"]
try:
validate_email(email)
except ValidationError:
return build_error_response(400, "email", required_field_errs["email"])
for header in ["HTTP_REFERER", "HTTP_USER_AGENT"]:
additional_info[header] = request.META.get(header)
zendesk_api = _ZendeskApi()
additional_info_string = (
"Additional information:\n\n" +
"\n".join("%s: %s" % (key, value) for (key, value) in additional_info.items() if value is not None)
)
new_ticket = {
"ticket": {
"requester": {"name": realname, "email": email},
"subject": subject,
"comment": {"body": details},
"tags": tags
}
}
try: try:
browser = request.META['HTTP_USER_AGENT'] ticket_id = zendesk_api.create_ticket(new_ticket)
except: except zendesk.ZendeskError as err:
browser = "Unknown" log.error("%s", str(err))
return HttpResponse(status=500)
feedback = render_to_string("feedback_email.txt",
{"subject": request.POST['subject'], # Additional information is provided as a private update so the information
"url": request.POST['url'], # is not visible to the user.
"time": datetime.datetime.now().isoformat(), ticket_update = {"ticket": {"comment": {"public": False, "body": additional_info_string}}}
"feedback": request.POST['message'], try:
"email": email, zendesk_api.update_ticket(ticket_id, ticket_update)
"browser": browser, except zendesk.ZendeskError as err:
"user": username}) log.error("%s", str(err))
# The update is not strictly necessary, so do not indicate failure to the user
send_mail("MITx Feedback / " + request.POST['subject'], pass
feedback,
settings.DEFAULT_FROM_EMAIL, return HttpResponse()
[settings.DEFAULT_FEEDBACK_EMAIL],
fail_silently=False
)
return HttpResponse(json.dumps({'success': True}))
def info(request): def info(request):
......
...@@ -65,7 +65,8 @@ class CapaFields(object): ...@@ -65,7 +65,8 @@ class CapaFields(object):
max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings) max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
due = Date(help="Date that this problem is due by", scope=Scope.settings) due = Date(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings) graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed") showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed",
values=["answered", "always", "attempted", "closed", "never"])
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False) force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings) rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
...@@ -882,16 +883,6 @@ class CapaDescriptor(CapaFields, RawDescriptor): ...@@ -882,16 +883,6 @@ class CapaDescriptor(CapaFields, RawDescriptor):
'enable_markdown': self.markdown is not None}) 'enable_markdown': self.markdown is not None})
return _context return _context
@property
def editable_metadata_fields(self):
"""Remove metadata from the editable fields since it has its own editor"""
subset = super(CapaDescriptor, self).editable_metadata_fields
if 'markdown' in subset:
del subset['markdown']
if 'empty' in subset:
del subset['empty']
return subset
# VS[compat] # VS[compat]
# TODO (cpennington): Delete this method once all fall 2012 course are being # TODO (cpennington): Delete this method once all fall 2012 course are being
# edited in the cms # edited in the cms
...@@ -901,3 +892,10 @@ class CapaDescriptor(CapaFields, RawDescriptor): ...@@ -901,3 +892,10 @@ class CapaDescriptor(CapaFields, RawDescriptor):
'problems/' + path[8:], 'problems/' + path[8:],
path[8:], path[8:],
] ]
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(CapaDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([CapaDescriptor.due, CapaDescriptor.graceperiod,
CapaDescriptor.force_save_button, CapaDescriptor.markdown])
return non_editable_fields
...@@ -162,8 +162,7 @@ class CourseFields(object): ...@@ -162,8 +162,7 @@ class CourseFields(object):
discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings) discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings)
discussion_topics = Object( discussion_topics = Object(
help="Map of topics names to ids", help="Map of topics names to ids",
scope=Scope.settings, scope=Scope.settings
computed_default=lambda c: {'General': {'id': c.location.html_id()}},
) )
testcenter_info = Object(help="Dictionary of Test Center info", scope=Scope.settings) testcenter_info = Object(help="Dictionary of Test Center info", scope=Scope.settings)
announcement = Date(help="Date this course is announced", scope=Scope.settings) announcement = Date(help="Date this course is announced", scope=Scope.settings)
...@@ -234,6 +233,8 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -234,6 +233,8 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
self._grading_policy = {} self._grading_policy = {}
self.set_grading_policy(self.grading_policy) self.set_grading_policy(self.grading_policy)
if self.discussion_topics == {}:
self.discussion_topics = {'General': {'id': self.location.html_id()}}
self.test_center_exams = [] self.test_center_exams = []
test_center_info = self.testcenter_info test_center_info = self.testcenter_info
......
...@@ -37,3 +37,10 @@ class DiscussionDescriptor(DiscussionFields, MetadataOnlyEditingDescriptor, RawD ...@@ -37,3 +37,10 @@ class DiscussionDescriptor(DiscussionFields, MetadataOnlyEditingDescriptor, RawD
metadata_translations = dict(RawDescriptor.metadata_translations) metadata_translations = dict(RawDescriptor.metadata_translations)
metadata_translations['id'] = 'discussion_id' metadata_translations['id'] = 'discussion_id'
metadata_translations['for'] = 'discussion_target' metadata_translations['for'] = 'discussion_target'
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(DiscussionDescriptor, self).non_editable_metadata_fields
# We may choose to enable sort_keys in the future, but while Kevin is investigating....
non_editable_fields.extend([DiscussionDescriptor.discussion_id, DiscussionDescriptor.sort_key])
return non_editable_fields
...@@ -19,6 +19,7 @@ log = logging.getLogger("mitx.courseware") ...@@ -19,6 +19,7 @@ log = logging.getLogger("mitx.courseware")
class HtmlFields(object): class HtmlFields(object):
data = String(help="Html contents to display for this module", scope=Scope.content) data = String(help="Html contents to display for this module", scope=Scope.content)
source_code = String(help="Source code for LaTeX documents. This feature is not well-supported.", scope=Scope.settings)
class HtmlModule(HtmlFields, XModule): class HtmlModule(HtmlFields, XModule):
...@@ -166,16 +167,6 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): ...@@ -166,16 +167,6 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
elt.set("filename", relname) elt.set("filename", relname)
return elt return elt
@property
def editable_metadata_fields(self):
"""Remove any metadata from the editable fields which have their own editor or shouldn't be edited by user."""
subset = super(HtmlDescriptor, self).editable_metadata_fields
if 'empty' in subset:
del subset['empty']
return subset
class AboutDescriptor(HtmlDescriptor): class AboutDescriptor(HtmlDescriptor):
""" """
......
# Please do not ignore *.js files. Some xmodules are written in JS. # Ignore .js files in this folder as they are compiled from coffeescript
# For each of the xmodules subdirectories, add a .gitignore file that
# will version any *.js file that is specifically written, not compiled.
*.js
from .x_module import XModuleDescriptor, DescriptorSystem from .x_module import XModuleDescriptor, DescriptorSystem
from .modulestore.inheritance import own_metadata
class MakoDescriptorSystem(DescriptorSystem): class MakoDescriptorSystem(DescriptorSystem):
...@@ -34,20 +33,10 @@ class MakoModuleDescriptor(XModuleDescriptor): ...@@ -34,20 +33,10 @@ class MakoModuleDescriptor(XModuleDescriptor):
""" """
return { return {
'module': self, 'module': self,
'editable_metadata_fields': self.editable_metadata_fields, 'editable_metadata_fields': self.editable_metadata_fields
} }
def get_html(self): def get_html(self):
return self.system.render_template( return self.system.render_template(
self.mako_template, self.get_context()) self.mako_template, self.get_context())
# cdodge: encapsulate a means to expose "editable" metadata fields (i.e. not internal system metadata)
@property
def editable_metadata_fields(self):
fields = {}
for field, value in own_metadata(self).items():
if field in self.system_metadata_fields:
continue
fields[field] = value
return fields
...@@ -37,11 +37,17 @@ class XModuleCourseFactory(Factory): ...@@ -37,11 +37,17 @@ class XModuleCourseFactory(Factory):
new_course.display_name = display_name new_course.display_name = display_name
new_course.lms.start = gmtime() new_course.lms.start = gmtime()
new_course.tabs = [{"type": "courseware"}, new_course.tabs = kwargs.get(
{"type": "course_info", "name": "Course Info"}, 'tabs',
{"type": "discussion", "name": "Discussion"}, [
{"type": "wiki", "name": "Wiki"}, {"type": "courseware"},
{"type": "progress", "name": "Progress"}] {"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}
]
)
new_course.discussion_link = kwargs.get('discussion_link')
# Update the data in the mongo datastore # Update the data in the mongo datastore
store.update_metadata(new_course.location.url(), own_metadata(new_course)) store.update_metadata(new_course.location.url(), own_metadata(new_course))
......
...@@ -40,34 +40,20 @@ class DummySystem(ImportSystem): ...@@ -40,34 +40,20 @@ class DummySystem(ImportSystem):
) )
class IsNewCourseTestCase(unittest.TestCase): def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None, end=None):
"""Make sure the property is_new works on courses""" """Get a dummy course"""
def setUp(self): system = DummySystem(load_error_modules=True)
# Needed for test_is_newish
datetime_patcher = patch.object(
xmodule.course_module, 'datetime',
Mock(wraps=datetime.datetime)
)
mocked_datetime = datetime_patcher.start()
mocked_datetime.utcnow.return_value = time_to_datetime(NOW)
self.addCleanup(datetime_patcher.stop)
@staticmethod
def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None, end=None):
"""Get a dummy course"""
system = DummySystem(load_error_modules=True) def to_attrb(n, v):
return '' if v is None else '{0}="{1}"'.format(n, v).lower()
def to_attrb(n, v): is_new = to_attrb('is_new', is_new)
return '' if v is None else '{0}="{1}"'.format(n, v).lower() announcement = to_attrb('announcement', announcement)
advertised_start = to_attrb('advertised_start', advertised_start)
end = to_attrb('end', end)
is_new = to_attrb('is_new', is_new) start_xml = '''
announcement = to_attrb('announcement', announcement)
advertised_start = to_attrb('advertised_start', advertised_start)
end = to_attrb('end', end)
start_xml = '''
<course org="{org}" course="{course}" <course org="{org}" course="{course}"
graceperiod="1 day" url_name="test" graceperiod="1 day" url_name="test"
start="{start}" start="{start}"
...@@ -80,9 +66,23 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -80,9 +66,23 @@ class IsNewCourseTestCase(unittest.TestCase):
</chapter> </chapter>
</course> </course>
'''.format(org=ORG, course=COURSE, start=start, is_new=is_new, '''.format(org=ORG, course=COURSE, start=start, is_new=is_new,
announcement=announcement, advertised_start=advertised_start, end=end) announcement=announcement, advertised_start=advertised_start, end=end)
return system.process_xml(start_xml)
return system.process_xml(start_xml)
class IsNewCourseTestCase(unittest.TestCase):
"""Make sure the property is_new works on courses"""
def setUp(self):
# Needed for test_is_newish
datetime_patcher = patch.object(
xmodule.course_module, 'datetime',
Mock(wraps=datetime.datetime)
)
mocked_datetime = datetime_patcher.start()
mocked_datetime.utcnow.return_value = time_to_datetime(NOW)
self.addCleanup(datetime_patcher.stop)
@patch('xmodule.course_module.time.gmtime') @patch('xmodule.course_module.time.gmtime')
def test_sorting_score(self, gmtime_mock): def test_sorting_score(self, gmtime_mock):
...@@ -120,8 +120,8 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -120,8 +120,8 @@ class IsNewCourseTestCase(unittest.TestCase):
] ]
for a, b, assertion in dates: for a, b, assertion in dates:
a_score = self.get_dummy_course(start=a[0], announcement=a[1], advertised_start=a[2]).sorting_score a_score = get_dummy_course(start=a[0], announcement=a[1], advertised_start=a[2]).sorting_score
b_score = self.get_dummy_course(start=b[0], announcement=b[1], advertised_start=b[2]).sorting_score b_score = get_dummy_course(start=b[0], announcement=b[1], advertised_start=b[2]).sorting_score
print "Comparing %s to %s" % (a, b) print "Comparing %s to %s" % (a, b)
assertion(a_score, b_score) assertion(a_score, b_score)
...@@ -138,36 +138,42 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -138,36 +138,42 @@ class IsNewCourseTestCase(unittest.TestCase):
] ]
for s in settings: for s in settings:
d = self.get_dummy_course(start=s[0], advertised_start=s[1]) d = get_dummy_course(start=s[0], advertised_start=s[1])
print "Checking start=%s advertised=%s" % (s[0], s[1]) print "Checking start=%s advertised=%s" % (s[0], s[1])
self.assertEqual(d.start_date_text, s[2]) self.assertEqual(d.start_date_text, s[2])
def test_is_newish(self): def test_is_newish(self):
descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True) descriptor = get_dummy_course(start='2012-12-02T12:00', is_new=True)
assert(descriptor.is_newish is True) assert(descriptor.is_newish is True)
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=False) descriptor = get_dummy_course(start='2013-02-02T12:00', is_new=False)
assert(descriptor.is_newish is False) assert(descriptor.is_newish is False)
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=True) descriptor = get_dummy_course(start='2013-02-02T12:00', is_new=True)
assert(descriptor.is_newish is True) assert(descriptor.is_newish is True)
descriptor = self.get_dummy_course(start='2013-01-15T12:00') descriptor = get_dummy_course(start='2013-01-15T12:00')
assert(descriptor.is_newish is True) assert(descriptor.is_newish is True)
descriptor = self.get_dummy_course(start='2013-03-01T12:00') descriptor = get_dummy_course(start='2013-03-01T12:00')
assert(descriptor.is_newish is True) assert(descriptor.is_newish is True)
descriptor = self.get_dummy_course(start='2012-10-15T12:00') descriptor = get_dummy_course(start='2012-10-15T12:00')
assert(descriptor.is_newish is False) assert(descriptor.is_newish is False)
descriptor = self.get_dummy_course(start='2012-12-31T12:00') descriptor = get_dummy_course(start='2012-12-31T12:00')
assert(descriptor.is_newish is True) assert(descriptor.is_newish is True)
def test_end_date_text(self): def test_end_date_text(self):
# No end date set, returns empty string. # No end date set, returns empty string.
d = self.get_dummy_course('2012-12-02T12:00') d = get_dummy_course('2012-12-02T12:00')
self.assertEqual('', d.end_date_text) self.assertEqual('', d.end_date_text)
d = self.get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00') d = get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00')
self.assertEqual('Sep 04, 2014', d.end_date_text) self.assertEqual('Sep 04, 2014', d.end_date_text)
class DiscussionTopicsTestCase(unittest.TestCase):
def test_default_discussion_topics(self):
d = get_dummy_course('2012-12-02T12:00')
self.assertEqual({'General': {'id': 'i4x-test_org-test_course-course-test'}}, d.discussion_topics)
from xmodule.x_module import XModuleFields
from xblock.core import Scope, String, Object
from xmodule.fields import Date, StringyInteger
from xmodule.xml_module import XmlDescriptor
import unittest
from . import test_system
from mock import Mock
class TestFields(object):
# Will be returned by editable_metadata_fields.
max_attempts = StringyInteger(scope=Scope.settings)
# Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields.
due = Date(scope=Scope.settings)
# Will not be returned by editable_metadata_fields because is not Scope.settings.
student_answers = Object(scope=Scope.user_state)
# Will be returned, and can override the inherited value from XModule.
display_name = String(scope=Scope.settings)
class EditableMetadataFieldsTest(unittest.TestCase):
def test_display_name_field(self):
editable_fields = self.get_xml_editable_fields({})
# Tests that the xblock fields (currently tags and name) get filtered out.
# Also tests that xml_attributes is filtered out of XmlDescriptor.
self.assertEqual(1, len(editable_fields), "Expected only 1 editable field for xml descriptor.")
self.assert_display_name_default(editable_fields)
def test_override_default(self):
# Tests that is_default is correct when a value overrides the default.
editable_fields = self.get_xml_editable_fields({'display_name': 'foo'})
display_name = editable_fields['display_name']
self.assertFalse(display_name['is_default'])
self.assertEqual('foo', display_name['value'])
def test_additional_field(self):
editable_fields = self.get_module_editable_fields({'max_attempts' : '7'})
self.assertEqual(2, len(editable_fields))
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts, False, False, 7)
self.assert_display_name_default(editable_fields)
editable_fields = self.get_module_editable_fields({})
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts, True, False, None)
def test_inherited_field(self):
editable_fields = self.get_module_editable_fields({'display_name' : 'inherited'})
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name, False, True, 'inherited')
# Start of helper methods
def get_xml_editable_fields(self, model_data):
system = test_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
return XmlDescriptor(system=system, location=None, model_data=model_data).editable_metadata_fields
def get_module_editable_fields(self, model_data):
class TestModuleDescriptor(TestFields, XmlDescriptor):
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(TestModuleDescriptor, self).non_editable_metadata_fields
non_editable_fields.append(TestModuleDescriptor.due)
return non_editable_fields
system = test_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
descriptor = TestModuleDescriptor(system=system, location=None, model_data=model_data)
descriptor._inherited_metadata = {'display_name' : 'inherited'}
return descriptor.editable_metadata_fields
def assert_display_name_default(self, editable_fields):
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name, True, False, None)
def assert_field_values(self, editable_fields, name, field, is_default, is_inherited, value):
test_field = editable_fields[name]
self.assertEqual(field, test_field['field'])
self.assertEqual(is_default, test_field['is_default'])
self.assertEqual(is_inherited, test_field['is_inherited'])
self.assertEqual(value, test_field['value'])
...@@ -82,7 +82,7 @@ class XModuleFields(object): ...@@ -82,7 +82,7 @@ class XModuleFields(object):
display_name = String( display_name = String(
help="Display name for this module", help="Display name for this module",
scope=Scope.settings, scope=Scope.settings,
default=None, default=None
) )
...@@ -334,12 +334,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -334,12 +334,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# (like a practice problem). # (like a practice problem).
has_score = False has_score = False
# cdodge: this is a list of metadata names which are 'system' metadata
# and should not be edited by an end-user
system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft',
'discussion_id', 'xml_attributes']
# A list of descriptor attributes that must be equal for the descriptors to # A list of descriptor attributes that must be equal for the descriptors to
# be equal # be equal
equality_attributes = ('_model_data', 'location') equality_attributes = ('_model_data', 'location')
...@@ -612,6 +606,48 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -612,6 +606,48 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
model_data=self._model_data, model_data=self._model_data,
)) ))
@property
def non_editable_metadata_fields(self):
"""
Return the list of fields that should not be editable in Studio.
When overriding, be sure to append to the superclasses' list.
"""
# We are not allowing editing of xblock tag and name fields at this time (for any component).
return [XBlock.tags, XBlock.name]
@property
def editable_metadata_fields(self):
"""
Returns the metadata fields to be edited in Studio. These are fields with scope `Scope.settings`.
Can be limited by extending `non_editable_metadata_fields`.
"""
inherited_metadata = getattr(self, '_inherited_metadata', {})
metadata = {}
for field in self.fields:
if field.scope != Scope.settings or field in self.non_editable_metadata_fields:
continue
inherited = False
default = False
value = getattr(self, field.name)
if field.name in self._model_data:
default = False
if field.name in inherited_metadata:
if self._model_data.get(field.name) == inherited_metadata.get(field.name):
inherited = True
else:
default = True
metadata[field.name] = {'field': field,
'value': value,
'is_inherited': inherited,
'is_default': default}
return metadata
class DescriptorSystem(object): class DescriptorSystem(object):
def __init__(self, load_item, resources_fs, error_tracker, **kwargs): def __init__(self, load_item, resources_fs, error_tracker, **kwargs):
......
...@@ -84,7 +84,8 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -84,7 +84,8 @@ class XmlDescriptor(XModuleDescriptor):
Mixin class for standardized parsing of from xml Mixin class for standardized parsing of from xml
""" """
xml_attributes = Object(help="Map of unhandled xml attributes, used only for storage between import and export", default={}, scope=Scope.settings) xml_attributes = Object(help="Map of unhandled xml attributes, used only for storage between import and export",
default={}, scope=Scope.settings)
# Extension to append to filename paths # Extension to append to filename paths
filename_extension = 'xml' filename_extension = 'xml'
...@@ -418,3 +419,9 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -418,3 +419,9 @@ class XmlDescriptor(XModuleDescriptor):
""" """
raise NotImplementedError( raise NotImplementedError(
"%s does not implement definition_to_xml" % self.__class__.__name__) "%s does not implement definition_to_xml" % self.__class__.__name__)
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(XmlDescriptor, self).non_editable_metadata_fields
non_editable_fields.append(XmlDescriptor.xml_attributes)
return non_editable_fields
...@@ -31,6 +31,14 @@ Check out the course data directories that you want to work with into the ...@@ -31,6 +31,14 @@ Check out the course data directories that you want to work with into the
rake resetdb rake resetdb
## Installing
To create your development environment, run the shell script in the root of
the repo:
create-dev-env.sh
## Starting development servers ## Starting development servers
Both the LMS and Studio can be started using the following shortcut tasks Both the LMS and Studio can be started using the following shortcut tasks
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
-e git://github.com/edx/django-pipeline.git#egg=django-pipeline -e git://github.com/edx/django-pipeline.git#egg=django-pipeline
-e git://github.com/edx/django-wiki.git@e2e84558#egg=django-wiki -e git://github.com/edx/django-wiki.git@e2e84558#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev -e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
-e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
# Our libraries: # Our libraries:
-e git+https://github.com/edx/XBlock.git@5ce6f70a#egg=XBlock -e git+https://github.com/edx/XBlock.git@5ce6f70a#egg=XBlock
This document describes how to set up the MITx development environment
for both Linux (Ubuntu) and MacOS (OSX Lion).
There is also a script "create-dev-env.sh" that automates these steps.
1) Make an mitx_all directory and clone the repos
(download and install git and mercurial if you don't have them already)
mkdir ~/mitx_all
cd ~/mitx_all
git clone git@github.com:MITx/mitx.git
hg clone ssh://hg-content@gp.mitx.mit.edu/data
2) Install OSX dependencies (Mac users only)
a) Install the brew utility if necessary
/usr/bin/ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/master/Library/Contributions/install_homebrew.rb)"
b) Install the brew package list
cat ~/mitx_all/mitx/brew-formulas.txt | xargs brew install
c) Install python pip if necessary
sudo easy_install pip
d) Install python virtualenv if necessary
sudo pip install virtualenv virtualenvwrapper
e) Install coffee script
curl http://npmjs.org/install.sh | sh
npm install -g coffee-script
3) Install Ubuntu dependencies (Linux users only)
sudo apt-get install curl python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript
4) Install rvm, ruby, and libraries
echo "export rvm_path=$HOME/mitx_all/ruby" > $HOME/.rvmrc
curl -sL get.rvm.io | bash -s stable
source ~/mitx_all/ruby/scripts/rvm
rvm install 1.9.3
gem install bundler
cd ~/mitx_all/mitx
bundle install
5) Install python libraries
source ~/mitx_all/python/bin/activate
cd ~/mitx_all
pip install -r mitx/pre-requirements.txt
pip install -r mitx/requirements.txt
6) Create log and db dirs
mkdir ~/mitx_all/log
mkdir ~/mitx_all/db
7) Start the dev server
To start using Django you will need
to activate the local Python and Ruby
environment:
$ source ~/mitx_all/ruby/scripts/rvm
$ source ~/mitx_all/python/bin/activate
To initialize and start a local instance of Django:
$ cd ~/mitx_all/mitx
$ django-admin.py syncdb --settings=envs.dev --pythonpath=.
$ django-admin.py migrate --settings=envs.dev --pythonpath=.
$ django-admin.py runserver --settings=envs.dev --pythonpath=.
...@@ -294,6 +294,27 @@ def get_course_tabs(user, course, active_page): ...@@ -294,6 +294,27 @@ def get_course_tabs(user, course, active_page):
return tabs return tabs
def get_discussion_link(course):
"""
Return the URL for the discussion tab for the given `course`.
If they have a discussion link specified, use that even if we disable
discussions. Disabling discsussions is mostly a server safety feature at
this point, and we don't need to worry about external sites. Otherwise,
if the course has a discussion tab or uses the default tabs, return the
discussion view URL. Otherwise, return None to indicate the lack of a
discussion tab.
"""
if course.discussion_link:
return course.discussion_link
elif not settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
return None
elif hasattr(course, 'tabs') and course.tabs and not any([tab['type'] == 'discussion' for tab in course.tabs]):
return None
else:
return reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id])
def get_default_tabs(user, course, active_page): def get_default_tabs(user, course, active_page):
# When calling the various _tab methods, can omit the 'type':'blah' from the # When calling the various _tab methods, can omit the 'type':'blah' from the
...@@ -308,15 +329,9 @@ def get_default_tabs(user, course, active_page): ...@@ -308,15 +329,9 @@ def get_default_tabs(user, course, active_page):
tabs.extend(_textbooks({}, user, course, active_page)) tabs.extend(_textbooks({}, user, course, active_page))
## If they have a discussion link specified, use that even if we feature discussion_link = get_discussion_link(course)
## flag discussions off. Disabling that is mostly a server safety feature if discussion_link:
## at this point, and we don't need to worry about external sites. tabs.append(CourseTab('Discussion', discussion_link, active_page == 'discussion'))
if course.discussion_link:
tabs.append(CourseTab('Discussion', course.discussion_link, active_page == 'discussion'))
elif settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
link = reverse('django_comment_client.forum.views.forum_form_discussion',
args=[course.id])
tabs.append(CourseTab('Discussion', link, active_page == 'discussion'))
tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page)) tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page))
......
from django.test import TestCase from django.test import TestCase
from mock import MagicMock from mock import MagicMock
from mock import patch
import courseware.tabs as tabs import courseware.tabs as tabs
from django.test.utils import override_settings from django.test.utils import override_settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
class ProgressTestCase(TestCase): class ProgressTestCase(TestCase):
...@@ -257,3 +261,62 @@ class ValidateTabsTestCase(TestCase): ...@@ -257,3 +261,62 @@ class ValidateTabsTestCase(TestCase):
self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[2]) self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[2])
self.assertIsNone(tabs.validate_tabs(self.courses[3])) self.assertIsNone(tabs.validate_tabs(self.courses[3]))
self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[4]) self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[4])
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class DiscussionLinkTestCase(ModuleStoreTestCase):
def setUp(self):
self.tabs_with_discussion = [
{'type':'courseware'},
{'type':'course_info'},
{'type':'discussion'},
{'type':'textbooks'},
]
self.tabs_without_discussion = [
{'type':'courseware'},
{'type':'course_info'},
{'type':'textbooks'},
]
@staticmethod
def _patch_reverse(course):
def patched_reverse(viewname, args):
if viewname == "django_comment_client.forum.views.forum_form_discussion" and args == [course.id]:
return "default_discussion_link"
else:
return None
return patch("courseware.tabs.reverse", patched_reverse)
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": False})
def test_explicit_discussion_link(self):
"""Test that setting discussion_link overrides everything else"""
course = CourseFactory.create(discussion_link="other_discussion_link", tabs=self.tabs_with_discussion)
self.assertEqual(tabs.get_discussion_link(course), "other_discussion_link")
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": False})
def test_discussions_disabled(self):
"""Test that other cases return None with discussions disabled"""
for i, t in enumerate([None, self.tabs_with_discussion, self.tabs_without_discussion]):
course = CourseFactory.create(tabs=t, number=str(i))
self.assertEqual(tabs.get_discussion_link(course), None)
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def test_no_tabs(self):
"""Test a course without tabs configured"""
course = CourseFactory.create(tabs=None)
with self._patch_reverse(course):
self.assertEqual(tabs.get_discussion_link(course), "default_discussion_link")
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def test_tabs_with_discussion(self):
"""Test a course with a discussion tab configured"""
course = CourseFactory.create(tabs=self.tabs_with_discussion)
with self._patch_reverse(course):
self.assertEqual(tabs.get_discussion_link(course), "default_discussion_link")
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def test_tabs_without_discussion(self):
"""Test a course with tabs configured but without a discussion tab"""
course = CourseFactory.create(tabs=self.tabs_without_discussion)
self.assertEqual(tabs.get_discussion_link(course), None)
''' '''
Test for lms courseware app Test for lms courseware app
''' '''
import logging import logging
import json import json
import time import time
import random import random
from urlparse import urlsplit, urlunsplit from urlparse import urlsplit, urlunsplit
from uuid import uuid4
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.test import TestCase from django.test import TestCase
...@@ -62,7 +62,7 @@ def mongo_store_config(data_dir): ...@@ -62,7 +62,7 @@ def mongo_store_config(data_dir):
'default_class': 'xmodule.raw_module.RawDescriptor', 'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost', 'host': 'localhost',
'db': 'test_xmodule', 'db': 'test_xmodule',
'collection': 'modulestore', 'collection': 'modulestore_%s' % uuid4().hex,
'fs_root': data_dir, 'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string', 'render_template': 'mitxmako.shortcuts.render_to_string',
} }
...@@ -81,7 +81,7 @@ def draft_mongo_store_config(data_dir): ...@@ -81,7 +81,7 @@ def draft_mongo_store_config(data_dir):
'default_class': 'xmodule.raw_module.RawDescriptor', 'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost', 'host': 'localhost',
'db': 'test_xmodule', 'db': 'test_xmodule',
'collection': 'modulestore', 'collection': 'modulestore_%s' % uuid4().hex,
'fs_root': data_dir, 'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string', 'render_template': 'mitxmako.shortcuts.render_to_string',
} }
...@@ -92,7 +92,7 @@ def draft_mongo_store_config(data_dir): ...@@ -92,7 +92,7 @@ def draft_mongo_store_config(data_dir):
'default_class': 'xmodule.raw_module.RawDescriptor', 'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost', 'host': 'localhost',
'db': 'test_xmodule', 'db': 'test_xmodule',
'collection': 'modulestore', 'collection': 'modulestore_%s' % uuid4().hex,
'fs_root': data_dir, 'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string', 'render_template': 'mitxmako.shortcuts.render_to_string',
} }
......
from django.conf.urls.defaults import url, patterns from django.conf.urls.defaults import url, patterns
import django_comment_client.base.views
urlpatterns = patterns('django_comment_client.base.views',
urlpatterns = patterns('django_comment_client.base.views', # nopep8
url(r'upload$', 'upload', name='upload'), url(r'upload$', 'upload', name='upload'),
url(r'users/(?P<user_id>\w+)/update_moderator_status$', 'update_moderator_status', name='update_moderator_status'), url(r'users/(?P<user_id>\w+)/update_moderator_status$', 'update_moderator_status', name='update_moderator_status'),
url(r'threads/tags/autocomplete$', 'tags_autocomplete', name='tags_autocomplete'), url(r'threads/tags/autocomplete$', 'tags_autocomplete', name='tags_autocomplete'),
......
from django.conf.urls.defaults import url, patterns from django.conf.urls.defaults import url, patterns
import django_comment_client.forum.views
urlpatterns = patterns('django_comment_client.forum.views', urlpatterns = patterns('django_comment_client.forum.views', # nopep8
url(r'users/(?P<user_id>\w+)/followed$', 'followed_threads', name='followed_threads'), url(r'users/(?P<user_id>\w+)/followed$', 'followed_threads', name='followed_threads'),
url(r'users/(?P<user_id>\w+)$', 'user_profile', name='user_profile'), url(r'users/(?P<user_id>\w+)$', 'user_profile', name='user_profile'),
url(r'^(?P<discussion_id>[\w\-.]+)/threads/(?P<thread_id>\w+)$', 'single_thread', name='single_thread'), url(r'^(?P<discussion_id>[\w\-.]+)/threads/(?P<thread_id>\w+)$', 'single_thread', name='single_thread'),
......
from django.conf.urls.defaults import url, patterns, include from django.conf.urls.defaults import url, patterns, include
urlpatterns = patterns('', urlpatterns = patterns('', # nopep8
url(r'forum/?', include('django_comment_client.forum.urls')), url(r'forum/?', include('django_comment_client.forum.urls')),
url(r'', include('django_comment_client.base.urls')), url(r'', include('django_comment_client.base.urls')),
) )
...@@ -49,7 +49,6 @@ class TestGradebook(ModuleStoreTestCase): ...@@ -49,7 +49,6 @@ class TestGradebook(ModuleStoreTestCase):
] ]
for user in self.users: for user in self.users:
UserProfileFactory.create(user=user)
CourseEnrollmentFactory.create(user=user, course_id=self.course.id) CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
for i in xrange(USER_COUNT-1): for i in xrange(USER_COUNT-1):
...@@ -151,4 +150,4 @@ class TestLetterCutoffPolicy(TestGradebook): ...@@ -151,4 +150,4 @@ class TestLetterCutoffPolicy(TestGradebook):
# User 0 has 0 on Homeworks [1] # User 0 has 0 on Homeworks [1]
# User 0 has 0 on the class [1] # User 0 has 0 on the class [1]
# One use at the top of the page [1] # One use at the top of the page [1]
self.assertEquals(3, self.response.content.count('grade_None')) self.assertEquals(3, self.response.content.count('grade_None'))
\ No newline at end of file
...@@ -4,16 +4,16 @@ namespace_regex = r"[a-zA-Z\d._-]+" ...@@ -4,16 +4,16 @@ namespace_regex = r"[a-zA-Z\d._-]+"
article_slug = r'/(?P<article_path>' + namespace_regex + r'/[a-zA-Z\d_-]*)' article_slug = r'/(?P<article_path>' + namespace_regex + r'/[a-zA-Z\d_-]*)'
namespace = r'/(?P<namespace>' + namespace_regex + r')' namespace = r'/(?P<namespace>' + namespace_regex + r')'
urlpatterns = patterns('', urlpatterns = patterns('', # nopep8
url(r'^$', 'simplewiki.views.root_redirect', name='wiki_root'), url(r'^$', 'simplewiki.views.root_redirect', name='wiki_root'),
url(r'^view' + article_slug, 'simplewiki.views.view', name='wiki_view'), url(r'^view' + article_slug, 'simplewiki.views.view', name='wiki_view'),
url(r'^view_revision/(?P<revision_number>[0-9]+)' + article_slug, 'simplewiki.views.view_revision', name='wiki_view_revision'), url(r'^view_revision/(?P<revision_number>[0-9]+)' + article_slug, 'simplewiki.views.view_revision', name='wiki_view_revision'),
url(r'^edit' + article_slug, 'simplewiki.views.edit', name='wiki_edit'), url(r'^edit' + article_slug, 'simplewiki.views.edit', name='wiki_edit'),
url(r'^create' + article_slug, 'simplewiki.views.create', name='wiki_create'), url(r'^create' + article_slug, 'simplewiki.views.create', name='wiki_create'),
url(r'^history' + article_slug + r'(?:/(?P<page>[0-9]+))?$', 'simplewiki.views.history', name='wiki_history'), url(r'^history' + article_slug + r'(?:/(?P<page>[0-9]+))?$', 'simplewiki.views.history', name='wiki_history'),
url(r'^search_related' + article_slug, 'simplewiki.views.search_add_related', name='search_related'), url(r'^search_related' + article_slug, 'simplewiki.views.search_add_related', name='search_related'),
url(r'^random/?$', 'simplewiki.views.random_article', name='wiki_random'), url(r'^random/?$', 'simplewiki.views.random_article', name='wiki_random'),
url(r'^revision_feed' + namespace + r'/(?P<page>[0-9]+)?$', 'simplewiki.views.revision_feed', name='wiki_revision_feed'), url(r'^revision_feed' + namespace + r'/(?P<page>[0-9]+)?$', 'simplewiki.views.revision_feed', name='wiki_revision_feed'),
url(r'^search' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_search_articles'), url(r'^search' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_search_articles'),
url(r'^list' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_list_articles'), # Just an alias for the search, but you usually don't submit a search term url(r'^list' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_list_articles'), # Just an alias for the search, but you usually don't submit a search term
) )
...@@ -88,6 +88,8 @@ META_UNIVERSITIES = ENV_TOKENS.get('META_UNIVERSITIES', {}) ...@@ -88,6 +88,8 @@ META_UNIVERSITIES = ENV_TOKENS.get('META_UNIVERSITIES', {})
COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL", '') COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL", '')
COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY", '') COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY", '')
CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull') CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull')
ZENDESK_URL = ENV_TOKENS.get("ZENDESK_URL")
FEEDBACK_SUBMISSION_EMAIL = ENV_TOKENS.get("FEEDBACK_SUBMISSION_EMAIL")
############################## SECURE AUTH ITEMS ############### ############################## SECURE AUTH ITEMS ###############
# Secret things: passwords, access keys, etc. # Secret things: passwords, access keys, etc.
...@@ -123,3 +125,6 @@ DATADOG_API = AUTH_TOKENS.get("DATADOG_API") ...@@ -123,3 +125,6 @@ DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
# Analytics dashboard server # Analytics dashboard server
ANALYTICS_SERVER_URL = ENV_TOKENS.get("ANALYTICS_SERVER_URL") ANALYTICS_SERVER_URL = ENV_TOKENS.get("ANALYTICS_SERVER_URL")
ANALYTICS_API_KEY = AUTH_TOKENS.get("ANALYTICS_API_KEY", "") ANALYTICS_API_KEY = AUTH_TOKENS.get("ANALYTICS_API_KEY", "")
ZENDESK_USER = AUTH_TOKENS.get("ZENDESK_USER")
ZENDESK_API_KEY = AUTH_TOKENS.get("ZENDESK_API_KEY")
...@@ -90,7 +90,10 @@ MITX_FEATURES = { ...@@ -90,7 +90,10 @@ MITX_FEATURES = {
# Give a UI to show a student's submission history in a problem by the # Give a UI to show a student's submission history in a problem by the
# Staff Debug tool. # Staff Debug tool.
'ENABLE_STUDENT_HISTORY_VIEW': True 'ENABLE_STUDENT_HISTORY_VIEW': True,
# Provide a UI to allow users to submit feedback from the LMS
'ENABLE_FEEDBACK_SUBMISSION': False,
} }
# Used for A/B testing # Used for A/B testing
...@@ -323,6 +326,14 @@ WIKI_LINK_DEFAULT_LEVEL = 2 ...@@ -323,6 +326,14 @@ WIKI_LINK_DEFAULT_LEVEL = 2
PEARSONVUE_SIGNINPAGE_URL = "https://www1.pearsonvue.com/testtaker/signin/SignInPage/EDX" PEARSONVUE_SIGNINPAGE_URL = "https://www1.pearsonvue.com/testtaker/signin/SignInPage/EDX"
# TESTCENTER_ACCOMMODATION_REQUEST_EMAIL = "exam-help@edx.org" # TESTCENTER_ACCOMMODATION_REQUEST_EMAIL = "exam-help@edx.org"
##### Feedback submission mechanism #####
FEEDBACK_SUBMISSION_EMAIL = None
##### Zendesk #####
ZENDESK_URL = None
ZENDESK_USER = None
ZENDESK_API_KEY = None
################################# open ended grading config ##################### ################################# open ended grading config #####################
#By setting up the default settings with an incorrect user name and password, #By setting up the default settings with an incorrect user name and password,
...@@ -582,3 +593,4 @@ INSTALLED_APPS = ( ...@@ -582,3 +593,4 @@ INSTALLED_APPS = (
# Discussion forums # Discussion forums
'django_comment_client', 'django_comment_client',
) )
...@@ -202,5 +202,62 @@ mark { ...@@ -202,5 +202,62 @@ mark {
} }
} }
.help-tab {
@include transform(rotate(-90deg));
@include transform-origin(0 0);
top: 50%;
left: 0;
position: fixed;
z-index: 99;
a:link, a:visited {
cursor: pointer;
border: 1px solid #ccc;
border-top-style: none;
@include border-radius(0px 0px 10px 10px);
background: transparentize(#fff, 0.25);
color: transparentize(#333, 0.25);
font-weight: bold;
text-decoration: none;
padding: 6px 22px 11px;
display: inline-block;
&:hover {
color: #fff;
background: #1D9DD9;
}
}
}
.help-buttons {
padding: 10px 50px;
a:link, a:visited {
padding: 15px 0px;
text-align: center;
cursor: pointer;
background: #fff;
text-decoration: none;
display: block;
border: 1px solid #ccc;
&#feedback_link_problem {
border-bottom-style: none;
@include border-radius(10px 10px 0px 0px);
}
&#feedback_link_question {
border-top-style: none;
@include border-radius(0px 0px 10px 10px);
}
&:hover {
color: #fff;
background: #1D9DD9;
}
}
}
#feedback_form textarea[name="details"] {
height: 150px;
}
...@@ -155,7 +155,7 @@ ...@@ -155,7 +155,7 @@
display: block; display: block;
color: #8F0E0E; color: #8F0E0E;
+ input { + input, + textarea {
border: 1px solid #CA1111; border: 1px solid #CA1111;
color: #8F0E0E; color: #8F0E0E;
} }
......
<%namespace name='static' file='static_content.html'/>
<%! from django.conf import settings %>
<%! from courseware.tabs import get_discussion_link %>
% if settings.MITX_FEATURES.get('ENABLE_FEEDBACK_SUBMISSION', False):
<div class="help-tab">
<a href="#help-modal" rel="leanModal">Help</a>
</div>
<section id="help-modal" class="modal">
<div class="inner-wrapper" id="help_wrapper">
<header>
<h2><span class="edx">edX</span> Help</h2>
<hr>
</header>
<%
discussion_link = get_discussion_link(course) if course else None
%>
% if discussion_link:
<p>
Have a course-specific question?
<a href="${discussion_link}" target="_blank"/>
Post it on the course forums.
</a>
</p>
<hr>
% endif
<p>Have a general question about edX? <a href="/help" target="_blank">Check the FAQ</a>.</p>
<hr>
<div class="help-buttons">
<a href="#" id="feedback_link_problem">Report a problem</a>
<a href="#" id="feedback_link_suggestion">Make a suggestion</a>
<a href="#" id="feedback_link_question">Ask a question</a>
</div>
## TODO: find a way to refactor this
<div class="close-modal">
<div class="inner">
<p>&#10005;</p>
</div>
</div>
</div>
<div class="inner-wrapper" id="feedback_form_wrapper">
<header></header>
<form id="feedback_form" class="feedback_form" method="post" data-remote="true" action="/submit_feedback">
<div id="feedback_error" class="modal-form-error"></div>
% if not user.is_authenticated():
<label data-field="name">Name*</label>
<input name="name" type="text">
<label data-field="email">E-mail*</label>
<input name="email" type="text">
% endif
<label data-field="subject">Subject*</label>
<input name="subject" type="text">
<label data-field="details">Details*</label>
<textarea name="details"></textarea>
<input name="tag" type="hidden">
<div class="submit">
<input name="submit" type="submit" value="Submit">
</div>
</form>
<div class="close-modal">
<div class="inner">
<p>&#10005;</p>
</div>
</div>
</div>
<div class="inner-wrapper" id="feedback_success_wrapper">
<header>
<h2>Thank You!</h2>
<hr>
</header>
<p>
Thanks for your feedback. We will read your message, and our
support team may contact you to respond or ask for further clarification.
</p>
<div class="close-modal">
<div class="inner">
<p>&#10005;</p>
</div>
</div>
</div>
</section>
<script type="text/javascript">
(function() {
$(".help-tab").click(function() {
$(".field-error").removeClass("field-error");
$("#feedback_form")[0].reset();
$("#feedback_form input[type='submit']").removeAttr("disabled");
$("#feedback_form_wrapper").css("display", "none");
$("#feedback_error").css("display", "none");
$("#feedback_success_wrapper").css("display", "none");
$("#help_wrapper").css("display", "block");
});
showFeedback = function(e, tag, title) {
$("#help_wrapper").css("display", "none");
$("#feedback_form input[name='tag']").val(tag);
$("#feedback_form_wrapper").css("display", "block");
$("#feedback_form_wrapper header").html("<h2>" + title + "</h2><hr>");
e.preventDefault();
};
$("#feedback_link_problem").click(function(e) {
showFeedback(e, "problem", "Report a Problem");
});
$("#feedback_link_suggestion").click(function(e) {
showFeedback(e, "suggestion", "Make a Suggestion");
});
$("#feedback_link_question").click(function(e) {
showFeedback(e, "question", "Ask a Question");
});
$("#feedback_form").submit(function() {
$("input[type='submit']", this).attr("disabled", "disabled");
});
$("#feedback_form").on("ajax:complete", function() {
$("input[type='submit']", this).removeAttr("disabled");
});
$("#feedback_form").on("ajax:success", function(event, data, status, xhr) {
$("#feedback_form_wrapper").css("display", "none");
$("#feedback_success_wrapper").css("display", "block");
});
$("#feedback_form").on("ajax:error", function(event, xhr, status, error) {
$(".field-error").removeClass("field-error");
var responseData;
try {
responseData = jQuery.parseJSON(xhr.responseText);
} catch(err) {
}
if (responseData) {
$("[data-field='"+responseData.field+"']").addClass("field-error");
$("#feedback_error").html(responseData.error).stop().css("display", "block");
} else {
// If no data (or malformed data) is returned, a server error occurred
htmlStr = "An error has occurred.";
% if settings.FEEDBACK_SUBMISSION_EMAIL:
htmlStr += " Please <a href='#' id='feedback_email'>send us e-mail</a>.";
% else:
// If no email is configured, we can't do much other than
// ask the user to try again later
htmlStr += " Please try again later.";
% endif
$("#feedback_error").html(htmlStr).stop().css("display", "block");
% if settings.FEEDBACK_SUBMISSION_EMAIL:
$("#feedback_email").click(function(e) {
mailto = "mailto:" + "${settings.FEEDBACK_SUBMISSION_EMAIL}" +
"?subject=" + $("#feedback_form input[name='subject']").val() +
"&body=" + $("#feedback_form textarea[name='details']").val();
window.open(mailto);
e.preventDefault();
});
%endif
}
});
})(this)
</script>
%endif
...@@ -96,3 +96,5 @@ site_status_msg = get_site_status_msg(course_id) ...@@ -96,3 +96,5 @@ site_status_msg = get_site_status_msg(course_id)
<%include file="signup_modal.html" /> <%include file="signup_modal.html" />
<%include file="forgot_password_modal.html" /> <%include file="forgot_password_modal.html" />
%endif %endif
<%include file="help_modal.html"/>
...@@ -18,10 +18,13 @@ ...@@ -18,10 +18,13 @@
</%block> </%block>
<%block name="university_description"> <%block name="university_description">
<p>EPFL is one of the two Swiss Federal Institutes of Technology. With the status of a national school since 1969, the young engineering school has grown in many dimensions, to the extent of becoming one of the most famous European institutions of science and technology. It has three core missions: training, research and technology transfer. </p>
<p>EPFL is located in Lausanne in Switzerland, on the shores of the largest lake in Europe, Lake Geneva and at the foot of the Alps and Mont-Blanc. Its main campus brings together over 11,000 persons, students, researchers and staff in the same magical place. Because of its dynamism and rich student community, EPFL has been able to create a special spirit imbued with curiosity and simplicity. Daily interactions amongst students, researchers and entrepreneurs on campus give rise to new scientific, technological and architectural projects. <p>EPFL is the Swiss Federal Institute of Technology in Lausanne. The past decade has seen EPFL ascend to the very top of European institutions of science and technology: it is ranked #1 in Europe in the field of engineering by the Times Higher Education (based on publications and citations), Leiden Rankings, and the Academic Ranking of World Universities.</p>
</p>
<p>EPFL's main campus brings together 12,600 students, faculty, researchers, and staff in a high-energy, dynamic learning and research environment. It directs the Human Brain Project, an undertaking to simulate the entire human brain using supercomputers, in order to gain new insights into how it operates and to better diagnose brain disorders. The university is building Solar Impulse, a long-range solar-powered plane that aims to be the first piloted fixed-wing aircraft to circumnavigate the Earth using only solar power. EPFL was part of the Alinghi project, developing advanced racing boats that won the America's Cup multiple times. The university operates, for education and research purposes, a Tokamak nuclear fusion reactor. EPFL also houses the Musée Bolo museum and hosts several music festivals, including Balelec, that draws over 15,000 guests every year.</p>
<p>EPFL is a major force in entrepreneurship, with 2012 bringing in $100M in funding for ten EPFL startups. Both young spin-offs (like Typesafe and Pix4D) and companies that have long grown past the startup stage (like Logitech) actively transfer the results of EPFL's scientific innovation to industry.</p>
</%block> </%block>
${parent.body()} ${parent.body()}
...@@ -2,7 +2,6 @@ from django.conf import settings ...@@ -2,7 +2,6 @@ from django.conf import settings
from django.conf.urls import patterns, include, url from django.conf.urls import patterns, include, url
from django.contrib import admin from django.contrib import admin
from django.conf.urls.static import static from django.conf.urls.static import static
from django.views.generic import RedirectView
from . import one_time_startup from . import one_time_startup
...@@ -10,10 +9,9 @@ import django.contrib.auth.views ...@@ -10,10 +9,9 @@ import django.contrib.auth.views
# Uncomment the next two lines to enable the admin: # Uncomment the next two lines to enable the admin:
if settings.DEBUG: if settings.DEBUG:
from django.contrib import admin
admin.autodiscover() admin.autodiscover()
urlpatterns = ('', urlpatterns = ('', # nopep8
# certificate view # certificate view
url(r'^update_certificate$', 'certificates.views.update_certificate'), url(r'^update_certificate$', 'certificates.views.update_certificate'),
...@@ -116,8 +114,9 @@ urlpatterns = ('', ...@@ -116,8 +114,9 @@ urlpatterns = ('',
# Favicon # Favicon
(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}), (r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}),
url(r'^submit_feedback$', 'util.views.submit_feedback_via_zendesk'),
# TODO: These urls no longer work. They need to be updated before they are re-enabled # TODO: These urls no longer work. They need to be updated before they are re-enabled
# url(r'^send_feedback$', 'util.views.send_feedback'),
# url(r'^reactivate/(?P<key>[^/]*)$', 'student.views.reactivation_email'), # url(r'^reactivate/(?P<key>[^/]*)$', 'student.views.reactivation_email'),
) )
...@@ -299,12 +298,12 @@ if settings.COURSEWARE_ENABLED: ...@@ -299,12 +298,12 @@ if settings.COURSEWARE_ENABLED:
'courseware.views.news', name="news"), 'courseware.views.news', name="news"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/discussion/', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/discussion/',
include('django_comment_client.urls')) include('django_comment_client.urls'))
) )
urlpatterns += ( urlpatterns += (
# This MUST be the last view in the courseware--it's a catch-all for custom tabs. # This MUST be the last view in the courseware--it's a catch-all for custom tabs.
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/(?P<tab_slug>[^/]+)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/(?P<tab_slug>[^/]+)/$',
'courseware.views.static_tab', name="static_tab"), 'courseware.views.static_tab', name="static_tab"),
) )
if settings.MITX_FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW'): if settings.MITX_FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW'):
urlpatterns += ( urlpatterns += (
...@@ -346,13 +345,13 @@ if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'): ...@@ -346,13 +345,13 @@ if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
url(r'^migrate/reload/(?P<reload_dir>[^/]+)/(?P<commit_id>[^/]+)$', 'lms_migration.migrate.manage_modulestores'), url(r'^migrate/reload/(?P<reload_dir>[^/]+)/(?P<commit_id>[^/]+)$', 'lms_migration.migrate.manage_modulestores'),
url(r'^gitreload$', 'lms_migration.migrate.gitreload'), url(r'^gitreload$', 'lms_migration.migrate.gitreload'),
url(r'^gitreload/(?P<reload_dir>[^/]+)$', 'lms_migration.migrate.gitreload'), url(r'^gitreload/(?P<reload_dir>[^/]+)$', 'lms_migration.migrate.gitreload'),
) )
if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'): if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
urlpatterns += ( urlpatterns += (
url(r'^event_logs$', 'track.views.view_tracking_log'), url(r'^event_logs$', 'track.views.view_tracking_log'),
url(r'^event_logs/(?P<args>.+)$', 'track.views.view_tracking_log'), url(r'^event_logs/(?P<args>.+)$', 'track.views.view_tracking_log'),
) )
# FoldIt views # FoldIt views
urlpatterns += ( urlpatterns += (
......
# We use `scipy` in our project, which relies on `numpy`. `pip` apparently
# installs packages in a two-step process, where it will first try to build
# all packages, and then try to install all packages. As a result, if we simply
# added these packages to the top of `requirements.txt`, `pip` would try to
# build `scipy` before `numpy` has been installed, and it would fail. By
# separating this out into a `pre-requirements.txt` file, we can make sure
# that `numpy` is built *and* installed before we try to build `scipy`.
numpy==1.6.2 numpy==1.6.2
distribute>=0.6.28 distribute>=0.6.28
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