Commit ec4547b5 by JonahStanley

Merge branch 'master' into jonahstanley/add-courseteam-tests

parents 70d48e2e d06a9a20
...@@ -75,4 +75,6 @@ Frances Botsford <frances@edx.org> ...@@ -75,4 +75,6 @@ Frances Botsford <frances@edx.org>
Jonah Stanley <Jonah_Stanley@brown.edu> Jonah Stanley <Jonah_Stanley@brown.edu>
Slater Victoroff <slater.r.victoroff@gmail.com> Slater Victoroff <slater.r.victoroff@gmail.com>
Peter Fogg <peter.p.fogg@gmail.com> Peter Fogg <peter.p.fogg@gmail.com>
Bethany LaPenta <lapentab@mit.edu>
Renzo Lucioni <renzolucioni@gmail.com> Renzo Lucioni <renzolucioni@gmail.com>
Felix Sun <felixsun@mit.edu>
...@@ -5,6 +5,27 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,6 +5,27 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
Studio: Remove XML from the video component editor. All settings are
moved to be edited as metadata.
XModule: Only write out assets files if the contents have changed.
XModule: Don't delete generated xmodule asset files when compiling (for
instance, when XModule provides a coffeescript file, don't delete
the associated javascript)
Studio: For courses running on edx.org (marketing site), disable fields in
Course Settings that do not apply.
Common: Make asset watchers run as singletons (so they won't start if the
watcher is already running in another shell).
Common: Use coffee directly when watching for coffeescript file changes.
Common: Make rake provide better error messages if packages are missing.
Common: Repairs development documentation generation by sphinx.
LMS: Problem rescoring. Added options on the Grades tab of the LMS: Problem rescoring. Added options on the Grades tab of the
Instructor Dashboard to allow all students' submissions for a Instructor Dashboard to allow all students' submissions for a
particular problem to be rescored. Also supports resetting all particular problem to be rescored. Also supports resetting all
...@@ -12,6 +33,8 @@ students' number of attempts to zero. Provides a list of background ...@@ -12,6 +33,8 @@ students' number of attempts to zero. Provides a list of background
tasks that are currently running for the course, and an option to tasks that are currently running for the course, and an option to
see a history of background tasks for a given problem. see a history of background tasks for a given problem.
LMS: Fixed the preferences scope for storing data in xmodules.
LMS: Forums. Added handling for case where discussion module can get `None` as LMS: Forums. Added handling for case where discussion module can get `None` as
value of lms.start in `lms/djangoapps/django_comment_client/utils.py` value of lms.start in `lms/djangoapps/django_comment_client/utils.py`
......
...@@ -4,3 +4,4 @@ gem 'sass', '3.1.15' ...@@ -4,3 +4,4 @@ gem 'sass', '3.1.15'
gem 'bourbon', '~> 1.3.6' gem 'bourbon', '~> 1.3.6'
gem 'colorize', '~> 0.5.8' gem 'colorize', '~> 0.5.8'
gem 'launchy', '~> 2.1.2' gem 'launchy', '~> 2.1.2'
gem 'sys-proctable', '~> 0.9.3'
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
#pylint: disable=W0621 #pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from nose.tools import assert_false, assert_equal, assert_regexp_matches from nose.tools import assert_false, assert_equal, assert_regexp_matches, assert_true
from common import type_in_codemirror from common import type_in_codemirror
KEY_CSS = '.key input.policy-key' KEY_CSS = '.key input.policy-key'
...@@ -28,7 +28,15 @@ def i_am_on_advanced_course_settings(step): ...@@ -28,7 +28,15 @@ def i_am_on_advanced_course_settings(step):
@step(u'I press the "([^"]*)" notification button$') @step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name): def press_the_notification_button(step, name):
css = 'a.%s-button' % name.lower() css = 'a.%s-button' % name.lower()
world.css_click(css)
# Save was clicked if either the save notification bar is gone, or we have a error notification
# overlaying it (expected in the case of typing Object into display_name).
def save_clicked():
confirmation_dismissed = world.is_css_not_present('.is-shown.wrapper-notification-warning')
error_showing = world.is_css_present('.is-shown.wrapper-notification-error')
return confirmation_dismissed or error_showing
assert_true(world.css_click(css, success_condition=save_clicked), 'Save button not clicked after 5 attempts.')
@step(u'I edit the value of a policy key$') @step(u'I edit the value of a policy key$')
......
...@@ -174,6 +174,16 @@ def open_new_unit(step): ...@@ -174,6 +174,16 @@ def open_new_unit(step):
world.css_click('a.new-unit-item') world.css_click('a.new-unit-item')
@step('when I view the video it (.*) show the captions')
def shows_captions(step, show_captions):
# Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions')
if show_captions == 'does not':
assert world.css_find('.video')[0].has_class('closed')
else:
assert world.is_css_not_present('.video.closed')
def type_in_codemirror(index, text): def type_in_codemirror(index, text):
world.css_click(".CodeMirror", index=index) world.css_click(".CodeMirror", index=index)
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea") g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
......
Feature: Course Grading
As a course author, I want to be able to configure how my course is graded
Scenario: Users can add grading ranges
Given I have opened a new course in Studio
And I am viewing the grading settings
When I add "1" new grade
Then I see I now have "3" grades
Scenario: Users can only have up to 5 grading ranges
Given I have opened a new course in Studio
And I am viewing the grading settings
When I add "6" new grades
Then I see I now have "5" grades
#Cannot reliably make the delete button appear so using javascript instead
Scenario: Users can delete grading ranges
Given I have opened a new course in Studio
And I am viewing the grading settings
When I add "1" new grade
And I delete a grade
Then I see I now have "2" grades
Scenario: Users can move grading ranges
Given I have opened a new course in Studio
And I am viewing the grading settings
When I move a grading section
Then I see that the grade range has changed
Scenario: Users can modify Assignment types
Given I have opened a new course in Studio
And I have populated the course
And I am viewing the grading settings
When I change assignment type "Homework" to "New Type"
And I go back to the main course page
Then I do see the assignment name "New Type"
And I do not see the assignment name "Homework"
Scenario: Users can delete Assignment types
Given I have opened a new course in Studio
And I have populated the course
And I am viewing the grading settings
When I delete the assignment type "Homework"
And I go back to the main course page
Then I do not see the assignment name "Homework"
Scenario: Users can add Assignment types
Given I have opened a new course in Studio
And I have populated the course
And I am viewing the grading settings
When I add a new assignment type "New Type"
And I go back to the main course page
Then I do see the assignment name "New Type"
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from common import *
@step(u'I am viewing the grading settings')
def view_grading_settings(step):
world.click_course_settings()
link_css = 'li.nav-course-settings-grading a'
world.css_click(link_css)
@step(u'I add "([^"]*)" new grade')
def add_grade(step, many):
grade_css = '.new-grade-button'
for i in range(int(many)):
world.css_click(grade_css)
@step(u'I delete a grade')
def delete_grade(step):
#grade_css = 'li.grade-specific-bar > a.remove-button'
#range_css = '.grade-specific-bar'
#world.css_find(range_css)[1].mouseover()
#world.css_click(grade_css)
world.browser.execute_script('document.getElementsByClassName("remove-button")[0].click()')
@step(u'I see I now have "([^"]*)" grades$')
def view_grade_slider(step, how_many):
grade_slider_css = '.grade-specific-bar'
all_grades = world.css_find(grade_slider_css)
assert len(all_grades) == int(how_many)
@step(u'I move a grading section')
def move_grade_slider(step):
moveable_css = '.ui-resizable-e'
f = world.css_find(moveable_css).first
f.action_chains.drag_and_drop_by_offset(f._element, 100, 0).perform()
@step(u'I see that the grade range has changed')
def confirm_change(step):
range_css = '.range'
all_ranges = world.css_find(range_css)
for i in range(len(all_ranges)):
assert all_ranges[i].html != '0-50'
@step(u'I change assignment type "([^"]*)" to "([^"]*)"$')
def change_assignment_name(step, old_name, new_name):
name_id = '#course-grading-assignment-name'
index = get_type_index(old_name)
f = world.css_find(name_id)[index]
assert index != -1
for count in range(len(old_name)):
f._element.send_keys(Keys.END, Keys.BACK_SPACE)
f._element.send_keys(new_name)
@step(u'I go back to the main course page')
def main_course_page(step):
main_page_link_css = 'a[href="/MITx/999/course/Robot_Super_Course"]'
world.css_click(main_page_link_css)
@step(u'I do( not)? see the assignment name "([^"]*)"$')
def see_assignment_name(step, do_not, name):
assignment_menu_css = 'ul.menu > li > a'
assignment_menu = world.css_find(assignment_menu_css)
allnames = [item.html for item in assignment_menu]
if do_not:
assert not name in allnames
else:
assert name in allnames
@step(u'I delete the assignment type "([^"]*)"$')
def delete_assignment_type(step, to_delete):
delete_css = '.remove-grading-data'
world.css_click(delete_css, index=get_type_index(to_delete))
@step(u'I add a new assignment type "([^"]*)"$')
def add_assignment_type(step, new_name):
add_button_css = '.add-grading-data'
world.css_click(add_button_css)
name_id = '#course-grading-assignment-name'
f = world.css_find(name_id)[4]
f._element.send_keys(new_name)
@step(u'I have populated the course')
def populate_course(step):
step.given('I have added a new section')
step.given('I have added a new subsection')
def get_type_index(name):
name_id = '#course-grading-assignment-name'
f = world.css_find(name_id)
for i in range(len(f)):
if f[i].value == name:
return i
return -1
...@@ -4,10 +4,20 @@ Feature: Video Component Editor ...@@ -4,10 +4,20 @@ Feature: Video Component Editor
Scenario: User can view metadata Scenario: User can view metadata
Given I have created a Video component Given I have created a Video component
And I edit and select Settings And I edit and select Settings
Then I see only the Video display name setting Then I see the correct settings and default values
Scenario: User can modify display name Scenario: User can modify display name
Given I have created a Video component Given I have created a Video component
And I edit and select Settings And I edit and select Settings
Then I can modify the display name Then I can modify the display name
And my display name change is persisted on save And my display name change is persisted on save
Scenario: Captions are hidden when "show captions" is false
Given I have created a Video component
And I have set "show captions" to False
Then when I view the video it does not show the captions
Scenario: Captions are shown when "show captions" is true
Given I have created a Video component
And I have set "show captions" to True
Then when I view the video it does show the captions
...@@ -4,6 +4,20 @@ ...@@ -4,6 +4,20 @@
from lettuce import world, step from lettuce import world, step
@step('I see only the video display name setting$') @step('I see the correct settings and default values$')
def i_see_only_the_video_display_name(step): def i_see_the_correct_settings_and_values(step):
world.verify_all_setting_entries([['Display Name', "default", True]]) world.verify_all_setting_entries([['Default Speed', 'OEoXaMPEzfM', False],
['Display Name', 'default', True],
['Download Track', '', False],
['Download Video', '', False],
['Show Captions', 'True', False],
['Speed: .75x', '', False],
['Speed: 1.25x', '', False],
['Speed: 1.5x', '', False]])
@step('I have set "show captions" to (.*)')
def set_show_captions(step, setting):
world.css_click('a.edit-button')
world.browser.select('Show Captions', setting)
world.css_click('a.save-button')
...@@ -9,7 +9,16 @@ Feature: Video Component ...@@ -9,7 +9,16 @@ Feature: Video Component
Given I have clicked the new unit button Given I have clicked the new unit button
Then creating a video takes a single click Then creating a video takes a single click
Scenario: Captions are shown correctly Scenario: Captions are hidden correctly
Given I have created a Video component Given I have created a Video component
And I have hidden captions And I have hidden captions
Then when I view the video it does not show the captions Then when I view the video it does not show the captions
Scenario: Captions are shown correctly
Given I have created a Video component
Then when I view the video it does show the captions
Scenario: Captions are toggled correctly
Given I have created a Video component
And I have toggled captions
Then when I view the video it does show the captions
...@@ -18,11 +18,16 @@ def video_takes_a_single_click(_step): ...@@ -18,11 +18,16 @@ def video_takes_a_single_click(_step):
assert(world.is_css_present('.xmodule_VideoModule')) assert(world.is_css_present('.xmodule_VideoModule'))
@step('I have hidden captions') @step('I have (hidden|toggled) captions')
def set_show_captions_false(step): def hide_or_show_captions(step, shown):
world.css_click('a.hide-subtitles') button_css = 'a.hide-subtitles'
if shown == 'hidden':
world.css_click(button_css)
@step('when I view the video it does not show the captions') if shown == 'toggled':
def does_not_show_captions(step): world.css_click(button_css)
assert world.css_find('.video')[0].has_class('closed') # When we click the first time, a tooltip shows up. We want to
# click the button rather than the tooltip, so move the mouse
# away to make it disappear.
button = world.css_find(button_css)
button.mouse_out()
world.css_click(button_css)
...@@ -19,7 +19,6 @@ class ChecklistTestCase(CourseTestCase): ...@@ -19,7 +19,6 @@ class ChecklistTestCase(CourseTestCase):
modulestore = get_modulestore(self.course.location) modulestore = get_modulestore(self.course.location)
return modulestore.get_item(self.course.location).checklists return modulestore.get_item(self.course.location).checklists
def compare_checklists(self, persisted, request): def compare_checklists(self, persisted, request):
""" """
Handles url expansion as possible difference and descends into guts Handles url expansion as possible difference and descends into guts
...@@ -99,7 +98,6 @@ class ChecklistTestCase(CourseTestCase): ...@@ -99,7 +98,6 @@ class ChecklistTestCase(CourseTestCase):
'name': self.course.location.name, 'name': self.course.location.name,
'checklist_index': 2}) 'checklist_index': 2})
def get_first_item(checklist): def get_first_item(checklist):
return checklist['items'][0] return checklist['items'][0]
......
"""
Tests for Studio Course Settings.
"""
import datetime import datetime
import json import json
import copy import copy
import mock
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test.client import Client from django.test.client import Client
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.timezone import UTC from django.utils.timezone import UTC
from django.test.utils import override_settings
from xmodule.modulestore import Location from xmodule.modulestore import Location
from models.settings.course_details import (CourseDetails, CourseSettingsEncoder) from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
...@@ -21,6 +26,9 @@ from xmodule.fields import Date ...@@ -21,6 +26,9 @@ from xmodule.fields import Date
class CourseTestCase(ModuleStoreTestCase): class CourseTestCase(ModuleStoreTestCase):
"""
Base class for test classes below.
"""
def setUp(self): def setUp(self):
""" """
These tests need a user in the DB so that the django Test Client These tests need a user in the DB so that the django Test Client
...@@ -51,6 +59,9 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -51,6 +59,9 @@ class CourseTestCase(ModuleStoreTestCase):
class CourseDetailsTestCase(CourseTestCase): class CourseDetailsTestCase(CourseTestCase):
"""
Tests the first course settings page (course dates, overview, etc.).
"""
def test_virgin_fetch(self): def test_virgin_fetch(self):
details = CourseDetails.fetch(self.course_location) details = CourseDetails.fetch(self.course_location)
self.assertEqual(details.course_location, self.course_location, "Location not copied into") self.assertEqual(details.course_location, self.course_location, "Location not copied into")
...@@ -118,8 +129,60 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -118,8 +129,60 @@ class CourseDetailsTestCase(CourseTestCase):
jsondetails.effort, "After set effort" jsondetails.effort, "After set effort"
) )
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
def test_marketing_site_fetch(self):
settings_details_url = reverse(
'settings_details',
kwargs={
'org': self.course_location.org,
'name': self.course_location.name,
'course': self.course_location.course
}
)
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
response = self.client.get(settings_details_url)
self.assertContains(response, "Course Summary Page")
self.assertContains(response, "course summary page will not be viewable")
self.assertContains(response, "Course Start Date")
self.assertContains(response, "Course End Date")
self.assertNotContains(response, "Enrollment Start Date")
self.assertNotContains(response, "Enrollment End Date")
self.assertContains(response, "not the dates shown on your course summary page")
self.assertNotContains(response, "Introducing Your Course")
self.assertNotContains(response, "Requirements")
def test_regular_site_fetch(self):
settings_details_url = reverse(
'settings_details',
kwargs={
'org': self.course_location.org,
'name': self.course_location.name,
'course': self.course_location.course
}
)
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}):
response = self.client.get(settings_details_url)
self.assertContains(response, "Course Summary Page")
self.assertNotContains(response, "course summary page will not be viewable")
self.assertContains(response, "Course Start Date")
self.assertContains(response, "Course End Date")
self.assertContains(response, "Enrollment Start Date")
self.assertContains(response, "Enrollment End Date")
self.assertNotContains(response, "not the dates shown on your course summary page")
self.assertContains(response, "Introducing Your Course")
self.assertContains(response, "Requirements")
class CourseDetailsViewTest(CourseTestCase): class CourseDetailsViewTest(CourseTestCase):
"""
Tests for modifying content on the first course settings page (course dates, overview, etc.).
"""
def alter_field(self, url, details, field, val): def alter_field(self, url, details, field, val):
setattr(details, field, val) setattr(details, field, val)
# Need to partially serialize payload b/c the mock doesn't handle it correctly # Need to partially serialize payload b/c the mock doesn't handle it correctly
...@@ -181,6 +244,9 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -181,6 +244,9 @@ class CourseDetailsViewTest(CourseTestCase):
class CourseGradingTest(CourseTestCase): class CourseGradingTest(CourseTestCase):
"""
Tests for the course settings grading page.
"""
def test_initial_grader(self): def test_initial_grader(self):
descriptor = get_modulestore(self.course_location).get_item(self.course_location) descriptor = get_modulestore(self.course_location).get_item(self.course_location)
test_grader = CourseGradingModel(descriptor) test_grader = CourseGradingModel(descriptor)
...@@ -256,6 +322,9 @@ class CourseGradingTest(CourseTestCase): ...@@ -256,6 +322,9 @@ class CourseGradingTest(CourseTestCase):
class CourseMetadataEditingTest(CourseTestCase): class CourseMetadataEditingTest(CourseTestCase):
"""
Tests for CourseMetadata.
"""
def setUp(self): def setUp(self):
CourseTestCase.setUp(self) CourseTestCase.setUp(self)
# add in the full class too # add in the full class too
......
...@@ -227,7 +227,8 @@ def get_course_settings(request, org, course, name): ...@@ -227,7 +227,8 @@ def get_course_settings(request, org, course, name):
kwargs={"org": org, kwargs={"org": org,
"course": course, "course": course,
"name": name, "name": name,
"section": "details"}) "section": "details"}),
'about_page_editable': not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False)
}) })
......
...@@ -9,7 +9,7 @@ function removeAsset(e){ ...@@ -9,7 +9,7 @@ function removeAsset(e){
e.preventDefault(); e.preventDefault();
var that = this; var that = this;
var msg = new CMS.Models.ConfirmAssetDeleteMessage({ var msg = new CMS.Views.Prompt.Confirmation({
title: gettext("Delete File Confirmation"), title: gettext("Delete File Confirmation"),
message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"), message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"),
actions: { actions: {
...@@ -17,15 +17,17 @@ function removeAsset(e){ ...@@ -17,15 +17,17 @@ function removeAsset(e){
text: gettext("OK"), text: gettext("OK"),
click: function(view) { click: function(view) {
// call the back-end to actually remove the asset // call the back-end to actually remove the asset
$.post(view.model.get('remove_asset_url'), var url = $('.asset-library').data('remove-asset-callback-url');
{ 'location': view.model.get('asset_location') }, var row = $(that).closest('tr');
$.post(url,
{ 'location': row.data('id') },
function() { function() {
// show the post-commit confirmation // show the post-commit confirmation
$(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false'); $(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false');
view.model.get('row_to_remove').remove(); row.remove();
analytics.track('Deleted Asset', { analytics.track('Deleted Asset', {
'course': course_location_analytics, 'course': course_location_analytics,
'id': view.model.get('asset_location') 'id': row.data('id')
}); });
} }
); );
...@@ -38,24 +40,9 @@ function removeAsset(e){ ...@@ -38,24 +40,9 @@ function removeAsset(e){
view.hide(); view.hide();
} }
}] }]
},
remove_asset_url: $('.asset-library').data('remove-asset-callback-url'),
asset_location: $(this).closest('tr').data('id'),
row_to_remove: $(this).closest('tr')
});
// workaround for now. We can't spawn multiple instances of the Prompt View
// so for now, a bit of hackery to just make sure we have a single instance
// note: confirm_delete_prompt is in asset_index.html
if (confirm_delete_prompt === null)
confirm_delete_prompt = new CMS.Views.Prompt({model: msg});
else
{
confirm_delete_prompt.model = msg;
confirm_delete_prompt.show();
} }
});
return; return msg.show();
} }
function showUploadModal(e) { function showUploadModal(e) {
......
...@@ -90,6 +90,7 @@ CMS.Views.SystemFeedback = Backbone.View.extend({ ...@@ -90,6 +90,7 @@ CMS.Views.SystemFeedback = Backbone.View.extend({
var parent = CMS.Views[_.str.capitalize(this.options.type)]; var parent = CMS.Views[_.str.capitalize(this.options.type)];
if(parent && parent.active && parent.active !== this) { if(parent && parent.active && parent.active !== this) {
parent.active.stopListening(); parent.active.stopListening();
parent.active.undelegateEvents();
} }
this.$el.html(this.template(this.options)); this.$el.html(this.template(this.options));
parent.active = this; parent.active = this;
......
// studio - elements - system help // studio - elements - system help
// ==================== // ====================
// notices - in-context: to be used as notices to users within the context of a form/action
.notice-incontext {
@extend .ui-well;
@include border-radius(($baseline/10));
.title {
@extend .t-title7;
margin-bottom: ($baseline/4);
font-weight: 600;
}
.copy {
@extend .t-copy-sub1;
@include transition(opacity 0.25s ease-in-out 0);
opacity: 0.75;
}
strong {
font-weight: 600;
}
&:hover {
.copy {
opacity: 1.0;
}
}
}
// particular warnings around a workflow for something
.notice-workflow {
background: $yellow-l5;
.copy {
color: $gray-d1;
}
}
...@@ -52,6 +52,12 @@ body.course.settings { ...@@ -52,6 +52,12 @@ body.course.settings {
} }
} }
// notices - used currently for edx mktg
.notice-workflow {
margin-top: ($baseline);
}
// in form - elements // in form - elements
.group-settings { .group-settings {
margin: 0 0 ($baseline*2) 0; margin: 0 0 ($baseline*2) 0;
......
...@@ -8,11 +8,6 @@ ...@@ -8,11 +8,6 @@
<%block name="jsextra"> <%block name="jsextra">
<script src="${static.url('js/vendor/mustache.js')}"></script> <script src="${static.url('js/vendor/mustache.js')}"></script>
<script type='text/javascript'>
// we just want a singleton
confirm_delete_prompt = null;
</script>
</%block> </%block>
<%block name="content"> <%block name="content">
......
Thank you for signing up for edX edge! To activate your account, Thank you for signing up for edX Studio! To activate your account,
please copy and paste this address into your web browser's please copy and paste this address into your web browser's
address bar: address bar:
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="title">Schedule &amp; Details Settings</%block> <%block name="title">Schedule &amp; Details Settings</%block>
<%block name="bodyclass">is-signedin course schedule settings</%block> <%block name="bodyclass">is-signedin course schedule settings</%block>
...@@ -50,8 +52,8 @@ from contentstore import utils ...@@ -50,8 +52,8 @@ from contentstore import utils
<div class="wrapper-mast wrapper"> <div class="wrapper-mast wrapper">
<header class="mast has-subtitle"> <header class="mast has-subtitle">
<h1 class="page-header"> <h1 class="page-header">
<small class="subtitle">Settings</small> <small class="subtitle">${_("Settings")}</small>
<span class="sr">&gt; </span>Schedule &amp; Details <span class="sr">&gt; </span>${_("Schedule & Details")}
</h1> </h1>
</header> </header>
</div> </div>
...@@ -62,59 +64,68 @@ from contentstore import utils ...@@ -62,59 +64,68 @@ from contentstore import utils
<form id="settings_details" class="settings-details" method="post" action=""> <form id="settings_details" class="settings-details" method="post" action="">
<section class="group-settings basic"> <section class="group-settings basic">
<header> <header>
<h2 class="title-2">Basic Information</h2> <h2 class="title-2">${_("Basic Information")}</h2>
<span class="tip">The nuts and bolts of your course</span> <span class="tip">${_("The nuts and bolts of your course")}</span>
</header> </header>
<ol class="list-input"> <ol class="list-input">
<li class="field text is-not-editable" id="field-course-organization"> <li class="field text is-not-editable" id="field-course-organization">
<label for="course-organization">Organization</label> <label for="course-organization">${_("Organization")}</label>
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-organization" value="[Course Organization]" readonly /> <input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="long" id="course-organization" value="[Course Organization]" readonly />
</li> </li>
<li class="field text is-not-editable" id="field-course-number"> <li class="field text is-not-editable" id="field-course-number">
<label for="course-number">Course Number</label> <label for="course-number">${_("Course Number")}</label>
<input title="This field is disabled: this information cannot be changed." type="text" class="short" id="course-number" value="[Course No.]" readonly> <input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="short" id="course-number" value="[Course No.]" readonly>
</li> </li>
<li class="field text is-not-editable" id="field-course-name"> <li class="field text is-not-editable" id="field-course-name">
<label for="course-name">Course Name</label> <label for="course-name">${_("Course Name")}</label>
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-name" value="[Course Name]" readonly /> <input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="long" id="course-name" value="[Course Name]" readonly />
</li> </li>
</ol> </ol>
<div class="note note-promotion note-promotion-courseURL has-actions"> <div class="note note-promotion note-promotion-courseURL has-actions">
<h3 class="title">Course Summary Page <span class="tip">(for student enrollment and access)</span></h3> <h3 class="title">${_("Course Summary Page")} <span class="tip">${_("(for student enrollment and access)")}</span></h3>
<div class="copy"> <div class="copy">
<p><a class="link-courseURL" rel="external" href="https:${utils.get_lms_link_for_about_page(course_location)}" />https:${utils.get_lms_link_for_about_page(course_location)}</a></p> <p><a class="link-courseURL" rel="external" href="https:${utils.get_lms_link_for_about_page(course_location)}" />https:${utils.get_lms_link_for_about_page(course_location)}</a></p>
</div> </div>
<ul class="list-actions"> <ul class="list-actions">
<li class="action-item"> <li class="action-item">
<a title="Send a note to students via email" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20&quot;${context_course.display_name_with_default}&quot;,%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="icon-envelope-alt icon-inline"></i> Invite your students</a> <a title="${_('Send a note to students via email')}" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20&quot;${context_course.display_name_with_default}&quot;,%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="icon-envelope-alt icon-inline"></i>${_("Invite your students")}</a>
</li> </li>
</ul> </ul>
</div> </div>
% if not about_page_editable:
<div class="notice notice-incontext notice-workflow">
<h3 class="title">${_("Promoting Your Course with edX")}</h3>
<div class="copy">
<p>${_('Your course summary page will not be viewable until your course has been announced. To provide content for the page and preview it, follow the instructions provided by your <abbr title="Program Manager">PM</abbr> or Conrad Warre <a rel="email" class="action action-email" href="mailto:conrad@edx.org">(conrad@edx.org)</a>.')}</p>
</div>
</div>
% endif
</section> </section>
<hr class="divide" /> <hr class="divide" />
<section class="group-settings schedule"> <section class="group-settings schedule">
<header> <header>
<h2 class="title-2">Course Schedule</h2> <h2 class="title-2">${_('Course Schedule')}</h2>
<span class="tip">Important steps and segments of your course</span> <span class="tip">${_('Dates that control when your course can be viewed.')}</span>
</header> </header>
<ol class="list-input"> <ol class="list-input">
<li class="field-group field-group-course-start" id="course-start"> <li class="field-group field-group-course-start" id="course-start">
<div class="field date" id="field-course-start-date"> <div class="field date" id="field-course-start-date">
<label for="course-start-date">Course Start Date</label> <label for="course-start-date">${_("Course Start Date")}</label>
<input type="text" class="start-date date start datepicker" id="course-start-date" placeholder="MM/DD/YYYY" autocomplete="off" /> <input type="text" class="start-date date start datepicker" id="course-start-date" placeholder="MM/DD/YYYY" autocomplete="off" />
<span class="tip tip-stacked">First day the course begins</span> <span class="tip tip-stacked">${_("First day the course begins")}</span>
</div> </div>
<div class="field time" id="field-course-start-time"> <div class="field time" id="field-course-start-time">
<label for="course-start-time">Course Start Time</label> <label for="course-start-time">${_("Course Start Time")}</label>
<input type="text" class="time start timepicker" id="course-start-time" value="" placeholder="HH:MM" autocomplete="off" /> <input type="text" class="time start timepicker" id="course-start-time" value="" placeholder="HH:MM" autocomplete="off" />
<span class="tip tip-stacked" id="timezone"></span> <span class="tip tip-stacked" id="timezone"></span>
</div> </div>
...@@ -122,29 +133,30 @@ from contentstore import utils ...@@ -122,29 +133,30 @@ from contentstore import utils
<li class="field-group field-group-course-end" id="course-end"> <li class="field-group field-group-course-end" id="course-end">
<div class="field date" id="field-course-end-date"> <div class="field date" id="field-course-end-date">
<label for="course-end-date">Course End Date</label> <label for="course-end-date">${_("Course End Date")}</label>
<input type="text" class="end-date date end" id="course-end-date" placeholder="MM/DD/YYYY" autocomplete="off" /> <input type="text" class="end-date date end" id="course-end-date" placeholder="MM/DD/YYYY" autocomplete="off" />
<span class="tip tip-stacked">Last day your course is active</span> <span class="tip tip-stacked">${_("Last day your course is active")}</span>
</div> </div>
<div class="field time" id="field-course-end-time"> <div class="field time" id="field-course-end-time">
<label for="course-end-time">Course End Time</label> <label for="course-end-time">${_("Course End Time")}</label>
<input type="text" class="time end" id="course-end-time" value="" placeholder="HH:MM" autocomplete="off" /> <input type="text" class="time end" id="course-end-time" value="" placeholder="HH:MM" autocomplete="off" />
<span class="tip tip-stacked" id="timezone"></span> <span class="tip tip-stacked" id="timezone"></span>
</div> </div>
</li> </li>
</ol> </ol>
% if about_page_editable:
<ol class="list-input"> <ol class="list-input">
<li class="field-group field-group-enrollment-start" id="enrollment-start"> <li class="field-group field-group-enrollment-start" id="enrollment-start">
<div class="field date" id="field-enrollment-start-date"> <div class="field date" id="field-enrollment-start-date">
<label for="course-enrollment-start-date">Enrollment Start Date</label> <label for="course-enrollment-start-date">${_("Enrollment Start Date")}</label>
<input type="text" class="start-date date start" id="course-enrollment-start-date" placeholder="MM/DD/YYYY" autocomplete="off" /> <input type="text" class="start-date date start" id="course-enrollment-start-date" placeholder="MM/DD/YYYY" autocomplete="off" />
<span class="tip tip-stacked">First day students can enroll</span> <span class="tip tip-stacked">${_("First day students can enroll")}</span>
</div> </div>
<div class="field time" id="field-enrollment-start-time"> <div class="field time" id="field-enrollment-start-time">
<label for="course-enrollment-start-time">Enrollment Start Time</label> <label for="course-enrollment-start-time">${_("Enrollment Start Time")}</label>
<input type="text" class="time start" id="course-enrollment-start-time" value="" placeholder="HH:MM" autocomplete="off" /> <input type="text" class="time start" id="course-enrollment-start-time" value="" placeholder="HH:MM" autocomplete="off" />
<span class="tip tip-stacked" id="timezone"></span> <span class="tip tip-stacked" id="timezone"></span>
</div> </div>
...@@ -152,49 +164,64 @@ from contentstore import utils ...@@ -152,49 +164,64 @@ from contentstore import utils
<li class="field-group field-group-enrollment-end" id="enrollment-end"> <li class="field-group field-group-enrollment-end" id="enrollment-end">
<div class="field date" id="field-enrollment-end-date"> <div class="field date" id="field-enrollment-end-date">
<label for="course-enrollment-end-date">Enrollment End Date</label> <label for="course-enrollment-end-date">${_("Enrollment End Date")}</label>
<input type="text" class="end-date date end" id="course-enrollment-end-date" placeholder="MM/DD/YYYY" autocomplete="off" /> <input type="text" class="end-date date end" id="course-enrollment-end-date" placeholder="MM/DD/YYYY" autocomplete="off" />
<span class="tip tip-stacked">Last day students can enroll</span> <span class="tip tip-stacked">${_("Last day students can enroll")}</span>
</div> </div>
<div class="field time" id="field-enrollment-end-time"> <div class="field time" id="field-enrollment-end-time">
<label for="course-enrollment-end-time">Enrollment End Time</label> <label for="course-enrollment-end-time">${_("Enrollment End Time")}</label>
<input type="text" class="time end" id="course-enrollment-end-time" value="" placeholder="HH:MM" autocomplete="off" /> <input type="text" class="time end" id="course-enrollment-end-time" value="" placeholder="HH:MM" autocomplete="off" />
<span class="tip tip-stacked" id="timezone"></span> <span class="tip tip-stacked" id="timezone"></span>
</div> </div>
</li> </li>
</ol> </ol>
</section> % endif
% if not about_page_editable:
<div class="notice notice-incontext notice-workflow">
<h3 class="title">${_("These Dates Are Not Used When Promoting Your Course")}</h3>
<div class="copy">
<p>${_('These dates impact <strong>when your courseware can be viewed</strong>, but they are <strong>not the dates shown on your course summary page</strong>. To provide the course start and registration dates as shown on your course summary page, follow the instructions provided by your <abbr title="Program Manager">PM</abbr> or Conrad Warre <a rel="email" class="action action-email" href="mailto:conrad@edx.org">(conrad@edx.org)</a>.')}</p>
</div>
</div>
% endif
</section>
<hr class="divide" /> <hr class="divide" />
% if about_page_editable:
<section class="group-settings marketing"> <section class="group-settings marketing">
<header> <header>
<h2 class="title-2">Introducing Your Course</h2> <h2 class="title-2">${_("Introducing Your Course")}</h2>
<span class="tip">Information for prospective students</span> <span class="tip">${_("Information for prospective students")}</span>
</header> </header>
<ol class="list-input"> <ol class="list-input">
<li class="field text" id="field-course-overview"> <li class="field text" id="field-course-overview">
<label for="course-overview">Course Overview</label> <label for="course-overview">${_("Course Overview")}</label>
<textarea class="tinymce text-editor" id="course-overview"></textarea> <textarea class="tinymce text-editor" id="course-overview"></textarea>
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a class="link-courseURL" rel="external" href="${utils.get_lms_link_for_about_page(course_location)}">your course summary page</a> (formatted in HTML)</span> <%def name='overview_text()'><%
a_link_start = '<a class="link-courseURL" rel="external" href="'
a_link_end = '">' + _("your course summary page") + '</a>'
a_link = a_link_start + utils.get_lms_link_for_about_page(course_location) + a_link_end
text = _("Introductions, prerequisites, FAQs that are used on %s (formatted in HTML)") % a_link
%>${text}</%def>
<span class="tip tip-stacked">${overview_text()}</span>
</li> </li>
<li class="field video" id="field-course-introduction-video"> <li class="field video" id="field-course-introduction-video">
<label for="course-overview">Course Introduction Video</label> <label for="course-overview">${_("Course Introduction Video")}</label>
<div class="input input-existing"> <div class="input input-existing">
<div class="current current-course-introduction-video"> <div class="current current-course-introduction-video">
<iframe width="618" height="350" src="" frameborder="0" allowfullscreen></iframe> <iframe width="618" height="350" src="" frameborder="0" allowfullscreen></iframe>
</div> </div>
<div class="actions"> <div class="actions">
<a href="#" class="remove-item remove-course-introduction-video remove-video-data"><span class="delete-icon"></span> Delete Current Video</a> <a href="#" class="remove-item remove-course-introduction-video remove-video-data"><span class="delete-icon"></span>${_("Delete Current Video")}</a>
</div> </div>
</div> </div>
<div class="input"> <div class="input">
<input type="text" class="long new-course-introduction-video add-video-data" id="course-introduction-video" value="" placeholder="your YouTube video's ID" autocomplete="off" /> <input type="text" class="long new-course-introduction-video add-video-data" id="course-introduction-video" value="" placeholder="your YouTube video's ID" autocomplete="off" />
<span class="tip tip-stacked">Enter your YouTube video's ID (along with any restriction parameters)</span> <span class="tip tip-stacked">${_("Enter your YouTube video's ID (along with any restriction parameters)")}</span>
</div> </div>
</li> </li>
</ol> </ol>
...@@ -204,39 +231,39 @@ from contentstore import utils ...@@ -204,39 +231,39 @@ from contentstore import utils
<section class="group-settings requirements"> <section class="group-settings requirements">
<header> <header>
<h2 class="title-2">Requirements</h2> <h2 class="title-2">${_("Requirements")}</h2>
<span class="tip">Expectations of the students taking this course</span> <span class="tip">${_("Expectations of the students taking this course")}</span>
</header> </header>
<ol class="list-input"> <ol class="list-input">
<li class="field text" id="field-course-effort"> <li class="field text" id="field-course-effort">
<label for="course-effort">Hours of Effort per Week</label> <label for="course-effort">${_("Hours of Effort per Week")}</label>
<input type="text" class="short time" id="course-effort" placeholder="HH:MM" /> <input type="text" class="short time" id="course-effort" placeholder="HH:MM" />
<span class="tip tip-inline">Time spent on all course work</span> <span class="tip tip-inline">${_("Time spent on all course work")}</span>
</li> </li>
</ol> </ol>
</section> </section>
% endif
</form> </form>
</article> </article>
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
<div class="bit"> <div class="bit">
<h3 class="title-3">How will these settings be used?</h3> <h3 class="title-3">${_("How will these settings be used?")}</h3>
<p>Your course's schedule settings determine when students can enroll in and begin a course as well as when the course.</p> <p>${_("Your course's schedule settings determine when students can enroll in and begin a course.")}</p>
<p>Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.</p> <p>${_("Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.")}</p>
</div> </div>
<div class="bit"> <div class="bit">
% if context_course: % if context_course:
<% ctx_loc = context_course.location %> <% ctx_loc = context_course.location %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<h3 class="title-3">Other Course Settings</h3> <h3 class="title-3">${_("Other Course Settings")}</h3>
<nav class="nav-related"> <nav class="nav-related">
<ul> <ul>
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Grading</a></li> <li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a></li> <li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">${_("Course Team")}</a></li>
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Advanced Settings</a></li> <li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li>
</ul> </ul>
</nav> </nav>
% endif % endif
......
...@@ -49,7 +49,7 @@ def css_has_text(css_selector, text): ...@@ -49,7 +49,7 @@ def css_has_text(css_selector, text):
@world.absorb @world.absorb
def css_find(css, wait_time=5): def css_find(css, wait_time=5):
def is_visible(driver): def is_visible(_driver):
return EC.visibility_of_element_located((By.CSS_SELECTOR, css,)) return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
world.browser.is_element_present_by_css(css, wait_time=wait_time) world.browser.is_element_present_by_css(css, wait_time=wait_time)
...@@ -58,17 +58,56 @@ def css_find(css, wait_time=5): ...@@ -58,17 +58,56 @@ def css_find(css, wait_time=5):
@world.absorb @world.absorb
def css_click(css_selector, index=0, attempts=5): def css_click(css_selector, index=0, max_attempts=5, success_condition=lambda: True):
""" """
Perform a click on a CSS selector, retrying if it initially fails Perform a click on a CSS selector, retrying if it initially fails.
This function will return if the click worked (since it is try/excepting all errors)
This function handles errors that may be thrown if the component cannot be clicked on.
However, there are cases where an error may not be thrown, and yet the operation did not
actually succeed. For those cases, a success_condition lambda can be supplied to verify that the click worked.
This function will return True if the click worked (taking into account both errors and the optional
success_condition).
""" """
assert is_css_present(css_selector) assert is_css_present(css_selector)
attempt = 0 attempt = 0
result = False result = False
while attempt < attempts: while attempt < max_attempts:
try: try:
world.css_find(css_selector)[index].click() world.css_find(css_selector)[index].click()
if success_condition():
result = True
break
except WebDriverException:
# Occasionally, MathJax or other JavaScript can cover up
# an element temporarily.
# If this happens, wait a second, then try again
world.wait(1)
attempt += 1
except:
attempt += 1
return result
@world.absorb
def css_check(css_selector, index=0, max_attempts=5, success_condition=lambda: True):
"""
Checks a check box based on a CSS selector, retrying if it initially fails.
This function handles errors that may be thrown if the component cannot be clicked on.
However, there are cases where an error may not be thrown, and yet the operation did not
actually succeed. For those cases, a success_condition lambda can be supplied to verify that the check worked.
This function will return True if the check worked (taking into account both errors and the optional
success_condition).
"""
assert is_css_present(css_selector)
attempt = 0
result = False
while attempt < max_attempts:
try:
world.css_find(css_selector)[index].check()
if success_condition():
result = True result = True
break break
except WebDriverException: except WebDriverException:
...@@ -83,15 +122,15 @@ def css_click(css_selector, index=0, attempts=5): ...@@ -83,15 +122,15 @@ def css_click(css_selector, index=0, attempts=5):
@world.absorb @world.absorb
def css_click_at(css, x=10, y=10): def css_click_at(css, x_cord=10, y_cord=10):
''' '''
A method to click at x,y coordinates of the element A method to click at x,y coordinates of the element
rather than in the center of the element rather than in the center of the element
''' '''
e = css_find(css).first element = css_find(css).first
e.action_chains.move_to_element_with_offset(e._element, x, y) element.action_chains.move_to_element_with_offset(element._element, x_cord, y_cord)
e.action_chains.click() element.action_chains.click()
e.action_chains.perform() element.action_chains.perform()
@world.absorb @world.absorb
...@@ -136,7 +175,7 @@ def css_visible(css_selector): ...@@ -136,7 +175,7 @@ def css_visible(css_selector):
@world.absorb @world.absorb
def dialogs_closed(): def dialogs_closed():
def are_dialogs_closed(driver): def are_dialogs_closed(_driver):
''' '''
Return True when no modal dialogs are visible Return True when no modal dialogs are visible
''' '''
...@@ -147,12 +186,12 @@ def dialogs_closed(): ...@@ -147,12 +186,12 @@ def dialogs_closed():
@world.absorb @world.absorb
def save_the_html(path='/tmp'): def save_the_html(path='/tmp'):
u = world.browser.url url = world.browser.url
html = world.browser.html.encode('ascii', 'ignore') html = world.browser.html.encode('ascii', 'ignore')
filename = '%s.html' % quote_plus(u) filename = '%s.html' % quote_plus(url)
f = open('%s/%s' % (path, filename), 'w') file = open('%s/%s' % (path, filename), 'w')
f.write(html) file.write(html)
f.close() file.close()
@world.absorb @world.absorb
......
...@@ -12,8 +12,8 @@ from path import path ...@@ -12,8 +12,8 @@ from path import path
from cStringIO import StringIO from cStringIO import StringIO
from collections import defaultdict from collections import defaultdict
from .calc import UndefinedVariable from calc import UndefinedVariable
from .capa_problem import LoncapaProblem from capa.capa_problem import LoncapaProblem
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
logging.basicConfig(format="%(levelname)s %(message)s") logging.basicConfig(format="%(levelname)s %(message)s")
......
"""
Tests to verify that CorrectMap behaves correctly
"""
import unittest import unittest
from capa.correctmap import CorrectMap from capa.correctmap import CorrectMap
import datetime import datetime
class CorrectMapTest(unittest.TestCase): class CorrectMapTest(unittest.TestCase):
"""
Tests to verify that CorrectMap behaves correctly
"""
def setUp(self): def setUp(self):
self.cmap = CorrectMap() self.cmap = CorrectMap()
def test_set_input_properties(self): def test_set_input_properties(self):
# Set the correctmap properties for two inputs # Set the correctmap properties for two inputs
self.cmap.set(answer_id='1_2_1', self.cmap.set(
answer_id='1_2_1',
correctness='correct', correctness='correct',
npoints=5, npoints=5,
msg='Test message', msg='Test message',
hint='Test hint', hint='Test hint',
hintmode='always', hintmode='always',
queuestate={'key':'secretstring', queuestate={
'time':'20130228100026'}) 'key': 'secretstring',
'time': '20130228100026'
self.cmap.set(answer_id='2_2_1', }
)
self.cmap.set(
answer_id='2_2_1',
correctness='incorrect', correctness='incorrect',
npoints=None, npoints=None,
msg=None, msg=None,
hint=None, hint=None,
hintmode=None, hintmode=None,
queuestate=None) queuestate=None
)
# Assert that each input has the expected properties # Assert that each input has the expected properties
self.assertTrue(self.cmap.is_correct('1_2_1')) self.assertTrue(self.cmap.is_correct('1_2_1'))
...@@ -62,7 +75,6 @@ class CorrectMapTest(unittest.TestCase): ...@@ -62,7 +75,6 @@ class CorrectMapTest(unittest.TestCase):
self.assertFalse(self.cmap.is_right_queuekey('2_2_1', '')) self.assertFalse(self.cmap.is_right_queuekey('2_2_1', ''))
self.assertFalse(self.cmap.is_right_queuekey('2_2_1', None)) self.assertFalse(self.cmap.is_right_queuekey('2_2_1', None))
def test_get_npoints(self): def test_get_npoints(self):
# Set the correctmap properties for 4 inputs # Set the correctmap properties for 4 inputs
# 1) correct, 5 points # 1) correct, 5 points
...@@ -70,25 +82,35 @@ class CorrectMapTest(unittest.TestCase): ...@@ -70,25 +82,35 @@ class CorrectMapTest(unittest.TestCase):
# 3) incorrect, 5 points # 3) incorrect, 5 points
# 4) incorrect, None points # 4) incorrect, None points
# 5) correct, 0 points # 5) correct, 0 points
self.cmap.set(answer_id='1_2_1', self.cmap.set(
answer_id='1_2_1',
correctness='correct', correctness='correct',
npoints=5) npoints=5
)
self.cmap.set(answer_id='2_2_1', self.cmap.set(
answer_id='2_2_1',
correctness='correct', correctness='correct',
npoints=None) npoints=None
)
self.cmap.set(answer_id='3_2_1', self.cmap.set(
answer_id='3_2_1',
correctness='incorrect', correctness='incorrect',
npoints=5) npoints=5
)
self.cmap.set(answer_id='4_2_1', self.cmap.set(
answer_id='4_2_1',
correctness='incorrect', correctness='incorrect',
npoints=None) npoints=None
)
self.cmap.set(answer_id='5_2_1', self.cmap.set(
answer_id='5_2_1',
correctness='correct', correctness='correct',
npoints=0) npoints=0
)
# Assert that we get the expected points # Assert that we get the expected points
# If points assigned --> npoints # If points assigned --> npoints
...@@ -100,7 +122,6 @@ class CorrectMapTest(unittest.TestCase): ...@@ -100,7 +122,6 @@ class CorrectMapTest(unittest.TestCase):
self.assertEqual(self.cmap.get_npoints('4_2_1'), 0) self.assertEqual(self.cmap.get_npoints('4_2_1'), 0)
self.assertEqual(self.cmap.get_npoints('5_2_1'), 0) self.assertEqual(self.cmap.get_npoints('5_2_1'), 0)
def test_set_overall_message(self): def test_set_overall_message(self):
# Default is an empty string string # Default is an empty string string
...@@ -118,14 +139,18 @@ class CorrectMapTest(unittest.TestCase): ...@@ -118,14 +139,18 @@ class CorrectMapTest(unittest.TestCase):
def test_update_from_correctmap(self): def test_update_from_correctmap(self):
# Initialize a CorrectMap with some properties # Initialize a CorrectMap with some properties
self.cmap.set(answer_id='1_2_1', self.cmap.set(
answer_id='1_2_1',
correctness='correct', correctness='correct',
npoints=5, npoints=5,
msg='Test message', msg='Test message',
hint='Test hint', hint='Test hint',
hintmode='always', hintmode='always',
queuestate={'key':'secretstring', queuestate={
'time':'20130228100026'}) 'key': 'secretstring',
'time': '20130228100026'
}
)
self.cmap.set_overall_message("Test message") self.cmap.set_overall_message("Test message")
...@@ -135,12 +160,15 @@ class CorrectMapTest(unittest.TestCase): ...@@ -135,12 +160,15 @@ class CorrectMapTest(unittest.TestCase):
other_cmap.update(self.cmap) other_cmap.update(self.cmap)
# Assert that it has all the same properties # Assert that it has all the same properties
self.assertEqual(other_cmap.get_overall_message(), self.assertEqual(
self.cmap.get_overall_message()) other_cmap.get_overall_message(),
self.cmap.get_overall_message()
self.assertEqual(other_cmap.get_dict(), )
self.cmap.get_dict())
self.assertEqual(
other_cmap.get_dict(),
self.cmap.get_dict()
)
def test_update_from_invalid(self): def test_update_from_invalid(self):
# Should get an exception if we try to update() a CorrectMap # Should get an exception if we try to update() a CorrectMap
......
...@@ -279,7 +279,7 @@ class CapaModule(CapaFields, XModule): ...@@ -279,7 +279,7 @@ class CapaModule(CapaFields, XModule):
""" """
Return True/False to indicate whether to show the "Check" button. Return True/False to indicate whether to show the "Check" button.
""" """
submitted_without_reset = (self.is_completed() and self.rerandomize == "always") submitted_without_reset = (self.is_submitted() and self.rerandomize == "always")
# If the problem is closed (past due / too many attempts) # If the problem is closed (past due / too many attempts)
# then we do NOT show the "check" button # then we do NOT show the "check" button
...@@ -302,7 +302,7 @@ class CapaModule(CapaFields, XModule): ...@@ -302,7 +302,7 @@ class CapaModule(CapaFields, XModule):
# then do NOT show the reset button. # then do NOT show the reset button.
# If the problem hasn't been submitted yet, then do NOT show # If the problem hasn't been submitted yet, then do NOT show
# the reset button. # the reset button.
if (self.closed() and not is_survey_question) or not self.is_completed(): if (self.closed() and not is_survey_question) or not self.is_submitted():
return False return False
else: else:
return True return True
...@@ -322,7 +322,7 @@ class CapaModule(CapaFields, XModule): ...@@ -322,7 +322,7 @@ class CapaModule(CapaFields, XModule):
return not self.closed() return not self.closed()
else: else:
is_survey_question = (self.max_attempts == 0) is_survey_question = (self.max_attempts == 0)
needs_reset = self.is_completed() and self.rerandomize == "always" needs_reset = self.is_submitted() and self.rerandomize == "always"
# If the student has unlimited attempts, and their answers # If the student has unlimited attempts, and their answers
# are not randomized, then we do not need a save button # are not randomized, then we do not need a save button
...@@ -516,13 +516,18 @@ class CapaModule(CapaFields, XModule): ...@@ -516,13 +516,18 @@ class CapaModule(CapaFields, XModule):
return False return False
def is_completed(self): def is_submitted(self):
# used by conditional module """
# return self.answer_available() Used to decide to show or hide RESET or CHECK buttons.
Means that student submitted problem and nothing more.
Problem can be completely wrong.
Pressing RESET button makes this function to return False.
"""
return self.lcp.done return self.lcp.done
def is_attempted(self): def is_attempted(self):
# used by conditional module """Used by conditional module"""
return self.attempts > 0 return self.attempts > 0
def is_correct(self): def is_correct(self):
......
...@@ -35,8 +35,11 @@ class ConditionalModule(ConditionalFields, XModule): ...@@ -35,8 +35,11 @@ class ConditionalModule(ConditionalFields, XModule):
<conditional> tag attributes: <conditional> tag attributes:
sources - location id of required modules, separated by ';' sources - location id of required modules, separated by ';'
completed - map to `is_completed` module method submitted - map to `is_submitted` module method.
(pressing RESET button makes this function to return False.)
attempted - map to `is_attempted` module method attempted - map to `is_attempted` module method
correct - map to `is_correct` module method
poll_answer - map to `poll_answer` module attribute poll_answer - map to `poll_answer` module attribute
voted - map to `voted` module attribute voted - map to `voted` module attribute
...@@ -70,8 +73,18 @@ class ConditionalModule(ConditionalFields, XModule): ...@@ -70,8 +73,18 @@ class ConditionalModule(ConditionalFields, XModule):
# value: <name of module attribute> # value: <name of module attribute>
conditions_map = { conditions_map = {
'poll_answer': 'poll_answer', # poll_question attr 'poll_answer': 'poll_answer', # poll_question attr
'completed': 'is_completed', # capa_problem attr
# problem was submitted (it can be wrong)
# if student will press reset button after that,
# state will be reverted
'submitted': 'is_submitted', # capa_problem attr
# if student attempted problem
'attempted': 'is_attempted', # capa_problem attr 'attempted': 'is_attempted', # capa_problem attr
# if problem is full points
'correct': 'is_correct',
'voted': 'voted' # poll_question attr 'voted': 'voted' # poll_question attr
} }
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
<div id="video_example"> <div id="video_example">
<div id="example"> <div id="example">
<div id="video_id" class="video" <div id="video_id" class="video"
data-streams="0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId" data-youtube-id-0-75="slowerSpeedYoutubeId"
data-youtube-id-1-0="normalSpeedYoutubeId"
data-show-captions="true" data-show-captions="true"
data-start="" data-start=""
data-end="" data-end=""
......
...@@ -5,7 +5,6 @@ describe 'Video', -> ...@@ -5,7 +5,6 @@ describe 'Video', ->
loadFixtures 'video.html' loadFixtures 'video.html'
jasmine.stubRequests() jasmine.stubRequests()
@videosDefinition = '0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
@slowerSpeedYoutubeId = 'slowerSpeedYoutubeId' @slowerSpeedYoutubeId = 'slowerSpeedYoutubeId'
@normalSpeedYoutubeId = 'normalSpeedYoutubeId' @normalSpeedYoutubeId = 'normalSpeedYoutubeId'
metadata = metadata =
...@@ -30,7 +29,7 @@ describe 'Video', -> ...@@ -30,7 +29,7 @@ describe 'Video', ->
beforeEach -> beforeEach ->
spyOn(window.Video.prototype, 'fetchMetadata').andCallFake -> spyOn(window.Video.prototype, 'fetchMetadata').andCallFake ->
@metadata = metadata @metadata = metadata
@video = new Video '#example', @videosDefinition @video = new Video '#example'
it 'reset the current video player', -> it 'reset the current video player', ->
expect(window.player).toBeNull() expect(window.player).toBeNull()
...@@ -60,7 +59,7 @@ describe 'Video', -> ...@@ -60,7 +59,7 @@ describe 'Video', ->
@originalYT = window.YT @originalYT = window.YT
window.YT = { Player: true } window.YT = { Player: true }
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer) spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video '#example', @videosDefinition @video = new Video '#example'
afterEach -> afterEach ->
window.YT = @originalYT window.YT = @originalYT
...@@ -73,7 +72,7 @@ describe 'Video', -> ...@@ -73,7 +72,7 @@ describe 'Video', ->
beforeEach -> beforeEach ->
@originalYT = window.YT @originalYT = window.YT
window.YT = {} window.YT = {}
@video = new Video '#example', @videosDefinition @video = new Video '#example'
afterEach -> afterEach ->
window.YT = @originalYT window.YT = @originalYT
...@@ -86,7 +85,7 @@ describe 'Video', -> ...@@ -86,7 +85,7 @@ describe 'Video', ->
@originalYT = window.YT @originalYT = window.YT
window.YT = {} window.YT = {}
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer) spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video '#example', @videosDefinition @video = new Video '#example'
window.onYouTubePlayerAPIReady() window.onYouTubePlayerAPIReady()
afterEach -> afterEach ->
...@@ -99,7 +98,7 @@ describe 'Video', -> ...@@ -99,7 +98,7 @@ describe 'Video', ->
describe 'youtubeId', -> describe 'youtubeId', ->
beforeEach -> beforeEach ->
$.cookie.andReturn '1.0' $.cookie.andReturn '1.0'
@video = new Video '#example', @videosDefinition @video = new Video '#example'
describe 'with speed', -> describe 'with speed', ->
it 'return the video id for given speed', -> it 'return the video id for given speed', ->
...@@ -112,7 +111,7 @@ describe 'Video', -> ...@@ -112,7 +111,7 @@ describe 'Video', ->
describe 'setSpeed', -> describe 'setSpeed', ->
beforeEach -> beforeEach ->
@video = new Video '#example', @videosDefinition @video = new Video '#example'
describe 'when new speed is available', -> describe 'when new speed is available', ->
beforeEach -> beforeEach ->
...@@ -133,14 +132,14 @@ describe 'Video', -> ...@@ -133,14 +132,14 @@ describe 'Video', ->
describe 'getDuration', -> describe 'getDuration', ->
beforeEach -> beforeEach ->
@video = new Video '#example', @videosDefinition @video = new Video '#example'
it 'return duration for current video', -> it 'return duration for current video', ->
expect(@video.getDuration()).toEqual 200 expect(@video.getDuration()).toEqual 200
describe 'log', -> describe 'log', ->
beforeEach -> beforeEach ->
@video = new Video '#example', @videosDefinition @video = new Video '#example'
@video.setSpeed '1.0' @video.setSpeed '1.0'
spyOn Logger, 'log' spyOn Logger, 'log'
@video.player = { currentTime: 25 } @video.player = { currentTime: 25 }
......
...@@ -8,7 +8,7 @@ class @Video ...@@ -8,7 +8,7 @@ class @Video
@show_captions = @el.data('show-captions') @show_captions = @el.data('show-captions')
window.player = null window.player = null
@el = $("#video_#{@id}") @el = $("#video_#{@id}")
@parseVideos @el.data('streams') @parseVideos()
@fetchMetadata() @fetchMetadata()
@parseSpeed() @parseSpeed()
$("#video_#{@id}").data('video', this).addClass('video-load-complete') $("#video_#{@id}").data('video', this).addClass('video-load-complete')
...@@ -27,10 +27,14 @@ class @Video ...@@ -27,10 +27,14 @@ class @Video
parseVideos: (videos) -> parseVideos: (videos) ->
@videos = {} @videos = {}
$.each videos.split(/,/), (index, video) => if @el.data('youtube-id-0-75')
video = video.split(/:/) @videos['0.75'] = @el.data('youtube-id-0-75')
speed = parseFloat(video[0]).toFixed(2).replace /\.00$/, '.0' if @el.data('youtube-id-1-0')
@videos[speed] = video[1] @videos['1.0'] = @el.data('youtube-id-1-0')
if @el.data('youtube-id-1-25')
@videos['1.25'] = @el.data('youtube-id-1-25')
if @el.data('youtube-id-1-5')
@videos['1.50'] = @el.data('youtube-id-1-5')
parseSpeed: -> parseSpeed: ->
@setSpeed($.cookie('video_speed')) @setSpeed($.cookie('video_speed'))
......
...@@ -4,6 +4,7 @@ This module has utility functions for gathering up the static content ...@@ -4,6 +4,7 @@ This module has utility functions for gathering up the static content
that is defined by XModules and XModuleDescriptors (javascript and css) that is defined by XModules and XModuleDescriptors (javascript and css)
""" """
import logging
import hashlib import hashlib
import os import os
import errno import errno
...@@ -15,6 +16,9 @@ from path import path ...@@ -15,6 +16,9 @@ from path import path
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
LOG = logging.getLogger(__name__)
def write_module_styles(output_root): def write_module_styles(output_root):
return _write_styles('.xmodule_display', output_root, _list_modules()) return _write_styles('.xmodule_display', output_root, _list_modules())
...@@ -121,18 +125,32 @@ def _write_js(output_root, classes): ...@@ -121,18 +125,32 @@ def _write_js(output_root, classes):
type=filetype) type=filetype)
contents[filename] = fragment contents[filename] = fragment
_write_files(output_root, contents) _write_files(output_root, contents, {'.coffee': '.js'})
return [output_root / filename for filename in contents.keys()] return [output_root / filename for filename in contents.keys()]
def _write_files(output_root, contents): def _write_files(output_root, contents, generated_suffix_map=None):
_ensure_dir(output_root) _ensure_dir(output_root)
for extra_file in set(output_root.files()) - set(contents.keys()): to_delete = set(file.basename() for file in output_root.files()) - set(contents.keys())
extra_file.remove_p()
if generated_suffix_map:
for output_file in contents.keys():
for suffix, generated_suffix in generated_suffix_map.items():
if output_file.endswith(suffix):
to_delete.discard(output_file.replace(suffix, generated_suffix))
for extra_file in to_delete:
(output_root / extra_file).remove_p()
for filename, file_content in contents.iteritems(): for filename, file_content in contents.iteritems():
(output_root / filename).write_bytes(file_content) output_file = output_root / filename
if not output_file.isfile() or output_file.read_md5() != hashlib.md5(file_content).digest():
LOG.debug("Writing %s", output_file)
output_file.write_bytes(file_content)
else:
LOG.debug("%s unchanged, skipping", output_file)
def main(): def main():
......
--- ---
metadata: metadata:
display_name: default display_name: default
data_dir: a_made_up_name data: ""
data: |
<video youtube="0.75:JMD_ifUUfsU,1.0:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY"/>
children: [] children: []
...@@ -29,14 +29,14 @@ open_ended_grading_interface = { ...@@ -29,14 +29,14 @@ open_ended_grading_interface = {
} }
def test_system(): def get_test_system():
""" """
Construct a test ModuleSystem instance. Construct a test ModuleSystem instance.
By default, the render_template() method simply returns the repr of the By default, the render_template() method simply returns the repr of the
context it is passed. You can override this behavior by monkey patching:: context it is passed. You can override this behavior by monkey patching::
system = test_system() system = get_test_system()
system.render_template = my_render_func system.render_template = my_render_func
where `my_render_func` is a function of the form my_render_func(template, context). where `my_render_func` is a function of the form my_render_func(template, context).
......
...@@ -8,7 +8,7 @@ from mock import Mock ...@@ -8,7 +8,7 @@ from mock import Mock
from xmodule.annotatable_module import AnnotatableModule from xmodule.annotatable_module import AnnotatableModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from . import test_system from . import get_test_system
class AnnotatableModuleTestCase(unittest.TestCase): class AnnotatableModuleTestCase(unittest.TestCase):
location = Location(["i4x", "edX", "toy", "annotatable", "guided_discussion"]) location = Location(["i4x", "edX", "toy", "annotatable", "guided_discussion"])
...@@ -32,7 +32,7 @@ class AnnotatableModuleTestCase(unittest.TestCase): ...@@ -32,7 +32,7 @@ class AnnotatableModuleTestCase(unittest.TestCase):
module_data = {'data': sample_xml, 'location': location} module_data = {'data': sample_xml, 'location': location}
def setUp(self): def setUp(self):
self.annotatable = AnnotatableModule(test_system(), self.descriptor, self.module_data) self.annotatable = AnnotatableModule(get_test_system(), self.descriptor, self.module_data)
def test_annotation_data_attr(self): def test_annotation_data_attr(self):
el = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>') el = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>')
......
...@@ -17,7 +17,7 @@ from xmodule.modulestore import Location ...@@ -17,7 +17,7 @@ from xmodule.modulestore import Location
from django.http import QueryDict from django.http import QueryDict
from . import test_system from . import get_test_system
from pytz import UTC from pytz import UTC
from capa.correctmap import CorrectMap from capa.correctmap import CorrectMap
...@@ -112,7 +112,7 @@ class CapaFactory(object): ...@@ -112,7 +112,7 @@ class CapaFactory(object):
# since everything else is a string. # since everything else is a string.
model_data['attempts'] = int(attempts) model_data['attempts'] = int(attempts)
system = test_system() system = get_test_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>") system.render_template = Mock(return_value="<div>Test Template HTML</div>")
module = CapaModule(system, descriptor, model_data) module = CapaModule(system, descriptor, model_data)
...@@ -1002,7 +1002,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -1002,7 +1002,7 @@ class CapaModuleTest(unittest.TestCase):
# is asked to render itself as HTML # is asked to render itself as HTML
module.lcp.get_html = Mock(side_effect=Exception("Test")) module.lcp.get_html = Mock(side_effect=Exception("Test"))
# Stub out the test_system rendering function # Stub out the get_test_system rendering function
module.system.render_template = Mock(return_value="<div>Test Template HTML</div>") module.system.render_template = Mock(return_value="<div>Test Template HTML</div>")
# Turn off DEBUG # Turn off DEBUG
......
...@@ -18,7 +18,7 @@ import logging ...@@ -18,7 +18,7 @@ import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
from . import test_system from . import get_test_system
ORG = 'edX' ORG = 'edX'
COURSE = 'open_ended' # name of directory with course data COURSE = 'open_ended' # name of directory with course data
...@@ -68,7 +68,7 @@ class OpenEndedChildTest(unittest.TestCase): ...@@ -68,7 +68,7 @@ class OpenEndedChildTest(unittest.TestCase):
descriptor = Mock() descriptor = Mock()
def setUp(self): def setUp(self):
self.test_system = test_system() self.test_system = get_test_system()
self.openendedchild = OpenEndedChild(self.test_system, self.location, self.openendedchild = OpenEndedChild(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata) self.definition, self.descriptor, self.static_data, self.metadata)
...@@ -192,7 +192,7 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -192,7 +192,7 @@ class OpenEndedModuleTest(unittest.TestCase):
descriptor = Mock() descriptor = Mock()
def setUp(self): def setUp(self):
self.test_system = test_system() self.test_system = get_test_system()
self.test_system.location = self.location self.test_system.location = self.location
self.mock_xqueue = MagicMock() self.mock_xqueue = MagicMock()
...@@ -367,7 +367,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -367,7 +367,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]} definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]}
full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2) full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2)
descriptor = Mock(data=full_definition) descriptor = Mock(data=full_definition)
test_system = test_system() test_system = get_test_system()
combinedoe_container = CombinedOpenEndedModule( combinedoe_container = CombinedOpenEndedModule(
test_system, test_system,
descriptor, descriptor,
...@@ -493,7 +493,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): ...@@ -493,7 +493,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
hint = "blah" hint = "blah"
def setUp(self): def setUp(self):
self.test_system = test_system() self.test_system = get_test_system()
self.test_system.xqueue['interface'] = Mock( self.test_system.xqueue['interface'] = Mock(
send_to_queue=Mock(side_effect=[1, "queued"]) send_to_queue=Mock(side_effect=[1, "queued"])
) )
...@@ -569,6 +569,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): ...@@ -569,6 +569,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
#Mock a student submitting an assessment #Mock a student submitting an assessment
assessment_dict = MockQueryDict() assessment_dict = MockQueryDict()
assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment}) assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment})
#from nose.tools import set_trace; set_trace()
module.handle_ajax("save_assessment", assessment_dict) module.handle_ajax("save_assessment", assessment_dict)
task_one_json = json.loads(module.task_states[0]) task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment) self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
......
...@@ -15,7 +15,7 @@ from xmodule.tests.test_export import DATA_DIR ...@@ -15,7 +15,7 @@ from xmodule.tests.test_export import DATA_DIR
ORG = 'test_org' ORG = 'test_org'
COURSE = 'conditional' # name of directory with course data COURSE = 'conditional' # name of directory with course data
from . import test_system from . import get_test_system
class DummySystem(ImportSystem): class DummySystem(ImportSystem):
...@@ -104,7 +104,7 @@ class ConditionalModuleBasicTest(unittest.TestCase): ...@@ -104,7 +104,7 @@ class ConditionalModuleBasicTest(unittest.TestCase):
""" """
def setUp(self): def setUp(self):
self.test_system = test_system() self.test_system = get_test_system()
def test_icon_class(self): def test_icon_class(self):
'''verify that get_icon_class works independent of condition satisfaction''' '''verify that get_icon_class works independent of condition satisfaction'''
...@@ -117,7 +117,7 @@ class ConditionalModuleBasicTest(unittest.TestCase): ...@@ -117,7 +117,7 @@ class ConditionalModuleBasicTest(unittest.TestCase):
def test_get_html(self): def test_get_html(self):
modules = ConditionalFactory.create(self.test_system) modules = ConditionalFactory.create(self.test_system)
# because test_system returns the repr of the context dict passed to render_template, # because get_test_system returns the repr of the context dict passed to render_template,
# we reverse it here # we reverse it here
html = modules['cond_module'].get_html() html = modules['cond_module'].get_html()
html_dict = literal_eval(html) html_dict = literal_eval(html)
...@@ -161,7 +161,7 @@ class ConditionalModuleXmlTest(unittest.TestCase): ...@@ -161,7 +161,7 @@ class ConditionalModuleXmlTest(unittest.TestCase):
return DummySystem(load_error_modules) return DummySystem(load_error_modules)
def setUp(self): def setUp(self):
self.test_system = test_system() self.test_system = get_test_system()
def get_course(self, name): def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error.""" """Get a test course by directory name. If there's more than one, error."""
......
...@@ -2,25 +2,30 @@ ...@@ -2,25 +2,30 @@
Tests for ErrorModule and NonStaffErrorModule Tests for ErrorModule and NonStaffErrorModule
""" """
import unittest import unittest
from xmodule.tests import test_system from xmodule.tests import get_test_system
import xmodule.error_module as error_module import xmodule.error_module as error_module
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from mock import MagicMock from mock import MagicMock
class TestErrorModule(unittest.TestCase): class SetupTestErrorModules():
"""
Tests for ErrorModule and ErrorDescriptor
"""
def setUp(self): def setUp(self):
self.system = test_system() self.system = get_test_system()
self.org = "org" self.org = "org"
self.course = "course" self.course = "course"
self.location = Location(['i4x', self.org, self.course, None, None]) self.location = Location(['i4x', self.org, self.course, None, None])
self.valid_xml = u"<problem>ABC \N{SNOWMAN}</problem>" self.valid_xml = u"<problem>ABC \N{SNOWMAN}</problem>"
self.error_msg = "Error" self.error_msg = "Error"
class TestErrorModule(unittest.TestCase, SetupTestErrorModules):
"""
Tests for ErrorModule and ErrorDescriptor
"""
def setUp(self):
SetupTestErrorModules.setUp(self)
def test_error_module_xml_rendering(self): def test_error_module_xml_rendering(self):
descriptor = error_module.ErrorDescriptor.from_xml( descriptor = error_module.ErrorDescriptor.from_xml(
self.valid_xml, self.system, self.org, self.course, self.error_msg) self.valid_xml, self.system, self.org, self.course, self.error_msg)
...@@ -45,10 +50,12 @@ class TestErrorModule(unittest.TestCase): ...@@ -45,10 +50,12 @@ class TestErrorModule(unittest.TestCase):
self.assertIn(repr(descriptor), context_repr) self.assertIn(repr(descriptor), context_repr)
class TestNonStaffErrorModule(TestErrorModule): class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
""" """
Tests for NonStaffErrorModule and NonStaffErrorDescriptor Tests for NonStaffErrorModule and NonStaffErrorDescriptor
""" """
def setUp(self):
SetupTestErrorModules.setUp(self)
def test_non_staff_error_module_create(self): def test_non_staff_error_module_create(self):
descriptor = error_module.NonStaffErrorDescriptor.from_xml( descriptor = error_module.NonStaffErrorDescriptor.from_xml(
......
...@@ -5,7 +5,7 @@ from mock import Mock ...@@ -5,7 +5,7 @@ from mock import Mock
from xmodule.html_module import HtmlModule from xmodule.html_module import HtmlModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from . import test_system from . import get_test_system
class HtmlModuleSubstitutionTestCase(unittest.TestCase): class HtmlModuleSubstitutionTestCase(unittest.TestCase):
descriptor = Mock() descriptor = Mock()
...@@ -13,7 +13,7 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase): ...@@ -13,7 +13,7 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase):
def test_substitution_works(self): def test_substitution_works(self):
sample_xml = '''%%USER_ID%%''' sample_xml = '''%%USER_ID%%'''
module_data = {'data': sample_xml} module_data = {'data': sample_xml}
module_system = test_system() module_system = get_test_system()
module = HtmlModule(module_system, self.descriptor, module_data) module = HtmlModule(module_system, self.descriptor, module_data)
self.assertEqual(module.get_html(), str(module_system.anonymous_student_id)) self.assertEqual(module.get_html(), str(module_system.anonymous_student_id))
...@@ -25,14 +25,14 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase): ...@@ -25,14 +25,14 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase):
</html> </html>
''' '''
module_data = {'data': sample_xml} module_data = {'data': sample_xml}
module = HtmlModule(test_system(), self.descriptor, module_data) module = HtmlModule(get_test_system(), self.descriptor, module_data)
self.assertEqual(module.get_html(), sample_xml) self.assertEqual(module.get_html(), sample_xml)
def test_substitution_without_anonymous_student_id(self): def test_substitution_without_anonymous_student_id(self):
sample_xml = '''%%USER_ID%%''' sample_xml = '''%%USER_ID%%'''
module_data = {'data': sample_xml} module_data = {'data': sample_xml}
module_system = test_system() module_system = get_test_system()
module_system.anonymous_student_id = None module_system.anonymous_student_id = None
module = HtmlModule(module_system, self.descriptor, module_data) module = HtmlModule(module_system, self.descriptor, module_data)
self.assertEqual(module.get_html(), sample_xml) self.assertEqual(module.get_html(), sample_xml)
......
...@@ -336,8 +336,8 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -336,8 +336,8 @@ class ImportTestCase(BaseCourseTestCase):
location = Location(["i4x", "edX", "toy", "video", "Welcome"]) location = Location(["i4x", "edX", "toy", "video", "Welcome"])
toy_video = modulestore.get_instance(toy_id, location) toy_video = modulestore.get_instance(toy_id, location)
two_toy_video = modulestore.get_instance(two_toy_id, location) two_toy_video = modulestore.get_instance(two_toy_id, location)
self.assertEqual(etree.fromstring(toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh8") self.assertEqual(toy_video.youtube_id_1_0, "p2Q6BrNhdh8")
self.assertEqual(etree.fromstring(two_toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh9") self.assertEqual(two_toy_video.youtube_id_1_0, "p2Q6BrNhdh9")
def test_colon_in_url_name(self): def test_colon_in_url_name(self):
"""Ensure that colons in url_names convert to file paths properly""" """Ensure that colons in url_names convert to file paths properly"""
......
...@@ -8,7 +8,7 @@ import unittest ...@@ -8,7 +8,7 @@ import unittest
from xmodule.poll_module import PollDescriptor from xmodule.poll_module import PollDescriptor
from xmodule.conditional_module import ConditionalDescriptor from xmodule.conditional_module import ConditionalDescriptor
from xmodule.word_cloud_module import WordCloudDescriptor from xmodule.word_cloud_module import WordCloudDescriptor
from xmodule.tests import test_system from xmodule.tests import get_test_system
class PostData: class PostData:
"""Class which emulate postdata.""" """Class which emulate postdata."""
...@@ -30,7 +30,7 @@ class LogicTest(unittest.TestCase): ...@@ -30,7 +30,7 @@ class LogicTest(unittest.TestCase):
"""Empty object.""" """Empty object."""
pass pass
self.system = test_system() self.system = get_test_system()
self.descriptor = EmptyClass() self.descriptor = EmptyClass()
self.xmodule_class = self.descriptor_class.module_class self.xmodule_class = self.descriptor_class.module_class
......
import unittest import unittest
from xmodule.modulestore import Location from xmodule.modulestore import Location
from .import test_system from .import get_test_system
from test_util_open_ended import MockQueryDict, DummyModulestore from test_util_open_ended import MockQueryDict, DummyModulestore
import json import json
...@@ -39,7 +39,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore): ...@@ -39,7 +39,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
Create a peer grading module from a test system Create a peer grading module from a test system
@return: @return:
""" """
self.test_system = test_system() self.test_system = get_test_system()
self.test_system.open_ended_grading_interface = None self.test_system.open_ended_grading_interface = None
self.setup_modulestore(COURSE) self.setup_modulestore(COURSE)
self.peer_grading = self.get_module_from_location(self.problem_location, COURSE) self.peer_grading = self.get_module_from_location(self.problem_location, COURSE)
...@@ -151,7 +151,7 @@ class PeerGradingModuleScoredTest(unittest.TestCase, DummyModulestore): ...@@ -151,7 +151,7 @@ class PeerGradingModuleScoredTest(unittest.TestCase, DummyModulestore):
Create a peer grading module from a test system Create a peer grading module from a test system
@return: @return:
""" """
self.test_system = test_system() self.test_system = get_test_system()
self.test_system.open_ended_grading_interface = None self.test_system.open_ended_grading_interface = None
self.setup_modulestore(COURSE) self.setup_modulestore(COURSE)
......
...@@ -5,7 +5,7 @@ import unittest ...@@ -5,7 +5,7 @@ import unittest
from xmodule.progress import Progress from xmodule.progress import Progress
from xmodule import x_module from xmodule import x_module
from . import test_system from . import get_test_system
class ProgressTest(unittest.TestCase): class ProgressTest(unittest.TestCase):
...@@ -134,6 +134,6 @@ class ModuleProgressTest(unittest.TestCase): ...@@ -134,6 +134,6 @@ class ModuleProgressTest(unittest.TestCase):
''' '''
def test_xmodule_default(self): def test_xmodule_default(self):
'''Make sure default get_progress exists, returns None''' '''Make sure default get_progress exists, returns None'''
xm = x_module.XModule(test_system(), None, {'location': 'a://b/c/d/e'}) xm = x_module.XModule(get_test_system(), None, {'location': 'a://b/c/d/e'})
p = xm.get_progress() p = xm.get_progress()
self.assertEqual(p, None) self.assertEqual(p, None)
...@@ -6,7 +6,7 @@ from xmodule.open_ended_grading_classes.self_assessment_module import SelfAssess ...@@ -6,7 +6,7 @@ from xmodule.open_ended_grading_classes.self_assessment_module import SelfAssess
from xmodule.modulestore import Location from xmodule.modulestore import Location
from lxml import etree from lxml import etree
from . import test_system from . import get_test_system
import test_util_open_ended import test_util_open_ended
...@@ -51,7 +51,7 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -51,7 +51,7 @@ class SelfAssessmentTest(unittest.TestCase):
'skip_basic_checks': False, 'skip_basic_checks': False,
} }
self.module = SelfAssessmentModule(test_system(), self.location, self.module = SelfAssessmentModule(get_test_system(), self.location,
self.definition, self.definition,
self.descriptor, self.descriptor,
static_data) static_data)
......
from .import test_system from .import get_test_system
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from xmodule.tests.test_export import DATA_DIR from xmodule.tests.test_export import DATA_DIR
...@@ -37,7 +37,7 @@ class DummyModulestore(object): ...@@ -37,7 +37,7 @@ class DummyModulestore(object):
""" """
A mixin that allows test classes to have convenience functions to get a module given a location A mixin that allows test classes to have convenience functions to get a module given a location
""" """
test_system = test_system() get_test_system = get_test_system()
def setup_modulestore(self, name): def setup_modulestore(self, name):
self.modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name]) self.modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
......
# -*- coding: utf-8 -*-
import unittest
from xmodule.video_module import VideoDescriptor
from .test_import import DummySystem
class VideoDescriptorImportTestCase(unittest.TestCase):
"""
Make sure that VideoDescriptor can import an old XML-based video correctly.
"""
def test_from_xml(self):
module_system = DummySystem(load_error_modules=True)
xml_data = '''
<video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
show_captions="false"
from="00:00:01"
to="00:01:00">
<source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/>
</video>
'''
output = VideoDescriptor.from_xml(xml_data, module_system)
self.assertEquals(output.youtube_id_0_75, 'izygArpw-Qo')
self.assertEquals(output.youtube_id_1_0, 'p2Q6BrNhdh8')
self.assertEquals(output.youtube_id_1_25, '1EeWXzPdhSA')
self.assertEquals(output.youtube_id_1_5, 'rABDYkeK0x8')
self.assertEquals(output.show_captions, False)
self.assertEquals(output.start_time, 1.0)
self.assertEquals(output.end_time, 60)
self.assertEquals(output.track, 'http://www.example.com/track')
self.assertEquals(output.source, 'http://www.example.com/source.mp4')
def test_from_xml_missing_attributes(self):
"""
Ensure that attributes have the right values if they aren't
explicitly set in XML.
"""
module_system = DummySystem(load_error_modules=True)
xml_data = '''
<video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA"
show_captions="true">
<source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/>
</video>
'''
output = VideoDescriptor.from_xml(xml_data, module_system)
self.assertEquals(output.youtube_id_0_75, '')
self.assertEquals(output.youtube_id_1_0, 'p2Q6BrNhdh8')
self.assertEquals(output.youtube_id_1_25, '1EeWXzPdhSA')
self.assertEquals(output.youtube_id_1_5, '')
self.assertEquals(output.show_captions, True)
self.assertEquals(output.start_time, 0.0)
self.assertEquals(output.end_time, 0.0)
self.assertEquals(output.track, 'http://www.example.com/track')
self.assertEquals(output.source, 'http://www.example.com/source.mp4')
def test_from_xml_no_attributes(self):
"""
Make sure settings are correct if none are explicitly set in XML.
"""
module_system = DummySystem(load_error_modules=True)
xml_data = '<video></video>'
output = VideoDescriptor.from_xml(xml_data, module_system)
self.assertEquals(output.youtube_id_0_75, '')
self.assertEquals(output.youtube_id_1_0, 'OEoXaMPEzfM')
self.assertEquals(output.youtube_id_1_25, '')
self.assertEquals(output.youtube_id_1_5, '')
self.assertEquals(output.show_captions, True)
self.assertEquals(output.start_time, 0.0)
self.assertEquals(output.end_time, 0.0)
self.assertEquals(output.track, '')
self.assertEquals(output.source, '')
...@@ -18,9 +18,9 @@ import unittest ...@@ -18,9 +18,9 @@ import unittest
from mock import Mock from mock import Mock
from lxml import etree from lxml import etree
from xmodule.video_module import VideoDescriptor, VideoModule from xmodule.video_module import VideoDescriptor, VideoModule, _parse_time, _parse_youtube
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.tests import test_system from xmodule.tests import get_test_system
from xmodule.tests.test_logic import LogicTest from xmodule.tests.test_logic import LogicTest
...@@ -49,9 +49,9 @@ class VideoFactory(object): ...@@ -49,9 +49,9 @@ class VideoFactory(object):
"SampleProblem1"]) "SampleProblem1"])
model_data = {'data': VideoFactory.sample_problem_xml_youtube, 'location': location} model_data = {'data': VideoFactory.sample_problem_xml_youtube, 'location': location}
descriptor = Mock(weight="1") descriptor = Mock(weight="1", url_name="SampleProblem1")
system = test_system() system = get_test_system()
system.render_template = lambda template, context: context system.render_template = lambda template, context: context
module = VideoModule(system, descriptor, model_data) module = VideoModule(system, descriptor, model_data)
...@@ -67,69 +67,57 @@ class VideoModuleLogicTest(LogicTest): ...@@ -67,69 +67,57 @@ class VideoModuleLogicTest(LogicTest):
'data': '<video />' 'data': '<video />'
} }
def test_get_timeframe_no_parameters(self): def test_parse_time(self):
"""Make sure that timeframe() works correctly w/o parameters""" """Ensure that times are parsed correctly into seconds."""
xmltree = etree.fromstring('<video>test</video>') output = _parse_time('00:04:07')
output = self.xmodule.get_timeframe(xmltree) self.assertEqual(output, 247)
self.assertEqual(output, ('', ''))
def test_parse_time_none(self):
def test_get_timeframe_with_one_parameter(self): """Check parsing of None."""
"""Make sure that timeframe() works correctly with one parameter""" output = _parse_time(None)
xmltree = etree.fromstring( self.assertEqual(output, '')
'<video from="00:04:07">test</video>'
) def test_parse_time_empty(self):
output = self.xmodule.get_timeframe(xmltree) """Check parsing of the empty string."""
self.assertEqual(output, (247, '')) output = _parse_time('')
self.assertEqual(output, '')
def test_get_timeframe_with_two_parameters(self):
"""Make sure that timeframe() works correctly with two parameters""" def test_parse_youtube(self):
xmltree = etree.fromstring( """Test parsing old-style Youtube ID strings into a dict."""
'''<video youtube_str = '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg'
from="00:04:07" output = _parse_youtube(youtube_str)
to="13:04:39" self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
>test</video>''' '1.00': 'ZwkTiUPN0mg',
) '1.25': 'rsq9auxASqI',
output = self.xmodule.get_timeframe(xmltree) '1.50': 'kMyNdzVHHgg'})
self.assertEqual(output, (247, 47079))
def test_parse_youtube_one_video(self):
"""
class VideoModuleUnitTest(unittest.TestCase): Ensure that all keys are present and missing speeds map to the
"""Unit tests for Video Xmodule.""" empty string.
"""
def test_video_constructor(self): youtube_str = '0.75:jNCf2gIqpeE'
"""Make sure that all parameters extracted correclty from xml""" output = _parse_youtube(youtube_str)
module = VideoFactory.create() self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
'1.00': '',
# `get_html` return only context, cause we '1.25': '',
# overwrite `system.render_template` '1.50': ''})
context = module.get_html()
expected_context = { def test_parse_youtube_key_format(self):
'track': None, """
'show_captions': 'true', Make sure that inconsistent speed keys are parsed correctly.
'display_name': 'SampleProblem1', """
'id': module.location.html_id(), youtube_str = '1.00:p2Q6BrNhdh8'
'end': 3610.0, youtube_str_hack = '1.0:p2Q6BrNhdh8'
'caption_asset_path': '/static/subs/', self.assertEqual(_parse_youtube(youtube_str), _parse_youtube(youtube_str_hack))
'source': '.../mit-3091x/M-3091X-FA12-L21-3_100.mp4',
'streams': '0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg',
'normal_speed_video_id': 'ZwkTiUPN0mg',
'position': 0,
'start': 3603.0
}
self.assertDictEqual(context, expected_context)
self.assertEqual(
module.youtube,
'0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg')
self.assertEqual(
module.video_list(),
module.youtube)
self.assertEqual(
module.position,
0)
self.assertDictEqual( def test_parse_youtube_empty(self):
json.loads(module.get_instance_state()), """
{'position': 0}) Some courses have empty youtube attributes, so we should handle
that well.
"""
self.assertEqual(_parse_youtube(''),
{'0.75': '',
'1.00': '',
'1.25': '',
'1.50': ''})
...@@ -6,7 +6,7 @@ from xblock.core import Scope, String, Dict, Boolean, Integer, Float, Any, List ...@@ -6,7 +6,7 @@ from xblock.core import Scope, String, Dict, Boolean, Integer, Float, Any, List
from xmodule.fields import Date, Timedelta from xmodule.fields import Date, Timedelta
from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field
import unittest import unittest
from .import test_system from .import get_test_system
from nose.tools import assert_equals from nose.tools import assert_equals
from mock import Mock from mock import Mock
...@@ -140,7 +140,7 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -140,7 +140,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
# Start of helper methods # Start of helper methods
def get_xml_editable_fields(self, model_data): def get_xml_editable_fields(self, model_data):
system = test_system() system = get_test_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>") system.render_template = Mock(return_value="<div>Test Template HTML</div>")
return XmlDescriptor(runtime=system, model_data=model_data).editable_metadata_fields return XmlDescriptor(runtime=system, model_data=model_data).editable_metadata_fields
...@@ -152,7 +152,7 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -152,7 +152,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
non_editable_fields.append(TestModuleDescriptor.due) non_editable_fields.append(TestModuleDescriptor.due)
return non_editable_fields return non_editable_fields
system = test_system() system = get_test_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>") system.render_template = Mock(return_value="<div>Test Template HTML</div>")
return TestModuleDescriptor(runtime=system, model_data=model_data) return TestModuleDescriptor(runtime=system, model_data=model_data)
......
...@@ -6,23 +6,31 @@ import logging ...@@ -6,23 +6,31 @@ import logging
from lxml import etree from lxml import etree
from pkg_resources import resource_string, resource_listdir from pkg_resources import resource_string, resource_listdir
import datetime
import time
from django.http import Http404 from django.http import Http404
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xblock.core import Integer, Scope, String from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xblock.core import Integer, Scope, String, Float, Boolean
import datetime
import time
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class VideoFields(object): class VideoFields(object):
"""Fields for `VideoModule` and `VideoDescriptor`.""" """Fields for `VideoModule` and `VideoDescriptor`."""
data = String(help="XML data for the problem", scope=Scope.content)
position = Integer(help="Current position in the video", scope=Scope.user_state, default=0) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
show_captions = Boolean(help="This controls whether or not captions are shown by default.", display_name="Show Captions", scope=Scope.settings, default=True)
youtube_id_1_0 = String(help="This is the Youtube ID reference for the normal speed video.", display_name="Default Speed", scope=Scope.settings, default="OEoXaMPEzfM")
youtube_id_0_75 = String(help="The Youtube ID for the .75x speed video.", display_name="Speed: .75x", scope=Scope.settings, default="")
youtube_id_1_25 = String(help="The Youtube ID for the 1.25x speed video.", display_name="Speed: 1.25x", scope=Scope.settings, default="")
youtube_id_1_5 = String(help="The Youtube ID for the 1.5x speed video.", display_name="Speed: 1.5x", scope=Scope.settings, default="")
start_time = Float(help="Time the video starts", display_name="Start Time", scope=Scope.settings, default=0.0)
end_time = Float(help="Time the video ends", display_name="End Time", scope=Scope.settings, default=0.0)
source = String(help="The external URL to download the video. This appears as a link beneath the video.", display_name="Download Video", scope=Scope.settings, default="")
track = String(help="The external URL to download the subtitle track. This appears as a link beneath the video.", display_name="Download Track", scope=Scope.settings, default="")
class VideoModule(VideoFields, XModule): class VideoModule(VideoFields, XModule):
...@@ -46,54 +54,6 @@ class VideoModule(VideoFields, XModule): ...@@ -46,54 +54,6 @@ class VideoModule(VideoFields, XModule):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs) XModule.__init__(self, *args, **kwargs)
xmltree = etree.fromstring(self.data)
self.youtube = xmltree.get('youtube')
self.show_captions = xmltree.get('show_captions', 'true')
self.source = self._get_source(xmltree)
self.track = self._get_track(xmltree)
self.start_time, self.end_time = self.get_timeframe(xmltree)
def _get_source(self, xmltree):
"""Find the first valid source."""
return self._get_first_external(xmltree, 'source')
def _get_track(self, xmltree):
"""Find the first valid track."""
return self._get_first_external(xmltree, 'track')
def _get_first_external(self, xmltree, tag):
"""
Will return the first valid element
of the given tag.
'valid' means has a non-empty 'src' attribute
"""
result = None
for element in xmltree.findall(tag):
src = element.get('src')
if src:
result = src
break
return result
def get_timeframe(self, xmltree):
""" Converts 'from' and 'to' parameters in video tag to seconds.
If there are no parameters, returns empty string. """
def parse_time(str_time):
"""Converts s in '12:34:45' format to seconds. If s is
None, returns empty string"""
if str_time is None:
return ''
else:
obj_time = time.strptime(str_time, '%H:%M:%S')
return datetime.timedelta(
hours=obj_time.tm_hour,
minutes=obj_time.tm_min,
seconds=obj_time.tm_sec
).total_seconds()
return parse_time(xmltree.get('from')), parse_time(xmltree.get('to'))
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
"""This is not being called right now and we raise 404 error.""" """This is not being called right now and we raise 404 error."""
log.debug(u"GET {0}".format(get)) log.debug(u"GET {0}".format(get))
...@@ -104,38 +64,135 @@ class VideoModule(VideoFields, XModule): ...@@ -104,38 +64,135 @@ class VideoModule(VideoFields, XModule):
"""Return information about state (position).""" """Return information about state (position)."""
return json.dumps({'position': self.position}) return json.dumps({'position': self.position})
def video_list(self):
"""Return video list."""
return self.youtube
def get_html(self): def get_html(self):
# We normally let JS parse this, but in the case that we need a hacked
# out <object> player because YouTube has broken their <iframe> API for
# the third time in a year, we need to extract it server side.
normal_speed_video_id = None # The 1.0 speed video
# video_list() example:
# "0.75:nugHYNiD3fI,1.0:7m8pab1MfYY,1.25:3CxdPGXShq8,1.50:F-D7bOFCnXA"
for video_id_str in self.video_list().split(","):
if video_id_str.startswith("1.0:"):
normal_speed_video_id = video_id_str.split(":")[1]
return self.system.render_template('video.html', { return self.system.render_template('video.html', {
'streams': self.video_list(), 'youtube_id_0_75': self.youtube_id_0_75,
'youtube_id_1_0': self.youtube_id_1_0,
'youtube_id_1_25': self.youtube_id_1_25,
'youtube_id_1_5': self.youtube_id_1_5,
'id': self.location.html_id(), 'id': self.location.html_id(),
'position': self.position, 'position': self.position,
'source': self.source, 'source': self.source,
'track': self.track, 'track': self.track,
'display_name': self.display_name_with_default, 'display_name': self.display_name_with_default,
'caption_asset_path': "/static/subs/", 'caption_asset_path': "/static/subs/",
'show_captions': self.show_captions, 'show_captions': 'true' if self.show_captions else 'false',
'start': self.start_time, 'start': self.start_time,
'end': self.end_time, 'end': self.end_time
'normal_speed_video_id': normal_speed_video_id
}) })
class VideoDescriptor(VideoFields, RawDescriptor): class VideoDescriptor(VideoFields,
"""Descriptor for `VideoModule`.""" MetadataOnlyEditingDescriptor,
RawDescriptor):
module_class = VideoModule module_class = VideoModule
template_dir_name = "video" template_dir_name = "video"
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(MetadataOnlyEditingDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([VideoModule.start_time,
VideoModule.end_time])
return non_editable_fields
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
"""
Creates an instance of this descriptor from the supplied xml_data.
This may be overridden by subclasses
xml_data: A string of xml that will be translated into data and children for
this module
system: A DescriptorSystem for interacting with external resources
org and course are optional strings that will be used in the generated modules
url identifiers
"""
video = super(VideoDescriptor, cls).from_xml(xml_data, system, org, course)
xml = etree.fromstring(xml_data)
display_name = xml.get('display_name')
if display_name:
video.display_name = display_name
youtube = xml.get('youtube')
if youtube:
speeds = _parse_youtube(youtube)
if speeds['0.75']:
video.youtube_id_0_75 = speeds['0.75']
if speeds['1.00']:
video.youtube_id_1_0 = speeds['1.00']
if speeds['1.25']:
video.youtube_id_1_25 = speeds['1.25']
if speeds['1.50']:
video.youtube_id_1_5 = speeds['1.50']
show_captions = xml.get('show_captions')
if show_captions:
video.show_captions = json.loads(show_captions)
source = _get_first_external(xml, 'source')
if source:
video.source = source
track = _get_first_external(xml, 'track')
if track:
video.track = track
start_time = _parse_time(xml.get('from'))
if start_time:
video.start_time = start_time
end_time = _parse_time(xml.get('to'))
if end_time:
video.end_time = end_time
return video
def _get_first_external(xmltree, tag):
"""
Returns the src attribute of the nested `tag` in `xmltree`, if it
exists.
"""
for element in xmltree.findall(tag):
src = element.get('src')
if src:
return src
return None
def _parse_youtube(data):
"""
Parses a string of Youtube IDs such as "1.0:AXdE34_U,1.5:VO3SxfeD"
into a dictionary. Necessary for backwards compatibility with
XML-based courses.
"""
ret = {'0.75': '', '1.00': '', '1.25': '', '1.50': ''}
if data == '':
return ret
videos = data.split(',')
for video in videos:
pieces = video.split(':')
# HACK
# To elaborate somewhat: in many LMS tests, the keys for
# Youtube IDs are inconsistent. Sometimes a particular
# speed isn't present, and formatting is also inconsistent
# ('1.0' versus '1.00'). So it's necessary to either do
# something like this or update all the tests to work
# properly.
ret['%.2f' % float(pieces[0])] = pieces[1]
return ret
def _parse_time(str_time):
"""Converts s in '12:34:45' format to seconds. If s is
None, returns empty string"""
if str_time is None or str_time == '':
return ''
else:
obj_time = time.strptime(str_time, '%H:%M:%S')
return datetime.timedelta(
hours=obj_time.tm_hour,
minutes=obj_time.tm_min,
seconds=obj_time.tm_sec
).total_seconds()
...@@ -189,3 +189,10 @@ ...@@ -189,3 +189,10 @@
} }
} }
// UI archetypes - well
.ui-well {
@include box-shadow(inset 0 1px 2px 1px $shadow);
padding: ($baseline*0.75);
}
...@@ -16,13 +16,16 @@ ...@@ -16,13 +16,16 @@
<vertical slug="vertical_94" graceperiod="1 day 12 hours 59 minutes 59 seconds" <vertical slug="vertical_94" graceperiod="1 day 12 hours 59 minutes 59 seconds"
showanswer="attempted" rerandomize="never"> showanswer="attempted" rerandomize="never">
<video <video
youtube="0.75:XNh13VZhThQ,1.0:XbDRmF6J0K0,1.25:JDty12WEQWk,1.50:wELKGj-5iyM" youtube_id_0_75="&quot;XNh13VZhThQ&quot;"
slug="What_s_next" name="What's next" /> youtube_id_1_0="&quot;XbDRmF6J0K0&quot;"
youtube_id_1_25="&quot;JDty12WEQWk&quot;"
youtube_id_1_5="&quot;wELKGj-5iyM&quot;"
slug="What_s_next"
name="What's next"/>
<html slug="html_95">Minor correction: Six elements (five resistors)… <html slug="html_95">Minor correction: Six elements (five resistors)…
</html> </html>
<customtag tag="S1" slug="discuss_96" impl="discuss" /> <customtag tag="S1" slug="discuss_96" impl="discuss" />
</vertical> </vertical>
<randomize url_name="PS1_Q4" display_name="Problem 4: Read a Molecule"> <randomize url_name="PS1_Q4" display_name="Problem 4: Read a Molecule">
<vertical> <vertical>
<html slug="html_900"> <html slug="html_900">
......
<sequential> <sequential>
<video youtube="1.50:8kARlsUt9lM,1.25:4cLA-IME32w,1.0:pFOrD8k9_p4,0.75:CcgAYu0n0bg" slug="S1V9_Demo_Setup_-_Lumped_Elements" name="S1V9: Demo Setup - Lumped Elements"/> <video youtube_id_1_5="&quot;8kARlsUt9lM&quot;" youtube_id_1_25="&quot;4cLA-IME32w&quot;" youtube_id_1_0="&quot;pFOrD8k9_p4&quot;" youtube_id_0_75="&quot;CcgAYu0n0bg&quot;" slug="S1V9_Demo_Setup_-_Lumped_Elements" name="S1V9: Demo Setup - Lumped Elements"/>
<customtag tag="S1" slug="discuss_59" impl="discuss"/> <customtag tag="S1" slug="discuss_59" impl="discuss"/>
<customtag page="29" slug="book_60" impl="book"/> <customtag page="29" slug="book_60" impl="book"/>
<customtag lecnum="1" slug="slides_61" impl="slides"/> <customtag lecnum="1" slug="slides_61" impl="slides"/>
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<!-- UTF-8 characters are acceptable… HTML entities are not --> <!-- UTF-8 characters are acceptable… HTML entities are not -->
<h1>Inline content…</h1> <h1>Inline content…</h1>
</html> </html>
<video youtube="1.50:vl9xrfxcr38,1.25:qxNX4REGqx4,1.0:BGU1poJDgOY,0.75:8rK9vnpystQ" slug="S1V14_Summary" name="S1V14: Summary"/> <video youtube_id_1_5="&quot;vl9xrfxcr38&quot;" youtube_id_1_25="&quot;qxNX4REGqx4&quot;" youtube_id_1_0="&quot;BGU1poJDgOY&quot;" youtube_id_0_75="&quot;8rK9vnpystQ&quot;" slug="S1V14_Summary" name="S1V14: Summary"/>
<customtag tag="S1" slug="discuss_91" impl="discuss"/> <customtag tag="S1" slug="discuss_91" impl="discuss"/>
<customtag page="70" slug="book_92" impl="book"/> <customtag page="70" slug="book_92" impl="book"/>
<customtag lecnum="1" slug="slides_93" impl="slides"/> <customtag lecnum="1" slug="slides_93" impl="slides"/>
......
<sequential> <sequential>
<video youtube="0.75:3NIegrCmA5k,1.0:eLAyO33baQ8,1.25:m1zWi_sh4Aw,1.50:EG-fRTJln_E" slug="S2V1_Review_KVL_KCL" name="S2V1: Review KVL, KCL"/> <video youtube_id_0_75="&quot;3NIegrCmA5k&quot;" youtube_id_1_0="&quot;eLAyO33baQ8&quot;" youtube_id_1_25="&quot;m1zWi_sh4Aw&quot;" youtube_id_1_5="&quot;EG-fRTJln_E&quot;" slug="S2V1_Review_KVL_KCL" name="S2V1: Review KVL, KCL"/>
<customtag tag="S2" slug="discuss_95" impl="discuss"/> <customtag tag="S2" slug="discuss_95" impl="discuss"/>
<customtag page="54" slug="book_96" impl="book"/> <customtag page="54" slug="book_96" impl="book"/>
<customtag lecnum="2" slug="slides_97" impl="slides"/> <customtag lecnum="2" slug="slides_97" impl="slides"/>
......
<sequential> <sequential>
<video youtube="0.75:S_1NaY5te8Q,1.0:G_2F9wivspM,1.25:b-r7dISY-Uc,1.50:jjxHom0oXWk" slug="S2V2_Demo-_KVL_KCL" name="S2V2: Demo- KVL, KCL"/> <video youtube_id_0_75="&quot;S_1NaY5te8Q&quot;" youtube_id_1_0="&quot;G_2F9wivspM&quot;" youtube_id_1_25="&quot;b-r7dISY-Uc&quot;" youtube_id_1_5="&quot;jjxHom0oXWk&quot;" slug="S2V2_Demo-_KVL_KCL" name="S2V2: Demo- KVL, KCL"/>
<customtag tag="S2" slug="discuss_99" impl="discuss"/> <customtag tag="S2" slug="discuss_99" impl="discuss"/>
<customtag page="56" slug="book_100" impl="book"/> <customtag page="56" slug="book_100" impl="book"/>
<customtag lecnum="2" slug="slides_101" impl="slides"/> <customtag lecnum="2" slug="slides_101" impl="slides"/>
......
<video youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" format="Video" display_name="Welcome…"/> <video youtube_id_0_75="&quot;izygArpw-Qo&quot;" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;" youtube_id_1_25="&quot;1EeWXzPdhSA&quot;" youtube_id_1_5="&quot;rABDYkeK0x8&quot;" format="Video" display_name="Welcome…"/>
<course name="A Simple Course" org="edX" course="simple" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall"> <course name="A Simple Course" org="edX" course="simple" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall">
<chapter name="Overview"> <chapter name="Overview">
<video name="Welcome" youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"/> <video name="Welcome" youtube_id_0_75="&quot;izygArpw-Qo&quot;" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;" youtube_id_1_25="&quot;1EeWXzPdhSA&quot;" youtube_id_1_5="&quot;rABDYkeK0x8&quot;"/>
<videosequence format="Lecture Sequence" name="A simple sequence"> <videosequence format="Lecture Sequence" name="A simple sequence">
<html name="toylab" filename="toylab"/> <html name="toylab" filename="toylab"/>
<video name="S0V1: Video Resources" youtube="0.75:EuzkdzfR0i8,1.0:1bK-WdDi6Qw,1.25:0v1VzoDVUTM,1.50:Bxk_-ZJb240"/> <video name="S0V1: Video Resources" youtube_id_0_75="&quot;EuzkdzfR0i8&quot;" youtube_id_1_0="&quot;1bK-WdDi6Qw&quot;" youtube_id_1_25="&quot;0v1VzoDVUTM&quot;" youtube_id_1_5="&quot;Bxk_-ZJb240&quot;"/>
</videosequence> </videosequence>
<section name="Lecture 2"> <section name="Lecture 2">
<sequential> <sequential>
<video youtube="1.0:TBvX7HzxexQ"/> <video youtube_id_1_0="&quot;TBvX7HzxexQ&quot;"/>
<problem name="L1 Problem 1" points="1" type="lecture" showanswer="attempted" filename="L1_Problem_1" rerandomize="never"/> <problem name="L1 Problem 1" points="1" type="lecture" showanswer="attempted" filename="L1_Problem_1" rerandomize="never"/>
</sequential> </sequential>
</section> </section>
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<problem type="lecture" showanswer="attempted" rerandomize="true" display_name="A simple coding problem" name="Simple coding problem" filename="ps01-simple" url_name="ps01-simple"/> <problem type="lecture" showanswer="attempted" rerandomize="true" display_name="A simple coding problem" name="Simple coding problem" filename="ps01-simple" url_name="ps01-simple"/>
</sequential> </sequential>
</section> </section>
<video name="Lost Video" youtube="1.0:TBvX7HzxexQ"/> <video name="Lost Video" youtube_id_1_0="&quot;TBvX7HzxexQ&quot;"/>
<sequential format="Lecture Sequence" url_name='test_sequence'> <sequential format="Lecture Sequence" url_name='test_sequence'>
<vertical url_name='test_vertical'> <vertical url_name='test_vertical'>
<html url_name='test_html'> <html url_name='test_html'>
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
<chapter url_name="Overview"> <chapter url_name="Overview">
<videosequence url_name="Toy_Videos"> <videosequence url_name="Toy_Videos">
<html url_name="toylab"/> <html url_name="toylab"/>
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/> <video url_name="Video_Resources" youtube_id_1_0="&quot;1bK-WdDi6Qw&quot;"/>
</videosequence> </videosequence>
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/> <video url_name="Welcome" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;"/>
</chapter> </chapter>
<chapter url_name="Ch2"> <chapter url_name="Ch2">
<html url_name="test_html"> <html url_name="test_html">
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
<chapter url_name="Overview"> <chapter url_name="Overview">
<videosequence url_name="Toy_Videos"> <videosequence url_name="Toy_Videos">
<html url_name="toylab"/> <html url_name="toylab"/>
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/> <video url_name="Video_Resources" youtube_id_1_0="&quot;1bK-WdDi6Qw&quot;"/>
</videosequence> </videosequence>
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/> <video url_name="Welcome" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;"/>
</chapter> </chapter>
<chapter url_name="Ch2"> <chapter url_name="Ch2">
<html url_name="test_html"> <html url_name="test_html">
......
<chapter> <chapter>
<video url_name="toyvideo" youtube="blahblah"/> <video url_name="toyvideo" youtube_id_1_0="&quot;OEoXaMPEzfM&quot;"/>
</chapter> </chapter>
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
<chapter url_name="Overview"> <chapter url_name="Overview">
<videosequence url_name="Toy_Videos"> <videosequence url_name="Toy_Videos">
<html url_name="secret:toylab"/> <html url_name="secret:toylab"/>
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/> <video url_name="Video_Resources" youtube_id_1_0="&quot;1bK-WdDi6Qw&quot;"/>
</videosequence> </videosequence>
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/> <video url_name="Welcome" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;"/>
<video url_name="video_123456789012" youtube="1.0:p2Q6BrNhdh8"/> <video url_name="video_123456789012" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;"/>
<video url_name="video_123456789012" youtube="1.0:p2Q6BrNhdh8"/> <video url_name="video_4f66f493ac8f" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;"/>
</chapter> </chapter>
<chapter url_name="secret:magic"/> <chapter url_name="secret:magic"/>
</course> </course>
<video youtube="1.0:1bK-WdDi6Qw" display_name="Video Resources"/> <video youtube_id_1_0="&quot;1bK-WdDi6Qw&quot;" display_name="Video Resources"/>
<video youtube="1.0:p2Q6BrNhdh9" display_name="Welcome"/> <video youtube_id_1_0="p2Q6BrNhdh9" display_name="Welcome"/>
...@@ -63,6 +63,25 @@ To get a full list of available rake tasks, use: ...@@ -63,6 +63,25 @@ To get a full list of available rake tasks, use:
rake -T rake -T
### Troubleshooting
#### Reference Error: XModule is not defined (javascript)
This means that the javascript defining an xmodule hasn't loaded correctly. There are a number
of different things that could be causing this:
1. See `Error: watch EMFILE`
#### Error: watch EMFILE (coffee)
When running a development server, we also start a watcher process alongside to recompile coffeescript
and sass as changes are made. On Mac OSX systems, the coffee watcher process takes more file handles
than are allowed by default. This will result in `EMFILE` errors when coffeescript is running, and
will prevent javascript from compiling, leading to the error 'XModule is not defined'
To work around this issue, we use `Process::setrlimit` to set the number of allowed open files.
Coffee watches both directories and files, so you will need to set this fairly high (anecdotally,
8000 seems to do the trick on OSX 10.7.5, 10.8.3, and 10.8.4)
## Running Tests ## Running Tests
See `testing.md` for instructions on running the test suite. See `testing.md` for instructions on running the test suite.
......
...@@ -122,11 +122,6 @@ In production, the django `collectstatic` command recompiles everything and puts ...@@ -122,11 +122,6 @@ In production, the django `collectstatic` command recompiles everything and puts
In development, we don't use collectstatic, instead accessing the files in place. The auto-compilation is run via `common/djangoapps/pipeline_mako/templates/static_content.html`. Details: templates include `<%namespace name='static' file='static_content.html'/>`, then something like `<%static:css group='application'/>` to call the functions in `common/djangoapps/pipeline_mako/__init__.py`, which call the `django-pipeline` compilers. In development, we don't use collectstatic, instead accessing the files in place. The auto-compilation is run via `common/djangoapps/pipeline_mako/templates/static_content.html`. Details: templates include `<%namespace name='static' file='static_content.html'/>`, then something like `<%static:css group='application'/>` to call the functions in `common/djangoapps/pipeline_mako/__init__.py`, which call the `django-pipeline` compilers.
### Other modules
- Wiki -- in `lms/djangoapps/simplewiki`. Has some markdown extentions for embedding circuits, videos, etc.
## Testing ## Testing
See `testing.md`. See `testing.md`.
......
...@@ -24,7 +24,10 @@ be specified for this tag:: ...@@ -24,7 +24,10 @@ be specified for this tag::
sources - location id of required modules, separated by ';' sources - location id of required modules, separated by ';'
[message | ""] - message for case, where one or more are not passed. Here you can use variable {link}, which generate link to required module. [message | ""] - message for case, where one or more are not passed. Here you can use variable {link}, which generate link to required module.
[completed] - map to `is_completed` module method [submitted] - map to `is_submitted` module method.
(pressing RESET button makes this function to return False.)
[correct] - map to `is_correct` module method
[attempted] - map to `is_attempted` module method [attempted] - map to `is_attempted` module method
[poll_answer] - map to `poll_answer` module attribute [poll_answer] - map to `poll_answer` module attribute
[voted] - map to `voted` module attribute [voted] - map to `voted` module attribute
...@@ -53,7 +56,7 @@ Examples of conditional depends on poll ...@@ -53,7 +56,7 @@ Examples of conditional depends on poll
</conditional> </conditional>
Examples of conditional depends on poll (use <show> tag) Examples of conditional depends on poll (use <show> tag)
------------------------------------------- --------------------------------------------------------
.. code-block:: xml .. code-block:: xml
......
...@@ -420,6 +420,6 @@ Draggables can be reused ...@@ -420,6 +420,6 @@ Draggables can be reused
.. literalinclude:: drag-n-drop-demo2.xml .. literalinclude:: drag-n-drop-demo2.xml
Examples of targets on draggables Examples of targets on draggables
------------------------ ---------------------------------
.. literalinclude:: drag-n-drop-demo3.xml .. literalinclude:: drag-n-drop-demo3.xml
...@@ -362,7 +362,7 @@ that has to be updated on a parameter's change, then one can define ...@@ -362,7 +362,7 @@ that has to be updated on a parameter's change, then one can define
a special function to handle this. The "output" of such a function must be a special function to handle this. The "output" of such a function must be
set to "none", and the JavaScript code inside this function must update the set to "none", and the JavaScript code inside this function must update the
MathJax element by itself. Before exiting, MathJax typeset function should MathJax element by itself. Before exiting, MathJax typeset function should
be called so that the new text will be re-rendered by MathJax. For example, be called so that the new text will be re-rendered by MathJax. For example::
<render> <render>
... ...
......
...@@ -28,6 +28,7 @@ Specific Problem Types ...@@ -28,6 +28,7 @@ Specific Problem Types
course_data_formats/conditional_module/conditional_module.rst course_data_formats/conditional_module/conditional_module.rst
course_data_formats/word_cloud/word_cloud.rst course_data_formats/word_cloud/word_cloud.rst
course_data_formats/custom_response.rst course_data_formats/custom_response.rst
course_data_formats/symbolic_response.rst
Internal Data Formats Internal Data Formats
......
*******************************************
Calc
*******************************************
.. automodule:: calc
:members:
:show-inheritance:
...@@ -8,14 +8,6 @@ Contents: ...@@ -8,14 +8,6 @@ Contents:
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
chem.rst
Calc
====
.. automodule:: capa.calc
:members:
:show-inheritance:
Capa_problem Capa_problem
============ ============
......
******************************************* *******************************************
Chem module Chemistry modules
******************************************* *******************************************
.. module:: chem .. module:: chem
...@@ -7,7 +7,7 @@ Chem module ...@@ -7,7 +7,7 @@ Chem module
Miller Miller
====== ======
.. automodule:: capa.chem.miller .. automodule:: chem.miller
:members: :members:
:show-inheritance: :show-inheritance:
...@@ -47,14 +47,14 @@ Documentation from **crystallography.js**:: ...@@ -47,14 +47,14 @@ Documentation from **crystallography.js**::
Chemcalc Chemcalc
======== ========
.. automodule:: capa.chem.chemcalc .. automodule:: chem.chemcalc
:members: :members:
:show-inheritance: :show-inheritance:
Chemtools Chemtools
========= =========
.. automodule:: capa.chem.chemtools .. automodule:: chem.chemtools
:members: :members:
:show-inheritance: :show-inheritance:
...@@ -62,7 +62,7 @@ Chemtools ...@@ -62,7 +62,7 @@ Chemtools
Tests Tests
===== =====
.. automodule:: capa.chem.tests .. automodule:: chem.tests
:members: :members:
:show-inheritance: :show-inheritance:
......
...@@ -4,86 +4,3 @@ CMS module ...@@ -4,86 +4,3 @@ CMS module
.. module:: cms .. module:: cms
Auth
====
.. automodule:: auth
:members:
:show-inheritance:
Authz
-----
.. automodule:: auth.authz
:members:
:show-inheritance:
Content store
=============
.. .. automodule:: contentstore
.. :members:
.. :show-inheritance:
.. Utils
.. -----
.. .. automodule:: contentstore.untils
.. :members:
.. :show-inheritance:
.. Views
.. -----
.. .. automodule:: contentstore.views
.. :members:
.. :show-inheritance:
.. Management
.. ----------
.. .. automodule:: contentstore.management
.. :members:
.. :show-inheritance:
.. Tests
.. -----
.. .. automodule:: contentstore.tests
.. :members:
.. :show-inheritance:
Github sync
===========
.. automodule:: github_sync
:members:
:show-inheritance:
Exceptions
----------
.. automodule:: github_sync.exceptions
:members:
:show-inheritance:
Views
-----
.. automodule:: github_sync.views
:members:
:show-inheritance:
Management
----------
.. automodule:: github_sync.management
:members:
:show-inheritance:
Tests
-----
.. .. automodule:: github_sync.tests
.. :members:
.. :show-inheritance:
\ No newline at end of file
...@@ -7,3 +7,8 @@ Contents: ...@@ -7,3 +7,8 @@ Contents:
xmodule.rst xmodule.rst
capa.rst capa.rst
chem.rst
sandbox-packages.rst
symmath.rst
calc.rst
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #pylint: disable=C0103
# MITx documentation build configuration file, created by #pylint: disable=W0622
# sphinx-quickstart on Fri Nov 2 15:43:00 2012. #pylint: disable=W0212
# #pylint: disable=W0613
# This file is execfile()d with the current directory set to its containing dir. """ EdX documentation build configuration file, created by
# sphinx-quickstart on Fri Nov 2 15:43:00 2012.
# Note that not all possible configuration values are present in this
# autogenerated file. This file is execfile()d with the current directory set to its containing dir.
#
# All configuration values have a default; values that are commented out Note that not all possible configuration values are present in this
# serve to show the default. autogenerated file.
import sys, os All configuration values have a default; values that are commented out
serve to show the default."""
import sys
import os
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('.')) # sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('../..')) # mitx folder sys.path.insert(0, os.path.abspath('../..')) # mitx folder
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'capa')) # capa module
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'xmodule')) # xmodule
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'lms', 'djangoapps')) # lms djangoapps
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'cms', 'djangoapps')) # cms djangoapps
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'djangoapps')) # common djangoapps
# django configuration - careful here # django configuration - careful here
import os os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.test'
os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.dev'
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------
...@@ -36,7 +34,9 @@ os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.dev' ...@@ -36,7 +34,9 @@ os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.dev'
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode'] extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage',
'sphinx.ext.pngmath', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
...@@ -51,17 +51,17 @@ source_suffix = '.rst' ...@@ -51,17 +51,17 @@ source_suffix = '.rst'
master_doc = 'index' master_doc = 'index'
# General information about the project. # General information about the project.
project = u'MITx' project = u'EdX Dev Data'
copyright = u'2012, MITx team' copyright = u'2012-13, EdX team'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '1.0' version = '0.2'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '1.0' release = '0.2'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
...@@ -75,7 +75,7 @@ release = '1.0' ...@@ -75,7 +75,7 @@ release = '1.0'
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
exclude_patterns = [] exclude_patterns = ['build']
# The reST default role (used for this markup: `text`) to use for all documents. # The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None #default_role = None
...@@ -175,27 +175,27 @@ html_static_path = ['_static'] ...@@ -175,27 +175,27 @@ html_static_path = ['_static']
#html_file_suffix = None #html_file_suffix = None
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'MITxdoc' htmlhelp_basename = 'edXDocs'
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------------
latex_elements = { latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper', #'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt', #'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
#'preamble': '', #'preamble': '',
} }
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
('index', 'MITx.tex', u'MITx Documentation', ('index', 'edXDocs.tex', u'EdX Dev Data Documentation',
u'MITx team', 'manual'), u'EdX Team', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
...@@ -224,8 +224,8 @@ latex_documents = [ ...@@ -224,8 +224,8 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
('index', 'mitx', u'MITx Documentation', ('index', 'edxdocs', u'EdX Dev Data Documentation',
[u'MITx team'], 1) [u'EdX Team'], 1)
] ]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
...@@ -238,8 +238,8 @@ man_pages = [ ...@@ -238,8 +238,8 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', 'MITx', u'MITx Documentation', ('index', 'EdXDocs', u'EdX Dev Data Documentation',
u'MITx team', 'MITx', 'One line description of project.', u'EdX Team', 'EdXDocs', 'One line description of project.',
'Miscellaneous'), 'Miscellaneous'),
] ]
...@@ -265,8 +265,12 @@ from django.utils.encoding import force_unicode ...@@ -265,8 +265,12 @@ from django.utils.encoding import force_unicode
def process_docstring(app, what, name, obj, options, lines): def process_docstring(app, what, name, obj, options, lines):
"""Autodoc django models"""
# This causes import errors if left outside the function # This causes import errors if left outside the function
from django.db import models from django.db import models
# If you want extract docs from django forms:
# from django import forms # from django import forms
# from django.forms.models import BaseInlineFormSet # from django.forms.models import BaseInlineFormSet
...@@ -326,5 +330,6 @@ def process_docstring(app, what, name, obj, options, lines): ...@@ -326,5 +330,6 @@ def process_docstring(app, what, name, obj, options, lines):
def setup(app): def setup(app):
# Register the docstring processor with sphinx """Setup docsting processors"""
#Register the docstring processor with sphinx
app.connect('autodoc-process-docstring', process_docstring) app.connect('autodoc-process-docstring', process_docstring)
.. MITx documentation master file, created by .. EdX Dev documentation master file, created by
sphinx-quickstart on Fri Nov 2 15:43:00 2012. sphinx-quickstart on Fri Nov 2 15:43:00 2012.
You can adapt this file completely to your liking, but it should at least You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive. contain the root `toctree` directive.
Welcome to MITx's documentation! Welcome to EdX's Dev documentation!
================================ ===================================
Contents: Contents:
......
...@@ -314,34 +314,6 @@ Psychoanalyze ...@@ -314,34 +314,6 @@ Psychoanalyze
:members: :members:
:show-inheritance: :show-inheritance:
Simple wiki
===========
.. automodule:: simplewiki
:members:
:show-inheritance:
Models
------
.. automodule:: simplewiki.models
:members:
:show-inheritance:
Views
-----
.. automodule:: simplewiki.views
:members:
:show-inheritance:
Tests
-----
.. automodule:: simplewiki.tests
:members:
:show-inheritance:
Static template view Static template view
==================== ====================
......
******************************************* *******************************************
What the pieces are? Overview
******************************************* *******************************************
What
====
... This is EdX Dev documentation, mainly extracted from docstrings.
Autogenerated by Sphinx from python code.
Soon support for JS will be impemented.
How
===
...
Who
===
...
\ No newline at end of file
*******************************************
Sandbox-packages
*******************************************
.. module:: sandbox-packages
Loncapa
=======
.. automodule:: loncapa.loncapa_check
:members:
:show-inheritance:
\ No newline at end of file
*******************************************
Symmath
*******************************************
.. module:: symmath
Formula
=======
.. automodule:: symmath.formula
:members:
:show-inheritance:
Symmath check
=============
.. automodule:: symmath.symmath_check
:members:
:show-inheritance:
Symmath tests
=============
.. automodule:: symmath.test_formula
:members:
:show-inheritance:
.. automodule:: symmath.test_symmath_check
:members:
:show-inheritance:
\ No newline at end of file
...@@ -144,13 +144,6 @@ Templates ...@@ -144,13 +144,6 @@ Templates
:members: :members:
:show-inheritance: :show-inheritance:
Time parse
==========
.. automodule:: xmodule.timeparse
:members:
:show-inheritance:
Vertical Vertical
======== ========
......
...@@ -3,6 +3,7 @@ from certificates.models import certificate_status_for_student ...@@ -3,6 +3,7 @@ from certificates.models import certificate_status_for_student
from certificates.models import CertificateStatuses as status from certificates.models import CertificateStatuses as status
from certificates.models import CertificateWhitelist from certificates.models import CertificateWhitelist
from mitxmako.middleware import MakoMiddleware
from courseware import grades, courses from courseware import grades, courses
from django.test.client import RequestFactory from django.test.client import RequestFactory
from capa.xqueue_interface import XQueueInterface from capa.xqueue_interface import XQueueInterface
...@@ -51,6 +52,14 @@ class XQueueCertInterface(object): ...@@ -51,6 +52,14 @@ class XQueueCertInterface(object):
""" """
def __init__(self, request=None): def __init__(self, request=None):
# MakoMiddleware Note:
# Line below has the side-effect of writing to a module level lookup
# table that will allow problems to render themselves. If this is not
# present, problems that a student hasn't seen will error when loading,
# causing the grading system to under-count the possible score and
# inflate their grade. This dependency is bad and was probably recently
# introduced. This is the bandage until we can trace the root cause.
m = MakoMiddleware()
# Get basic auth (username/password) for # Get basic auth (username/password) for
# xqueue connection if it's in the settings # xqueue connection if it's in the settings
...@@ -161,6 +170,10 @@ class XQueueCertInterface(object): ...@@ -161,6 +170,10 @@ class XQueueCertInterface(object):
cert, created = GeneratedCertificate.objects.get_or_create( cert, created = GeneratedCertificate.objects.get_or_create(
user=student, course_id=course_id) user=student, course_id=course_id)
# Needed
self.request.user = student
self.request.session = {}
grade = grades.grade(student, self.request, course) grade = grades.grade(student, self.request, course)
is_whitelisted = self.whitelist.filter( is_whitelisted = self.whitelist.filter(
user=student, course_id=course_id, whitelist=True).exists() user=student, course_id=course_id, whitelist=True).exists()
...@@ -211,5 +224,5 @@ class XQueueCertInterface(object): ...@@ -211,5 +224,5 @@ class XQueueCertInterface(object):
(error, msg) = self.xqueue_interface.send_to_queue( (error, msg) = self.xqueue_interface.send_to_queue(
header=xheader, body=json.dumps(contents)) header=xheader, body=json.dumps(contents))
if error: if error:
logger.critical('Unable to add a request to the queue') logger.critical('Unable to add a request to the queue: {} {}'.format(error, msg))
raise Exception('Unable to send queue message') raise Exception('Unable to send queue message')
...@@ -83,15 +83,18 @@ def click_on_section(step, section): ...@@ -83,15 +83,18 @@ def click_on_section(step, section):
world.css_click(section_css) world.css_click(section_css)
subid = "ui-accordion-accordion-panel-" + str(int(section) - 1) subid = "ui-accordion-accordion-panel-" + str(int(section) - 1)
subsection_css = 'ul[id="%s"]> li > a' % subid subsection_css = 'ul.ui-accordion-content-active[id=\'%s\'] > li > a' % subid
prev_url = world.browser.url
changed_section = lambda: prev_url != world.browser.url
#for some reason needed to do it in two steps #for some reason needed to do it in two steps
world.css_find(subsection_css).click() world.css_click(subsection_css, success_condition=changed_section)
@step(u'I click on subsection "([^"]*)"$') @step(u'I click on subsection "([^"]*)"$')
def click_on_subsection(step, subsection): def click_on_subsection(step, subsection):
subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]> li > a' subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]> li > a'
world.css_find(subsection_css)[int(subsection) - 1].click() world.css_click(subsection_css, index=(int(subsection) - 1))
@step(u'I click on sequence "([^"]*)"$') @step(u'I click on sequence "([^"]*)"$')
......
...@@ -135,12 +135,10 @@ def action_button_present(_step, buttonname, doesnt_appear): ...@@ -135,12 +135,10 @@ def action_button_present(_step, buttonname, doesnt_appear):
@step(u'the button with the label "([^"]*)" does( not)? appear') @step(u'the button with the label "([^"]*)" does( not)? appear')
def button_with_label_present(step, buttonname, doesnt_appear): def button_with_label_present(step, buttonname, doesnt_appear):
button_css = 'button span.show-label'
elem = world.css_find(button_css).first
if doesnt_appear: if doesnt_appear:
assert_not_equal(elem.text, buttonname) world.browser.is_text_not_present(buttonname, wait_time=5)
else: else:
assert_equal(elem.text, buttonname) world.browser.is_text_present(buttonname, wait_time=5)
@step(u'My "([^"]*)" answer is marked "([^"]*)"') @step(u'My "([^"]*)" answer is marked "([^"]*)"')
......
...@@ -142,34 +142,34 @@ def answer_problem(problem_type, correctness): ...@@ -142,34 +142,34 @@ def answer_problem(problem_type, correctness):
elif problem_type == "multiple choice": elif problem_type == "multiple choice":
if correctness == 'correct': if correctness == 'correct':
inputfield('multiple choice', choice='choice_2').check() world.css_check(inputfield('multiple choice', choice='choice_2'))
else: else:
inputfield('multiple choice', choice='choice_1').check() world.css_check(inputfield('multiple choice', choice='choice_1'))
elif problem_type == "checkbox": elif problem_type == "checkbox":
if correctness == 'correct': if correctness == 'correct':
inputfield('checkbox', choice='choice_0').check() world.css_check(inputfield('checkbox', choice='choice_0'))
inputfield('checkbox', choice='choice_2').check() world.css_check(inputfield('checkbox', choice='choice_2'))
else: else:
inputfield('checkbox', choice='choice_3').check() world.css_check(inputfield('checkbox', choice='choice_3'))
elif problem_type == 'radio': elif problem_type == 'radio':
if correctness == 'correct': if correctness == 'correct':
inputfield('radio', choice='choice_2').check() world.css_check(inputfield('radio', choice='choice_2'))
else: else:
inputfield('radio', choice='choice_1').check() world.css_check(inputfield('radio', choice='choice_1'))
elif problem_type == 'string': elif problem_type == 'string':
textvalue = 'correct string' if correctness == 'correct' else 'incorrect' textvalue = 'correct string' if correctness == 'correct' else 'incorrect'
inputfield('string').fill(textvalue) world.css_fill(inputfield('string'), textvalue)
elif problem_type == 'numerical': elif problem_type == 'numerical':
textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2)) textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2))
inputfield('numerical').fill(textvalue) world.css_fill(inputfield('numerical'), textvalue)
elif problem_type == 'formula': elif problem_type == 'formula':
textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2'
inputfield('formula').fill(textvalue) world.css_fill(inputfield('formula'), textvalue)
elif problem_type == 'script': elif problem_type == 'script':
# Correct answer is any two integers that sum to 10 # Correct answer is any two integers that sum to 10
...@@ -181,8 +181,8 @@ def answer_problem(problem_type, correctness): ...@@ -181,8 +181,8 @@ def answer_problem(problem_type, correctness):
if correctness == 'incorrect': if correctness == 'incorrect':
second_addend += random.randint(1, 10) second_addend += random.randint(1, 10)
inputfield('script', input_num=1).fill(str(first_addend)) world.css_fill(inputfield('script', input_num=1), str(first_addend))
inputfield('script', input_num=2).fill(str(second_addend)) world.css_fill(inputfield('script', input_num=2), str(second_addend))
elif problem_type == 'code': elif problem_type == 'code':
# The fake xqueue server is configured to respond # The fake xqueue server is configured to respond
...@@ -281,11 +281,11 @@ def add_problem_to_course(course, problem_type, extraMeta=None): ...@@ -281,11 +281,11 @@ def add_problem_to_course(course, problem_type, extraMeta=None):
def inputfield(problem_type, choice=None, input_num=1): def inputfield(problem_type, choice=None, input_num=1):
""" Return the <input> element for *problem_type*. """ Return the css selector for `problem_type`.
For example, if problem_type is 'string', return For example, if problem_type is 'string', return
the text field for the string problem in the test course. the text field for the string problem in the test course.
*choice* is the name of the checkbox input in a group `choice` is the name of the checkbox input in a group
of checkboxes. """ of checkboxes. """
sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" % sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" %
...@@ -299,7 +299,7 @@ def inputfield(problem_type, choice=None, input_num=1): ...@@ -299,7 +299,7 @@ def inputfield(problem_type, choice=None, input_num=1):
assert world.is_css_present(sel) assert world.is_css_present(sel)
# Retrieve the input element # Retrieve the input element
return world.browser.find_by_css(sel) return sel
def assert_checked(problem_type, choices): def assert_checked(problem_type, choices):
...@@ -312,7 +312,7 @@ def assert_checked(problem_type, choices): ...@@ -312,7 +312,7 @@ def assert_checked(problem_type, choices):
all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3'] all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3']
for this_choice in all_choices: for this_choice in all_choices:
element = inputfield(problem_type, choice=this_choice) element = world.css_find(inputfield(problem_type, choice=this_choice))
if this_choice in choices: if this_choice in choices:
assert element.checked assert element.checked
...@@ -321,5 +321,5 @@ def assert_checked(problem_type, choices): ...@@ -321,5 +321,5 @@ def assert_checked(problem_type, choices):
def assert_textfield(problem_type, expected_text, input_num=1): def assert_textfield(problem_type, expected_text, input_num=1):
element = inputfield(problem_type, input_num=input_num) element = world.css_find(inputfield(problem_type, input_num=input_num))
assert element.value == expected_text assert element.value == expected_text
Feature: Video component Feature: Video component
As a student, I want to view course videos in LMS. As a student, I want to view course videos in LMS.
Scenario: Autoplay is enabled in LMS Scenario: Autoplay is enabled in LMS for a Video component
Given the course has a Video component Given the course has a Video component
Then when I view the video it has autoplay enabled Then when I view the video it has autoplay enabled
Scenario: Autoplay is enabled in the LMS for a VideoAlpha component
Given the course has a VideoAlpha component
Then when I view the video it has autoplay enabled
...@@ -27,8 +27,30 @@ def view_video(_step): ...@@ -27,8 +27,30 @@ def view_video(_step):
world.browser.visit(url) world.browser.visit(url)
@step('the course has a VideoAlpha component')
def view_videoalpha(step):
coursename = TEST_COURSE_NAME.replace(' ', '_')
i_am_registered_for_the_course(step, coursename)
# Make sure we have a videoalpha
add_videoalpha_to_course(coursename)
chapter_name = TEST_SECTION_NAME.replace(" ", "_")
section_name = chapter_name
url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' %
(chapter_name, section_name))
world.browser.visit(url)
def add_video_to_course(course): def add_video_to_course(course):
template_name = 'i4x://edx/templates/video/default' template_name = 'i4x://edx/templates/video/default'
world.ItemFactory.create(parent_location=section_location(course), world.ItemFactory.create(parent_location=section_location(course),
template=template_name, template=template_name,
display_name='Video') display_name='Video')
def add_videoalpha_to_course(course):
template_name = 'i4x://edx/templates/videoalpha/Video_Alpha'
world.ItemFactory.create(parent_location=section_location(course),
template=template_name,
display_name='Video Alpha')
Feature: Video Alpha component
As a student, I want to view course videos in LMS.
Scenario: Autoplay is enabled in LMS
Given the course has a Video component
Then when I view the video it has autoplay enabled
#pylint: disable=C0111
#pylint: disable=W0613
#pylint: disable=W0621
from lettuce import world, step
from lettuce.django import django_url
from common import TEST_COURSE_NAME, TEST_SECTION_NAME, i_am_registered_for_the_course, section_location
############### ACTIONS ####################
@step('when I view the video it has autoplay enabled')
def does_autoplay(step):
assert(world.css_find('.videoalpha')[0]['data-autoplay'] == 'True')
@step('the course has a Video component')
def view_videoalpha(step):
coursename = TEST_COURSE_NAME.replace(' ', '_')
i_am_registered_for_the_course(step, coursename)
# Make sure we have a videoalpha
add_videoalpha_to_course(coursename)
chapter_name = TEST_SECTION_NAME.replace(" ", "_")
section_name = chapter_name
url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' %
(chapter_name, section_name))
world.browser.visit(url)
def add_videoalpha_to_course(course):
template_name = 'i4x://edx/templates/videoalpha/default'
world.ItemFactory.create(parent_location=section_location(course),
template=template_name,
display_name='Video Alpha 1')
...@@ -163,7 +163,7 @@ class ModelDataCache(object): ...@@ -163,7 +163,7 @@ class ModelDataCache(object):
return self._chunked_query( return self._chunked_query(
XModuleStudentPrefsField, XModuleStudentPrefsField,
'module_type__in', 'module_type__in',
set(descriptor.location.category for descriptor in self.descriptors), set(descriptor.module_class.__name__ for descriptor in self.descriptors),
student=self.user.pk, student=self.user.pk,
field_name__in=set(field.name for field in fields), field_name__in=set(field.name for field in fields),
) )
......
...@@ -13,7 +13,7 @@ from django.test.client import Client ...@@ -13,7 +13,7 @@ from django.test.client import Client
from student.tests.factories import UserFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from xmodule.tests import test_system from xmodule.tests import get_test_system
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
...@@ -77,7 +77,7 @@ class BaseTestXmodule(ModuleStoreTestCase): ...@@ -77,7 +77,7 @@ class BaseTestXmodule(ModuleStoreTestCase):
data=self.DATA data=self.DATA
) )
system = test_system() system = get_test_system()
system.render_template = lambda template, context: context system.render_template = lambda template, context: context
model_data = {'location': self.item_descriptor.location} model_data = {'location': self.item_descriptor.location}
model_data.update(self.MODEL_DATA) model_data.update(self.MODEL_DATA)
......
...@@ -75,7 +75,7 @@ class StudentPrefsFactory(DjangoModelFactory): ...@@ -75,7 +75,7 @@ class StudentPrefsFactory(DjangoModelFactory):
field_name = 'existing_field' field_name = 'existing_field'
value = json.dumps('old_value') value = json.dumps('old_value')
student = SubFactory(UserFactory) student = SubFactory(UserFactory)
module_type = 'problem' module_type = 'MockProblemModule'
class StudentInfoFactory(DjangoModelFactory): class StudentInfoFactory(DjangoModelFactory):
......
...@@ -29,6 +29,7 @@ def mock_descriptor(fields=[], lms_fields=[]): ...@@ -29,6 +29,7 @@ def mock_descriptor(fields=[], lms_fields=[]):
descriptor.location = location('def_id') descriptor.location = location('def_id')
descriptor.module_class.fields = fields descriptor.module_class.fields = fields
descriptor.module_class.lms.fields = lms_fields descriptor.module_class.lms.fields = lms_fields
descriptor.module_class.__name__ = 'MockProblemModule'
return descriptor return descriptor
location = partial(Location, 'i4x', 'edX', 'test_course', 'problem') location = partial(Location, 'i4x', 'edX', 'test_course', 'problem')
...@@ -37,7 +38,7 @@ course_id = 'edX/test_course/test' ...@@ -37,7 +38,7 @@ course_id = 'edX/test_course/test'
content_key = partial(LmsKeyValueStore.Key, Scope.content, None, location('def_id')) content_key = partial(LmsKeyValueStore.Key, Scope.content, None, location('def_id'))
settings_key = partial(LmsKeyValueStore.Key, Scope.settings, None, location('def_id')) settings_key = partial(LmsKeyValueStore.Key, Scope.settings, None, location('def_id'))
user_state_key = partial(LmsKeyValueStore.Key, Scope.user_state, 'user', location('def_id')) user_state_key = partial(LmsKeyValueStore.Key, Scope.user_state, 'user', location('def_id'))
prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'problem') prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'MockProblemModule')
user_info_key = partial(LmsKeyValueStore.Key, Scope.user_info, 'user', None) user_info_key = partial(LmsKeyValueStore.Key, Scope.user_info, 'user', None)
...@@ -190,6 +191,10 @@ class StorageTestBase(object): ...@@ -190,6 +191,10 @@ class StorageTestBase(object):
self.mdc = ModelDataCache([mock_descriptor([mock_field(self.scope, 'existing_field')])], course_id, self.user) self.mdc = ModelDataCache([mock_descriptor([mock_field(self.scope, 'existing_field')])], course_id, self.user)
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc) self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
def test_set_and_get_existing_field(self):
self.kvs.set(self.key_factory('existing_field'), 'test_value')
self.assertEquals('test_value', self.kvs.get(self.key_factory('existing_field')))
def test_get_existing_field(self): def test_get_existing_field(self):
"Test that getting an existing field in an existing Storage Field works" "Test that getting an existing field in an existing Storage Field works"
self.assertEquals('old_value', self.kvs.get(self.key_factory('existing_field'))) self.assertEquals('old_value', self.kvs.get(self.key_factory('existing_field')))
......
...@@ -22,7 +22,7 @@ from django.conf import settings ...@@ -22,7 +22,7 @@ from django.conf import settings
from xmodule.videoalpha_module import VideoAlphaDescriptor, VideoAlphaModule from xmodule.videoalpha_module import VideoAlphaDescriptor, VideoAlphaModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.tests import test_system from xmodule.tests import get_test_system
from xmodule.tests.test_logic import LogicTest from xmodule.tests.test_logic import LogicTest
...@@ -58,7 +58,7 @@ class VideoAlphaFactory(object): ...@@ -58,7 +58,7 @@ class VideoAlphaFactory(object):
descriptor = Mock(weight="1") descriptor = Mock(weight="1")
system = test_system() system = get_test_system()
system.render_template = lambda template, context: context system.render_template = lambda template, context: context
VideoAlphaModule.location = location VideoAlphaModule.location = location
module = VideoAlphaModule(system, descriptor, model_data) module = VideoAlphaModule(system, descriptor, model_data)
......
...@@ -95,13 +95,19 @@ class FolditTestCase(TestCase): ...@@ -95,13 +95,19 @@ class FolditTestCase(TestCase):
response = self.make_puzzle_score_request([1, 2], [0.078034, 0.080000]) response = self.make_puzzle_score_request([1, 2], [0.078034, 0.080000])
self.assertEqual(response.content, json.dumps( self.assertEqual(response.content, json.dumps(
[{"OperationID": "SetPlayerPuzzleScores", [{
"Value": [{ "OperationID": "SetPlayerPuzzleScores",
"Value": [
{
"PuzzleID": 1, "PuzzleID": 1,
"Status": "Success"}, "Status": "Success"
}, {
{"PuzzleID": 2, "PuzzleID": 2,
"Status": "Success"}]}])) "Status": "Success"
}
]
}]
))
def test_SetPlayerPuzzleScores_multiple(self): def test_SetPlayerPuzzleScores_multiple(self):
...@@ -126,9 +132,11 @@ class FolditTestCase(TestCase): ...@@ -126,9 +132,11 @@ class FolditTestCase(TestCase):
self.assertEqual(len(top_10), 1) self.assertEqual(len(top_10), 1)
# Floats always get in the way, so do almostequal # Floats always get in the way, so do almostequal
self.assertAlmostEqual(top_10[0]['score'], self.assertAlmostEqual(
top_10[0]['score'],
Score.display_score(better_score), Score.display_score(better_score),
delta=0.5) delta=0.5
)
# reporting a worse score shouldn't # reporting a worse score shouldn't
worse_score = 0.065 worse_score = 0.065
...@@ -137,9 +145,11 @@ class FolditTestCase(TestCase): ...@@ -137,9 +145,11 @@ class FolditTestCase(TestCase):
top_10 = Score.get_tops_n(10, puzzle_id) top_10 = Score.get_tops_n(10, puzzle_id)
self.assertEqual(len(top_10), 1) self.assertEqual(len(top_10), 1)
# should still be the better score # should still be the better score
self.assertAlmostEqual(top_10[0]['score'], self.assertAlmostEqual(
top_10[0]['score'],
Score.display_score(better_score), Score.display_score(better_score),
delta=0.5) delta=0.5
)
def test_SetPlayerPuzzleScores_manyplayers(self): def test_SetPlayerPuzzleScores_manyplayers(self):
""" """
...@@ -150,28 +160,34 @@ class FolditTestCase(TestCase): ...@@ -150,28 +160,34 @@ class FolditTestCase(TestCase):
puzzle_id = ['1'] puzzle_id = ['1']
player1_score = 0.08 player1_score = 0.08
player2_score = 0.02 player2_score = 0.02
response1 = self.make_puzzle_score_request(puzzle_id, player1_score, response1 = self.make_puzzle_score_request(
self.user) puzzle_id, player1_score, self.user
)
# There should now be a score in the db. # There should now be a score in the db.
top_10 = Score.get_tops_n(10, puzzle_id) top_10 = Score.get_tops_n(10, puzzle_id)
self.assertEqual(len(top_10), 1) self.assertEqual(len(top_10), 1)
self.assertEqual(top_10[0]['score'], Score.display_score(player1_score)) self.assertEqual(top_10[0]['score'], Score.display_score(player1_score))
response2 = self.make_puzzle_score_request(puzzle_id, player2_score, response2 = self.make_puzzle_score_request(
self.user2) puzzle_id, player2_score, self.user2
)
# There should now be two scores in the db # There should now be two scores in the db
top_10 = Score.get_tops_n(10, puzzle_id) top_10 = Score.get_tops_n(10, puzzle_id)
self.assertEqual(len(top_10), 2) self.assertEqual(len(top_10), 2)
# Top score should be player2_score. Second should be player1_score # Top score should be player2_score. Second should be player1_score
self.assertAlmostEqual(top_10[0]['score'], self.assertAlmostEqual(
top_10[0]['score'],
Score.display_score(player2_score), Score.display_score(player2_score),
delta=0.5) delta=0.5
self.assertAlmostEqual(top_10[1]['score'], )
self.assertAlmostEqual(
top_10[1]['score'],
Score.display_score(player1_score), Score.display_score(player1_score),
delta=0.5) delta=0.5
)
# Top score user should be self.user2.username # Top score user should be self.user2.username
self.assertEqual(top_10[0]['username'], self.user2.username) self.assertEqual(top_10[0]['username'], self.user2.username)
......
...@@ -36,9 +36,13 @@ def foldit_ops(request): ...@@ -36,9 +36,13 @@ def foldit_ops(request):
"Success": "false", "Success": "false",
"ErrorString": "Verification failed", "ErrorString": "Verification failed",
"ErrorCode": "VerifyFailed"}) "ErrorCode": "VerifyFailed"})
log.warning("Verification of SetPlayerPuzzleScores failed:" + log.warning(
"Verification of SetPlayerPuzzleScores failed:"
"user %s, scores json %r, verify %r", "user %s, scores json %r, verify %r",
request.user, puzzle_scores_json, pz_verify_json) request.user,
puzzle_scores_json,
pz_verify_json
)
else: else:
# This is needed because we are not getting valid json - the # This is needed because we are not getting valid json - the
# value of ScoreType is an unquoted string. Right now regexes are # value of ScoreType is an unquoted string. Right now regexes are
...@@ -65,9 +69,13 @@ def foldit_ops(request): ...@@ -65,9 +69,13 @@ def foldit_ops(request):
"Success": "false", "Success": "false",
"ErrorString": "Verification failed", "ErrorString": "Verification failed",
"ErrorCode": "VerifyFailed"}) "ErrorCode": "VerifyFailed"})
log.warning("Verification of SetPuzzlesComplete failed:" + log.warning(
"Verification of SetPuzzlesComplete failed:"
" user %s, puzzles json %r, verify %r", " user %s, puzzles json %r, verify %r",
request.user, puzzles_complete_json, pc_verify_json) request.user,
puzzles_complete_json,
pc_verify_json
)
else: else:
puzzles_complete = json.loads(puzzles_complete_json) puzzles_complete = json.loads(puzzles_complete_json)
responses.append(save_complete(request.user, puzzles_complete)) responses.append(save_complete(request.user, puzzles_complete))
......
...@@ -84,13 +84,15 @@ class InstructorTask(models.Model): ...@@ -84,13 +84,15 @@ class InstructorTask(models.Model):
raise ValueError(msg) raise ValueError(msg)
# create the task, then save it: # create the task, then save it:
instructor_task = cls(course_id=course_id, instructor_task = cls(
course_id=course_id,
task_type=task_type, task_type=task_type,
task_id=task_id, task_id=task_id,
task_key=task_key, task_key=task_key,
task_input=json_task_input, task_input=json_task_input,
task_state=QUEUING, task_state=QUEUING,
requester=requester) requester=requester
)
instructor_task.save_now() instructor_task.save_now()
return instructor_task return instructor_task
......
...@@ -118,10 +118,12 @@ def manage_modulestores(request, reload_dir=None, commit_id=None): ...@@ -118,10 +118,12 @@ def manage_modulestores(request, reload_dir=None, commit_id=None):
html += '<h2>Courses loaded in the modulestore</h2>' html += '<h2>Courses loaded in the modulestore</h2>'
html += '<ol>' html += '<ol>'
for cdir, course in def_ms.courses.items(): for cdir, course in def_ms.courses.items():
html += '<li><a href="%s/migrate/reload/%s">%s</a> (%s)</li>' % (settings.MITX_ROOT_URL, html += '<li><a href="%s/migrate/reload/%s">%s</a> (%s)</li>' % (
settings.MITX_ROOT_URL,
escape(cdir), escape(cdir),
escape(cdir), escape(cdir),
course.location.url()) course.location.url()
)
html += '</ol>' html += '</ol>'
#---------------------------------------- #----------------------------------------
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# keys being the COURSE_NAME (spaces ok), and the value being a dict of # keys being the COURSE_NAME (spaces ok), and the value being a dict of
# parameter,value pairs. The required parameters are: # parameter,value pairs. The required parameters are:
# #
# - number : course number (used in the simplewiki pages) # - number : course number (used in the wiki pages)
# - title : humanized descriptive course title # - title : humanized descriptive course title
# #
# Optional parameters: # Optional parameters:
......
...@@ -270,8 +270,10 @@ def get_problem_list(request, course_id): ...@@ -270,8 +270,10 @@ def get_problem_list(request, course_id):
mimetype="application/json") mimetype="application/json")
except GradingServiceError: except GradingServiceError:
#This is a dev_facing_error #This is a dev_facing_error
log.exception("Error from staff grading service in open ended grading. server url: {0}" log.exception(
.format(staff_grading_service().url)) "Error from staff grading service in open "
"ended grading. server url: {0}".format(staff_grading_service().url)
)
#This is a staff_facing_error #This is a staff_facing_error
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'error': STAFF_ERROR_MESSAGE})) 'error': STAFF_ERROR_MESSAGE}))
...@@ -285,8 +287,10 @@ def _get_next(course_id, grader_id, location): ...@@ -285,8 +287,10 @@ def _get_next(course_id, grader_id, location):
return staff_grading_service().get_next(course_id, location, grader_id) return staff_grading_service().get_next(course_id, location, grader_id)
except GradingServiceError: except GradingServiceError:
#This is a dev facing error #This is a dev facing error
log.exception("Error from staff grading service in open ended grading. server url: {0}" log.exception(
.format(staff_grading_service().url)) "Error from staff grading service in open "
"ended grading. server url: {0}".format(staff_grading_service().url)
)
#This is a staff_facing_error #This is a staff_facing_error
return json.dumps({'success': False, return json.dumps({'success': False,
'error': STAFF_ERROR_MESSAGE}) 'error': STAFF_ERROR_MESSAGE})
......
# Source: django-simplewiki. GPL license.
import os
import sys
# allow mdx_* parsers to be just dropped in the simplewiki folder
module_path = os.path.abspath(os.path.dirname(__file__))
if module_path not in sys.path:
sys.path.append(module_path)
# Source: django-simplewiki. GPL license.
from django import forms
from django.contrib import admin
from django.utils.translation import ugettext as _
from .models import Article, Revision, Permission, ArticleAttachment
class RevisionInline(admin.TabularInline):
model = Revision
extra = 1
class RevisionAdmin(admin.ModelAdmin):
list_display = ('article', '__unicode__', 'revision_date', 'revision_user', 'revision_text')
search_fields = ('article', 'counter')
class AttachmentAdmin(admin.ModelAdmin):
list_display = ('article', '__unicode__', 'uploaded_on', 'uploaded_by')
class ArticleAdminForm(forms.ModelForm):
def clean(self):
cleaned_data = self.cleaned_data
if cleaned_data.get("slug").startswith('_'):
raise forms.ValidationError(_('Slug cannot start with _ character.'
'Reserved for internal use.'))
if not self.instance.pk:
parent = cleaned_data.get("parent")
slug = cleaned_data.get("slug")
if Article.objects.filter(slug__exact=slug, parent__exact=parent):
raise forms.ValidationError(_('Article slug and parent must be '
'unique together.'))
return cleaned_data
class Meta:
model = Article
class ArticleAdmin(admin.ModelAdmin):
list_display = ('created_by', 'slug', 'modified_on', 'namespace')
search_fields = ('slug',)
prepopulated_fields = {'slug': ('title',)}
inlines = [RevisionInline]
form = ArticleAdminForm
save_on_top = True
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'current_revision':
# Try to determine the id of the article being edited
id = request.path.split('/')
import re
if len(id) > 0 and re.match(r"\d+", id[-2]):
kwargs["queryset"] = Revision.objects.filter(article=id[-2])
return db_field.formfield(**kwargs)
else:
db_field.editable = False
return db_field.formfield(**kwargs)
return super(ArticleAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
class PermissionAdmin(admin.ModelAdmin):
search_fields = ('article', 'counter')
admin.site.register(Article, ArticleAdmin)
admin.site.register(Revision, RevisionAdmin)
admin.site.register(Permission, PermissionAdmin)
admin.site.register(ArticleAttachment, AttachmentAdmin)
#!/usr/bin/env python
'''
Image Circuit Extension for Python-Markdown
======================================
Any single line beginning with circuit-schematic: and followed by data (which should be json data, but this
is not enforced at this level) will be displayed as a circuit schematic. This is simply an input element with
the value set to the data. It is left to javascript on the page to render that input as a circuit schematic.
ex:
circuit-schematic:[["r",[128,48,0],{"r":"1","_json_":0},["2","1"]],["view",0,0,2,null,null,null,null,null,null,null],["dc",{"0":0,"1":1,"I(_3)":-1}]]
(This is a schematic with a single one-ohm resistor. Note that this data is not meant to be user-editable.)
'''
import markdown
import re
from django.utils.html import escape
try:
# Markdown 2.1.0 changed from 2.0.3. We try importing the new version first,
# but import the 2.0.3 version if it fails
from markdown.util import etree
except:
from markdown import etree
class CircuitExtension(markdown.Extension):
def __init__(self, configs):
for key, value in configs:
self.setConfig(key, value)
def extendMarkdown(self, md, md_globals):
## Because Markdown treats contigous lines as one block of text, it is hard to match
## a regex that must occupy the whole line (like the circuit regex). This is why we have
## a preprocessor that inspects the lines and replaces the matched lines with text that is
## easier to match
md.preprocessors.add('circuit', CircuitPreprocessor(md), "_begin")
pattern = CircuitLink(r'processed-schematic:(?P<data>.*?)processed-schematic-end')
pattern.md = md
pattern.ext = self
md.inlinePatterns.add('circuit', pattern, "<reference")
class CircuitPreprocessor(markdown.preprocessors.Preprocessor):
preRegex = re.compile(r'^circuit-schematic:(?P<data>.*)$')
def run(self, lines):
def convertLine(line):
m = self.preRegex.match(line)
if m:
return 'processed-schematic:{0}processed-schematic-end'.format(m.group('data'))
else:
return line
return [convertLine(line) for line in lines]
class CircuitLink(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
data = m.group('data')
data = escape(data)
return etree.fromstring("<div align='center'><input type='hidden' parts='' value='" + data + "' analyses='' class='schematic ctrls' width='640' height='480'/></div>")
def makeExtension(configs=None):
to_return = CircuitExtension(configs=configs)
print "circuit returning ", to_return
return to_return
#!/usr/bin/env python
'''
Image Embedding Extension for Python-Markdown
======================================
Converts lone links to embedded images, provided the file extension is allowed.
Ex:
http://www.ericfehse.net/media/img/ef/blog/django-pony.jpg
becomes
<img src="http://www.ericfehse.net/media/img/ef/blog/django-pony.jpg">
mypic.jpg becomes <img src="/MEDIA_PATH/mypic.jpg">
Requires Python-Markdown 1.6+
'''
import simplewiki.settings as settings
import markdown
try:
# Markdown 2.1.0 changed from 2.0.3. We try importing the new version first,
# but import the 2.0.3 version if it fails
from markdown.util import etree
except:
from markdown import etree
class ImageExtension(markdown.Extension):
def __init__(self, configs):
for key, value in configs:
self.setConfig(key, value)
def add_inline(self, md, name, klass, re):
pattern = klass(re)
pattern.md = md
pattern.ext = self
md.inlinePatterns.add(name, pattern, "<reference")
def extendMarkdown(self, md, md_globals):
self.add_inline(md, 'image', ImageLink,
r'^(?P<proto>([^:/?#])+://)?(?P<domain>([^/?#]*)/)?(?P<path>[^?#]*\.(?P<ext>[^?#]{3,4}))(?:\?([^#]*))?(?:#(.*))?$')
class ImageLink(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
img = etree.Element('img')
proto = m.group('proto') or "http://"
domain = m.group('domain')
path = m.group('path')
ext = m.group('ext')
# A fixer upper
if ext.lower() in settings.WIKI_IMAGE_EXTENSIONS:
if domain:
src = proto + domain + path
elif path:
# We need a nice way to source local attachments...
src = "/wiki/media/" + path + ".upload"
else:
src = ''
img.set('src', src)
return img
def makeExtension(configs=None):
return ImageExtension(configs=configs)
if __name__ == "__main__":
import doctest
doctest.testmod()
# Source: https://github.com/mayoff/python-markdown-mathjax
import markdown
try:
# Markdown 2.1.0 changed from 2.0.3. We try importing the new version first,
# but import the 2.0.3 version if it fails
from markdown.util import etree, AtomicString
except:
from markdown import etree, AtomicString
class MathJaxPattern(markdown.inlinepatterns.Pattern):
def __init__(self):
markdown.inlinepatterns.Pattern.__init__(self, r'(?<!\\)(\$\$?)(.+?)\2')
def handleMatch(self, m):
el = etree.Element('span')
el.text = AtomicString(m.group(2) + m.group(3) + m.group(2))
return el
class MathJaxExtension(markdown.Extension):
def extendMarkdown(self, md, md_globals):
# Needs to come before escape matching because \ is pretty important in LaTeX
md.inlinePatterns.add('mathjax', MathJaxPattern(), '<escape')
def makeExtension(configs=None):
return MathJaxExtension(configs)
#!/usr/bin/env python
"""
Embeds web videos using URLs. For instance, if a URL to an youtube video is
found in the text submitted to markdown and it isn't enclosed in parenthesis
like a normal link in markdown, then the URL will be swapped with a embedded
youtube video.
All resulting HTML is XHTML Strict compatible.
>>> import markdown
Test Metacafe
>>> s = "http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.metacafe.com/fplayer/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room.swf" height="423" type="application/x-shockwave-flash" width="498"><param name="movie" value="http://www.metacafe.com/fplayer/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room.swf" /><param name="allowFullScreen" value="true" /></object></p>'
Test Metacafe with arguments
>>> markdown.markdown(s, ['video(metacafe_width=500,metacafe_height=425)'])
u'<p><object data="http://www.metacafe.com/fplayer/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room.swf" height="425" type="application/x-shockwave-flash" width="500"><param name="movie" value="http://www.metacafe.com/fplayer/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room.swf" /><param name="allowFullScreen" value="true" /></object></p>'
Test Link To Metacafe
>>> s = "[Metacafe link](http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/)"
>>> markdown.markdown(s, ['video'])
u'<p><a href="http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/">Metacafe link</a></p>'
Test Markdown Escaping
>>> s = "\\http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/"
>>> markdown.markdown(s, ['video'])
u'<p>http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/</p>'
>>> s = "`http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/`"
>>> markdown.markdown(s, ['video'])
u'<p><code>http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/</code></p>'
Test Youtube
>>> s = "http://www.youtube.com/watch?v=u1mA-0w8XPo&hd=1&fs=1&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.youtube.com/v/u1mA-0w8XPo&amp;hd=1&amp;fs=1&amp;feature=PlayList&amp;p=34C6046F7FEACFD3&amp;playnext=1&amp;playnext_from=PL&amp;index=1" height="344" type="application/x-shockwave-flash" width="425"><param name="movie" value="http://www.youtube.com/v/u1mA-0w8XPo&amp;hd=1&amp;fs=1&amp;feature=PlayList&amp;p=34C6046F7FEACFD3&amp;playnext=1&amp;playnext_from=PL&amp;index=1" /><param name="allowFullScreen" value="true" /></object></p>'
Test Youtube with argument
>>> markdown.markdown(s, ['video(youtube_width=200,youtube_height=100)'])
u'<p><object data="http://www.youtube.com/v/u1mA-0w8XPo&amp;hd=1&amp;fs=1&amp;feature=PlayList&amp;p=34C6046F7FEACFD3&amp;playnext=1&amp;playnext_from=PL&amp;index=1" height="100" type="application/x-shockwave-flash" width="200"><param name="movie" value="http://www.youtube.com/v/u1mA-0w8XPo&amp;hd=1&amp;fs=1&amp;feature=PlayList&amp;p=34C6046F7FEACFD3&amp;playnext=1&amp;playnext_from=PL&amp;index=1" /><param name="allowFullScreen" value="true" /></object></p>'
Test Youtube Link
>>> s = "[Youtube link](http://www.youtube.com/watch?v=u1mA-0w8XPo&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1)"
>>> markdown.markdown(s, ['video'])
u'<p><a href="http://www.youtube.com/watch?v=u1mA-0w8XPo&amp;feature=PlayList&amp;p=34C6046F7FEACFD3&amp;playnext=1&amp;playnext_from=PL&amp;index=1">Youtube link</a></p>'
Test Dailymotion
>>> s = "http://www.dailymotion.com/relevance/search/ut2004/video/x3kv65_ut2004-ownage_videogames"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.dailymotion.com/swf/x3kv65_ut2004-ownage_videogames" height="405" type="application/x-shockwave-flash" width="480"><param name="movie" value="http://www.dailymotion.com/swf/x3kv65_ut2004-ownage_videogames" /><param name="allowFullScreen" value="true" /></object></p>'
Test Dailymotion again (Dailymotion and their crazy URLs)
>>> s = "http://www.dailymotion.com/us/video/x8qak3_iron-man-vs-bruce-lee_fun"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.dailymotion.com/swf/x8qak3_iron-man-vs-bruce-lee_fun" height="405" type="application/x-shockwave-flash" width="480"><param name="movie" value="http://www.dailymotion.com/swf/x8qak3_iron-man-vs-bruce-lee_fun" /><param name="allowFullScreen" value="true" /></object></p>'
Test Yahoo! Video
>>> s = "http://video.yahoo.com/watch/1981791/4769603"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://d.yimg.com/static.video.yahoo.com/yep/YV_YEP.swf?ver=2.2.40" height="322" type="application/x-shockwave-flash" width="512"><param name="movie" value="http://d.yimg.com/static.video.yahoo.com/yep/YV_YEP.swf?ver=2.2.40" /><param name="allowFullScreen" value="true" /><param name="flashVars" value="id=4769603&amp;vid=1981791" /></object></p>'
Test Veoh Video
>>> s = "http://www.veoh.com/search/videos/q/mario#watch%3De129555XxCZanYD"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.veoh.com/videodetails2.swf?permalinkId=e129555XxCZanYD" height="341" type="application/x-shockwave-flash" width="410"><param name="movie" value="http://www.veoh.com/videodetails2.swf?permalinkId=e129555XxCZanYD" /><param name="allowFullScreen" value="true" /></object></p>'
Test Veoh Video Again (More fun URLs)
>>> s = "http://www.veoh.com/group/BigCatRescuers#watch%3Dv16771056hFtSBYEr"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.veoh.com/videodetails2.swf?permalinkId=v16771056hFtSBYEr" height="341" type="application/x-shockwave-flash" width="410"><param name="movie" value="http://www.veoh.com/videodetails2.swf?permalinkId=v16771056hFtSBYEr" /><param name="allowFullScreen" value="true" /></object></p>'
Test Veoh Video Yet Again (Even more fun URLs)
>>> s = "http://www.veoh.com/browse/videos/category/anime/watch/v181645607JyXPWcQ"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.veoh.com/videodetails2.swf?permalinkId=v181645607JyXPWcQ" height="341" type="application/x-shockwave-flash" width="410"><param name="movie" value="http://www.veoh.com/videodetails2.swf?permalinkId=v181645607JyXPWcQ" /><param name="allowFullScreen" value="true" /></object></p>'
Test Vimeo Video
>>> s = "http://www.vimeo.com/1496152"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://vimeo.com/moogaloop.swf?clip_id=1496152&amp;amp;server=vimeo.com" height="321" type="application/x-shockwave-flash" width="400"><param name="movie" value="http://vimeo.com/moogaloop.swf?clip_id=1496152&amp;amp;server=vimeo.com" /><param name="allowFullScreen" value="true" /></object></p>'
Test Vimeo Video with some GET values
>>> s = "http://vimeo.com/1496152?test=test"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://vimeo.com/moogaloop.swf?clip_id=1496152&amp;amp;server=vimeo.com" height="321" type="application/x-shockwave-flash" width="400"><param name="movie" value="http://vimeo.com/moogaloop.swf?clip_id=1496152&amp;amp;server=vimeo.com" /><param name="allowFullScreen" value="true" /></object></p>'
Test Blip.tv
>>> s = "http://blip.tv/file/get/Pycon-PlenarySprintIntro563.flv"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://blip.tv/scripts/flash/showplayer.swf?file=http://blip.tv/file/get/Pycon-PlenarySprintIntro563.flv" height="300" type="application/x-shockwave-flash" width="480"><param name="movie" value="http://blip.tv/scripts/flash/showplayer.swf?file=http://blip.tv/file/get/Pycon-PlenarySprintIntro563.flv" /><param name="allowFullScreen" value="true" /></object></p>'
Test Gametrailers
>>> s = "http://www.gametrailers.com/video/console-comparison-borderlands/58079"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.gametrailers.com/remote_wrap.php?mid=58079" height="392" type="application/x-shockwave-flash" width="480"><param name="movie" value="http://www.gametrailers.com/remote_wrap.php?mid=58079" /><param name="allowFullScreen" value="true" /></object></p>'
"""
import markdown
try:
# Markdown 2.1.0 changed from 2.0.3. We try importing the new version first,
# but import the 2.0.3 version if it fails
from markdown.util import etree
except:
from markdown import etree
version = "0.1.6"
class VideoExtension(markdown.Extension):
def __init__(self, configs):
self.config = {
'bliptv_width': ['480', 'Width for Blip.tv videos'],
'bliptv_height': ['300', 'Height for Blip.tv videos'],
'dailymotion_width': ['480', 'Width for Dailymotion videos'],
'dailymotion_height': ['405', 'Height for Dailymotion videos'],
'gametrailers_width': ['480', 'Width for Gametrailers videos'],
'gametrailers_height': ['392', 'Height for Gametrailers videos'],
'metacafe_width': ['498', 'Width for Metacafe videos'],
'metacafe_height': ['423', 'Height for Metacafe videos'],
'veoh_width': ['410', 'Width for Veoh videos'],
'veoh_height': ['341', 'Height for Veoh videos'],
'vimeo_width': ['400', 'Width for Vimeo videos'],
'vimeo_height': ['321', 'Height for Vimeo videos'],
'yahoo_width': ['512', 'Width for Yahoo! videos'],
'yahoo_height': ['322', 'Height for Yahoo! videos'],
'youtube_width': ['425', 'Width for Youtube videos'],
'youtube_height': ['344', 'Height for Youtube videos'],
}
# Override defaults with user settings
for key, value in configs:
self.setConfig(key, value)
def add_inline(self, md, name, klass, re):
pattern = klass(re)
pattern.md = md
pattern.ext = self
md.inlinePatterns.add(name, pattern, "<reference")
def extendMarkdown(self, md, md_globals):
self.add_inline(md, 'bliptv', Bliptv,
r'([^(]|^)http://(\w+\.|)blip.tv/file/get/(?P<bliptvfile>\S+.flv)')
self.add_inline(md, 'dailymotion', Dailymotion,
r'([^(]|^)http://www\.dailymotion\.com/(?P<dailymotionid>\S+)')
self.add_inline(md, 'gametrailers', Gametrailers,
r'([^(]|^)http://www.gametrailers.com/video/[a-z0-9-]+/(?P<gametrailersid>\d+)')
self.add_inline(md, 'metacafe', Metacafe,
r'([^(]|^)http://www\.metacafe\.com/watch/(?P<metacafeid>\S+)/')
self.add_inline(md, 'veoh', Veoh,
r'([^(]|^)http://www\.veoh\.com/\S*(#watch%3D|watch/)(?P<veohid>\w+)')
self.add_inline(md, 'vimeo', Vimeo,
r'([^(]|^)http://(www.|)vimeo\.com/(?P<vimeoid>\d+)\S*')
self.add_inline(md, 'yahoo', Yahoo,
r'([^(]|^)http://video\.yahoo\.com/watch/(?P<yahoovid>\d+)/(?P<yahooid>\d+)')
self.add_inline(md, 'youtube', Youtube,
r'([^(]|^)http://www\.youtube\.com/watch\?\S*v=(?P<youtubeargs>[A-Za-z0-9_&=-]+)\S*')
class Bliptv(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = 'http://blip.tv/scripts/flash/showplayer.swf?file=http://blip.tv/file/get/%s' % m.group('bliptvfile')
width = self.ext.config['bliptv_width'][0]
height = self.ext.config['bliptv_height'][0]
return flash_object(url, width, height)
class Dailymotion(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = 'http://www.dailymotion.com/swf/%s' % m.group('dailymotionid').split('/')[-1]
width = self.ext.config['dailymotion_width'][0]
height = self.ext.config['dailymotion_height'][0]
return flash_object(url, width, height)
class Gametrailers(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = 'http://www.gametrailers.com/remote_wrap.php?mid=%s' % \
m.group('gametrailersid').split('/')[-1]
width = self.ext.config['gametrailers_width'][0]
height = self.ext.config['gametrailers_height'][0]
return flash_object(url, width, height)
class Metacafe(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = 'http://www.metacafe.com/fplayer/%s.swf' % m.group('metacafeid')
width = self.ext.config['metacafe_width'][0]
height = self.ext.config['metacafe_height'][0]
return flash_object(url, width, height)
class Veoh(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = 'http://www.veoh.com/videodetails2.swf?permalinkId=%s' % m.group('veohid')
width = self.ext.config['veoh_width'][0]
height = self.ext.config['veoh_height'][0]
return flash_object(url, width, height)
class Vimeo(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = 'http://vimeo.com/moogaloop.swf?clip_id=%s&amp;server=vimeo.com' % m.group('vimeoid')
width = self.ext.config['vimeo_width'][0]
height = self.ext.config['vimeo_height'][0]
return flash_object(url, width, height)
class Yahoo(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = "http://d.yimg.com/static.video.yahoo.com/yep/YV_YEP.swf?ver=2.2.40"
width = self.ext.config['yahoo_width'][0]
height = self.ext.config['yahoo_height'][0]
obj = flash_object(url, width, height)
param = etree.Element('param')
param.set('name', 'flashVars')
param.set('value', "id=%s&vid=%s" % (m.group('yahooid'),
m.group('yahoovid')))
obj.append(param)
return obj
class Youtube(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = 'http://www.youtube.com/v/%s' % m.group('youtubeargs')
width = self.ext.config['youtube_width'][0]
height = self.ext.config['youtube_height'][0]
return flash_object(url, width, height)
def flash_object(url, width, height):
obj = etree.Element('object')
obj.set('type', 'application/x-shockwave-flash')
obj.set('width', width)
obj.set('height', height)
obj.set('data', url)
param = etree.Element('param')
param.set('name', 'movie')
param.set('value', url)
obj.append(param)
param = etree.Element('param')
param.set('name', 'allowFullScreen')
param.set('value', 'true')
obj.append(param)
#param = etree.Element('param')
#param.set('name', 'allowScriptAccess')
#param.set('value', 'sameDomain')
#obj.append(param)
return obj
def makeExtension(configs=None):
return VideoExtension(configs=configs)
if __name__ == "__main__":
import doctest
doctest.testmod()
#!/usr/bin/env python
'''
Wikipath Extension for Python-Markdown
======================================
Converts [Link Name](wiki:ArticleName) to relative links pointing to article. Requires Python-Markdown 2.0+
Basic usage:
>>> import markdown
>>> text = "Some text with a [Link Name](wiki:ArticleName)."
>>> html = markdown.markdown(text, ['wikipath(base_url="/wiki/view/")'])
>>> html
u'<p>Some text with a <a class="wikipath" href="/wiki/view/ArticleName/">Link Name</a>.</p>'
Dependencies:
* [Python 2.3+](http://python.org)
* [Markdown 2.0+](http://www.freewisdom.org/projects/python-markdown/)
'''
import markdown
try:
# Markdown 2.1.0 changed from 2.0.3. We try importing the new version first,
# but import the 2.0.3 version if it fails
from markdown.util import etree
except:
from markdown import etree
class WikiPathExtension(markdown.Extension):
def __init__(self, configs):
# set extension defaults
self.config = {
'default_namespace': ['edX', 'Default namespace for when one isn\'t specified.'],
'html_class': ['wikipath', 'CSS hook. Leave blank for none.']
}
# Override defaults with user settings
for key, value in configs:
# self.config[key][0] = value
self.setConfig(key, value)
def extendMarkdown(self, md, md_globals):
self.md = md
# append to end of inline patterns
WIKI_RE = r'\[(?P<linkTitle>.+?)\]\(wiki:(?P<wikiTitle>[a-zA-Z\d/_-]*)\)'
wikiPathPattern = WikiPath(WIKI_RE, self.config)
wikiPathPattern.md = md
md.inlinePatterns.add('wikipath', wikiPathPattern, "<reference")
class WikiPath(markdown.inlinepatterns.Pattern):
def __init__(self, pattern, config):
markdown.inlinepatterns.Pattern.__init__(self, pattern)
self.config = config
def handleMatch(self, m):
article_title = m.group('wikiTitle')
if article_title.startswith("/"):
article_title = article_title[1:]
if not "/" in article_title:
article_title = self.config['default_namespace'][0] + "/" + article_title
url = "../" + article_title
label = m.group('linkTitle')
a = etree.Element('a')
a.set('href', url)
a.text = label
if self.config['html_class'][0]:
a.set('class', self.config['html_class'][0])
return a
def _getMeta(self):
""" Return meta data or config data. """
base_url = self.config['base_url'][0]
html_class = self.config['html_class'][0]
if hasattr(self.md, 'Meta'):
if self.md.Meta.has_key('wiki_base_url'):
base_url = self.md.Meta['wiki_base_url'][0]
if self.md.Meta.has_key('wiki_html_class'):
html_class = self.md.Meta['wiki_html_class'][0]
return base_url, html_class
def makeExtension(configs=None):
return WikiPathExtension(configs=configs)
if __name__ == "__main__":
import doctest
doctest.testmod()
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Article'
db.create_table('simplewiki_article', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('title', self.gf('django.db.models.fields.CharField')(max_length=512)),
('slug', self.gf('django.db.models.fields.SlugField')(max_length=100, blank=True)),
('created_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
('created_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=1, blank=True)),
('modified_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=1, blank=True)),
('parent', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Article'], null=True, blank=True)),
('locked', self.gf('django.db.models.fields.BooleanField')(default=False)),
('permissions', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Permission'], null=True, blank=True)),
('current_revision', self.gf('django.db.models.fields.related.OneToOneField')(blank=True, related_name='current_rev', unique=True, null=True, to=orm['simplewiki.Revision'])),
))
db.send_create_signal('simplewiki', ['Article'])
# Adding unique constraint on 'Article', fields ['slug', 'parent']
db.create_unique('simplewiki_article', ['slug', 'parent_id'])
# Adding M2M table for field related on 'Article'
db.create_table('simplewiki_article_related', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('from_article', models.ForeignKey(orm['simplewiki.article'], null=False)),
('to_article', models.ForeignKey(orm['simplewiki.article'], null=False))
))
db.create_unique('simplewiki_article_related', ['from_article_id', 'to_article_id'])
# Adding model 'ArticleAttachment'
db.create_table('simplewiki_articleattachment', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('article', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Article'])),
('file', self.gf('django.db.models.fields.files.FileField')(max_length=255)),
('uploaded_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
('uploaded_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
))
db.send_create_signal('simplewiki', ['ArticleAttachment'])
# Adding model 'Revision'
db.create_table('simplewiki_revision', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('article', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Article'])),
('revision_text', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
('revision_user', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='wiki_revision_user', null=True, to=orm['auth.User'])),
('revision_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('contents', self.gf('django.db.models.fields.TextField')()),
('contents_parsed', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
('counter', self.gf('django.db.models.fields.IntegerField')(default=1)),
('previous_revision', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Revision'], null=True, blank=True)),
('deleted', self.gf('django.db.models.fields.IntegerField')(default=0)),
))
db.send_create_signal('simplewiki', ['Revision'])
# Adding model 'Permission'
db.create_table('simplewiki_permission', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('permission_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
))
db.send_create_signal('simplewiki', ['Permission'])
# Adding M2M table for field can_write on 'Permission'
db.create_table('simplewiki_permission_can_write', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('permission', models.ForeignKey(orm['simplewiki.permission'], null=False)),
('user', models.ForeignKey(orm['auth.user'], null=False))
))
db.create_unique('simplewiki_permission_can_write', ['permission_id', 'user_id'])
# Adding M2M table for field can_read on 'Permission'
db.create_table('simplewiki_permission_can_read', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('permission', models.ForeignKey(orm['simplewiki.permission'], null=False)),
('user', models.ForeignKey(orm['auth.user'], null=False))
))
db.create_unique('simplewiki_permission_can_read', ['permission_id', 'user_id'])
def backwards(self, orm):
# Removing unique constraint on 'Article', fields ['slug', 'parent']
db.delete_unique('simplewiki_article', ['slug', 'parent_id'])
# Deleting model 'Article'
db.delete_table('simplewiki_article')
# Removing M2M table for field related on 'Article'
db.delete_table('simplewiki_article_related')
# Deleting model 'ArticleAttachment'
db.delete_table('simplewiki_articleattachment')
# Deleting model 'Revision'
db.delete_table('simplewiki_revision')
# Deleting model 'Permission'
db.delete_table('simplewiki_permission')
# Removing M2M table for field can_write on 'Permission'
db.delete_table('simplewiki_permission_can_write')
# Removing M2M table for field can_read on 'Permission'
db.delete_table('simplewiki_permission_can_read')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'simplewiki.article': {
'Meta': {'unique_together': "(('slug', 'parent'),)", 'object_name': 'Article'},
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}),
'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']", 'null': 'True', 'blank': 'True'}),
'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}),
'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '512'})
},
'simplewiki.articleattachment': {
'Meta': {'object_name': 'ArticleAttachment'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}),
'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
},
'simplewiki.permission': {
'Meta': {'object_name': 'Permission'},
'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'simplewiki.revision': {
'Meta': {'object_name': 'Revision'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}),
'contents': ('django.db.models.fields.TextField', [], {}),
'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}),
'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"})
}
}
complete_apps = ['simplewiki']
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
class Migration(DataMigration):
def forwards(self, orm):
# We collect every article slug in a set. Any time we see a duplicate, we change the duplicate's name
unique_slugs = set()
for article in orm.Article.objects.all():
if article.slug in unique_slugs:
i = 2
new_name = article.slug + str(i)
while new_name in unique_slugs:
i += 1
new_name = article.slug + str(i)
print "Changing", article.slug, "to", new_name
article.slug = new_name
article.save()
unique_slugs.add(article.slug)
def backwards(self, orm):
"Write your backwards methods here."
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'simplewiki.article': {
'Meta': {'unique_together': "(('slug', 'parent'),)", 'object_name': 'Article'},
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}),
'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']", 'null': 'True', 'blank': 'True'}),
'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}),
'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '512'})
},
'simplewiki.articleattachment': {
'Meta': {'object_name': 'ArticleAttachment'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}),
'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
},
'simplewiki.permission': {
'Meta': {'object_name': 'Permission'},
'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'simplewiki.revision': {
'Meta': {'object_name': 'Revision'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}),
'contents': ('django.db.models.fields.TextField', [], {}),
'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}),
'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"})
}
}
complete_apps = ['simplewiki']
symmetrical = True
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Removing unique constraint on 'Article', fields ['slug', 'parent']
db.delete_unique('simplewiki_article', ['slug', 'parent_id'])
# Adding model 'Namespace'
db.create_table('simplewiki_namespace', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=30)),
))
db.send_create_signal('simplewiki', ['Namespace'])
# Deleting field 'Article.parent'
db.delete_column('simplewiki_article', 'parent_id')
# Adding field 'Article.namespace'
db.add_column('simplewiki_article', 'namespace',
self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['simplewiki.Namespace']),
keep_default=False)
# Adding unique constraint on 'Article', fields ['namespace', 'slug']
db.create_unique('simplewiki_article', ['namespace_id', 'slug'])
def backwards(self, orm):
# Removing unique constraint on 'Article', fields ['namespace', 'slug']
db.delete_unique('simplewiki_article', ['namespace_id', 'slug'])
# Deleting model 'Namespace'
db.delete_table('simplewiki_namespace')
# Adding field 'Article.parent'
db.add_column('simplewiki_article', 'parent',
self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Article'], null=True, blank=True),
keep_default=False)
# Deleting field 'Article.namespace'
db.delete_column('simplewiki_article', 'namespace_id')
# Adding unique constraint on 'Article', fields ['slug', 'parent']
db.create_unique('simplewiki_article', ['slug', 'parent_id'])
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'simplewiki.article': {
'Meta': {'unique_together': "(('slug', 'namespace'),)", 'object_name': 'Article'},
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}),
'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}),
'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Namespace']"}),
'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}),
'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '512'})
},
'simplewiki.articleattachment': {
'Meta': {'object_name': 'ArticleAttachment'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}),
'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
},
'simplewiki.namespace': {
'Meta': {'object_name': 'Namespace'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '30'})
},
'simplewiki.permission': {
'Meta': {'object_name': 'Permission'},
'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'simplewiki.revision': {
'Meta': {'object_name': 'Revision'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}),
'contents': ('django.db.models.fields.TextField', [], {}),
'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}),
'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"})
}
}
complete_apps = ['simplewiki']
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
class Migration(DataMigration):
def forwards(self, orm):
namespace6002x, created = orm.Namespace.objects.get_or_create(name="6.002xS12")
if created:
namespace6002x.save()
for article in orm.Article.objects.all():
article.namespace = namespace6002x
article.save()
def backwards(self, orm):
raise RuntimeError("Cannot reverse this migration.")
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'simplewiki.article': {
'Meta': {'unique_together': "(('slug', 'namespace'),)", 'object_name': 'Article'},
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}),
'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}),
'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Namespace']"}),
'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}),
'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '512'})
},
'simplewiki.articleattachment': {
'Meta': {'object_name': 'ArticleAttachment'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}),
'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
},
'simplewiki.namespace': {
'Meta': {'object_name': 'Namespace'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '30'})
},
'simplewiki.permission': {
'Meta': {'object_name': 'Permission'},
'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'simplewiki.revision': {
'Meta': {'object_name': 'Revision'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}),
'contents': ('django.db.models.fields.TextField', [], {}),
'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}),
'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"})
}
}
complete_apps = ['simplewiki']
symmetrical = True
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding unique constraint on 'Namespace', fields ['name']
db.create_unique('simplewiki_namespace', ['name'])
def backwards(self, orm):
# Removing unique constraint on 'Namespace', fields ['name']
db.delete_unique('simplewiki_namespace', ['name'])
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'simplewiki.article': {
'Meta': {'unique_together': "(('slug', 'namespace'),)", 'object_name': 'Article'},
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}),
'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}),
'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Namespace']"}),
'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}),
'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '512'})
},
'simplewiki.articleattachment': {
'Meta': {'object_name': 'ArticleAttachment'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}),
'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
},
'simplewiki.namespace': {
'Meta': {'object_name': 'Namespace'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'simplewiki.permission': {
'Meta': {'object_name': 'Permission'},
'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'simplewiki.revision': {
'Meta': {'object_name': 'Revision'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}),
'contents': ('django.db.models.fields.TextField', [], {}),
'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}),
'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"})
}
}
complete_apps = ['simplewiki']
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding index on 'Namespace', fields ['name']
db.create_index('simplewiki_namespace', ['name'])
def backwards(self, orm):
# Removing index on 'Namespace', fields ['name']
db.delete_index('simplewiki_namespace', ['name'])
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'simplewiki.article': {
'Meta': {'unique_together': "(('slug', 'namespace'),)", 'object_name': 'Article'},
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}),
'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}),
'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Namespace']"}),
'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}),
'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '512'})
},
'simplewiki.articleattachment': {
'Meta': {'object_name': 'ArticleAttachment'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}),
'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
},
'simplewiki.namespace': {
'Meta': {'object_name': 'Namespace'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30', 'db_index': 'True'})
},
'simplewiki.permission': {
'Meta': {'object_name': 'Permission'},
'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'simplewiki.revision': {
'Meta': {'object_name': 'Revision'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}),
'contents': ('django.db.models.fields.TextField', [], {}),
'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}),
'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"})
}
}
complete_apps = ['simplewiki']
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Removing index on 'Namespace', fields ['name']
db.delete_index('simplewiki_namespace', ['name'])
def backwards(self, orm):
# Adding index on 'Namespace', fields ['name']
db.create_index('simplewiki_namespace', ['name'])
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'simplewiki.article': {
'Meta': {'unique_together': "(('slug', 'namespace'),)", 'object_name': 'Article'},
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}),
'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}),
'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Namespace']"}),
'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}),
'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '512'})
},
'simplewiki.articleattachment': {
'Meta': {'object_name': 'ArticleAttachment'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}),
'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
},
'simplewiki.namespace': {
'Meta': {'object_name': 'Namespace'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'simplewiki.permission': {
'Meta': {'object_name': 'Permission'},
'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'simplewiki.revision': {
'Meta': {'object_name': 'Revision'},
'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}),
'contents': ('django.db.models.fields.TextField', [], {}),
'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}),
'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"})
}
}
complete_apps = ['simplewiki']
import difflib
import os
from django import forms
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import signals
from django.utils.translation import ugettext_lazy as _
from markdown import markdown
from .wiki_settings import *
from util.cache import cache
from pytz import UTC
class ShouldHaveExactlyOneRootSlug(Exception):
pass
class Namespace(models.Model):
name = models.CharField(max_length=30, unique=True, verbose_name=_('namespace'))
# TODO: We may want to add permissions, etc later
@classmethod
def ensure_namespace(cls, name):
try:
namespace = Namespace.objects.get(name__exact=name)
except Namespace.DoesNotExist:
new_namespace = Namespace(name=name)
new_namespace.save()
class Article(models.Model):
"""Wiki article referring to Revision model for actual content.
'slug' and 'title' field should be maintained centrally, since users
aren't allowed to change them, anyways.
"""
title = models.CharField(max_length=512, verbose_name=_('Article title'),
blank=False)
slug = models.SlugField(max_length=100, verbose_name=_('slug'),
help_text=_('Letters, numbers, underscore and hyphen.'),
blank=True)
namespace = models.ForeignKey(Namespace, verbose_name=_('Namespace'))
created_by = models.ForeignKey(User, verbose_name=_('Created by'), blank=True, null=True)
created_on = models.DateTimeField(auto_now_add=1)
modified_on = models.DateTimeField(auto_now_add=1)
locked = models.BooleanField(default=False, verbose_name=_('Locked for editing'))
permissions = models.ForeignKey('Permission', verbose_name=_('Permissions'),
blank=True, null=True,
help_text=_('Permission group'))
current_revision = models.OneToOneField('Revision', related_name='current_rev',
blank=True, null=True, editable=True)
related = models.ManyToManyField('self', verbose_name=_('Related articles'), symmetrical=True,
help_text=_('Sets a symmetrical relation other articles'),
blank=True, null=True)
def attachments(self):
return ArticleAttachment.objects.filter(article__exact=self)
def get_path(self):
return self.namespace.name + "/" + self.slug
@classmethod
def get_article(cls, article_path):
"""
Given an article_path like namespace/slug, this returns the article. It may raise
a Article.DoesNotExist if no matching article is found or ValueError if the
article_path is not constructed properly.
"""
#TODO: Verify the path, throw a meaningful error?
namespace, slug = article_path.split("/")
return Article.objects.get(slug__exact=slug, namespace__name__exact=namespace)
@classmethod
def get_root(cls, namespace):
"""Return the root article, which should ALWAYS exist..
except the very first time the wiki is loaded, in which
case the user is prompted to create this article."""
try:
return Article.objects.filter(slug__exact="", namespace__name__exact=namespace)[0]
except:
raise ShouldHaveExactlyOneRootSlug()
# @classmethod
# def get_url_reverse(cls, path, article, return_list=[]):
# """Lookup a URL and return the corresponding set of articles
# in the path."""
# if path == []:
# return return_list + [article]
# # Lookup next child in path
# try:
# a = Article.objects.get(parent__exact = article, slug__exact=str(path[0]))
# return cls.get_url_reverse(path[1:], a, return_list+[article])
# except Exception, e:
# return None
def can_read(self, user):
""" Check read permissions and return True/False."""
if user.is_superuser:
return True
if self.permissions:
perms = self.permissions.can_read.all()
return perms.count() == 0 or (user in perms)
else:
# TODO: We can inherit namespace permissions here
return True
def can_write(self, user):
""" Check write permissions and return True/False."""
if user.is_superuser:
return True
if self.permissions:
perms = self.permissions.can_write.all()
return perms.count() == 0 or (user in perms)
else:
# TODO: We can inherit namespace permissions here
return True
def can_write_l(self, user):
"""Check write permissions and locked status"""
if user.is_superuser:
return True
return not self.locked and self.can_write(user)
def can_attach(self, user):
return self.can_write_l(user) and (WIKI_ALLOW_ANON_ATTACHMENTS or not user.is_anonymous())
def __unicode__(self):
if self.slug == '':
return unicode(_('Root article'))
else:
return self.slug
class Meta:
unique_together = (('slug', 'namespace'),)
verbose_name = _('Article')
verbose_name_plural = _('Articles')
def get_attachment_filepath(instance, filename):
"""Store file, appending new extension for added security"""
dir_ = WIKI_ATTACHMENTS + instance.article.get_url()
dir_ = '/'.join(filter(lambda x: x != '', dir_.split('/')))
if not os.path.exists(WIKI_ATTACHMENTS_ROOT + dir_):
os.makedirs(WIKI_ATTACHMENTS_ROOT + dir_)
return dir_ + '/' + filename + '.upload'
class ArticleAttachment(models.Model):
article = models.ForeignKey(Article, verbose_name=_('Article'))
file = models.FileField(max_length=255, upload_to=get_attachment_filepath, verbose_name=_('Attachment'))
uploaded_by = models.ForeignKey(User, blank=True, verbose_name=_('Uploaded by'), null=True)
uploaded_on = models.DateTimeField(auto_now_add=True, verbose_name=_('Upload date'))
def download_url(self):
return reverse('wiki_view_attachment', args=(self.article.get_url(), self.filename()))
def filename(self):
return '.'.join(self.file.name.split('/')[-1].split('.')[:-1])
def get_size(self):
try:
size = self.file.size
except OSError:
size = 0
return size
def filename(self):
return '.'.join(self.file.name.split('/')[-1].split('.')[:-1])
def is_image(self):
fname = self.filename().split('.')
if len(fname) > 1 and fname[-1].lower() in WIKI_IMAGE_EXTENSIONS:
return True
return False
def get_thumb(self):
return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE)
def get_thumb_small(self):
return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE_SMALL)
def mk_thumbs(self):
self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE, **{'force': True})
self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE_SMALL, **{'force': True})
def mk_thumb(self, width, height, force=False):
"""Requires Python Imaging Library (PIL)"""
if not self.get_size():
return False
if not self.is_image():
return False
base_path = os.path.dirname(self.file.path)
orig_name = self.filename().split('.')
thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1])
thumb_filepath = "%s%s%s" % (base_path, os.sep, thumb_filename)
if force or not os.path.exists(thumb_filepath):
try:
import Image
img = Image.open(self.file.path)
img.thumbnail((width, height), Image.ANTIALIAS)
img.save(thumb_filepath)
except IOError:
return False
return True
def get_thumb_impl(self, width, height):
"""Requires Python Imaging Library (PIL)"""
if not self.get_size():
return False
if not self.is_image():
return False
self.mk_thumb(width, height)
orig_name = self.filename().split('.')
thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1])
thumb_url = settings.MEDIA_URL + WIKI_ATTACHMENTS + self.article.get_url() + '/' + thumb_filename
return thumb_url
def __unicode__(self):
return self.filename()
class Revision(models.Model):
article = models.ForeignKey(Article, verbose_name=_('Article'))
revision_text = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Description of change'))
revision_user = models.ForeignKey(User, verbose_name=_('Modified by'),
blank=True, null=True, related_name='wiki_revision_user')
revision_date = models.DateTimeField(auto_now_add=True, verbose_name=_('Revision date'))
contents = models.TextField(verbose_name=_('Contents (Use MarkDown format)'))
contents_parsed = models.TextField(editable=False, blank=True, null=True)
counter = models.IntegerField(verbose_name=_('Revision#'), default=1, editable=False)
previous_revision = models.ForeignKey('self', blank=True, null=True, editable=False)
# Deleted has three values. 0 is normal, non-deleted. 1 is if it was deleted by a normal user. It should
# be a NEW revision, so that it appears in the history. 2 is a special flag that can be applied or removed
# from a normal revision. It means it has been admin-deleted, and can only been seen by an admin. It doesn't
# show up in the history.
deleted = models.IntegerField(verbose_name=_('Deleted group'), default=0)
def get_user(self):
return self.revision_user if self.revision_user else _('Anonymous')
# Called after the deleted fied has been changed (between 0 and 2). This bypasses the normal checks put in
# save that update the revision or reject the save if contents haven't changed
def adminSetDeleted(self, deleted):
self.deleted = deleted
super(Revision, self).save()
def save(self, **kwargs):
# Check if contents have changed... if not, silently ignore save
if self.article and self.article.current_revision:
if self.deleted == 0 and self.article.current_revision.contents == self.contents:
return
else:
import datetime
self.article.modified_on = datetime.datetime.now(UTC)
self.article.save()
# Increment counter according to previous revision
previous_revision = Revision.objects.filter(article=self.article).order_by('-counter')
if previous_revision.count() > 0:
if previous_revision.count() > previous_revision[0].counter:
self.counter = previous_revision.count() + 1
else:
self.counter = previous_revision[0].counter + 1
else:
self.counter = 1
if (self.article.current_revision and self.article.current_revision.deleted == 0):
self.previous_revision = self.article.current_revision
# Create pre-parsed contents - no need to parse on-the-fly
ext = WIKI_MARKDOWN_EXTENSIONS
ext += ["wikipath(default_namespace=%s)" % self.article.namespace.name]
self.contents_parsed = markdown(self.contents,
extensions=ext,
safe_mode='escape',)
super(Revision, self).save(**kwargs)
def delete(self, **kwargs):
"""If a current revision is deleted, then regress to the previous
revision or insert a stub, if no other revisions are available"""
article = self.article
if article.current_revision == self:
prev_revision = Revision.objects.filter(article__exact=article,
pk__not=self.pk).order_by('-counter')
if prev_revision:
article.current_revision = prev_revision[0]
article.save()
else:
r = Revision(article=article,
revision_user=article.created_by)
r.contents = unicode(_('Auto-generated stub'))
r.revision_text = unicode(_('Auto-generated stub'))
r.save()
article.current_revision = r
article.save()
super(Revision, self).delete(**kwargs)
def get_diff(self):
if (self.deleted == 1):
yield "Article Deletion"
return
if self.previous_revision:
previous = self.previous_revision.contents.splitlines(1)
else:
previous = []
# Todo: difflib.HtmlDiff would look pretty for our history pages!
diff = difflib.unified_diff(previous, self.contents.splitlines(1))
# let's skip the preamble
diff.next(); diff.next(); diff.next()
for d in diff:
yield d
def __unicode__(self):
return "r%d" % self.counter
class Meta:
verbose_name = _('article revision')
verbose_name_plural = _('article revisions')
class Permission(models.Model):
permission_name = models.CharField(max_length=255, verbose_name=_('Permission name'))
can_write = models.ManyToManyField(User, blank=True, null=True, related_name='write',
help_text=_('Select none to grant anonymous access.'))
can_read = models.ManyToManyField(User, blank=True, null=True, related_name='read',
help_text=_('Select none to grant anonymous access.'))
def __unicode__(self):
return self.permission_name
class Meta:
verbose_name = _('Article permission')
verbose_name_plural = _('Article permissions')
class RevisionForm(forms.ModelForm):
contents = forms.CharField(label=_('Contents'), widget=forms.Textarea(attrs={'rows': 8, 'cols': 50}))
class Meta:
model = Revision
fields = ['contents', 'revision_text']
class RevisionFormWithTitle(forms.ModelForm):
title = forms.CharField(label=_('Title'))
class Meta:
model = Revision
fields = ['title', 'contents', 'revision_text']
class CreateArticleForm(RevisionForm):
title = forms.CharField(label=_('Title'))
class Meta:
model = Revision
fields = ['title', 'contents', ]
def set_revision(sender, *args, **kwargs):
"""Signal handler to ensure that a new revision is always chosen as the
current revision - automatically. It simplifies stuff greatly. Also
stores previous revision for diff-purposes"""
instance = kwargs['instance']
created = kwargs['created']
if created and instance.article:
instance.article.current_revision = instance
instance.article.save()
signals.post_save.connect(set_revision, Revision)
from django import template
from django.conf import settings
from django.template.defaultfilters import stringfilter
from django.utils.http import urlquote as django_urlquote
from simplewiki.wiki_settings import *
register = template.Library()
@register.filter()
def prepend_media_url(value):
"""Prepend user defined media root to url"""
return settings.MEDIA_URL + value
@register.filter()
def urlquote(value):
"""Prepend user defined media root to url"""
return django_urlquote(value)
"""
This file demonstrates two different styles of tests (one doctest and one
unittest). These will both pass when you run "manage.py test".
Replace these with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.failUnlessEqual(1 + 1, 2)
__test__ = {"doctest": """
Another way to test that 1 + 1 is equal to 2.
>>> 1 + 1 == 2
True
"""}
from django.conf.urls import patterns, url
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')'
urlpatterns = patterns('', # nopep8
url(r'^$', 'simplewiki.views.root_redirect', name='wiki_root'),
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'^edit' + article_slug, 'simplewiki.views.edit', name='wiki_edit'),
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'^search_related' + article_slug, 'simplewiki.views.search_add_related', name='search_related'),
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'^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
)
# Markdown: Syntax
[TOC]
## Overview
### Philosophy
Markdown is intended to be as easy-to-read and easy-to-write as is feasible.
Readability, however, is emphasized above all else. A Markdown-formatted
document should be publishable as-is, as plain text, without looking
like it's been marked up with tags or formatting instructions. While
Markdown's syntax has been influenced by several existing text-to-HTML
filters -- including [Setext] [1], [atx] [2], [Textile] [3], [reStructuredText] [4],
[Grutatext] [5], and [EtText] [6] -- the single biggest source of
inspiration for Markdown's syntax is the format of plain text email.
[1]: http://docutils.sourceforge.net/mirror/setext.html
[2]: http://www.aaronsw.com/2002/atx/
[3]: http://textism.com/tools/textile/
[4]: http://docutils.sourceforge.net/rst.html
[5]: http://www.triptico.com/software/grutatxt.html
[6]: http://ettext.taint.org/doc/
To this end, Markdown's syntax is comprised entirely of punctuation
characters, which punctuation characters have been carefully chosen so
as to look like what they mean. E.g., asterisks around a word actually
look like \*emphasis\*. Markdown lists look like, well, lists. Even
blockquotes look like quoted passages of text, assuming you've ever
used email.
### Automatic Escaping for Special Characters
In HTML, there are two characters that demand special treatment: `<`
and `&`. Left angle brackets are used to start tags; ampersands are
used to denote HTML entities. If you want to use them as literal
characters, you must escape them as entities, e.g. `&lt;`, and
`&amp;`.
Ampersands in particular are bedeviling for web writers. If you want to
write about 'AT&T', you need to write '`AT&amp;T`'. You even need to
escape ampersands within URLs. Thus, if you want to link to:
http://images.google.com/images?num=30&q=larry+bird
you need to encode the URL as:
http://images.google.com/images?num=30&amp;q=larry+bird
in your anchor tag `href` attribute. Needless to say, this is easy to
forget, and is probably the single most common source of HTML validation
errors in otherwise well-marked-up web sites.
Markdown allows you to use these characters naturally, taking care of
all the necessary escaping for you. If you use an ampersand as part of
an HTML entity, it remains unchanged; otherwise it will be translated
into `&amp;`.
So, if you want to include a copyright symbol in your article, you can write:
&copy;
and Markdown will leave it alone. But if you write:
AT&T
Markdown will translate it to:
AT&amp;T
Similarly, because Markdown supports [inline HTML](#html), if you use
angle brackets as delimiters for HTML tags, Markdown will treat them as
such. But if you write:
4 < 5
Markdown will translate it to:
4 &lt; 5
However, inside Markdown code spans and blocks, angle brackets and
ampersands are *always* encoded automatically. This makes it easy to use
Markdown to write about HTML code. (As opposed to raw HTML, which is a
terrible format for writing about HTML syntax, because every single `<`
and `&` in your example code needs to be escaped.)
* * *
## Block Elements
### Paragraphs and Line Breaks
A paragraph is simply one or more consecutive lines of text, separated
by one or more blank lines. (A blank line is any line that looks like a
blank line -- a line containing nothing but spaces or tabs is considered
blank.) Normal paragraphs should not be indented with spaces or tabs.
The implication of the "one or more consecutive lines of text" rule is
that Markdown supports "hard-wrapped" text paragraphs. This differs
significantly from most other text-to-HTML formatters (including Movable
Type's "Convert Line Breaks" option) which translate every line break
character in a paragraph into a `<br />` tag.
When you *do* want to insert a `<br />` break tag using Markdown, you
end a line with two or more spaces, then type return.
Yes, this takes a tad more effort to create a `<br />`, but a simplistic
"every line break is a `<br />`" rule wouldn't work for Markdown.
Markdown's email-style [blockquoting][bq] and multi-paragraph [list items][l]
work best -- and look better -- when you format them with hard breaks.
[bq]: #blockquote
[l]: #list
### Headers
Markdown supports two styles of headers, [Setext] [1] and [atx] [2].
Setext-style headers are "underlined" using equal signs (for first-level
headers) and dashes (for second-level headers). For example:
This is an H1
=============
This is an H2
-------------
This is an H3
_____________
Any number of underlining `=`'s or `-`'s will work.
Atx-style headers use 1-6 hash characters at the start of the line,
corresponding to header levels 1-6. For example:
# This is an H1
## This is an H2
###### This is an H6
Optionally, you may "close" atx-style headers. This is purely
cosmetic -- you can use this if you think it looks better. The
closing hashes don't even need to match the number of hashes
used to open the header. (The number of opening hashes
determines the header level.) :
# This is an H1 #
## This is an H2 ##
### This is an H3 ######
### Blockquotes
Markdown uses email-style `>` characters for blockquoting. If you're
familiar with quoting passages of text in an email message, then you
know how to create a blockquote in Markdown. It looks best if you hard
wrap the text and put a `>` before every line:
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
> consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
> Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
>
> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
> id sem consectetuer libero luctus adipiscing.
Markdown allows you to be lazy and only put the `>` before the first
line of a hard-wrapped paragraph:
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
id sem consectetuer libero luctus adipiscing.
Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by
adding additional levels of `>`:
> This is the first level of quoting.
>
> > This is nested blockquote.
>
> Back to the first level.
Blockquotes can contain other Markdown elements, including headers, lists,
and code blocks:
> ## This is a header.
>
> 1. This is the first list item.
> 2. This is the second list item.
>
> Here's some example code:
>
> return shell_exec("echo $input | $markdown_script");
Any decent text editor should make email-style quoting easy. For
example, with BBEdit, you can make a selection and choose Increase
Quote Level from the Text menu.
### Lists
Markdown supports ordered (numbered) and unordered (bulleted) lists.
Unordered lists use asterisks, pluses, and hyphens -- interchangably
-- as list markers:
* Red
* Green
* Blue
is equivalent to:
+ Red
+ Green
+ Blue
and:
- Red
- Green
- Blue
Ordered lists use numbers followed by periods:
1. Bird
2. McHale
3. Parish
It's important to note that the actual numbers you use to mark the
list have no effect on the HTML output Markdown produces. The HTML
Markdown produces from the above list is:
<ol>
<li>Bird</li>
<li>McHale</li>
<li>Parish</li>
</ol>
If you instead wrote the list in Markdown like this:
1. Bird
1. McHale
1. Parish
or even:
3. Bird
1. McHale
8. Parish
you'd get the exact same HTML output. The point is, if you want to,
you can use ordinal numbers in your ordered Markdown lists, so that
the numbers in your source match the numbers in your published HTML.
But if you want to be lazy, you don't have to.
If you do use lazy list numbering, however, you should still start the
list with the number 1. At some point in the future, Markdown may support
starting ordered lists at an arbitrary number.
List markers typically start at the left margin, but may be indented by
up to three spaces. List markers must be followed by one or more spaces
or a tab.
To make lists look nice, you can wrap items with hanging indents:
* Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
viverra nec, fringilla in, laoreet vitae, risus.
* Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
Suspendisse id sem consectetuer libero luctus adipiscing.
But if you want to be lazy, you don't have to:
* Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
viverra nec, fringilla in, laoreet vitae, risus.
* Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
Suspendisse id sem consectetuer libero luctus adipiscing.
If list items are separated by blank lines, Markdown will wrap the
items in `<p>` tags in the HTML output. For example, this input:
* Bird
* Magic
will turn into:
<ul>
<li>Bird</li>
<li>Magic</li>
</ul>
But this:
* Bird
* Magic
will turn into:
<ul>
<li><p>Bird</p></li>
<li><p>Magic</p></li>
</ul>
List items may consist of multiple paragraphs. Each subsequent
paragraph in a list item must be indented by either 4 spaces
or one tab:
1. This is a list item with two paragraphs. Lorem ipsum dolor
sit amet, consectetuer adipiscing elit. Aliquam hendrerit
mi posuere lectus.
Vestibulum enim wisi, viverra nec, fringilla in, laoreet
vitae, risus. Donec sit amet nisl. Aliquam semper ipsum
sit amet velit.
2. Suspendisse id sem consectetuer libero luctus adipiscing.
It looks nice if you indent every line of the subsequent
paragraphs, but here again, Markdown will allow you to be
lazy:
* This is a list item with two paragraphs.
This is the second paragraph in the list item. You're
only required to indent the first line. Lorem ipsum dolor
sit amet, consectetuer adipiscing elit.
* Another item in the same list.
To put a blockquote within a list item, the blockquote's `>`
delimiters need to be indented:
* A list item with a blockquote:
> This is a blockquote
> inside a list item.
To put a code block within a list item, the code block needs
to be indented *twice* -- 8 spaces or two tabs:
* A list item with a code block:
<code goes here>
It's worth noting that it's possible to trigger an ordered list by
accident, by writing something like this:
1986. What a great season.
In other words, a *number-period-space* sequence at the beginning of a
line. To avoid this, you can backslash-escape the period:
1986\. What a great season.
### Code Blocks
Pre-formatted code blocks are used for writing about programming or
markup source code. Rather than forming normal paragraphs, the lines
of a code block are interpreted literally. Markdown wraps a code block
in both `<pre>` and `<code>` tags.
To produce a code block in Markdown, simply indent every line of the
block by at least 4 spaces or 1 tab. For example, given this input:
This is a normal paragraph:
This is a code block.
Markdown will generate:
<p>This is a normal paragraph:</p>
<pre><code>This is a code block.
</code></pre>
One level of indentation -- 4 spaces or 1 tab -- is removed from each
line of the code block. For example, this:
Here is an example of AppleScript:
tell application "Foo"
beep
end tell
will turn into:
<p>Here is an example of AppleScript:</p>
<pre><code>tell application "Foo"
beep
end tell
</code></pre>
A code block continues until it reaches a line that is not indented
(or the end of the article).
Within a code block, ampersands (`&`) and angle brackets (`<` and `>`)
are automatically converted into HTML entities. This makes it very
easy to include example HTML source code using Markdown -- just paste
it and indent it, and Markdown will handle the hassle of encoding the
ampersands and angle brackets. For example, this:
<div class="footer">
&copy; 2004 Foo Corporation
</div>
will turn into:
<pre><code>&lt;div class="footer"&gt;
&amp;copy; 2004 Foo Corporation
&lt;/div&gt;
</code></pre>
Regular Markdown syntax is not processed within code blocks. E.g.,
asterisks are just literal asterisks within a code block. This means
it's also easy to use Markdown to write about Markdown's own syntax.
### Horizontal Rules
You can produce a horizontal rule tag (`<hr />`) by placing three or
more hyphens, asterisks, or underscores on a line by themselves. If you
wish, you may use spaces between the hyphens or asterisks. Each of the
following lines will produce a horizontal rule:
* * *
***
*****
- - -
---------------------------------------
## Span Elements
### Links
Markdown supports two style of links: *inline* and *reference*.
In both styles, the link text is delimited by [square brackets].
To create an inline link, use a set of regular parentheses immediately
after the link text's closing square bracket. Inside the parentheses,
put the URL where you want the link to point, along with an *optional*
title for the link, surrounded in quotes. For example:
This is [an example](http://example.com/ "Title") inline link.
[This link](http://example.net/) has no title attribute.
Will produce:
<p>This is <a href="http://example.com/" title="Title">
an example</a> inline link.</p>
<p><a href="http://example.net/">This link</a> has no
title attribute.</p>
If you're referring to a local resource on the same server, you can
use relative paths:
See my [About](/about/) page for details.
Reference-style links use a second set of square brackets, inside
which you place a label of your choosing to identify the link:
This is [an example][id] reference-style link.
You can optionally use a space to separate the sets of brackets:
This is [an example] [id] reference-style link.
Then, anywhere in the document, you define your link label like this,
on a line by itself:
[id]: http://example.com/ "Optional Title Here"
That is:
* Square brackets containing the link identifier (optionally
indented from the left margin using up to three spaces);
* followed by a colon;
* followed by one or more spaces (or tabs);
* followed by the URL for the link;
* optionally followed by a title attribute for the link, enclosed
in double or single quotes, or enclosed in parentheses.
The following three link definitions are equivalent:
[foo]: http://example.com/ "Optional Title Here"
[foo]: http://example.com/ 'Optional Title Here'
[foo]: http://example.com/ (Optional Title Here)
**Note:** There is a known bug in Markdown.pl 1.0.1 which prevents
single quotes from being used to delimit link titles.
The link URL may, optionally, be surrounded by angle brackets:
[id]: <http://example.com/> "Optional Title Here"
You can put the title attribute on the next line and use extra spaces
or tabs for padding, which tends to look better with longer URLs:
[id]: http://example.com/longish/path/to/resource/here
"Optional Title Here"
Link definitions are only used for creating links during Markdown
processing, and are stripped from your document in the HTML output.
Link definition names may consist of letters, numbers, spaces, and
punctuation -- but they are *not* case sensitive. E.g. these two
links:
[link text][a]
[link text][A]
are equivalent.
The *implicit link name* shortcut allows you to omit the name of the
link, in which case the link text itself is used as the name.
Just use an empty set of square brackets -- e.g., to link the word
"Google" to the google.com web site, you could simply write:
[Google][]
And then define the link:
[Google]: http://google.com/
Because link names may contain spaces, this shortcut even works for
multiple words in the link text:
Visit [Daring Fireball][] for more information.
And then define the link:
[Daring Fireball]: http://daringfireball.net/
Link definitions can be placed anywhere in your Markdown document. I
tend to put them immediately after each paragraph in which they're
used, but if you want, you can put them all at the end of your
document, sort of like footnotes.
Here's an example of reference links in action:
I get 10 times more traffic from [Google] [1] than from
[Yahoo] [2] or [MSN] [3].
[1]: http://google.com/ "Google"
[2]: http://search.yahoo.com/ "Yahoo Search"
[3]: http://search.msn.com/ "MSN Search"
Using the implicit link name shortcut, you could instead write:
I get 10 times more traffic from [Google][] than from
[Yahoo][] or [MSN][].
[google]: http://google.com/ "Google"
[yahoo]: http://search.yahoo.com/ "Yahoo Search"
[msn]: http://search.msn.com/ "MSN Search"
Both of the above examples will produce the following HTML output:
<p>I get 10 times more traffic from <a href="http://google.com/"
title="Google">Google</a> than from
<a href="http://search.yahoo.com/" title="Yahoo Search">Yahoo</a>
or <a href="http://search.msn.com/" title="MSN Search">MSN</a>.</p>
For comparison, here is the same paragraph written using
Markdown's inline link style:
I get 10 times more traffic from [Google](http://google.com/ "Google")
than from [Yahoo](http://search.yahoo.com/ "Yahoo Search") or
[MSN](http://search.msn.com/ "MSN Search").
The point of reference-style links is not that they're easier to
write. The point is that with reference-style links, your document
source is vastly more readable. Compare the above examples: using
reference-style links, the paragraph itself is only 81 characters
long; with inline-style links, it's 176 characters; and as raw HTML,
it's 234 characters. In the raw HTML, there's more markup than there
is text.
With Markdown's reference-style links, a source document much more
closely resembles the final output, as rendered in a browser. By
allowing you to move the markup-related metadata out of the paragraph,
you can add links without interrupting the narrative flow of your
prose.
### Emphasis
Markdown treats asterisks (`*`) and underscores (`_`) as indicators of
emphasis. Text wrapped with one `*` or `_` will be wrapped with an
HTML `<em>` tag; double `*`'s or `_`'s will be wrapped with an HTML
`<strong>` tag. E.g., this input:
*single asterisks*
_single underscores_
**double asterisks**
__double underscores__
will produce:
<em>single asterisks</em>
<em>single underscores</em>
<strong>double asterisks</strong>
<strong>double underscores</strong>
You can use whichever style you prefer; the lone restriction is that
the same character must be used to open and close an emphasis span.
Emphasis can be used in the middle of a word:
un*frigging*believable
But if you surround an `*` or `_` with spaces, it'll be treated as a
literal asterisk or underscore.
To produce a literal asterisk or underscore at a position where it
would otherwise be used as an emphasis delimiter, you can backslash
escape it:
\*this text is surrounded by literal asterisks\*
### Code
To indicate a span of code, wrap it with backtick quotes (`` ` ``).
Unlike a pre-formatted code block, a code span indicates code within a
normal paragraph. For example:
Use the `printf()` function.
will produce:
<p>Use the <code>printf()</code> function.</p>
To include a literal backtick character within a code span, you can use
multiple backticks as the opening and closing delimiters:
``There is a literal backtick (`) here.``
which will produce this:
<p><code>There is a literal backtick (`) here.</code></p>
The backtick delimiters surrounding a code span may include spaces --
one after the opening, one before the closing. This allows you to place
literal backtick characters at the beginning or end of a code span:
A single backtick in a code span: `` ` ``
A backtick-delimited string in a code span: `` `foo` ``
will produce:
<p>A single backtick in a code span: <code>`</code></p>
<p>A backtick-delimited string in a code span: <code>`foo`</code></p>
With a code span, ampersands and angle brackets are encoded as HTML
entities automatically, which makes it easy to include example HTML
tags. Markdown will turn this:
Please don't use any `<blink>` tags.
into:
<p>Please don't use any <code>&lt;blink&gt;</code> tags.</p>
You can write this:
`&#8212;` is the decimal-encoded equivalent of `&mdash;`.
to produce:
<p><code>&amp;#8212;</code> is the decimal-encoded
equivalent of <code>&amp;mdash;</code>.</p>
### Images
Admittedly, it's fairly difficult to devise a "natural" syntax for
placing images into a plain text document format.
Markdown uses an image syntax that is intended to resemble the syntax
for links, allowing for two styles: *inline* and *reference*.
Inline image syntax looks like this:
![Alt text](/path/to/img.jpg)
![Alt text](/path/to/img.jpg "Optional title")
That is:
* An exclamation mark: `!`;
* followed by a set of square brackets, containing the `alt`
attribute text for the image;
* followed by a set of parentheses, containing the URL or path to
the image, and an optional `title` attribute enclosed in double
or single quotes.
Reference-style image syntax looks like this:
![Alt text][id]
Where "id" is the name of a defined image reference. Image references
are defined using syntax identical to link references:
[id]: url/to/image "Optional title attribute"
As of this writing, Markdown has no syntax for specifying the
dimensions of an image; if this is important to you, you can simply
use regular HTML `<img>` tags.
## Miscellaneous
### Automatic Links
Markdown supports a shortcut style for creating "automatic" links for URLs and email addresses: simply surround the URL or email address with angle brackets. What this means is that if you want to show the actual text of a URL or email address, and also have it be a clickable link, you can do this:
<http://example.com/>
Markdown will turn this into:
<a href="http://example.com/">http://example.com/</a>
Automatic links for email addresses work similarly, except that
Markdown will also perform a bit of randomized decimal and hex
entity-encoding to help obscure your address from address-harvesting
spambots. For example, Markdown will turn this:
<address@example.com>
into something like this:
<a href="&#x6D;&#x61;i&#x6C;&#x74;&#x6F;:&#x61;&#x64;&#x64;&#x72;&#x65;
&#115;&#115;&#64;&#101;&#120;&#x61;&#109;&#x70;&#x6C;e&#x2E;&#99;&#111;
&#109;">&#x61;&#x64;&#x64;&#x72;&#x65;&#115;&#115;&#64;&#101;&#120;&#x61;
&#109;&#x70;&#x6C;e&#x2E;&#99;&#111;&#109;</a>
which will render in a browser as a clickable link to "address@example.com".
(This sort of entity-encoding trick will indeed fool many, if not
most, address-harvesting bots, but it definitely won't fool all of
them. It's better than nothing, but an address published in this way
will probably eventually start receiving spam.)
### Backslash Escapes
Markdown allows you to use backslash escapes to generate literal
characters which would otherwise have special meaning in Markdown's
formatting syntax. For example, if you wanted to surround a word
with literal asterisks (instead of an HTML `<em>` tag), you can use
backslashes before the asterisks, like this:
\*literal asterisks\*
Markdown provides backslash escapes for the following characters:
\ backslash
` backtick
* asterisk
_ underscore
{} curly braces
[] square brackets
() parentheses
# hash mark
+ plus sign
- minus sign (hyphen)
. dot
! exclamation mark
# -*- coding: utf-8 -*-
from django.conf import settings as settings
from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.utils import simplejson
from django.utils.translation import ugettext_lazy as _
from mitxmako.shortcuts import render_to_response
from courseware.courses import get_opt_course_with_access
from courseware.access import has_access
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
from .models import Revision, Article, Namespace, CreateArticleForm, RevisionFormWithTitle, RevisionForm
import wiki_settings
def wiki_reverse(wiki_page, article=None, course=None, namespace=None, args=[], kwargs={}):
kwargs = dict(kwargs) # TODO: Figure out why if I don't do this kwargs sometimes contains {'article_path'}
if not 'course_id' in kwargs and course:
kwargs['course_id'] = course.id
if not 'article_path' in kwargs and article:
kwargs['article_path'] = article.get_path()
if not 'namespace' in kwargs and namespace:
kwargs['namespace'] = namespace
return reverse(wiki_page, kwargs=kwargs, args=args)
def update_template_dictionary(dictionary, request=None, course=None, article=None, revision=None):
if article:
dictionary['wiki_article'] = article
dictionary['wiki_title'] = article.title # TODO: What is the title when viewing the article in a course?
if not course and 'namespace' not in dictionary:
dictionary['namespace'] = article.namespace.name
if course:
dictionary['course'] = course
if 'namespace' not in dictionary:
dictionary['namespace'] = "edX"
else:
dictionary['course'] = None
if revision:
dictionary['wiki_article_revision'] = revision
dictionary['wiki_current_revision_deleted'] = not (revision.deleted == 0)
if request:
dictionary.update(csrf(request))
if request and course:
dictionary['staff_access'] = has_access(request.user, course, 'staff')
else:
dictionary['staff_access'] = False
def view(request, article_path, course_id=None):
course = get_opt_course_with_access(request.user, course_id, 'load')
(article, err) = get_article(request, article_path, course)
if err:
return err
perm_err = check_permissions(request, article, course, check_read=True, check_deleted=True)
if perm_err:
return perm_err
d = {}
update_template_dictionary(d, request, course, article, article.current_revision)
return render_to_response('simplewiki/simplewiki_view.html', d)
def view_revision(request, revision_number, article_path, course_id=None):
course = get_opt_course_with_access(request.user, course_id, 'load')
(article, err) = get_article(request, article_path, course)
if err:
return err
try:
revision = Revision.objects.get(counter=int(revision_number), article=article)
except:
d = {'wiki_err_norevision': revision_number}
update_template_dictionary(d, request, course, article)
return render_to_response('simplewiki/simplewiki_error.html', d)
perm_err = check_permissions(request, article, course, check_read=True, check_deleted=True, revision=revision)
if perm_err:
return perm_err
d = {}
update_template_dictionary(d, request, course, article, revision)
return render_to_response('simplewiki/simplewiki_view.html', d)
def root_redirect(request, course_id=None):
course = get_opt_course_with_access(request.user, course_id, 'load')
#TODO: Add a default namespace to settings.
namespace = "edX"
try:
root = Article.get_root(namespace)
return HttpResponseRedirect(reverse('wiki_view', kwargs={'course_id': course_id, 'article_path': root.get_path()}))
except:
# If the root is not found, we probably are loading this class for the first time
# We should make sure the namespace exists so the root article can be created.
Namespace.ensure_namespace(namespace)
err = not_found(request, namespace + '/', course)
return err
def create(request, article_path, course_id=None):
course = get_opt_course_with_access(request.user, course_id, 'load')
article_path_components = article_path.split('/')
# Ensure the namespace exists
if not len(article_path_components) >= 1 or len(article_path_components[0]) == 0:
d = {'wiki_err_no_namespace': True}
update_template_dictionary(d, request, course)
return render_to_response('simplewiki/simplewiki_error.html', d)
namespace = None
try:
namespace = Namespace.objects.get(name__exact=article_path_components[0])
except Namespace.DoesNotExist, ValueError:
d = {'wiki_err_bad_namespace': True}
update_template_dictionary(d, request, course)
return render_to_response('simplewiki/simplewiki_error.html', d)
# See if the article already exists
article_slug = article_path_components[1] if len(article_path_components) >= 2 else ''
#TODO: Make sure the slug only contains legal characters (which is already done a bit by the url regex)
try:
existing_article = Article.objects.get(namespace=namespace, slug__exact=article_slug)
#It already exists, so we just redirect to view the article
return HttpResponseRedirect(wiki_reverse("wiki_view", existing_article, course))
except Article.DoesNotExist:
#This is good. The article doesn't exist
pass
#TODO: Once we have permissions for namespaces, we should check for create permissions
#check_permissions(request, #namespace#, check_locked=False, check_write=True, check_deleted=True)
if request.method == 'POST':
f = CreateArticleForm(request.POST)
if f.is_valid():
article = Article()
article.slug = article_slug
if not request.user.is_anonymous():
article.created_by = request.user
article.title = f.cleaned_data.get('title')
article.namespace = namespace
a = article.save()
new_revision = f.save(commit=False)
if not request.user.is_anonymous():
new_revision.revision_user = request.user
new_revision.article = article
new_revision.save()
return HttpResponseRedirect(wiki_reverse("wiki_view", article, course))
else:
f = CreateArticleForm(initial={'title': request.GET.get('wiki_article_name', article_slug),
'contents': _('Headline\n===\n\n')})
d = {'wiki_form': f, 'create_article': True, 'namespace': namespace.name}
update_template_dictionary(d, request, course)
return render_to_response('simplewiki/simplewiki_edit.html', d)
def edit(request, article_path, course_id=None):
course = get_opt_course_with_access(request.user, course_id, 'load')
(article, err) = get_article(request, article_path, course)
if err:
return err
# Check write permissions
perm_err = check_permissions(request, article, course, check_write=True, check_locked=True, check_deleted=False)
if perm_err:
return perm_err
if wiki_settings.WIKI_ALLOW_TITLE_EDIT:
EditForm = RevisionFormWithTitle
else:
EditForm = RevisionForm
if request.method == 'POST':
f = EditForm(request.POST)
if f.is_valid():
new_revision = f.save(commit=False)
new_revision.article = article
if request.POST.__contains__('delete'):
if (article.current_revision.deleted == 1): # This article has already been deleted. Redirect
return HttpResponseRedirect(wiki_reverse('wiki_view', article, course))
new_revision.contents = ""
new_revision.deleted = 1
elif not new_revision.get_diff():
return HttpResponseRedirect(wiki_reverse('wiki_view', article, course))
if not request.user.is_anonymous():
new_revision.revision_user = request.user
new_revision.save()
if wiki_settings.WIKI_ALLOW_TITLE_EDIT:
new_revision.article.title = f.cleaned_data['title']
new_revision.article.save()
return HttpResponseRedirect(wiki_reverse('wiki_view', article, course))
else:
startContents = article.current_revision.contents if (article.current_revision.deleted == 0) else 'Headline\n===\n\n'
f = EditForm({'contents': startContents, 'title': article.title})
d = {'wiki_form': f}
update_template_dictionary(d, request, course, article)
return render_to_response('simplewiki/simplewiki_edit.html', d)
def history(request, article_path, page=1, course_id=None):
course = get_opt_course_with_access(request.user, course_id, 'load')
(article, err) = get_article(request, article_path, course)
if err:
return err
perm_err = check_permissions(request, article, course, check_read=True, check_deleted=False)
if perm_err:
return perm_err
page_size = 10
if page is None:
page = 1
try:
p = int(page)
except ValueError:
p = 1
history = Revision.objects.filter(article__exact=article).order_by('-counter').select_related('previous_revision__counter', 'revision_user', 'wiki_article')
if request.method == 'POST':
if request.POST.__contains__('revision'): # They selected a version, but they can be either deleting or changing the version
perm_err = check_permissions(request, article, course, check_write=True, check_locked=True)
if perm_err:
return perm_err
redirectURL = wiki_reverse('wiki_view', article, course)
try:
r = int(request.POST['revision'])
revision = Revision.objects.get(id=r)
if request.POST.__contains__('change'):
article.current_revision = revision
article.save()
elif request.POST.__contains__('view'):
redirectURL = wiki_reverse('wiki_view_revision', course=course,
kwargs={'revision_number': revision.counter, 'article_path': article.get_path()})
#The rese of these are admin functions
elif request.POST.__contains__('delete') and request.user.is_superuser:
if (revision.deleted == 0):
revision.adminSetDeleted(2)
elif request.POST.__contains__('restore') and request.user.is_superuser:
if (revision.deleted == 2):
revision.adminSetDeleted(0)
elif request.POST.__contains__('delete_all') and request.user.is_superuser:
Revision.objects.filter(article__exact=article, deleted=0).update(deleted=2)
elif request.POST.__contains__('lock_article'):
article.locked = not article.locked
article.save()
except Exception as e:
print str(e)
pass
finally:
return HttpResponseRedirect(redirectURL)
#
#
# <input type="submit" name="delete" value="Delete revision"/>
# <input type="submit" name="restore" value="Restore revision"/>
# <input type="submit" name="delete_all" value="Delete all revisions">
# %else:
# <input type="submit" name="delete_article" value="Delete all revisions">
#
page_count = (history.count() + (page_size - 1)) / page_size
if p > page_count:
p = 1
beginItem = (p - 1) * page_size
next_page = p + 1 if page_count > p else None
prev_page = p - 1 if p > 1 else None
d = {'wiki_page': p,
'wiki_next_page': next_page,
'wiki_prev_page': prev_page,
'wiki_history': history[beginItem:beginItem + page_size],
'show_delete_revision': request.user.is_superuser}
update_template_dictionary(d, request, course, article)
return render_to_response('simplewiki/simplewiki_history.html', d)
def revision_feed(request, page=1, namespace=None, course_id=None):
course = get_opt_course_with_access(request.user, course_id, 'load')
page_size = 10
if page is None:
page = 1
try:
p = int(page)
except ValueError:
p = 1
history = Revision.objects.order_by('-revision_date').select_related('revision_user', 'article', 'previous_revision')
page_count = (history.count() + (page_size - 1)) / page_size
if p > page_count:
p = 1
beginItem = (p - 1) * page_size
next_page = p + 1 if page_count > p else None
prev_page = p - 1 if p > 1 else None
d = {'wiki_page': p,
'wiki_next_page': next_page,
'wiki_prev_page': prev_page,
'wiki_history': history[beginItem:beginItem + page_size],
'show_delete_revision': request.user.is_superuser,
'namespace': namespace}
update_template_dictionary(d, request, course)
return render_to_response('simplewiki/simplewiki_revision_feed.html', d)
def search_articles(request, namespace=None, course_id=None):
course = get_opt_course_with_access(request.user, course_id, 'load')
# blampe: We should check for the presence of other popular django search
# apps and use those if possible. Only fall back on this as a last resort.
# Adding some context to results (eg where matches were) would also be nice.
# todo: maybe do some perm checking here
if request.method == 'GET':
querystring = request.GET.get('value', '').strip()
else:
querystring = ""
results = Article.objects.all()
if namespace:
results = results.filter(namespace__name__exact=namespace)
if request.user.is_superuser:
results = results.order_by('current_revision__deleted')
else:
results = results.filter(current_revision__deleted=0)
if querystring:
for queryword in querystring.split():
# Basic negation is as fancy as we get right now
if queryword[0] == '-' and len(queryword) > 1:
results._search = lambda x: results.exclude(x)
queryword = queryword[1:]
else:
results._search = lambda x: results.filter(x)
results = results._search(Q(current_revision__contents__icontains=queryword) | \
Q(title__icontains=queryword))
results = results.select_related('current_revision__deleted', 'namespace')
results = sorted(results, key=lambda article: (article.current_revision.deleted, article.get_path().lower()))
if len(results) == 1 and querystring:
return HttpResponseRedirect(wiki_reverse('wiki_view', article=results[0], course=course))
else:
d = {'wiki_search_results': results,
'wiki_search_query': querystring,
'namespace': namespace}
update_template_dictionary(d, request, course)
return render_to_response('simplewiki/simplewiki_searchresults.html', d)
def search_add_related(request, course_id, slug, namespace):
course = get_opt_course_with_access(request.user, course_id, 'load')
(article, err) = get_article(request, slug, namespace if namespace else course_id)
if err:
return err
perm_err = check_permissions(request, article, course, check_read=True)
if perm_err:
return perm_err
search_string = request.GET.get('query', None)
self_pk = request.GET.get('self', None)
if search_string:
results = []
related = Article.objects.filter(title__istartswith=search_string)
others = article.related.all()
if self_pk:
related = related.exclude(pk=self_pk)
if others:
related = related.exclude(related__in=others)
related = related.order_by('title')[:10]
for item in related:
results.append({'id': str(item.id),
'value': item.title,
'info': item.get_url()})
else:
results = []
json = simplejson.dumps({'results': results})
return HttpResponse(json, mimetype='application/json')
def add_related(request, course_id, slug, namespace):
course = get_opt_course_with_access(request.user, course_id, 'load')
(article, err) = get_article(request, slug, namespace if namespace else course_id)
if err:
return err
perm_err = check_permissions(request, article, course, check_write=True, check_locked=True)
if perm_err:
return perm_err
try:
related_id = request.POST['id']
rel = Article.objects.get(id=related_id)
has_already = article.related.filter(id=related_id).count()
if has_already == 0 and not rel == article:
article.related.add(rel)
article.save()
except:
pass
finally:
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
def remove_related(request, course_id, namespace, slug, related_id):
course = get_opt_course_with_access(request.user, course_id, 'load')
(article, err) = get_article(request, slug, namespace if namespace else course_id)
if err:
return err
perm_err = check_permissions(request, article, course, check_write=True, check_locked=True)
if perm_err:
return perm_err
try:
rel_id = int(related_id)
rel = Article.objects.get(id=rel_id)
article.related.remove(rel)
article.save()
except:
pass
finally:
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
def random_article(request, course_id=None):
course = get_opt_course_with_access(request.user, course_id, 'load')
from random import randint
num_arts = Article.objects.count()
article = Article.objects.all()[randint(0, num_arts - 1)]
return HttpResponseRedirect(wiki_reverse('wiki_view', article, course))
def not_found(request, article_path, course):
"""Generate a NOT FOUND message for some URL"""
d = {'wiki_err_notfound': True,
'article_path': article_path,
'namespace': "edX"}
update_template_dictionary(d, request, course)
return render_to_response('simplewiki/simplewiki_error.html', d)
def get_article(request, article_path, course):
err = None
article = None
try:
article = Article.get_article(article_path)
except Article.DoesNotExist, ValueError:
err = not_found(request, article_path, course)
return (article, err)
def check_permissions(request, article, course, check_read=False, check_write=False, check_locked=False, check_deleted=False, revision=None):
read_err = check_read and not article.can_read(request.user)
write_err = check_write and not article.can_write(request.user)
locked_err = check_locked and article.locked
if revision is None:
revision = article.current_revision
deleted_err = check_deleted and not (revision.deleted == 0)
if (request.user.is_superuser):
deleted_err = False
locked_err = False
if read_err or write_err or locked_err or deleted_err:
d = {'wiki_article': article,
'wiki_err_noread': read_err,
'wiki_err_nowrite': write_err,
'wiki_err_locked': locked_err,
'wiki_err_deleted': deleted_err, }
update_template_dictionary(d, request, course)
# TODO: Make this a little less jarring by just displaying an error
# on the current page? (no such redirect happens for an anon upload yet)
# benjaoming: I think this is the nicest way of displaying an error, but
# these errors shouldn't occur, but rather be prevented on the other pages.
return render_to_response('simplewiki/simplewiki_error.html', d)
else:
return None
####################
# LOGIN PROTECTION #
####################
if wiki_settings.WIKI_REQUIRE_LOGIN_VIEW:
view = login_required(view)
history = login_required(history)
search_articles = login_required(search_articles)
root_redirect = login_required(root_redirect)
revision_feed = login_required(revision_feed)
random_article = login_required(random_article)
search_add_related = login_required(search_add_related)
not_found = login_required(not_found)
view_revision = login_required(view_revision)
if wiki_settings.WIKI_REQUIRE_LOGIN_EDIT:
create = login_required(create)
edit = login_required(edit)
add_related = login_required(add_related)
remove_related = login_required(remove_related)
if wiki_settings.WIKI_CONTEXT_PREPROCESSORS:
settings.TEMPLATE_CONTEXT_PROCESSORS += wiki_settings.WIKI_CONTEXT_PREPROCESSORS
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
# Default settings.. overwrite in your own settings.py
# Planned feature.
WIKI_USE_MARKUP_WIDGET = True
####################
# LOGIN PROTECTION #
####################
# Before setting the below parameters, please note that permissions can
# be set in the django permission system on individual articles and their
# child articles. In this way you can add a user group and give them
# special permissions, be it on the root article or some other. Permissions
# are inherited on lower levels.
# Adds standard django login protection for viewing
WIKI_REQUIRE_LOGIN_VIEW = getattr(settings, 'SIMPLE_WIKI_REQUIRE_LOGIN_VIEW',
True)
# Adds standard django login protection for editing
WIKI_REQUIRE_LOGIN_EDIT = getattr(settings, 'SIMPLE_WIKI_REQUIRE_LOGIN_EDIT',
True)
####################
# ATTACHMENTS #
####################
# This should be a directory that's writable for the web server.
# It's relative to the MEDIA_ROOT.
WIKI_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS',
'simplewiki/attachments/')
# If false, attachments will completely disappear
WIKI_ALLOW_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ALLOW_ATTACHMENTS',
False)
# If WIKI_REQUIRE_LOGIN_EDIT is False, then attachments can still be disallowed
WIKI_ALLOW_ANON_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ALLOW_ANON_ATTACHMENTS', False)
# Attachments are automatically stored with a dummy extension and delivered
# back to the user with their original extension.
# This setting does not add server security, but might add user security
# if set -- or force users to use standard formats, which might also
# be a good idea.
# Example: ('pdf', 'doc', 'gif', 'jpeg', 'jpg', 'png')
WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS',
None)
# At the moment this variable should not be modified, because
# it breaks compatibility with the normal Django FileField and uploading
# from the admin interface.
WIKI_ATTACHMENTS_ROOT = settings.MEDIA_ROOT
# Bytes! Default: 1 MB.
WIKI_ATTACHMENTS_MAX = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS_MAX',
1 * 1024 * 1024)
# Allow users to edit titles of pages
# (warning! titles are not maintained in the revision system.)
WIKI_ALLOW_TITLE_EDIT = getattr(settings, 'SIMPLE_WIKI_ALLOW_TITLE_EDIT', False)
# Global context processors
# These are appended to TEMPLATE_CONTEXT_PROCESSORS in your Django settings
# whenever the wiki is in use. It can be used as a simple, but effective
# way of extending simplewiki without touching original code (and thus keeping
# everything easily maintainable)
WIKI_CONTEXT_PREPROCESSORS = getattr(settings, 'SIMPLE_WIKI_CONTEXT_PREPROCESSORS',
())
####################
# AESTHETICS #
####################
# List of extensions to be used by Markdown. Custom extensions (i.e., with file
# names of mdx_*.py) can be dropped into the simplewiki (or project) directory
# and then added to this list to be utilized. Wiki is enabled automatically.
#
# For more information, see
# http://www.freewisdom.org/projects/python-markdown/Available_Extensions
WIKI_MARKDOWN_EXTENSIONS = getattr(settings, 'SIMPLE_WIKI_MARKDOWN_EXTENSIONS',
['footnotes',
'tables',
'headerid',
'fenced_code',
'def_list',
#'codehilite', #This was throwing errors
'abbr',
'toc',
'mathjax',
'video', # In-line embedding for YouTube, etc.
'circuit',
])
WIKI_IMAGE_EXTENSIONS = getattr(settings,
'SIMPLE_WIKI_IMAGE_EXTENSIONS',
('jpg', 'jpeg', 'gif', 'png', 'tiff', 'bmp'))
# Planned features
WIKI_PAGE_WIDTH = getattr(settings,
'SIMPLE_WIKI_PAGE_WIDTH', "100%")
WIKI_PAGE_ALIGN = getattr(settings,
'SIMPLE_WIKI_PAGE_ALIGN', "center")
WIKI_IMAGE_THUMB_SIZE = getattr(settings,
'SIMPLE_WIKI_IMAGE_THUMB_SIZE', (200, 150))
WIKI_IMAGE_THUMB_SIZE_SMALL = getattr(settings,
'SIMPLE_WIKI_IMAGE_THUMB_SIZE_SMALL', (100, 100))
...@@ -21,7 +21,8 @@ class SimpleTest(TestCase): ...@@ -21,7 +21,8 @@ class SimpleTest(TestCase):
""" """
# since I had to remap files, pedantically test all press releases # since I had to remap files, pedantically test all press releases
# published to date. Decent positive test while we're at it. # published to date. Decent positive test while we're at it.
all_releases = ["/press/mit-and-harvard-announce-edx", all_releases = [
"/press/mit-and-harvard-announce-edx",
"/press/uc-berkeley-joins-edx", "/press/uc-berkeley-joins-edx",
"/press/edX-announces-proctored-exam-testing", "/press/edX-announces-proctored-exam-testing",
"/press/elsevier-collaborates-with-edx", "/press/elsevier-collaborates-with-edx",
......
...@@ -15,7 +15,8 @@ from util.cache import cache_if_anonymous ...@@ -15,7 +15,8 @@ from util.cache import cache_if_anonymous
valid_templates = [] valid_templates = []
if settings.STATIC_GRAB: if settings.STATIC_GRAB:
valid_templates = valid_templates + ['server-down.html', valid_templates = valid_templates + [
'server-down.html',
'server-error.html' 'server-error.html'
'server-overloaded.html', 'server-overloaded.html',
] ]
......
...@@ -689,7 +689,6 @@ INSTALLED_APPS = ( ...@@ -689,7 +689,6 @@ INSTALLED_APPS = (
'student', 'student',
'static_template_view', 'static_template_view',
'staticbook', 'staticbook',
'simplewiki',
'track', 'track',
'util', 'util',
'certificates', 'certificates',
......
...@@ -5,16 +5,14 @@ def delete_threads(commentable_id, *args, **kwargs): ...@@ -5,16 +5,14 @@ def delete_threads(commentable_id, *args, **kwargs):
def get_threads(commentable_id, recursive=False, query_params={}, *args, **kwargs): def get_threads(commentable_id, recursive=False, query_params={}, *args, **kwargs):
default_params = {'page': 1, 'per_page': 20, 'recursive': recursive} default_params = {'page': 1, 'per_page': 20, 'recursive': recursive}
attributes = dict(default_params.items() + query_params.items()) attributes = dict(default_params.items() + query_params.items())
response = _perform_request('get', _url_for_threads(commentable_id), \ response = _perform_request('get', _url_for_threads(commentable_id), attributes, *args, **kwargs)
attributes, *args, **kwargs)
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1) return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
def search_threads(course_id, recursive=False, query_params={}, *args, **kwargs): def search_threads(course_id, recursive=False, query_params={}, *args, **kwargs):
default_params = {'page': 1, 'per_page': 20, 'course_id': course_id, 'recursive': recursive} default_params = {'page': 1, 'per_page': 20, 'course_id': course_id, 'recursive': recursive}
attributes = dict(default_params.items() + query_params.items()) attributes = dict(default_params.items() + query_params.items())
response = _perform_request('get', _url_for_search_threads(), \ response = _perform_request('get', _url_for_search_threads(), attributes, *args, **kwargs)
attributes, *args, **kwargs)
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1) return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
......
##This file is based on the template from the SimpleWiki source which carries the GPL license
<%inherit file="../main.html"/>
<%namespace name='static' file='../static_content.html'/>
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%!
from django.core.urlresolvers import reverse
from simplewiki.views import wiki_reverse
%>
<%block name="js_extra">
<script type="text/javascript" src="${static.url('js/simplewiki-AutoSuggest_c_2.0.js')}"></script>
## TODO (cpennington): Remove this when we have a good way for modules to specify js to load on the page
## and in the wiki
<script type="text/javascript" src="${static.url('js/schematic.js')}"></script>
<script type="text/javascript">
function set_related_article_id(s) {
document.getElementById('wiki_related_input_id').value = s.id;
document.getElementById('wiki_related_input_submit').disabled=false;
}
%if wiki_article is not UNDEFINED:
var x = window.onload;
window.onload = function(){
var options = {
script: "${ wiki_reverse('search_related', wiki_article, course)}/?self=${wiki_article.pk}&",
json: true,
varname: "query",
maxresults: 35,
callback: set_related_article_id,
noresults: "Nothing found!"
};
var as = new AutoSuggest('wiki_related_input', options);
if (typeof x == 'function')
x();
}
%endif
</script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {inlineMath: [ ['$','$'], ["\\(","\\)"]],
displayMath: [ ['$$','$$'], ["\\[","\\]"]]}
});
</script>
<script>
$(function(){
$.ajaxSetup ({
// Disable caching of AJAX responses
cache: false
});
$(".div_wiki_circuit").each(function(d,e) {
id = $(this).attr("id");
name = id.substring(17);
//alert(name);
$("#"+id).load("/edit_circuit/"+name, function(){update_schematics();});
f=this;
});
$("#wiki_create_form").hide();
$("#create-article").click(function() {
$("#wiki_create_form").slideToggle();
$(this).parent().toggleClass("active");
});
});
</script>
<%block name="wiki_head"/>
</%block>
<%block name="bodyextra">
%if course:
<%include file="/courseware/course_navigation.html" args="active_page='wiki'" />
%endif
<section class="container">
<div class="wiki-wrapper">
<%block name="wiki_panel">
<div aria-label="Wiki Navigation" id="wiki_panel">
<h2>Course Wiki</h2>
<ul class="action">
<li>
<h3>
<a href="${wiki_reverse("wiki_list_articles", course=course, namespace=namespace)}">All Articles</a>
</h3>
</li>
<li class="create-article">
<h3>
<a href="#" id="create-article"/>Create Article</a>
</h3>
<div id="wiki_create_form">
<%
baseURL = wiki_reverse("wiki_create", course=course, kwargs={"article_path" : namespace + "/" })
%>
<form method="GET" onsubmit="this.action='${baseURL}' + this.wiki_article_name.value.replace(/([^a-zA-Z0-9\-])/g, '');">
<div>
<label for="id_wiki_article_name">Title of article</label>
<input type="text" name="wiki_article_name" id="id_wiki_article_name" />
</div>
<ul>
<li>
<input type="submit" class="button" value="Create" />
</li>
</ul>
</form>
</div>
</li>
<li class="search">
<form method="GET" action='${wiki_reverse("wiki_search_articles", course=course, namespace=namespace)}'>
<label class="wiki_box_title">Search</label>
<input type="text" placeholder="Search" name="value" id="wiki_search_input" value="${wiki_search_query if wiki_search_query is not UNDEFINED else '' |h}"/>
<input type="submit" id="wiki_search_input_submit" value="Go!" />
</form>
</li>
</ul>
</div>
</%block>
<section class="wiki-body">
%if wiki_article is not UNDEFINED:
<header>
%if wiki_article.locked:
<p><strong>This article has been locked</strong></p>
%endif
<p>Last modified: ${wiki_article.modified_on.strftime("%b %d, %Y, %I:%M %p")}</p>
%endif
%if wiki_article is not UNDEFINED:
<ul>
<li>
<a href="${ wiki_reverse('wiki_view', wiki_article, course)}" class="view">View</a>
</li>
<li>
<a href="${ wiki_reverse('wiki_edit', wiki_article, course)}" class="edit">Edit</a>
</li>
<li>
<a href="${ wiki_reverse('wiki_history', wiki_article, course)}" class="history">History</a>
</li>
</ul>
</header>
%endif
<%block name="wiki_page_title"/>
<%block name="wiki_body"/>
</section>
</div>
</section>
</%block>
##This file is based on the template from the SimpleWiki source which carries the GPL license
<%inherit file="simplewiki_base.html"/>
<%block name="title">
<title>
%if create_article:
Wiki – Create Article – MITx 6.002x
%else:
${"Edit " + wiki_title + " - " if wiki_title is not UNDEFINED else ""}MITx 6.002x Wiki
%endif
</title></%block>
<%block name="wiki_page_title">
%if create_article:
<h1>Create article</h1>
%else:
<h1>${ wiki_article.title }</h1>
%endif
</%block>
<%block name="wiki_head">
<script type="text/javascript" src="${ settings.LIB_URL }vendor/CodeMirror/codemirror.js"></script>
<link rel="stylesheet" href="${ settings.LIB_URL }vendor/CodeMirror/codemirror.css" />
<script type="text/javascript" src="${ settings.LIB_URL }vendor/CodeMirror/xml.js"></script>
<script type="text/javascript" src="${ settings.LIB_URL }vendor/CodeMirror/mitx_markdown.js"></script>
<script>
$(function(){
$(document).ready(function() {
//TODO: Re-enable this once the styling supports it
// var editor = CodeMirror.fromTextArea(document.getElementById("id_contents"), {
// mode: 'mitx_markdown',
// matchBrackets: true,
// theme: "default",
// lineWrapping: true,
// });
//
// //Store the inital contents so we can compare for unsaved changes
// var initial_contents = editor.getValue();
//
// window.onbeforeunload = function askConfirm() { //Warn the user before they navigate away
// if ( editor.getValue() != initial_contents ) {
// return "You have made changes to the article that have not been saved yet.";
// }
// };
//
// $("#submit_edit").click(function() {
// initial_contents = editor.getValue();
// });
});
});
</script>
</%block>
<%block name="wiki_body">
<form method="POST" id="wiki_revision">
<div style="display:none">
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}"/>
</div>
${wiki_form}
%if create_article:
<input type="submit" id="submit_edit" value="Create article" />
%else:
<input type="submit" id="submit_edit" name="edit" value="Save Changes" />
<input type="submit" id="submit_delete" name="delete" value="Delete article" />
%endif
<%include file="simplewiki_instructions.html"/>
</%block>
##This file is based on the template from the SimpleWiki source which carries the GPL license
<%inherit file="simplewiki_base.html"/>
<%!
from simplewiki.views import wiki_reverse
%>
<%block name="title"><title>Wiki Error – MITx 6.002x</title></%block>
<%block name="wiki_page_title">
<h1>Oops...</h1>
</%block>
<%block name="wiki_body">
<div class="wiki_error">
%if wiki_error is not UNDEFINED:
${wiki_error}
%endif
%if wiki_err_notfound is not UNDEFINED:
<p>
The page you requested could not be found.
Click <a href="${wiki_reverse("wiki_create", course=course, kwargs={'article_path' : article_path})}">here</a> to create it.
</p>
%elif wiki_err_no_namespace is not UNDEFINED and wiki_err_no_namespace:
<p>
You must specify a namespace to create an article in.
</p>
%elif wiki_err_bad_namespace is not UNDEFINED and wiki_err_bad_namespace:
<p>
The namespace for this article does not exist. This article cannot be created.
</p>
%elif wiki_err_locked is not UNDEFINED and wiki_err_locked:
<p>
The article you are trying to modify is locked.
</p>
%elif wiki_err_noread is not UNDEFINED and wiki_err_noread:
<p>
You do not have access to read this article.
</p>
%elif wiki_err_nowrite is not UNDEFINED and wiki_err_nowrite:
<p>
You do not have access to edit this article.
</p>
%elif wiki_err_noanon is not UNDEFINED and wiki_err_noanon:
<p>
Anonymous attachments are not allowed. Try logging in.
</p>
%elif wiki_err_create is not UNDEFINED and wiki_err_create:
<p>
You do not have access to create this article.
</p>
%elif wiki_err_encode is not UNDEFINED and wiki_err_encode:
<p>
The url you requested could not be handled by the wiki.
Probably you used a bad character in the URL.
Only use digits, English letters, underscore and dash. For instance
/wiki/An_Article-1
</p>
%elif wiki_err_deleted is not UNDEFINED and wiki_err_deleted:
<p>
The article you tried to access has been deleted. You may be able to restore it to an earlier version in its <a href="${wiki_reverse("wiki_history", wiki_article, course)}">history</a>, or <a href="${wiki_reverse("wiki_edit", wiki_article, course)}">create a new version</a>.
</p>
%elif wiki_err_norevision is not UNDEFINED:
<p>
This article does not contain revision ${wiki_err_norevision | h}.
</p>
%else:
<p>
An error has occured.
</p>
%endif
</div>
</%block>
##This file is based on the template from the SimpleWiki source which carries the GPL license
<%inherit file="simplewiki_base.html"/>
<%block name="title"><title>${"Revision history of " + wiki_title + " - " if wiki_title is not UNDEFINED else ""}Wiki – MITx 6.002x</title></%block>
<%!
from django.core.urlresolvers import reverse
from simplewiki.views import wiki_reverse
%>
<%block name="wiki_page_title">
<h1>
${ wiki_article.title }
</h1>
</%block>
<%block name="wiki_body">
<form method="POST">
<div style="display:none">
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}"/>
</div>
<table id="wiki_history_table" class="wiki-history">
<thead>
<tr>
<th id="revision">Revision</th>
<th id="comment">Comment</th>
<th id="diff">Diff</th>
<th id="modified">Modified</th>
</tr>
</thead>
<tbody>
<% loopCount = 0 %>
%for revision in wiki_history:
%if revision.deleted < 2 or show_delete_revision:
<% loopCount += 1 %>
<tr style="border-top: 1px" class="${'dark ' if (loopCount % 2) == 0 else ''}${'deleted ' if (revision.deleted==2) else ''}" >
<td width="15px">
<input type="radio" name="revision" id="${revision.id}" value="${revision.id}"${"checked" if wiki_article.current_revision.id == revision.id else ""}/>
<label for="${revision.id}">
${ revision }
%if revision.previous_revision:
%if not revision.counter == revision.previous_revision.counter + 1:
<br/>(based on ${revision.previous_revision})
%endif
%endif
</label>
</td>
<td>
${ revision.revision_text if revision.revision_text else "<i>None</i>" }</td>
<td class="diff">
%for x in revision.get_diff():
${x|h}<br/>
%endfor </td>
<td>${revision.get_user()}
<br/>
${revision.revision_date.strftime("%b %d, %Y, %I:%M %p")}
</td>
</tr>
%endif
%endfor
</tbody>
%if wiki_prev_page or wiki_next_page:
<tfoot>
<tr>
<td colspan="4">
%if wiki_prev_page:
<a href="${wiki_reverse('wiki_history', wiki_article, course, kwargs={'page' : wiki_prev_page})}">Previous page</a>
%endif
%if wiki_next_page:
<a href="${wiki_reverse('wiki_history', wiki_article, course, kwargs={'page' : wiki_next_page})}">Next page</a>
%endif
</td>
</tr>
</tfoot>
%endif
</table>
<div class="history-controls"><input type="submit" name="view" value="View revision"/>
<input type="submit" name="change" value="Change to revision"
%if not wiki_write:
disabled="true"
%endif
/>
%if show_delete_revision:
<input type="submit" name="delete" value="Delete revision"/>
<input type="submit" name="restore" value="Restore revision"/>
<input type="submit" name="delete_all" value="Delete all revisions">
<input type="submit" name="lock_article" value="${'Lock Article' if not wiki_article.locked else 'Unlock Article'}">
%endif
</div>
</form>
</%block>
<div id="wiki_edit_instructions">
This wiki uses <strong>Markdown</strong> for styling.
<h2> MITx Additions: </h2>
<p class="markdown-example">circuit-schematic:</p>
<p class="markdown-example"><span>$</span>LaTeX Math Expression<span>$</span></p>
To create a new wiki article, create a link to it. Clicking the link gives you the creation page.
<p class="markdown-example">[Article Name](wiki:ArticleName)</p>
<h2>Useful examples: </h2>
<p class="markdown-example">[Link](http://google.com)</p>
<p class="markdown-example">Huge Header
<br>====</p>
<p class="markdown-example">Smaller Header
<br>-------</p>
<p class="markdown-example">*emphasis* or _emphasis_</p>
<p class="markdown-example">**strong** or __strong__</p>
<p class="markdown-example">- Unordered List
<br>&nbsp&nbsp- Sub Item 1
<br>&nbsp&nbsp- Sub Item 2</p>
<p class="markdown-example">1. Ordered
<br>2. List</p>
<p>Need more help? There are several <a href="http://daringfireball.net/projects/markdown/basics">useful</a> <a href="http://greg.vario.us/doc/markdown.txt">guides</a> <a href="http://www.lowendtalk.com/discussion/6/miniature-markdown-guide">online</a>.</p>
</div>
##This file is based on the template from the SimpleWiki source which carries the GPL license
<%inherit file="simplewiki_base.html"/>
<%block name="title"><title>Wiki - Revision feed - MITx 6.002x</title></%block>
<%!
from simplewiki.views import wiki_reverse
%>
<%block name="wiki_page_title">
<h1>Revision Feed - Page ${wiki_page}</h1>
</%block>
<%block name="wiki_body">
<table id="wiki_history_table" class="wiki-history">
<thead>
<tr>
<th id="revision">Revision</th>
<th id="comment">Comment</th>
<th id="diff">Diff</th>
<th id="modified">Modified</th>
</tr>
</thead>
<tbody>
<% loopCount = 0 %>
%for revision in wiki_history:
%if revision.deleted < 2 or show_delete_revision:
<% loopCount += 1 %>
<tr style="border-top: 1px" class="${'dark ' if (loopCount % 2) == 0 else ''}${'deleted ' if (revision.deleted==2) else ''}" >
<td width="15px">
<a href="${wiki_reverse('wiki_view_revision', revision.article, course, kwargs={'revision_number' : revision.counter})}"> ${revision.article.title} - ${revision}</a>
</td>
<td>
${ revision.revision_text if revision.revision_text else "<i>None</i>" }</td>
<td class="diff">
%for x in revision.get_diff():
${x|h}<br/>
%endfor </td>
<td>${revision.get_user()}
<br/>
${revision.revision_date.strftime("%b %d, %Y, %I:%M %p")}
</td>
</tr>
%endif
%endfor
</tbody>
%if wiki_prev_page or wiki_next_page:
<tfoot>
<tr>
<td colspan="4">
%if wiki_prev_page:
<a href="${wiki_reverse("wiki_revision_feed", course=course, namespace=namespace, kwargs={'page': wiki_prev_page})}">Previous page</a>
%endif
%if wiki_next_page:
<a href="${wiki_reverse("wiki_revision_feed", course=course, namespace=namespace, kwargs={'page': wiki_next_page})}">Next page</a>
%endif
</td>
</tr>
</tfoot>
%endif
</table>
</%block>
##This file is based on the template from the SimpleWiki source which carries the GPL license
<%inherit file="simplewiki_base.html"/>
<%block name="title"><title>Wiki - Search Results - MITx 6.002x</title></%block>
<%!
from simplewiki.views import wiki_reverse
%>
<%block name="wiki_page_title">
<h2 class="wiki-title">
%if wiki_search_query:
Search results for ${wiki_search_query | h}
%else:
Displaying all articles
%endif
</h2>
</%block>
<%block name="wiki_body">
<section class="results">
<ul class="article-list">
%for article in wiki_search_results:
<% article_deleted = not article.current_revision.deleted == 0 %>
<li><h3><a href="${wiki_reverse("wiki_view", article, course)}">${article.title} ${'(Deleted)' if article_deleted else ''}</a></h3></li>
%endfor
%if not wiki_search_results:
No articles matching <b>${wiki_search_query if wiki_search_query is not UNDEFINED else ""} </b>!
%endif
</ul>
</section>
</%block>
##This file is based on the template from the SimpleWiki source which carries the GPL license
##This file has been converted to Mako, but not tested. It is because uploads are disabled for the wiki. If they are reenabled, this may contain bugs.
<%!
from django.template.defaultfilters import filesizeformat
%>
%if started:
<script type="text/javascript">
parent.document.getElementById("wiki_attach_progress_container").style.display='block';
</script>
%else:
%if finished:
<script type="text/javascript">
parent.document.getElementById("wiki_attach_progress_container").style.display='none';
parent.location.reload();
</script>
%else:
%if overwrite_warning:
<script type="text/javascript">
if (confirm('Warning: The filename already exists? Really overwrite ${ filename }?'))
parent.document.getElementById("wiki_attach_overwrite").checked=true;
parent.document.getElementById("wiki_attach_overwrite").form.submit();
</script>
%else:
%if too_big:
<script type="text/javascript">
alert('File is too big. Maximum: ${filesizeformat(max_size)}\nYour file was: ${filesizeformat(file.size)}');
</script>
%else:
<script type="text/javascript">
parent.document.getElementById("wiki_attach_progress").style.width='${progress_width}%';
</script>
%endif
%endif
%endif
%endif
##This file is based on the template from the SimpleWiki source which carries the GPL license
<%inherit file="simplewiki_base.html"/>
<%block name="title"><title>${wiki_title + " - " if wiki_title is not UNDEFINED else ""}Wiki – MITx 6.002x</title></%block>
<%block name="wiki_page_title">
<h1>${ wiki_article.title } ${'<span style="color: red;">- Deleted Revision!</span>' if wiki_current_revision_deleted else ''}</h1>
</%block>
<%block name="wiki_body">
<div id="wiki_article">
${ wiki_article_revision.contents_parsed| n}
</div>
</%block>
...@@ -2,37 +2,34 @@ ...@@ -2,37 +2,34 @@
<h2> ${display_name} </h2> <h2> ${display_name} </h2>
% endif % endif
%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: %if settings.MITX_FEATURES.get('USE_YOUTUBE_OBJECT_API') and normal_speed_video_id:
<div id="stub_out_video_for_testing">
<div class="video" data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}">
<section class="video-controls">
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#"></a></li>
<li>
<div class="vidtime">0:00 / 0:00</div>
</li>
</ul>
<div class="secondary-controls">
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
</div>
</div>
</section>
</div>
</div>
%elif settings.MITX_FEATURES.get('USE_YOUTUBE_OBJECT_API') and normal_speed_video_id:
<object width="640" height="390"> <object width="640" height="390">
<param name="movie" <param name="movie"
value="https://www.youtube.com/v/${normal_speed_video_id}?version=3&amp;autoplay=1&amp;rel=0"></param> % if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
value="https://www.youtube.com/v/${normal_speed_video_id}?version=3&amp;autoplay=1&amp;rel=0">
% endif
</param>
<param name="allowScriptAccess" value="always"></param> <param name="allowScriptAccess" value="always"></param>
<embed src="https://www.youtube.com/v/${normal_speed_video_id}?version=3&amp;autoplay=1&amp;rel=0" <embed
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
src="https://www.youtube.com/v/${normal_speed_video_id}?version=3&amp;autoplay=1&amp;rel=0"
% endif
type="application/x-shockwave-flash" type="application/x-shockwave-flash"
allowscriptaccess="always" allowscriptaccess="always"
width="640" height="390"></embed> width="640" height="390"></embed>
</object> </object>
%else: %else:
<div id="video_${id}" class="video" data-streams="${streams}" data-show-captions="${show_captions}" data-start="${start}" data-end="${end}" data-caption-asset-path="${caption_asset_path}" data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}"> <div id="video_${id}"
class="video"
data-youtube-id-0-75="${youtube_id_0_75}"
data-youtube-id-1-0="${youtube_id_1_0}"
data-youtube-id-1-25="${youtube_id_1_25}"
data-youtube-id-1-5="${youtube_id_1_5}"
data-show-captions="${show_captions}"
data-start="${start}"
data-end="${end}"
data-caption-asset-path="${caption_asset_path}"
data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}">
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<section class="video-player"> <section class="video-player">
......
...@@ -2,34 +2,38 @@ ...@@ -2,34 +2,38 @@
<h2> ${display_name} </h2> <h2> ${display_name} </h2>
% endif % endif
%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: <div
<div id="stub_out_video_for_testing"></div>
%else:
<div
id="video_${id}" id="video_${id}"
class="video" class="video"
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
data-streams="${youtube_streams}" data-streams="${youtube_streams}"
% endif
${'data-sub="{}"'.format(sub) if sub else ''} ${'data-sub="{}"'.format(sub) if sub else ''}
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''} ${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''}
${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''} ${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''}
${'data-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''} ${'data-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''}
% endif
data-caption-data-dir="${data_dir}" data-caption-data-dir="${data_dir}"
data-show-captions="${show_captions}" data-show-captions="${show_captions}"
data-start="${start}" data-start="${start}"
data-end="${end}" data-end="${end}"
data-caption-asset-path="${caption_asset_path}" data-caption-asset-path="${caption_asset_path}"
data-autoplay="${autoplay}" data-autoplay="${autoplay}"
> >
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<section class="video-player"> <section class="video-player">
<div id="${id}"></div> <div id="${id}"></div>
</section> </section>
<section class="video-controls"></section> <section class="video-controls"></section>
</article> </article>
</div> </div>
</div> </div>
%endif
% if sources.get('main'): % if sources.get('main'):
<div class="video-sources"> <div class="video-sources">
......
% if settings.MITX_FEATURES.get('SEGMENT_IO_LMS'):
<!-- begin Segment.io --> <!-- begin Segment.io -->
<script type="text/javascript"> <script type="text/javascript">
// Leaving this line out of the feature flag block is intentional. Pulling the line outside of the if statement allows it to serve as its own dummy object.
var analytics=analytics||[];analytics.load=function(e){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=("https:"===document.location.protocol?"https://":"http://")+"d2dq2ahtl5zl1z.cloudfront.net/analytics.js/v1/"+e+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);var r=function(e){return function(){analytics.push([e].concat(Array.prototype.slice.call(arguments,0)))}},i=["identify","track","trackLink","trackForm","trackClick","trackSubmit","pageview","ab","alias","ready"];for(var s=0;s<i.length;s++)analytics[i[s]]=r(i[s])}; var analytics=analytics||[];analytics.load=function(e){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=("https:"===document.location.protocol?"https://":"http://")+"d2dq2ahtl5zl1z.cloudfront.net/analytics.js/v1/"+e+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);var r=function(e){return function(){analytics.push([e].concat(Array.prototype.slice.call(arguments,0)))}},i=["identify","track","trackLink","trackForm","trackClick","trackSubmit","pageview","ab","alias","ready"];for(var s=0;s<i.length;s++)analytics[i[s]]=r(i[s])};
% if settings.MITX_FEATURES.get('SEGMENT_IO_LMS'):
analytics.load("${ settings.SEGMENT_IO_LMS_KEY }"); analytics.load("${ settings.SEGMENT_IO_LMS_KEY }");
% if user.is_authenticated(): % if user.is_authenticated():
...@@ -13,6 +11,14 @@ ...@@ -13,6 +11,14 @@
}); });
% endif % endif
% endif
</script> </script>
<!-- end Segment.io --> <!-- end Segment.io -->
% else:
<!-- dummy segment.io -->
<script type="text/javascript">
var analytics = {
track: function() { return; }
};
</script>
<!-- end dummy segment.io -->
% endif
\ No newline at end of file
require 'json' begin
require 'rake/clean' require 'json'
require './rakefiles/helpers.rb' require 'rake/clean'
require './rakelib/helpers.rb'
Dir['rakefiles/*.rake'].each do |rakefile| rescue LoadError => error
import rakefile puts "Import faild (#{error})"
puts "Please run `bundle install` to bootstrap ruby dependencies"
exit 1
end end
# Build Constants # Build Constants
......
...@@ -6,6 +6,8 @@ if USE_CUSTOM_THEME ...@@ -6,6 +6,8 @@ if USE_CUSTOM_THEME
THEME_SASS = File.join(THEME_ROOT, "static", "sass") THEME_SASS = File.join(THEME_ROOT, "static", "sass")
end end
MINIMAL_DARWIN_NOFILE_LIMIT = 8000
def xmodule_cmd(watch=false, debug=false) def xmodule_cmd(watch=false, debug=false)
xmodule_cmd = 'xmodule_assets common/static/xmodule' xmodule_cmd = 'xmodule_assets common/static/xmodule'
if watch if watch
...@@ -21,24 +23,14 @@ def xmodule_cmd(watch=false, debug=false) ...@@ -21,24 +23,14 @@ def xmodule_cmd(watch=false, debug=false)
end end
def coffee_cmd(watch=false, debug=false) def coffee_cmd(watch=false, debug=false)
if watch if watch && Launchy::Application.new.host_os_family.darwin?
# On OSx, coffee fails with EMFILE when available_files = Process::getrlimit(:NOFILE)[0]
# trying to watch all of our coffee files at the same if available_files < MINIMAL_DARWIN_NOFILE_LIMIT
# time. Process.setrlimit(:NOFILE, MINIMAL_DARWIN_NOFILE_LIMIT)
#
# Ref: https://github.com/joyent/node/issues/2479 end
#
# So, instead, we use watchmedo, which works around the problem
"watchmedo shell-command " +
"--command 'node_modules/.bin/coffee -c ${watch_src_path}' " +
"--recursive " +
"--patterns '*.coffee' " +
"--ignore-directories " +
"--wait " +
"."
else
'node_modules/.bin/coffee --compile .'
end end
"node_modules/.bin/coffee --compile #{watch ? '--watch' : ''} ."
end end
def sass_cmd(watch=false, debug=false) def sass_cmd(watch=false, debug=false)
...@@ -55,8 +47,9 @@ def sass_cmd(watch=false, debug=false) ...@@ -55,8 +47,9 @@ def sass_cmd(watch=false, debug=false)
"#{watch ? '--watch' : '--update'} -E utf-8 #{sass_watch_paths.join(' ')}" "#{watch ? '--watch' : '--update'} -E utf-8 #{sass_watch_paths.join(' ')}"
end end
# This task takes arguments purely to pass them via dependencies to the preprocess task
desc "Compile all assets" desc "Compile all assets"
multitask :assets => 'assets:all' task :assets, [:system, :env] => 'assets:all'
namespace :assets do namespace :assets do
...@@ -80,8 +73,9 @@ namespace :assets do ...@@ -80,8 +73,9 @@ namespace :assets do
{:xmodule => [:install_python_prereqs], {:xmodule => [:install_python_prereqs],
:coffee => [:install_node_prereqs, :'assets:coffee:clobber'], :coffee => [:install_node_prereqs, :'assets:coffee:clobber'],
:sass => [:install_ruby_prereqs, :preprocess]}.each_pair do |asset_type, prereq_tasks| :sass => [:install_ruby_prereqs, :preprocess]}.each_pair do |asset_type, prereq_tasks|
# This task takes arguments purely to pass them via dependencies to the preprocess task
desc "Compile all #{asset_type} assets" desc "Compile all #{asset_type} assets"
task asset_type => prereq_tasks do task asset_type, [:system, :env] => prereq_tasks do |t, args|
cmd = send(asset_type.to_s + "_cmd", watch=false, debug=false) cmd = send(asset_type.to_s + "_cmd", watch=false, debug=false)
if cmd.kind_of?(Array) if cmd.kind_of?(Array)
cmd.each {|c| sh(c)} cmd.each {|c| sh(c)}
...@@ -90,7 +84,8 @@ namespace :assets do ...@@ -90,7 +84,8 @@ namespace :assets do
end end
end end
multitask :all => asset_type # This task takes arguments purely to pass them via dependencies to the preprocess task
multitask :all, [:system, :env] => asset_type
multitask :debug => "assets:#{asset_type}:debug" multitask :debug => "assets:#{asset_type}:debug"
multitask :_watch => "assets:#{asset_type}:_watch" multitask :_watch => "assets:#{asset_type}:_watch"
...@@ -111,9 +106,9 @@ namespace :assets do ...@@ -111,9 +106,9 @@ namespace :assets do
task :_watch => (prereq_tasks + ["assets:#{asset_type}:debug"]) do task :_watch => (prereq_tasks + ["assets:#{asset_type}:debug"]) do
cmd = send(asset_type.to_s + "_cmd", watch=true, debug=true) cmd = send(asset_type.to_s + "_cmd", watch=true, debug=true)
if cmd.kind_of?(Array) if cmd.kind_of?(Array)
cmd.each {|c| background_process(c)} cmd.each {|c| singleton_process(c)}
else else
background_process(cmd) singleton_process(cmd)
end end
end end
end end
......
...@@ -22,9 +22,7 @@ task :showdocs, [:options] do |t, args| ...@@ -22,9 +22,7 @@ task :showdocs, [:options] do |t, args|
path = "docs" path = "docs"
end end
Dir.chdir("#{path}/build/html") do Launchy.open("#{path}/build/html/index.html")
Launchy.open('index.html')
end
end end
desc "Build docs and show them in browser" desc "Build docs and show them in browser"
......
require 'digest/md5' require 'digest/md5'
require 'sys/proctable'
require 'colorize'
def find_executable(exec) def find_executable(exec)
path = %x(which #{exec}).strip path = %x(which #{exec}).strip
...@@ -84,6 +86,16 @@ def background_process(*command) ...@@ -84,6 +86,16 @@ def background_process(*command)
end end
end end
# Runs a command as a background process, as long as no other processes
# tagged with the same tag are running
def singleton_process(*command)
if Sys::ProcTable.ps.select {|proc| proc.cmdline.include?(command.join(' '))}.empty?
background_process(*command)
else
puts "Process '#{command.join(' ')} already running, skipping".blue
end
end
def environments(system) def environments(system)
Dir["#{system}/envs/**/*.py"].select{|file| ! (/__init__.py$/ =~ file)}.map do |env_file| Dir["#{system}/envs/**/*.py"].select{|file| ! (/__init__.py$/ =~ file)}.map do |env_file|
env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.') env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.')
......
require './rakefiles/helpers.rb'
PREREQS_MD5_DIR = ENV["PREREQ_CACHE_DIR"] || File.join(REPO_ROOT, '.prereqs_cache') PREREQS_MD5_DIR = ENV["PREREQ_CACHE_DIR"] || File.join(REPO_ROOT, '.prereqs_cache')
CLOBBER.include(PREREQS_MD5_DIR) CLOBBER.include(PREREQS_MD5_DIR)
......
...@@ -33,6 +33,17 @@ def run_acceptance_tests(system, report_dir, harvest_args) ...@@ -33,6 +33,17 @@ def run_acceptance_tests(system, report_dir, harvest_args)
test_sh(django_admin(system, 'acceptance', 'harvest', '--debug-mode', '--tag -skip', harvest_args)) test_sh(django_admin(system, 'acceptance', 'harvest', '--debug-mode', '--tag -skip', harvest_args))
end end
# Run documentation tests
desc "Run documentation tests"
task :test_docs do
# Be sure that sphinx can build docs w/o exceptions.
test_message = "If test fails, you shoud run %s and look at whole output and fix exceptions.
(You shouldn't fix rst warnings and errors for this to pass, just get rid of exceptions.)"
puts (test_message % ["rake doc"]).colorize( :light_green )
test_sh('rake builddocs')
puts (test_message % ["rake doc[pub]"]).colorize( :light_green )
test_sh('rake builddocs[pub]')
end
directory REPORT_DIR directory REPORT_DIR
...@@ -103,7 +114,7 @@ TEST_TASK_DIRS.each do |dir| ...@@ -103,7 +114,7 @@ TEST_TASK_DIRS.each do |dir|
end end
desc "Run all tests" desc "Run all tests"
task :test task :test => :test_docs
desc "Build the html, xml, and diff coverage reports" desc "Build the html, xml, and diff coverage reports"
task :coverage => :report_dirs do task :coverage => :report_dirs do
......
...@@ -494,10 +494,11 @@ cd $BASE ...@@ -494,10 +494,11 @@ cd $BASE
pip install argcomplete pip install argcomplete
cd $BASE/edx-platform cd $BASE/edx-platform
bundle install bundle install
rake install_prereqs
mkdir "$BASE/log" || true mkdir -p "$BASE/log"
mkdir "$BASE/db" || true mkdir -p "$BASE/db"
mkdir "$BASE/data" || true mkdir -p "$BASE/data"
rake django-admin[syncdb] rake django-admin[syncdb]
rake django-admin[migrate] rake django-admin[migrate]
......
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