Commit f747c762 by David Baumgold

Merge pull request #1772 from edx/db/locator-refactor-textbooks

Refactor textbooks to use locator URLs
parents effee03b d8103d43
...@@ -1659,14 +1659,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1659,14 +1659,7 @@ class ContentStoreTest(ModuleStoreTestCase):
test_get_html('settings/details') test_get_html('settings/details')
test_get_html('settings/grading') test_get_html('settings/grading')
test_get_html('settings/advanced') test_get_html('settings/advanced')
test_get_html('textbooks')
# textbook index
resp = self.client.get_html(reverse('textbook_index',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assertEqual(resp.status_code, 200)
_test_no_locations(self, resp)
# go look at a subsection page # go look at a subsection page
subsection_location = loc.replace(category='sequential', name='test_sequence') subsection_location = loc.replace(category='sequential', name='test_sequence')
......
...@@ -14,11 +14,7 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -14,11 +14,7 @@ class TextbookIndexTestCase(CourseTestCase):
def setUp(self): def setUp(self):
"Set the URL for tests" "Set the URL for tests"
super(TextbookIndexTestCase, self).setUp() super(TextbookIndexTestCase, self).setUp()
self.url = reverse('textbook_index', kwargs={ self.url = self.course_locator.url_reverse('textbooks')
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
})
def test_view_index(self): def test_view_index(self):
"Basic check that the textbook index page responds correctly" "Basic check that the textbook index page responds correctly"
...@@ -77,13 +73,13 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -77,13 +73,13 @@ class TextbookIndexTestCase(CourseTestCase):
obj = json.loads(resp.content) obj = json.loads(resp.content)
self.assertEqual(content, obj) self.assertEqual(content, obj)
def test_view_index_xhr_post(self): def test_view_index_xhr_put(self):
"Check that you can save information to the server" "Check that you can save information to the server"
textbooks = [ textbooks = [
{"tab_title": "Hi, mom!"}, {"tab_title": "Hi, mom!"},
{"tab_title": "Textbook 2"}, {"tab_title": "Textbook 2"},
] ]
resp = self.client.post( resp = self.client.put(
self.url, self.url,
data=json.dumps(textbooks), data=json.dumps(textbooks),
content_type="application/json", content_type="application/json",
...@@ -102,9 +98,9 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -102,9 +98,9 @@ class TextbookIndexTestCase(CourseTestCase):
no_ids.append(textbook) no_ids.append(textbook)
self.assertEqual(no_ids, textbooks) self.assertEqual(no_ids, textbooks)
def test_view_index_xhr_post_invalid(self): def test_view_index_xhr_put_invalid(self):
"Check that you can't save invalid JSON" "Check that you can't save invalid JSON"
resp = self.client.post( resp = self.client.put(
self.url, self.url,
data="invalid", data="invalid",
content_type="application/json", content_type="application/json",
...@@ -122,11 +118,7 @@ class TextbookCreateTestCase(CourseTestCase): ...@@ -122,11 +118,7 @@ class TextbookCreateTestCase(CourseTestCase):
def setUp(self): def setUp(self):
"Set up a url and some textbook content for tests" "Set up a url and some textbook content for tests"
super(TextbookCreateTestCase, self).setUp() super(TextbookCreateTestCase, self).setUp()
self.url = reverse('create_textbook', kwargs={ self.url = self.course_locator.url_reverse('textbooks')
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
})
self.textbook = { self.textbook = {
"tab_title": "Economics", "tab_title": "Economics",
"chapters": { "chapters": {
...@@ -151,15 +143,6 @@ class TextbookCreateTestCase(CourseTestCase): ...@@ -151,15 +143,6 @@ class TextbookCreateTestCase(CourseTestCase):
del textbook["id"] del textbook["id"]
self.assertEqual(self.textbook, textbook) self.assertEqual(self.textbook, textbook)
def test_get(self):
"Test that GET is not allowed"
resp = self.client.get(
self.url,
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(resp.status_code, 405)
def test_valid_id(self): def test_valid_id(self):
"Textbook IDs must begin with a number; try a valid one" "Textbook IDs must begin with a number; try a valid one"
self.textbook["id"] = "7x5" self.textbook["id"] = "7x5"
...@@ -188,12 +171,12 @@ class TextbookCreateTestCase(CourseTestCase): ...@@ -188,12 +171,12 @@ class TextbookCreateTestCase(CourseTestCase):
self.assertNotIn("Location", resp) self.assertNotIn("Location", resp)
class TextbookByIdTestCase(CourseTestCase): class TextbookDetailTestCase(CourseTestCase):
"Test cases for the `textbook_by_id` view" "Test cases for the `textbook_detail_handler` view"
def setUp(self): def setUp(self):
"Set some useful content and URLs for tests" "Set some useful content and URLs for tests"
super(TextbookByIdTestCase, self).setUp() super(TextbookDetailTestCase, self).setUp()
self.textbook1 = { self.textbook1 = {
"tab_title": "Economics", "tab_title": "Economics",
"id": 1, "id": 1,
...@@ -202,12 +185,7 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -202,12 +185,7 @@ class TextbookByIdTestCase(CourseTestCase):
"url": "/a/b/c/ch1.pdf", "url": "/a/b/c/ch1.pdf",
} }
} }
self.url1 = reverse('textbook_by_id', kwargs={ self.url1 = self.course_locator.url_reverse("textbooks", "1")
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'tid': 1,
})
self.textbook2 = { self.textbook2 = {
"tab_title": "Algebra", "tab_title": "Algebra",
"id": 2, "id": 2,
...@@ -216,24 +194,14 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -216,24 +194,14 @@ class TextbookByIdTestCase(CourseTestCase):
"url": "/a/b/ch11.pdf", "url": "/a/b/ch11.pdf",
} }
} }
self.url2 = reverse('textbook_by_id', kwargs={ self.url2 = self.course_locator.url_reverse("textbooks", "2")
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'tid': 2,
})
self.course.pdf_textbooks = [self.textbook1, self.textbook2] self.course.pdf_textbooks = [self.textbook1, self.textbook2]
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
self.course.save() self.course.save()
self.store = get_modulestore(self.course.location) self.store = get_modulestore(self.course.location)
self.store.update_metadata(self.course.location, own_metadata(self.course)) self.store.update_metadata(self.course.location, own_metadata(self.course))
self.url_nonexist = reverse('textbook_by_id', kwargs={ self.url_nonexist = self.course_locator.url_reverse("textbooks", "20")
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'tid': 20,
})
def test_get_1(self): def test_get_1(self):
"Get the first textbook" "Get the first textbook"
...@@ -275,12 +243,7 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -275,12 +243,7 @@ class TextbookByIdTestCase(CourseTestCase):
"url": "supercool.pdf", "url": "supercool.pdf",
"id": "1supercool", "id": "1supercool",
} }
url = reverse("textbook_by_id", kwargs={ url = self.course_locator.url_reverse("textbooks", "1supercool")
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'tid': "1supercool",
})
resp = self.client.post( resp = self.client.post(
url, url,
data=json.dumps(textbook), data=json.dumps(textbook),
......
...@@ -57,6 +57,7 @@ class AjaxEnabledTestClient(Client): ...@@ -57,6 +57,7 @@ class AjaxEnabledTestClient(Client):
""" """
return self.get(path, data or {}, follow, HTTP_ACCEPT="application/json", **extra) return self.get(path, data or {}, follow, HTTP_ACCEPT="application/json", **extra)
@override_settings(MODULESTORE=TEST_MODULESTORE) @override_settings(MODULESTORE=TEST_MODULESTORE)
class CourseTestCase(ModuleStoreTestCase): class CourseTestCase(ModuleStoreTestCase):
def setUp(self): def setUp(self):
...@@ -111,7 +112,7 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -111,7 +112,7 @@ class CourseTestCase(ModuleStoreTestCase):
client = Client() client = Client()
client.login(username=uname, password=password) client.login(username=uname, password=password)
return client, nonstaff return client, nonstaff
def populateCourse(self): def populateCourse(self):
""" """
Add 2 chapters, 4 sections, 8 verticals, 16 problems to self.course (branching 2) Add 2 chapters, 4 sections, 8 verticals, 16 problems to self.course (branching 2)
......
require ["jquery", "backbone", "coffee/src/main", "sinon", "jasmine-stealth"], require ["jquery", "backbone", "coffee/src/main", "sinon", "jasmine-stealth", "jquery.cookie"],
($, Backbone, main, sinon) -> ($, Backbone, main, sinon) ->
describe "CMS", -> describe "CMS", ->
it "should initialize URL", -> it "should initialize URL", ->
......
...@@ -11,6 +11,10 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/ ...@@ -11,6 +11,10 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/
beforeEach -> beforeEach ->
main() main()
@model = new Textbook() @model = new Textbook()
CMS.URL.TEXTBOOKS = "/textbooks"
afterEach ->
delete CMS.URL.TEXTBOOKS
describe "Basic", -> describe "Basic", ->
it "should have an empty name by default", -> it "should have an empty name by default", ->
...@@ -28,8 +32,9 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/ ...@@ -28,8 +32,9 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/
it "should be empty by default", -> it "should be empty by default", ->
expect(@model.isEmpty()).toBeTruthy() expect(@model.isEmpty()).toBeTruthy()
it "should have a URL set", -> it "should have a URL root", ->
expect(@model.url()).toBeTruthy() urlRoot = _.result(@model, 'urlRoot')
expect(urlRoot).toBeTruthy()
it "should be able to reset itself", -> it "should be able to reset itself", ->
@model.set("name", "foobar") @model.set("name", "foobar")
...@@ -135,12 +140,8 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/ ...@@ -135,12 +140,8 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/
delete CMS.URL.TEXTBOOKS delete CMS.URL.TEXTBOOKS
it "should have a url set", -> it "should have a url set", ->
expect(@collection.url()).toEqual("/textbooks") url = _.result(@collection, 'url')
expect(url).toEqual("/textbooks")
it "can call save", ->
spyOn(@collection, "sync")
@collection.save()
expect(@collection.sync).toHaveBeenCalledWith("update", @collection, undefined)
describe "Chapter model", -> describe "Chapter model", ->
......
define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js/models/section", define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js/models/course",
"js/collections/textbook", "js/views/show_textbook", "js/views/edit_textbook", "js/views/list_textbooks", "js/collections/textbook", "js/views/show_textbook", "js/views/edit_textbook", "js/views/list_textbooks",
"js/views/edit_chapter", "js/views/feedback_prompt", "js/views/feedback_notification", "js/views/edit_chapter", "js/views/feedback_prompt", "js/views/feedback_notification",
"sinon", "jasmine-stealth"], "sinon", "jasmine-stealth"],
(Textbook, Chapter, ChapterSet, Section, TextbookSet, ShowTextbook, EditTextbook, ListTexbook, EditChapter, Prompt, Notification, sinon) -> (Textbook, Chapter, ChapterSet, Course, TextbookSet, ShowTextbook, EditTextbook, ListTexbook, EditChapter, Prompt, Notification, sinon) ->
feedbackTpl = readFixtures('system-feedback.underscore') feedbackTpl = readFixtures('system-feedback.underscore')
beforeEach -> beforeEach ->
...@@ -30,7 +30,7 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js ...@@ -30,7 +30,7 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js
@promptSpies = spyOnConstructor(Prompt, "Warning", ["show", "hide"]) @promptSpies = spyOnConstructor(Prompt, "Warning", ["show", "hide"])
@promptSpies.show.andReturn(@promptSpies) @promptSpies.show.andReturn(@promptSpies)
window.section = new Section({ window.course = new Course({
id: "5", id: "5",
name: "Course Name", name: "Course Name",
url_name: "course_name", url_name: "course_name",
...@@ -40,7 +40,7 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js ...@@ -40,7 +40,7 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js
}); });
afterEach -> afterEach ->
delete window.section delete window.course
describe "Basic", -> describe "Basic", ->
it "should render properly", -> it "should render properly", ->
...@@ -81,9 +81,11 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js ...@@ -81,9 +81,11 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js
@savingSpies = spyOnConstructor(Notification, "Mini", @savingSpies = spyOnConstructor(Notification, "Mini",
["show", "hide"]) ["show", "hide"])
@savingSpies.show.andReturn(@savingSpies) @savingSpies.show.andReturn(@savingSpies)
CMS.URL.TEXTBOOKS = "/textbooks"
afterEach -> afterEach ->
@xhr.restore() @xhr.restore()
delete CMS.URL.TEXTBOOKS
it "should destroy itself on confirmation", -> it "should destroy itself on confirmation", ->
@view.render().$(".delete").click() @view.render().$(".delete").click()
...@@ -283,11 +285,11 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js ...@@ -283,11 +285,11 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js
@view = new EditChapter({model: @model}) @view = new EditChapter({model: @model})
spyOn(@view, "remove").andCallThrough() spyOn(@view, "remove").andCallThrough()
CMS.URL.UPLOAD_ASSET = "/upload" CMS.URL.UPLOAD_ASSET = "/upload"
window.section = new Section({name: "abcde"}) window.course = new Course({name: "abcde"})
afterEach -> afterEach ->
delete CMS.URL.UPLOAD_ASSET delete CMS.URL.UPLOAD_ASSET
delete window.section delete window.course
it "can render", -> it "can render", ->
@view.render() @view.render()
......
...@@ -2,10 +2,7 @@ define(["backbone", "js/models/textbook"], ...@@ -2,10 +2,7 @@ define(["backbone", "js/models/textbook"],
function(Backbone, TextbookModel) { function(Backbone, TextbookModel) {
var TextbookCollection = Backbone.Collection.extend({ var TextbookCollection = Backbone.Collection.extend({
model: TextbookModel, model: TextbookModel,
url: function() { return CMS.URL.TEXTBOOKS; }, url: function() { return CMS.URL.TEXTBOOKS; }
save: function(options) {
return this.sync('update', this, options);
}
}); });
return TextbookCollection; return TextbookCollection;
}); });
define(["backbone", "underscore", "js/models/chapter", "js/collections/chapter", "backbone.associations"], define(["backbone", "underscore", "js/models/chapter", "js/collections/chapter",
"backbone.associations", "coffee/src/main"],
function(Backbone, _, ChapterModel, ChapterCollection) { function(Backbone, _, ChapterModel, ChapterCollection) {
var Textbook = Backbone.AssociatedModel.extend({ var Textbook = Backbone.AssociatedModel.extend({
...@@ -32,13 +33,7 @@ define(["backbone", "underscore", "js/models/chapter", "js/collections/chapter", ...@@ -32,13 +33,7 @@ define(["backbone", "underscore", "js/models/chapter", "js/collections/chapter",
isEmpty: function() { isEmpty: function() {
return !this.get('name') && this.get('chapters').isEmpty(); return !this.get('name') && this.get('chapters').isEmpty();
}, },
url: function() { urlRoot: function() { return CMS.URL.TEXTBOOKS; },
if(this.isNew()) {
return CMS.URL.TEXTBOOKS + "/new";
} else {
return CMS.URL.TEXTBOOKS + "/" + this.id;
}
},
parse: function(response) { parse: function(response) {
var ret = $.extend(true, {}, response); var ret = $.extend(true, {}, response);
if("tab_title" in ret && !("name" in ret)) { if("tab_title" in ret && !("name" in ret)) {
......
...@@ -53,7 +53,7 @@ define(["backbone", "underscore", "underscore.string", "jquery", "gettext", "js/ ...@@ -53,7 +53,7 @@ define(["backbone", "underscore", "underscore.string", "jquery", "gettext", "js/
}); });
var msg = new FileUploadModel({ var msg = new FileUploadModel({
title: _.template(gettext("Upload a new PDF to “<%= name %>”"), title: _.template(gettext("Upload a new PDF to “<%= name %>”"),
{name: section.escape('name')}), {name: course.escape('name')}),
message: "Files must be in PDF format.", message: "Files must be in PDF format.",
mimeTypes: ['application/pdf'] mimeTypes: ['application/pdf']
}); });
......
...@@ -16,7 +16,7 @@ define(["backbone", "underscore", "gettext", "js/views/feedback_notification", " ...@@ -16,7 +16,7 @@ define(["backbone", "underscore", "gettext", "js/views/feedback_notification", "
render: function() { render: function() {
var attrs = $.extend({}, this.model.attributes); var attrs = $.extend({}, this.model.attributes);
attrs.bookindex = this.model.collection.indexOf(this.model); attrs.bookindex = this.model.collection.indexOf(this.model);
attrs.course = window.section.attributes; attrs.course = window.course.attributes;
this.$el.html(this.template(attrs)); this.$el.html(this.template(attrs));
return this; return this;
}, },
......
...@@ -23,14 +23,7 @@ CMS.URL.TEXTBOOKS = "${textbook_url}" ...@@ -23,14 +23,7 @@ CMS.URL.TEXTBOOKS = "${textbook_url}"
CMS.URL.LMS_BASE = "${settings.LMS_BASE}" CMS.URL.LMS_BASE = "${settings.LMS_BASE}"
require(["js/models/section", "js/collections/textbook", "js/views/list_textbooks"], require(["js/models/section", "js/collections/textbook", "js/views/list_textbooks"],
function(Section, TextbookCollection, ListTextbooksView) { function(Section, TextbookCollection, ListTextbooksView) {
window.section = new Section({ var textbooks = new TextbookCollection(${json.dumps(textbooks)}, {parse: true});
name: "${course.display_name_with_default | h}",
url_name: "${course.location.name | h}",
org: "${course.location.org | h}",
num: "${course.location.course | h}",
revision: "${course.location.revision | h}"
});
var textbooks = new TextbookCollection(${json.dumps(course.pdf_textbooks)}, {parse: true});
var tbView = new ListTextbooksView({collection: textbooks}); var tbView = new ListTextbooksView({collection: textbooks});
$(function() { $(function() {
......
...@@ -13,13 +13,14 @@ ...@@ -13,13 +13,14 @@
<h1 class="branding"><a href="/"><img src="${static.url("img/logo-edx-studio.png")}" alt="edX Studio" /></a></h1> <h1 class="branding"><a href="/"><img src="${static.url("img/logo-edx-studio.png")}" alt="edX Studio" /></a></h1>
% if context_course: % if context_course:
<% <%
ctx_loc = context_course.location ctx_loc = context_course.location
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True) location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)
index_url = location.url_reverse('course') index_url = location.url_reverse('course')
checklists_url = location.url_reverse('checklists') checklists_url = location.url_reverse('checklists')
course_team_url = location.url_reverse('course_team') course_team_url = location.url_reverse('course_team')
assets_url = location.url_reverse('assets') assets_url = location.url_reverse('assets')
textbooks_url = location.url_reverse('textbooks')
import_url = location.url_reverse('import') import_url = location.url_reverse('import')
course_info_url = location.url_reverse('course_info') course_info_url = location.url_reverse('course_info')
export_url = location.url_reverse('export') export_url = location.url_reverse('export')
...@@ -58,7 +59,7 @@ ...@@ -58,7 +59,7 @@
<a href="${assets_url}">${_("Files &amp; Uploads")}</a> <a href="${assets_url}">${_("Files &amp; Uploads")}</a>
</li> </li>
<li class="nav-item nav-course-courseware-textbooks"> <li class="nav-item nav-course-courseware-textbooks">
<a href="${reverse('textbook_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Textbooks")}</a> <a href="${textbooks_url}">${_("Textbooks")}</a>
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -23,13 +23,6 @@ urlpatterns = patterns('', # nopep8 ...@@ -23,13 +23,6 @@ urlpatterns = patterns('', # nopep8
url(r'^preview/xblock/(?P<usage_id>.*?)/handler/(?P<handler>[^/]*)(?:/(?P<suffix>[^/]*))?$', url(r'^preview/xblock/(?P<usage_id>.*?)/handler/(?P<handler>[^/]*)(?:/(?P<suffix>[^/]*))?$',
'contentstore.views.preview_handler', name='preview_handler'), 'contentstore.views.preview_handler', name='preview_handler'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)$',
'contentstore.views.textbook_index', name='textbook_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)/new$',
'contentstore.views.create_textbook', name='create_textbook'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)/(?P<tid>\d[^/]*)$',
'contentstore.views.textbook_by_id', name='textbook_by_id'),
# temporary landing page for a course # temporary landing page for a course
url(r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', url(r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$',
'contentstore.views.landing', name='landing'), 'contentstore.views.landing', name='landing'),
...@@ -89,6 +82,8 @@ urlpatterns += patterns( ...@@ -89,6 +82,8 @@ urlpatterns += patterns(
url(r'(?ix)^settings/details/{}$'.format(parsers.URL_RE_SOURCE), 'settings_handler'), url(r'(?ix)^settings/details/{}$'.format(parsers.URL_RE_SOURCE), 'settings_handler'),
url(r'(?ix)^settings/grading/{}(/)?(?P<grader_index>\d+)?$'.format(parsers.URL_RE_SOURCE), 'grading_handler'), url(r'(?ix)^settings/grading/{}(/)?(?P<grader_index>\d+)?$'.format(parsers.URL_RE_SOURCE), 'grading_handler'),
url(r'(?ix)^settings/advanced/{}$'.format(parsers.URL_RE_SOURCE), 'advanced_settings_handler'), url(r'(?ix)^settings/advanced/{}$'.format(parsers.URL_RE_SOURCE), 'advanced_settings_handler'),
url(r'(?ix)^textbooks/{}$'.format(parsers.URL_RE_SOURCE), 'textbooks_list_handler'),
url(r'(?ix)^textbooks/{}/(?P<tid>\d[^/]*)$'.format(parsers.URL_RE_SOURCE), 'textbooks_detail_handler'),
) )
js_info_dict = { js_info_dict = {
......
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