Commit cd4bec87 by Arthur Barrett

merged master branch into feature/abarrett/lms-notes-app

parents ade6d408 29a5d3b4
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.
...@@ -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):
......
...@@ -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
......
# 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
...@@ -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)
...@@ -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=.
...@@ -300,6 +300,27 @@ def get_course_tabs(user, course, active_page): ...@@ -300,6 +300,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
...@@ -314,15 +335,9 @@ def get_default_tabs(user, course, active_page): ...@@ -314,15 +335,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',
} }
......
...@@ -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
...@@ -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")
...@@ -93,7 +93,10 @@ MITX_FEATURES = { ...@@ -93,7 +93,10 @@ MITX_FEATURES = {
'ENABLE_STUDENT_HISTORY_VIEW': True, 'ENABLE_STUDENT_HISTORY_VIEW': True,
# Enables the student notes API and UI. # Enables the student notes API and UI.
'ENABLE_STUDENT_NOTES': True 'ENABLE_STUDENT_NOTES': 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
...@@ -326,6 +329,14 @@ WIKI_LINK_DEFAULT_LEVEL = 2 ...@@ -326,6 +329,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,
...@@ -598,3 +609,4 @@ INSTALLED_APPS = ( ...@@ -598,3 +609,4 @@ INSTALLED_APPS = (
# Student notes # Student notes
'notes', 'notes',
) )
...@@ -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"/>
...@@ -114,8 +114,9 @@ urlpatterns = ('', # nopep8 ...@@ -114,8 +114,9 @@ urlpatterns = ('', # nopep8
# 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'),
) )
......
# 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