diff --git a/common/djangoapps/mitxmako/middleware.py b/common/djangoapps/mitxmako/middleware.py index 50f2840a05f0cdf9932de3c59984b5749053861c..64cb2e5415cccb0ff64bd1ce2068dd51280e5d17 100644 --- a/common/djangoapps/mitxmako/middleware.py +++ b/common/djangoapps/mitxmako/middleware.py @@ -46,4 +46,4 @@ class MakoMiddleware(object): global requestcontext requestcontext = RequestContext(request) requestcontext['is_secure'] = request.is_secure() - requestcontext['site'] = settings.SITE_NAME + requestcontext['site'] = request.get_host() diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 55204f19f6b3689c3a273e8bc732acce33c1a1db..9cde878d21ad282093a825173fbf9b51aee85048 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -273,7 +273,7 @@ def add_user_to_default_group(user, group): utg.users.add(User.objects.get(username=user)) utg.save() -# @receiver(post_save, sender=User) +@receiver(post_save, sender=User) def update_user_information(sender, instance, created, **kwargs): try: cc_user = cc.User.from_django_user(instance) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index e7864337b3f9ba408b2203f8737942dbbc874636..ce15a2a0031e0e4ec695af8c152664785df2a32a 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -511,7 +511,7 @@ def password_reset(request): form.save(use_https = request.is_secure(), from_email = settings.DEFAULT_FROM_EMAIL, request = request, - domain_override = settings.SITE_NAME) + domain_override = request.get_host()) return HttpResponse(json.dumps({'success':True, 'value': render_to_string('registration/password_reset_done.html', {})})) else: diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py index c727626a339bc7f0648d34efb9cf8564c758864c..eb6ef2d00c7807d14e922c6b214a8273d6baa170 100644 --- a/common/lib/capa/capa/correctmap.py +++ b/common/lib/capa/capa/correctmap.py @@ -73,10 +73,12 @@ class CorrectMap(object): return answer_id in self.cmap and self.cmap[answer_id]['queuekey'] == test_key def get_npoints(self, answer_id): - if self.is_correct(answer_id): - npoints = self.cmap[answer_id].get('npoints', 1) # default to 1 point if correct - return npoints or 1 - return 0 # if not correct, return 0 + npoints = self.get_property(answer_id, 'npoints') + if npoints is not None: + return npoints + elif self.is_correct(answer_id): + return 1 + return 0 # if not correct and no points have been assigned, return 0 def set_property(self, answer_id, property, value): if answer_id in self.cmap: self.cmap[answer_id][property] = value diff --git a/common/lib/xmodule/xmodule/css/sequence/display.scss b/common/lib/xmodule/xmodule/css/sequence/display.scss index 05002e881d1e0465d151e2abb2d33d6a5ac3d3d3..7b8dc5f57c33f2c97354c32e87e7ff3be5dbd288 100644 --- a/common/lib/xmodule/xmodule/css/sequence/display.scss +++ b/common/lib/xmodule/xmodule/css/sequence/display.scss @@ -61,13 +61,12 @@ nav.sequence-nav { min-width: 20px; a { - width: 34px; - height: 34px; - margin: 4px auto; - background-position: center 10px; + width: 100%; + height: 42px; + margin: 0; + background-position: center 14px; background-repeat: no-repeat; border: 1px solid transparent; - @include border-radius(35px); cursor: pointer; display: block; padding: 0; @@ -82,7 +81,7 @@ nav.sequence-nav { &:hover { background-color: #fff; background-repeat: no-repeat; - background-position: center 10px; + background-position: center 14px; } &.active { @@ -103,7 +102,7 @@ nav.sequence-nav { &:hover { background-color: #fff; background-repeat: no-repeat; - background-position: center 10px; + background-position: center 14px; } } @@ -298,14 +297,16 @@ nav.sequence-bottom { ul { @extend .clearfix; - border: 1px solid $border-color; - @include border-radius(3px); @include inline-block(); - width: 100px; + width: 103px; li { float: left; - width: 50%; + width: 50px; + height: 44px; + border: 1px solid #ccc; + @include linear-gradient(top, #eee, #ddd); + @include box-shadow(0 1px 0 rgba(255, 255, 255, .7) inset); &.prev, &.next { margin-bottom: 0; @@ -318,38 +319,39 @@ nav.sequence-bottom { padding: lh(.5) 4px; text-indent: -9999px; @include transition(all, .2s, $ease-in-out-quad); + outline: 0; &:hover { - background-color: #ddd; - color: #000; opacity: .5; - text-decoration: none; + background-position: center 15px; } &.disabled { opacity: .4; } + + &:focus { + outline: 0; + } } } &.prev { + border-radius: 35px 0 0 35px; + a { background-image: url('../images/sequence-nav/previous-icon.png'); - border-right: 1px solid lighten(#c6c6c6, 10%); - - &:hover { - background-color: none; - } + background-position: center 15px; } } &.next { + border-radius: 0 35px 35px 0; + border-left: none; + a { background-image: url('../images/sequence-nav/next-icon.png'); - - &:hover { - background-color: none; - } + background-position: center 15px; } } } @@ -360,5 +362,12 @@ div.course-wrapper section.course-content ol.vert-mod > li ul.sequence-nav-butto list-style: none !important; } +.xmodule_SequenceModule nav.sequence-bottom ul li.next a, +.xmodule_SequenceModule nav.sequence-bottom ul li.prev a { + outline: 0; + &:focus { + outline: 0; + } +} diff --git a/common/lib/xmodule/xmodule/js/src/sequence/display.coffee b/common/lib/xmodule/xmodule/js/src/sequence/display.coffee index 38291be2efb5b7092ae886081cdd1f28b9987013..35a33a9ea191296305e37fbafe31991fd4274fc5 100644 --- a/common/lib/xmodule/xmodule/js/src/sequence/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/sequence/display.coffee @@ -55,6 +55,7 @@ class @Sequence element.removeClass('progress-none') .removeClass('progress-some') .removeClass('progress-done') + switch progress when 'none' then element.addClass('progress-none') when 'in_progress' then element.addClass('progress-some') diff --git a/doc/xml-format.md b/doc/xml-format.md index 2a9e379cccd90388a20709cf7e934f8d3ff5a7c3..55bcda4480b4dae3218712d8a2b2fc2e40b14521 100644 --- a/doc/xml-format.md +++ b/doc/xml-format.md @@ -1,147 +1,305 @@ -This doc is a rough spec of our xml format +# edX xml format tutorial -Every content element (within a course) should have a unique id. This id is formed as {category}/{url_name}. Categories are the different tag types ('chapter', 'problem', 'html', 'sequential', etc). Url_name is a string containing a-z, A-Z, dot (.) and _. This is what appears in urls that point to this object. +## Goals of this document -File layout: +* This was written assuming the reader has no prior programming/CS knowledge and has jumped cold turkey into the edX platform. +* To educate the reader on how to build and maintain the back end structure of the course content. This is important for debugging and standardization. +* After reading this, you should be able to add content to a course and make sure it shows up in the courseware and does not break the code. +* __Prerequisites:__ it would be helpful to know a little bit about xml. Here is a [simple example](http://www.ultraslavonic.info/intro-to-xml/) if you've never seen it before. -- Xml files have content -- "policy", which is also called metadata in various places, should live in a policy file. +## Outline -- each module (except customtag and course, which are special, see below) should live in a file, located at {category}/{url_name].xml -To include this module in another one (e.g. to put a problem in a vertical), put in a "pointer tag": <{category} url_name="{url_name}"/>. When we read that, we'll load the actual contents. +* First, we will show a sample course structure as a case study/model of how xml and files in a course are organized to introductory understanding. -Customtag is already a pointer, you can just use it in place: <customtag url_name="my_custom_tag" impl="blah" attr1="..."/> +* More technical details are below, including discussion of some special cases. -Course tags: - - the top level course pointer tag lives in course.xml - - have 2 extra required attributes: "org" and "course" -- organization name, and course name. Note that the course name is referring to the platonic ideal of this course, not to any particular run of this course. The url_name should be particular run of this course. E.g. -If course.xml contains: -<course org="HarvardX" course="cs50" url_name="2012"/> +## Introduction -we would load the actual course definition from course/2012.xml +* The course is organized hierarchically. We start by describing course-wide parameters, then break the course into chapters, and then go deeper and deeper until we reach a specific pset, video, etc. -To support multiple different runs of the course, you could have a different course.xml, containing +* You could make an analogy to finding a green shirt in your house - front door -> bedroom -> closet -> drawer -> shirts -> green shirt -<course org="HarvardX" course="cs50" url_name="2012H"/> -which would load the Harvard-internal version from course/2012H.xml +## Case Study -If there is only one run of the course for now, just have a single course.xml with the right url_name. +Let's jump right in by looking at the directory structure of a very simple toy course: -If there is more than one run of the course, the different course root pointer files should live in -roots/url_name.xml, and course.xml should be a symbolic link to the one you want to run in your dev instance. + toy/ + course + course.xml + problem + policies + roots -If you want to run both versions, you need to checkout the repo twice, and have course.xml point to different root/{url_name}.xml files. +The only top level file is `course.xml`, which should contain one line, looking something like this: -Policies: - - the policy for a course url_name lives in policies/{url_name}.json + <course org="edX" course="toy" url_name="2012_Fall"/> -The format is called "json", and is best shown by example (though also feel free to google :) +This gives all the information to uniquely identify a particular run of any course--which organization is producing the course, what the course name is, and what "run" this is, specified via the `url_name` attribute. -the file is a dictionary (mapping from keys to values, syntax "{ key : value, key2 : value2, etc}" +Obviously, this doesn't actually specify any of the course content, so we need to find that next. To know where to look, you need to know the standard organizational structure of our system: _course elements are uniquely identified by the combination `(category, url_name)`_. In this case, we are looking for a `course` element with the `url_name` "2012_Fall". The definition of this element will be in `course/2012_Fall.xml`. Let's look there next: -Keys are in the form "{category}/{url_name}", which should uniquely id a content element. -Values are dictionaries of the form {"metadata-key" : "metadata-value"}. +`course/2012_Fall.xml` + + <course> + <chapter url_name="Overview"> + <videosequence url_name="Toy_Videos"> + <problem url_name="warmup"/> + <video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/> + </videosequence> + <video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/> + </chapter> + </course> + +Aha. Now we found some content. We can see that the course is organized hierarchically, in this case with only one chapter, with `url_name` "Overview". The chapter contains a `videosequence` and a `video`, with the sequence containing a problem and another video. When viewed in the courseware, chapters are shown at the top level of the navigation accordion on the left, with any elements directly included in the chapter below. + +Looking at this file, we can see the course structure, and the youtube urls for the videos, but what about the "warmup" problem? There is no problem content here! Where should we look? This is a good time to pause and try to answer that question based on our organizational structure above. + +As you hopefully guessed, the problem would be in `problem/warmup.xml`. (Note: This tutorial doesn't discuss the xml format for problems--there are chapters of edx4edx that describe it.) This is an instance of a _pointer tag:_ any xml tag with only the category and a url_name attribute will point to the file `{category}/{url_name}.xml`. For example, this means that our toy `course.xml` could have also been written as + +`course/2012_Fall.xml` + + <course> + <chapter url_name="Overview"/> + </course> + +with `chapter/Overview.xml` containing + + <chapter> + <videosequence url_name="Toy_Videos"> + <problem url_name="warmup"/> + <video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/> + </videosequence> + <video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/> + </chapter> + +In fact, this is the recommended structure for real courses--putting each chapter into its own file makes it easy to have different people work on each without conflicting or having to merge. Similarly, as sequences get large, it can be handy to split them out as well (in `sequence/{url_name}.xml`, of course). + +Note that the `url_name` is only specified once per element--either the inline definition, or in the pointer tag. + +## Policy files + +We still haven't looked at two of the directoies in the top-level listing above: `policies` and `roots`. Let's look at policies next. The policy directory contains one file: + + policies: + 2012_Fall.json -metadata can also live in the xml files, but anything defined in the policy file overrides anything in the xml. This is primarily for backwards compatibility, and you should probably not use both. If you do leave some metadata tags in the xml, please be consistent (e.g. if display_names stay in xml, they should all stay in xml). - - note, some xml attributes are not metadata. e.g. in <video youtube="xyz987293487293847"/>, the youtube attribute specifies what video this is, and is logically part of the content, not the policy, so it should stay in video/{url_name}.xml. - -Example policy file: -{ - "course/2012": { - "graceperiod": "1 day", - "start": "2012-10-15T12:00", - "display_name": "Introduction to Computer Science I", - "xqa_key": "z1y4vdYcy0izkoPeihtPClDxmbY1ogDK" - }, - "chapter/Week_0": { - "display_name": "Week 0" - }, - "sequential/Pre-Course_Survey": { - "display_name": "Pre-Course Survey", - "format": "Survey" +and that file is named {course-url_name}.json. As you might expect, this file contains a policy for the course. In our example, it looks like this: + + 2012_Fall.json: + { + "course/2012_Fall": { + "graceperiod": "2 days 5 hours 59 minutes 59 seconds", + "start": "2015-07-17T12:00", + "display_name": "Toy Course" + }, + "chapter/Overview": { + "display_name": "Overview" + }, + "videosequence/Toy_Videos": { + "display_name": "Toy Videos", + "format": "Lecture Sequence" + }, + "problem/warmup": { + "display_name": "Getting ready for the semester" + }, + "video/Video_Resources": { + "display_name": "Video Resources" + }, + "video/Welcome": { + "display_name": "Welcome" + } } -} -NOTE: json is picky about commas. If you have trailing commas before closing braces, it will complain and refuse to parse the file. This is irritating. +The policy specifies metadata about the content elements--things which are not inherent to the definition of the content, but which describe how the content is presented to the user and used in the course. See below for a full list of metadata attributes; as the example shows, they include `display_name`, which is what is shown when this piece of content is referenced or shown in the courseware, and various dates and times, like `start`, which specifies when the content becomes visible to students, and various problem-specific parameters like the allowed number of attempts. One important point is that some metadata is inherited: for example, specifying the start date on the course makes it the default for every element in the course. See below for more details. + +It is possible to put metadata directly in the xml, as attributes of the appropriate tag, but using a policy file has two benefits: it puts all the policy in one place, making it easier to check that things like due dates are set properly, and it allows the content definitions to be easily used in another run of the same course, with the same or similar content, but different policy. + +## Roots + +The last directory in the top level listing is `roots`. In our toy course, it contains a single file: + + roots/ + 2012_Fall.xml + +This file is identical to the top-level `course.xml`, containing + + <course org="edX" course="toy" url_name="2012_Fall"/> + +In fact, the top level `course.xml` is a symbolic link to this file. When there is only one run of a course, the roots directory is not really necessary, and the top-level course.xml file can just specify the `url_name` of the course. However, if we wanted to make a second run of our toy course, we could add another file called, e.g., `roots/2013_Spring.xml`, containing + + <course org="edX" course="toy" url_name="2013_Spring"/> + +After creating `course/2013_Spring.xml` with the course structure (possibly as a symbolic link or copy of `course/2012_Fall.xml` if no content was changing), and `policies/2013_Spring.json`, we would have two different runs of the toy course in the course repository. Our build system understands this roots structure, and will build a course package for each root. (Dev note: if you're using a local development environment, make the top level `course.xml` point to the desired root, and check out the repo multiple times if you need multiple runs simultaneously). + +That's basically all there is to the organizational structure. Read the next section for details on the tags we support, including some special case tags like `customtag` and `html` invariants, and look at the end for some tips that will make the editing process easier. + +---------- + +# Tag types +* `abtest` -- Support for A/B testing. TODO: add details.. +* `chapter` -- top level organization unit of a course. The courseware display code currently expects the top level `course` element to contain only chapters, though there is no philosophical reason why this is required, so we may change it to properly display non-chapters at the top level. +* `course` -- top level tag. Contains everything else. +* `customtag` -- render an html template, filling in some parameters, and return the resulting html. See below for details. +* `html` -- a reference to an html file. +* `error` -- don't put these in by hand :) The internal representation of content that has an error, such as malformed xml or some broken invariant. You may see this in the xml once the CMS is in use... +* `problem` -- a problem. See elsewhere in edx4edx for documentation on the format. +* `problemset` -- logically, a series of related problems. Currently displayed vertically. May contain explanatory html, videos, etc. +* `sequential` -- a sequence of content, currently displayed with a horizontal list of tabs. If possible, use a more semantically meaningful tag (currently, we only have `videosequence`). +* `vertical` -- a sequence of content, displayed vertically. If possible, use a more semantically meaningful tag (currently, we only have `problemset`). +* `video` -- a link to a video, currently expected to be hosted on youtube. +* `videosequence` -- a sequence of videos. This can contain various non-video content; it just signals to the system that this is logically part of an explanatory sequence of content, as opposed to say an exam sequence. + +## Tag details + +### Container tags + +Container tags include `chapter`, `sequential`, `videosequence`, `vertical`, and `problemset`. They are all specified in the same way in the xml, as shown in the tutorial above. + +### `course` + +`course` is also a container, and is similar, with one extra wrinkle: the top level pointer tag _must_ have `org` and `course` attributes specified--the organization name, and course name. Note that `course` is referring to the platonic ideal of this course (e.g. "6.002x"), not to any particular run of this course. The `url_name` should be the particular run of this course. + +### `customtag` + +When we see `<customtag impl="special" animal="unicorn" hat="blue"/>`, we will: + +* look for a file called `custom_tags/special` in your course dir. +* render it as a mako template, passing parameters {'animal':'unicorn', 'hat':'blue'}, generating html. (Google `mako` for template syntax, or look at existing examples). + +Since `customtag` is already a pointer, there is generally no need to put it into a separate file--just use it in place: <customtag url_name="my_custom_tag" impl="blah" attr1="..."/> + + +### `html` + +Most of our content is in xml, but some html content may not be proper xml (all tags matched, single top-level tag, etc), since browsers are fairly lenient in what they'll display. So, there are two ways to include html content: + +* If your html content is in a proper xml format, just put it in `html/{url_name}.xml`. +* If your html content is not in proper xml format, you can put it in `html/{filename}.html`, and put `<html filename={filename} />` in `html/{filename}.xml`. This allows another level of indirection, and makes sure that we can read the xml file and then just return the actual html content without trying to parse it. + +### `video` + +Videos have an attribute youtube, which specifies a series of speeds + youtube videos id: + + <video youtube="0.75:1yk1A8-FPbw,1.0:vNMrbPvwhU4,1.25:gBW_wqe7rDc,1.50:7AE_TKgaBwA" url_name="S15V14_Response_to_impulse_limit_case"/> + +This video has been encoded at 4 different speeds: 0.75x, 1x, 1.25x, and 1.5x. + +## More on `url_name`s + +Every content element (within a course) should have a unique id. This id is formed as `{category}/{url_name}`, or automatically generated from the content if `url_name` is not specified. Categories are the different tag types ('chapter', 'problem', 'html', 'sequential', etc). Url_name is a string containing a-z, A-Z, dot (.) and _. This is what appears in urls that point to this object. + +__IMPORTANT__: A student's state for a particular content element is tied to the element id, so the automatic id generation if only ok for elements that do not need to store any student state (e.g. verticals or customtags). For problems, sequentials, and videos, and any other element where we keep track of what the student has done and where they are at, you should specify a unique `url_name`. Of course, any content element that is split out into a file will need a `url_name` to specify where to find the definition. When the CMS comes online, it will use these ids to enable content reuse, so if there is a logical name for something, please do specify it. + +----- + +## Policy files + +* A policy file is useful when running different versions of a course e.g. internal, external, fall, spring, etc. as you can change due dates, etc, by creating multiple policy files. +* A policy file provides information on the metadata of the course--things that are not inherent to the definitions of the contents, but that may vary from run to run. +* Note: We will be expanding our understanding and format for metadata in the not-too-distant future, but for now it is simply a set of key-value pairs. + +### Policy file location +* The policy for a course run `some_url_name` lives in `policies/some_url_name.json` + +### Policy file contents +* The file format is "json", and is best shown by example, as in the tutorial above (though also feel free to google :) +* The expected contents are a dictionary mapping from keys to values (syntax "{ key : value, key2 : value2, etc}") +* Keys are in the form "{category}/{url_name}", which should uniquely identify a content element. +Values are dictionaries of the form {"metadata-key" : "metadata-value"}. +* The order in which things appear does not matter, though it may be helpful to organize the file in the same order as things appear in the content. +* NOTE: json is picky about commas. If you have trailing commas before closing braces, it will complain and refuse to parse the file. This can be irritating at first. + +### Available metadata + +__Not inherited:__ + +* `display_name` - name that will appear when this content is displayed in the courseware. Useful for all tag types. +* `format` - subheading under display name -- currently only displayed for chapter sub-sections. +* `hide_from_toc` -- If set to true for a chapter or chapter subsection, will hide that element from the courseware navigation accordion. This is useful if you'd like to link to the content directly instead (e.g. for tutorials) +* `ispublic` -- specify whether the course is public. You should be able to use start dates instead (?) + +__Inherited:__ + +* `start` -- when this content should be shown to students. Note that anyone with staff access to the course will always see everything. +* `showanswer` - only for psets, is binary (closed/open). +* `graded` - Tutorial vs. grade, again binary (true/false). If true, will be used in calculation of student grade. +* `rerandomise` - Provide different numbers/variables for problems to prevent cheating. Provide different answers from questions bank? +* `due` - Due date for assignment. Assignment will be closed after that. This is a very important function of a policy file. +* `graceperiod` - +* `xqa_key` -- for integration with Ike's content QA server. -- should typically be specified at the course level. + +__Inheritance example:__ + +This is a sketch ("tue" is not a valid start date), that should help illustrate how metadata inheritance works. + + <course start="tue"> + <chap1> -- start tue + <problem> --- start tue + </chap1> + <chap2 start="wed"> -- start wed + <problem2 start="thu"> -- start thu + <problem3> -- start wed + </chap2> + </course> + + +## Specifying metadata in the xml file + +Metadata can also live in the xml files, but anything defined in the policy file overrides anything in the xml. This is primarily for backwards compatibility, and you should probably not use both. If you do leave some metadata tags in the xml, you should be consistent (e.g. if `display_name`s stay in xml, they should all stay in xml). + - note, some xml attributes are not metadata. e.g. in `<video youtube="xyz987293487293847"/>`, the `youtube` attribute specifies what video this is, and is logically part of the content, not the policy, so it should stay in the xml. + +Another example policy file: + { + "course/2012": { + "graceperiod": "1 day", + "start": "2012-10-15T12:00", + "display_name": "Introduction to Computer Science I", + "xqa_key": "z1y4vdYcy0izkoPeihtPClDxmbY1ogDK" + }, + "chapter/Week_0": { + "display_name": "Week 0" + }, + "sequential/Pre-Course_Survey": { + "display_name": "Pre-Course Survey", + "format": "Survey" + } + } -Valid tag categories: -abtest -chapter -course -customtag -html -error -- don't put these in by hand :) -problem -problemset -sequential -vertical -video -videosequence -Obsolete tags: -Use customtag instead: - videodev - book - slides - image - discuss +## Deprecated formats -Ex: instead of <book page="12"/>, use <customtag impl="book" page="12"/> +If you look at some older xml, you may see some tags or metadata attributes that aren't listed above. They are deprecated, and should not be used in new content. We include them here so that you can understand how old-format content works. -Use something semantic instead, as makes sense: sequential, vertical, videosequence if it's actually a sequence. If the section would only contain a single element, just include that element directly. - section +### Obsolete tags: -In general, prefer the most "semantic" name for containers: e.g. use problemset rather than vertical for a problem set. That way, if we decide to display problem sets differently, we don't have to change the xml. +* `section` : this used to be necessary within chapters. Now, you can just use any standard tag inside a chapter, so use the container tag that makes the most sense for grouping content--e.g. `problemset`, `videosequence`, and just include content directly if it belongs inside a chapter (e.g. `html`, `video`, `problem`) -How customtags work: - When we see <customtag impl="special" animal="unicorn" hat="blue"/>, we will: +* There used to be special purpose tags that all basically did the same thing, and have been subsumed by `customtag`. The list is `videodev, book, slides, image, discuss`. Use `customtag` in new content. (e.g. instead of `<book page="12"/>`, use `<customtag impl="book" page="12"/>`) - - look for a file called custom_tags/special in your course dir. - - render it as a mako template, passing parameters {'animal':'unicorn', 'hat':'blue'}, generating html. +### Obsolete attributes +* `slug` -- old term for `url_name`. Use `url_name` +* `name` -- we didn't originally have a distinction between `url_name` and `display_name` -- this made content element ids fragile, so please use `url_name` as a stable unique identifier for the content, and `display_name` as the particular string you'd like to display for it. -METADATA -Metadata that we generally understand: -Only on course tag in courses/url_name.xml - ispublic - xqa_key -- set only on course, inherited to everything else +# Static links -Everything: - display_name - format (maybe only content containers, e.g. "Lecture sequence", "problem set", "lab", etc. ) - start -- modules will not show up to non-course-staff users before the start date (in production) - hide_from_toc -- if this is true, don't show in table of contents for the course. Useful on chapters, and chapter subsections that are linked to from somewhere else. +if your content links (e.g. in an html file) to `"static/blah/ponies.jpg"`, we will look for this in `YOUR_COURSE_DIR/blah/ponies.jpg`. Note that this is not looking in a `static/` subfolder in your course dir. This may (should?) change at some point. Links that include `/course` will be rewritten to the root of your course in the courseware (e.g. `courses/{org}/{course}/{url_name}/` in the current url structure). This is useful for linking to the course wiki, for example. -Used for problems -graceperiod -showanswer -rerandomize -graded -due +# Tips for content developers +* We will be making better tools for managing policy files soon. In the meantime, you can add dummy definitions to make it easier to search and separate the file visually. For example, you could add: -These are _inherited_ : if specified on the course, will apply to everything in the course, except for things that explicitly specify them, and their children. - 'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize', - # TODO (ichuang): used for Fall 2012 xqa server access - 'xqa_key', + "WEEK 1" : "##################################################", -Example sketch: -<course start="tue"> - <chap1> -- start tue - <problem> --- start tue - </chap1> - <chap2 start="wed"> -- start wed - <problem2 start="thu"> -- start thu - <problem3> -- start wed - </chap2> -</course> +before the week 1 material to make it easy to find in the file. +* Come up with a consistent pattern for url_names, so that it's easy to know where to look for any piece of content. It will also help to come up with a standard way of splitting your content files. As a point of departure, we suggest splitting chapters, sequences, html, and problems into separate files. -STATIC LINKS: +* A heads up: our content management system will allow you to develop content through a web browser, but will be backed by this same xml at first. Once that happens, every element will be in its own file to make access and updates faster. -if your content links (e.g. in an html file) to "static/blah/ponies.jpg", we will look for this in YOUR_COURSE_DIR/blah/ponies.jpg. Note that this is not looking in a static/ subfolder in your course dir. This may (should?) change at some point. +* Prefer the most "semantic" name for containers: e.g., use problemset rather than vertical for a problem set. That way, if we decide to display problem sets differently, we don't have to change the xml. diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index e5ef915e2572740b6cef8a3b463de3447f37dfdc..1c0b2203ca6c04a6162f256bb00b43ecdc6ce6fd 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -5,6 +5,7 @@ import logging from path import path from django.conf import settings +from django.core.urlresolvers import reverse from django.http import Http404 from xmodule.course_module import CourseDescriptor @@ -133,8 +134,13 @@ def get_course_info_section(course, section_key): if section_key in ['handouts', 'guest_handouts', 'updates', 'guest_updates']: try: with course.system.resources_fs.open(path("info") / section_key + ".html") as htmlFile: - return replace_urls(htmlFile.read().decode('utf-8'), - course.metadata['data_dir']) + # Replace '/static/' urls + info_html = replace_urls(htmlFile.read().decode('utf-8'), course.metadata['data_dir']) + + # Replace '/course/' urls + course_root = reverse('course_root', args=[course.id])[:-1] # Remove trailing slash + info_html = replace_urls(info_html, course_root, '/course/') + return info_html except ResourceNotFoundError: log.exception("Missing info section {key} in course {url}".format( key=section_key, url=course.location.url())) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 7967452647363a27250c8a32cf313ed9defa14f3..8a96d2533f11dd75d7e382eb741f63263e4785df 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -143,8 +143,9 @@ def get_module(user, request, location, student_module_cache, course_id, positio exists. Arguments: - - user : current django User - - request : current django HTTPrequest + - user : User for whom we're getting the module + - request : current django HTTPrequest -- used in particular for auth + (This is important e.g. for prof impersonation of students in progress view) - location : A Location-like object identifying the module to load - student_module_cache : a StudentModuleCache - course_id : the course_id in the context of which to load module @@ -170,7 +171,9 @@ def _get_module(user, request, location, student_module_cache, course_id, positi descriptor = modulestore().get_instance(course_id, location) # Short circuit--if the user shouldn't have access, bail without doing any work - if not has_access(user, descriptor, 'load'): + # NOTE: Do access check on request.user -- that's who actually needs access (e.g. could be prof + # impersonating a user) + if not has_access(request.user, descriptor, 'load'): return None #TODO Only check the cache if this module can possibly have state @@ -199,7 +202,10 @@ def _get_module(user, request, location, student_module_cache, course_id, positi ) # Fully qualified callback URL for external queueing system - xqueue_callback_url = request.build_absolute_uri('/')[:-1] # Trailing slash provided by reverse + xqueue_callback_url = '{proto}://{host}'.format( + host=request.get_host(), + proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http') + ) xqueue_callback_url += reverse('xqueue_callback', kwargs=dict(course_id=course_id, userid=str(user.id), @@ -346,10 +352,10 @@ def xqueue_callback(request, course_id, userid, id, dispatch): get = request.POST.copy() for key in ['xqueue_header', 'xqueue_body']: if not get.has_key(key): - return Http404 + raise Http404 header = json.loads(get['xqueue_header']) if not isinstance(header, dict) or not header.has_key('lms_key'): - return Http404 + raise Http404 # Retrieve target StudentModule user = User.objects.get(id=userid) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 83957c17d7e650128c0c5937eba0191c004aa472..50b7a2d6459f53c966828ade0d97fb27d7923fdd 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -325,14 +325,17 @@ def progress(request, course_id, student_id=None): raise Http404 student = User.objects.get(id=int(student_id)) + # NOTE: To make sure impersonation by instructor works, use + # student instead of request.user in the rest of the function. + student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( - course_id, request.user, course) - course_module = get_module(request.user, request, course.location, + course_id, student, course) + course_module = get_module(student, request, course.location, student_module_cache, course_id) courseware_summary = grades.progress_summary(student, course_module, course.grader, student_module_cache) - grade_summary = grades.grade(request.user, request, course, student_module_cache) + grade_summary = grades.grade(student, request, course, student_module_cache) context = {'course': course, 'courseware_summary': courseware_summary, diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index 280e6d2780be524e6794d839f2969975dacfe82d..68250a035e232d7e2781e83c4c12ad43800780a9 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -51,11 +51,11 @@ def ajax_content_response(request, course_id, content, template_name): 'content': content, } html = render_to_string(template_name, context) - user_info = cc.User.from_django_user(request.user).safe_attributes() + user_info = cc.User.from_django_user(request.user).to_dict() annotated_content_info = utils.get_annotated_content_info(course_id, content, request.user, user_info) return JsonResponse({ 'html': html, - 'content': content, + 'content': utils.safe_content(content), 'annotated_content_info': annotated_content_info, }) @@ -78,7 +78,7 @@ def create_thread(request, course_id, commentable_id): if request.is_ajax(): return ajax_content_response(request, course_id, thread.to_dict(), 'discussion/ajax_create_thread.html') else: - return JsonResponse(thread.to_dict()) + return JsonResponse(utils.safe_content(thread.to_dict())) @require_POST @login_required @@ -90,7 +90,7 @@ def update_thread(request, course_id, thread_id): if request.is_ajax(): return ajax_content_response(request, course_id, thread.to_dict(), 'discussion/ajax_update_thread.html') else: - return JsonResponse(thread.to_dict()) + return JsonResponse(utils.safe_content(thread.to_dict())) def _create_comment(request, course_id, thread_id=None, parent_id=None): post = request.POST @@ -109,7 +109,7 @@ def _create_comment(request, course_id, thread_id=None, parent_id=None): if request.is_ajax(): return ajax_content_response(request, course_id, comment.to_dict(), 'discussion/ajax_create_comment.html') else: - return JsonResponse(comment.to_dict()) + return JsonResponse(utils.safe_content(comment.to_dict())) @require_POST @login_required @@ -126,7 +126,7 @@ def create_comment(request, course_id, thread_id): def delete_thread(request, course_id, thread_id): thread = cc.Thread.find(thread_id) thread.delete() - return JsonResponse(thread.to_dict()) + return JsonResponse(utils.safe_content(thread.to_dict())) @require_POST @login_required @@ -138,7 +138,7 @@ def update_comment(request, course_id, comment_id): if request.is_ajax(): return ajax_content_response(request, course_id, comment.to_dict(), 'discussion/ajax_update_comment.html') else: - return JsonResponse(comment.to_dict()), + return JsonResponse(utils.safe_content(comment.to_dict())) @require_POST @login_required @@ -147,7 +147,7 @@ def endorse_comment(request, course_id, comment_id): comment = cc.Comment.find(comment_id) comment.endorsed = request.POST.get('endorsed', 'false').lower() == 'true' comment.save() - return JsonResponse(comment.to_dict()) + return JsonResponse(utils.safe_content(comment.to_dict())) @require_POST @login_required @@ -156,7 +156,11 @@ def openclose_thread(request, course_id, thread_id): thread = cc.Thread.find(thread_id) thread.closed = request.POST.get('closed', 'false').lower() == 'true' thread.save() - return JsonResponse(thread.to_dict()) + thread = thread.to_dict() + return JsonResponse({ + 'content': utils.safe_content(thread), + 'ability': utils.get_ability(course_id, thread, request.user), + }) @require_POST @login_required @@ -173,7 +177,7 @@ def create_sub_comment(request, course_id, comment_id): def delete_comment(request, course_id, comment_id): comment = cc.Comment.find(comment_id) comment.delete() - return JsonResponse(comment.to_dict()) + return JsonResponse(utils.safe_content(comment.to_dict())) @require_POST @login_required @@ -182,7 +186,7 @@ def vote_for_comment(request, course_id, comment_id, value): user = cc.User.from_django_user(request.user) comment = cc.Comment.find(comment_id) user.vote(comment, value) - return JsonResponse(comment.to_dict()) + return JsonResponse(utils.safe_content(comment.to_dict())) @require_POST @login_required @@ -191,7 +195,7 @@ def undo_vote_for_comment(request, course_id, comment_id): user = cc.User.from_django_user(request.user) comment = cc.Comment.find(comment_id) user.unvote(comment) - return JsonResponse(comment.to_dict()) + return JsonResponse(utils.safe_content(comment.to_dict())) @require_POST @login_required @@ -200,7 +204,7 @@ def vote_for_thread(request, course_id, thread_id, value): user = cc.User.from_django_user(request.user) thread = cc.Thread.find(thread_id) user.vote(thread, value) - return JsonResponse(thread.to_dict()) + return JsonResponse(utils.safe_content(thread.to_dict())) @require_POST @login_required @@ -209,7 +213,7 @@ def undo_vote_for_thread(request, course_id, thread_id): user = cc.User.from_django_user(request.user) thread = cc.Thread.find(thread_id) user.unvote(thread) - return JsonResponse(thread.to_dict()) + return JsonResponse(utils.safe_content(thread.to_dict())) @require_POST diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 38f8fa985dc0a41f1dac13eaaaa1941d358e0c31..62055f0ab7241e4f4987a23c8bfa436f065b9ea0 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -64,7 +64,7 @@ def render_discussion(request, course_id, threads, *args, **kwargs): 'user': (lambda: reverse('django_comment_client.forum.views.user_profile', args=[course_id, user_id])), }[discussion_type]() - user_info = cc.User.from_django_user(request.user).safe_attributes() + user_info = cc.User.from_django_user(request.user).to_dict() def infogetter(thread): return utils.get_annotated_content_infos(course_id, thread, request.user, user_info) @@ -83,7 +83,7 @@ def render_discussion(request, course_id, threads, *args, **kwargs): 'base_url': base_url, 'query_params': strip_blank(strip_none(extract(query_params, ['page', 'sort_key', 'sort_order', 'tags', 'text']))), 'annotated_content_info': json.dumps(annotated_content_info), - 'discussion_data': json.dumps({ (discussion_id or user_id): threads }) + 'discussion_data': json.dumps({ (discussion_id or user_id): map(utils.safe_content, threads) }) } context = dict(context.items() + query_params.items()) return render_to_string(template, context) @@ -128,7 +128,7 @@ def inline_discussion(request, course_id, discussion_id): return utils.JsonResponse({ 'html': html, - 'discussionData': threads, + 'discussion_data': map(utils.safe_content, threads), }) def render_search_bar(request, course_id, discussion_id=None, text=''): @@ -149,7 +149,7 @@ def forum_form_discussion(request, course_id): if request.is_ajax(): return utils.JsonResponse({ 'html': content, - 'discussionData': threads, + 'discussion_data': map(utils.safe_content, threads), }) else: recent_active_threads = cc.search_recent_active_threads( @@ -176,7 +176,7 @@ def render_single_thread(request, discussion_id, course_id, thread_id): thread = cc.Thread.find(thread_id).retrieve(recursive=True).to_dict() - user_info = cc.User.from_django_user(request.user).safe_attributes() + user_info = cc.User.from_django_user(request.user).to_dict() annotated_content_info = utils.get_annotated_content_infos(course_id, thread=thread, user=request.user, user_info=user_info) @@ -186,7 +186,7 @@ def render_single_thread(request, discussion_id, course_id, thread_id): 'annotated_content_info': json.dumps(annotated_content_info), 'course_id': course_id, 'request': request, - 'discussion_data': json.dumps({ discussion_id: [thread] }), + 'discussion_data': json.dumps({ discussion_id: [utils.safe_content(thread)] }), } return render_to_string('discussion/_single_thread.html', context) @@ -194,7 +194,7 @@ def single_thread(request, course_id, discussion_id, thread_id): if request.is_ajax(): - user_info = cc.User.from_django_user(request.user).safe_attributes() + user_info = cc.User.from_django_user(request.user).to_dict() thread = cc.Thread.find(thread_id).retrieve(recursive=True) annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info) context = {'thread': thread.to_dict(), 'course_id': course_id} @@ -202,7 +202,7 @@ def single_thread(request, course_id, discussion_id, thread_id): return utils.JsonResponse({ 'html': html, - 'content': thread.to_dict(), + 'content': utils.safe_content(thread.to_dict()), 'annotated_content_info': annotated_content_info, }) @@ -252,7 +252,7 @@ def user_profile(request, course_id, user_id): if request.is_ajax(): return utils.JsonResponse({ 'html': content, - 'discussionData': threads, + 'discussion_data': map(utils.safe_content, threads), }) else: context = { diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index fded387462d3aa97070d5b87cf5cb626d63ff1a7..516344d79b6260b2e873dc363f124757bf48cbe5 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -164,6 +164,16 @@ class QueryCountDebugMiddleware(object): logging.info('%s queries run, total %s seconds' % (len(connection.queries), total_time)) return response +def get_ability(course_id, content, user): + return { + 'editable': check_permissions_by_view(user, course_id, content, "update_thread" if content['type'] == 'thread' else "update_comment"), + 'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_comment"), + 'can_endorse': check_permissions_by_view(user, course_id, content, "endorse_comment") if content['type'] == 'comment' else False, + 'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if content['type'] == 'thread' else "delete_comment"), + 'can_openclose': check_permissions_by_view(user, course_id, content, "openclose_thread") if content['type'] == 'thread' else False, + 'can_vote': check_permissions_by_view(user, course_id, content, "vote_for_thread" if content['type'] == 'thread' else "vote_for_comment"), + } + def get_annotated_content_info(course_id, content, user, user_info): voted = '' if content['id'] in user_info['upvoted_ids']: @@ -173,14 +183,7 @@ def get_annotated_content_info(course_id, content, user, user_info): return { 'voted': voted, 'subscribed': content['id'] in user_info['subscribed_thread_ids'], - 'ability': { - 'editable': check_permissions_by_view(user, course_id, content, "update_thread" if content['type'] == 'thread' else "update_comment"), - 'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_comment"), - 'can_endorse': check_permissions_by_view(user, course_id, content, "endorse_comment") if content['type'] == 'comment' else False, - 'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if content['type'] == 'thread' else "delete_comment"), - 'can_openclose': check_permissions_by_view(user, course_id, content, "openclose_thread") if content['type'] == 'thread' else False, - 'can_vote': check_permissions_by_view(user, course_id, content, "vote_for_thread" if content['type'] == 'thread' else "vote_for_comment"), - }, + 'ability': get_ability(course_id, content, user), } def get_annotated_content_infos(course_id, thread, user, user_info): @@ -216,3 +219,17 @@ def extend_content(content): 'permalink': permalink(content), } return merge_dict(content, content_info) + +def safe_content(content): + fields = [ + 'id', 'title', 'body', 'course_id', 'anonymous', 'endorsed', + 'parent_id', 'thread_id', 'votes', 'closed', + 'created_at', 'updated_at', 'depth', 'type', + 'commentable_id', 'comments_count', 'at_position_list', + 'children', 'highlighted_title', 'highlighted_body', + ] + + if content.get('anonymous') is False: + fields += ['username', 'user_id'] + + return strip_none(extract(content, fields)) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 75ae712b88ff92b6187c91c7eb20cc10f6471b02..fbcb9b8e4c918c6c578a1f82f9e8faa5d4bcb4fd 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -23,9 +23,16 @@ DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage' MITX_FEATURES['ENABLE_DISCUSSION'] = False MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True +# IMPORTANT: With this enabled, the server must always be behind a proxy that +# strips the header HTTP_X_FORWARDED_PROTO from client requests. Otherwise, +# a user can fool our server into thinking it was an https connection. +# See https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header +# for other warnings. +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') ########################### NON-SECURE ENV CONFIG ############################## # Things like server locations, ports, etc. + with open(ENV_ROOT / "env.json") as env_file: ENV_TOKENS = json.load(env_file) @@ -49,6 +56,8 @@ LOGGING = get_logger_config(LOG_DIR, COURSE_LISTINGS = ENV_TOKENS.get('COURSE_LISTINGS', {}) SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {}) +COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL",'') +COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY",'') ############################## SECURE AUTH ITEMS ############################### # Secret things: passwords, access keys, etc. @@ -67,5 +76,3 @@ XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE'] if 'COURSE_ID' in ENV_TOKENS: ASKBOT_URL = "courses/{0}/discussions/".format(ENV_TOKENS['COURSE_ID']) -COMMENTS_SERVICE_URL = ENV_TOKENS["COMMENTS_SERVICE_URL"] -COMMENTS_SERVICE_KEY = ENV_TOKENS["COMMENTS_SERVICE_KEY"] diff --git a/lms/envs/common.py b/lms/envs/common.py index 99c68afbe3115875f8ad59203c9bd6b234a08ec5..938c4036ae67bd5d1e2efa07caf52fff7fdea2d3 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -332,6 +332,8 @@ WIKI_CAN_CHANGE_PERMISSIONS = lambda article, user: user.is_staff or user.is_sup WIKI_CAN_ASSIGN = lambda article, user: user.is_staff or user.is_superuser WIKI_USE_BOOTSTRAP_SELECT_WIDGET = False +WIKI_LINK_LIVE_LOOKUPS = False +WIKI_LINK_DEFAULT_LEVEL = 2 ################################# Jasmine ################################### JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' diff --git a/lms/envs/dev_ike.py b/lms/envs/dev_ike.py index 3ae141a037488dd70fd4d46660bbd80991dba814..297b179fae8921842828fb2eaa3005336693d098 100644 --- a/lms/envs/dev_ike.py +++ b/lms/envs/dev_ike.py @@ -16,6 +16,8 @@ WIKI_ENABLED = False MITX_FEATURES['ENABLE_TEXTBOOK'] = False MITX_FEATURES['ENABLE_DISCUSSION'] = False MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = True # require that user be in the staff_* group to be able to enroll +MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = False +MITX_FEATURES['SUBDOMAIN_BRANDING'] = False MITX_FEATURES['DISABLE_START_DATES'] = True # MITX_FEATURES['USE_DJANGO_PIPELINE']=False # don't recompile scss diff --git a/lms/lib/comment_client/user.py b/lms/lib/comment_client/user.py index 1f61e9d625e040e27a7f14170321d6a1bf133d74..ae4abf91b7987d51574c1f210f16ea48419e431f 100644 --- a/lms/lib/comment_client/user.py +++ b/lms/lib/comment_client/user.py @@ -33,16 +33,6 @@ class User(models.Model): params = {'source_type': source.type, 'source_id': source.id} response = perform_request('delete', _url_for_subscription(self.id), params) - # TODO this is a hack to compensate for the fact that synchronization isn't - # happening properly. - def safe_attributes(self): - try: - return self.to_dict() - except: - self.save() - self._retrieve() - return self.to_dict() - def vote(self, voteable, value): if voteable.type == 'thread': url = _url_for_vote_thread(voteable.id) diff --git a/lms/static/coffee/src/discussion/content.coffee b/lms/static/coffee/src/discussion/content.coffee index f7e7bcc5dcf15595ea31f7eeed4bb09c963c26e4..73c3688c2a2c6028cc881f576711557daaab8a2f 100644 --- a/lms/static/coffee/src/discussion/content.coffee +++ b/lms/static/coffee/src/discussion/content.coffee @@ -90,7 +90,9 @@ if Backbone? ability: (ability) -> for action, elemSelector of @model.actions if not ability[action] - @$(elemSelector).parent().remove() + @$(elemSelector).parent().hide() + else + @$(elemSelector).parent().show() $discussionContent: -> @_discussionContent ||= @$el.children(".discussion-content") @@ -122,7 +124,7 @@ if Backbone? url = @model.urlFor('retrieve') DiscussionUtil.safeAjax $elem: $elem - $loading: $(event.target) if event + $loading: @$(".discussion-show-comments") type: "GET" url: url success: (response, textStatus) => @@ -191,6 +193,8 @@ if Backbone? comment = @model.addComment response.content commentView = new CommentView el: $comment[0], model: comment comment.updateInfo response.annotated_content_info + if autowatch + @model.get('thread').set('subscribed', true) @cancelReply() cancelReply: -> @@ -269,6 +273,7 @@ if Backbone? data: data success: (response, textStatus) => @model.set('closed', not closed) + @model.set('ability', response.ability) edit: (event) -> @$(".discussion-content-wrapper").hide() @@ -279,11 +284,11 @@ if Backbone? view = {} view.id = @model.id if @model.get('type') == 'thread' - view.title = @$(".thread-raw-title").html() - view.body = @$(".thread-raw-body").html() - view.tags = @$(".thread-raw-tags").html() + view.title = @model.get('title') + view.body = @model.get('body') + view.tags = @model.get('tags') else - view.body = @$(".comment-raw-body").html() + view.body = @model.get('body') @$discussionContent().append Mustache.render DiscussionUtil.getTemplate("_edit_#{@model.get('type')}"), view DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "#{@model.get('type')}-body-edit" @$(".thread-tags-edit").tagsInput DiscussionUtil.tagsInputOptions() @@ -311,8 +316,12 @@ if Backbone? success: (response, textStatus) => DiscussionUtil.clearFormErrors @$(".discussion-update-errors") @$discussionContent().replaceWith(response.html) - @model.set response.content - @model.updateInfo response.annotated_content_info + if @model.get('type') == 'thread' + @model = new Thread response.content + else + @model = new Comment $.extend {}, response.content, { thread: @model.get('thread') } + @reconstruct() + @model.updateInfo response.annotated_content_info, { forceUpdate: true } cancelEdit: (event) -> @$(".discussion-content-edit").hide() @@ -330,9 +339,11 @@ if Backbone? DiscussionUtil.safeAjax $elem: $elem url: url + type: "POST" success: (response, textStatus) => @$el.remove() - @model.get('thread').removeComment(@model) + if @model.get('type') == 'comment' + @model.get('thread').removeComment(@model) events: "click .discussion-follow-thread": "toggleFollow" @@ -381,6 +392,14 @@ if Backbone? @initTitle() @initBody() @initCommentViews() + + reconstruct: -> + @initBindings() + @initLocal() + @initTimeago() + @initTitle() + @initBody() + @delegateEvents() class @Thread extends @Content urlMappers: diff --git a/lms/static/coffee/src/discussion/discussion.coffee b/lms/static/coffee/src/discussion/discussion.coffee index d4b6a8a9c4c74711b641a8bbc00b58ea1c2c6353..d8df704d1c3e93b6e2538df7eb9e42b114ca831b 100644 --- a/lms/static/coffee/src/discussion/discussion.coffee +++ b/lms/static/coffee/src/discussion/discussion.coffee @@ -42,13 +42,17 @@ if Backbone? DiscussionUtil.safeAjax $elem: $elem $loading: $elem + loadingCallback: -> + $(this).parent().append("<span class='discussion-loading'></span>") + loadedCallback: -> + $(this).parent().children(".discussion-loading").remove() url: url type: "GET" success: (response, textStatus) => $parent = @$el.parent() @$el.replaceWith(response.html) $discussion = $parent.find("section.discussion") - @model.reset(response.discussionData, { silent: false }) + @model.reset(response.discussion_data, { silent: false }) view = new DiscussionView el: $discussion[0], model: @model DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info) $("html, body").animate({ scrollTop: 0 }, 0) @@ -109,6 +113,7 @@ if Backbone? @$(".discussion-cancel-post").click $.proxy(@cancelNewPost, @) + @$el.children(".blank").hide() @$(".new-post-form").show() submitNewPost: (event) -> @@ -136,6 +141,8 @@ if Backbone? $thread = $(response.html) @$el.children(".threads").prepend($thread) + @$el.children(".blank").remove() + @$(".new-post-similar-posts").empty() @$(".new-post-similar-posts-wrapper").hide() @$(".new-post-title").val("").attr("prev-text", "") @@ -154,6 +161,7 @@ if Backbone? @$(".new-post-form").addClass("collapsed") else if @$el.hasClass("forum-discussion") @$(".new-post-form").hide() + @$el.children(".blank").show() search: (event) -> event.preventDefault() diff --git a/lms/static/coffee/src/discussion/discussion_module.coffee b/lms/static/coffee/src/discussion/discussion_module.coffee index 4bcacc14744d350786083c8ee61c367dc362b0ce..8306d4d2ae8c27f1716b7bd45693ffc628112427 100644 --- a/lms/static/coffee/src/discussion/discussion_module.coffee +++ b/lms/static/coffee/src/discussion/discussion_module.coffee @@ -27,7 +27,7 @@ if Backbone? $discussion = @$el.find("section.discussion") $(event.target).html("Hide Discussion") discussion = new Discussion() - discussion.reset(response.discussionData, {silent: false}) + discussion.reset(response.discussion_data, {silent: false}) view = new DiscussionView(el: $discussion[0], model: discussion) DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info) @retrieved = true diff --git a/lms/static/coffee/src/discussion/utils.coffee b/lms/static/coffee/src/discussion/utils.coffee index e156b09a635c108549765ec231a5c84c2e54daa5..0990f0c87cc7c2ec59fad42fda0238cab3977513 100644 --- a/lms/static/coffee/src/discussion/utils.coffee +++ b/lms/static/coffee/src/discussion/utils.coffee @@ -1,9 +1,10 @@ $ -> $.fn.extend loading: -> - $(this).after("<span class='discussion-loading'></span>") + @$_loading = $("<span class='discussion-loading'></span>") + $(this).after(@$_loading) loaded: -> - $(this).parent().children(".discussion-loading").remove() + @$_loading.remove() class @DiscussionUtil @@ -72,13 +73,17 @@ class @DiscussionUtil params["beforeSend"] = -> $elem.attr("disabled", "disabled") if params["$loading"] - console.log "loading" - params["$loading"].loading() + if params["loadingCallback"]? + params["loadingCallback"].apply(params["$loading"]) + else + params["$loading"].loading() $.ajax(params).always -> $elem.removeAttr("disabled") if params["$loading"] - console.log "loaded" - params["$loading"].loaded() + if params["loadedCallback"]? + params["loadedCallback"].apply(params["$loading"]) + else + params["$loading"].loaded() @get: ($elem, url, data, success) -> @safeAjax diff --git a/lms/static/coffee/src/navigation.coffee b/lms/static/coffee/src/navigation.coffee index 1d6b8a811763597ede2d0866b7c91497ac69ff5a..94d53c17c4c47a94e6eb536f66e048ab68c0c1ef 100644 --- a/lms/static/coffee/src/navigation.coffee +++ b/lms/static/coffee/src/navigation.coffee @@ -13,9 +13,10 @@ class @Navigation active: active header: 'h3' autoHeight: false + $('#accordion .ui-state-active').closest('.chapter').addClass('is-open') $('#open_close_accordion a').click @toggle - $('#accordion').show() + $('#accordion a').click @setChapter log: (event, ui) -> log_event 'accordion', @@ -24,3 +25,8 @@ class @Navigation toggle: -> $('.course-wrapper').toggleClass('closed') + + setChapter: -> + $('#accordion .is-open').removeClass('is-open') + $(this).closest('.chapter').addClass('is-open') + \ No newline at end of file diff --git a/lms/static/js/jquery.gradebook.js b/lms/static/js/jquery.gradebook.js index 187e58189f49d262e13cdce1a6e0fdcd14a4f9c3..7a5ce66081bce2cf7cd2411c0886468a282853b4 100644 --- a/lms/static/js/jquery.gradebook.js +++ b/lms/static/js/jquery.gradebook.js @@ -4,13 +4,16 @@ var Gradebook = function($element) { var _this = this; var $element = $element; + var $body = $('body'); var $grades = $element.find('.grades'); + var $studentTable = $element.find('.student-table'); var $gradeTable = $element.find('.grade-table'); + var $search = $element.find('.student-search-field'); var $leftShadow = $('<div class="left-shadow"></div>'); var $rightShadow = $('<div class="right-shadow"></div>'); var tableHeight = $gradeTable.height(); var maxScroll = $gradeTable.width() - $grades.width(); - var $body = $('body'); + var mouseOrigin; var tableOrigin; @@ -58,12 +61,35 @@ var Gradebook = function($element) { var targetLeft = clamp($gradeTable.position().left, -maxScroll, 0); updateHorizontalPosition(targetLeft); setShadows(targetLeft); - } + }; var updateHorizontalPosition = function(left) { $gradeTable.css({ 'left': left + 'px' }); + }; + + var highlightRow = function(e) { + $element.find('.highlight').removeClass('highlight'); + + var index = $(this).index(); + $studentTable.find('tr').eq(index + 1).addClass('highlight'); + $gradeTable.find('tr').eq(index + 1).addClass('highlight'); + }; + + var filter = function(e) { + var term = $(this).val(); + if(term.length > 0) { + $studentTable.find('tbody tr').hide(); + $gradeTable.find('tbody tr').hide(); + $studentTable.find('tbody tr:contains(' + term + ')').each(function(i) { + $(this).show(); + $gradeTable.find('tr').eq($(this).index() + 1).show(); + }); + } else { + $studentTable.find('tbody tr').show(); + $gradeTable.find('tbody tr').show(); + } } $leftShadow.css('height', tableHeight + 'px'); @@ -72,5 +98,11 @@ var Gradebook = function($element) { setShadows(0); $grades.css('height', tableHeight); $gradeTable.bind('mousedown', startDrag); + $element.find('tr').bind('mouseover', highlightRow); + $search.bind('keyup', filter); $(window).bind('resize', updateWidths); -} \ No newline at end of file +} + + + + diff --git a/lms/static/js/jquery.sequence.js b/lms/static/js/jquery.sequence.js index 5406444eed6827b3e712709a26bbfedf3c8d4642..fbefb8a8cec67fe6c3e1cae842a015fd0396c307 100644 --- a/lms/static/js/jquery.sequence.js +++ b/lms/static/js/jquery.sequence.js @@ -27,11 +27,6 @@ var SequenceNav = function($element) { var offset = e.pageX - mouseOrigin; var targetLeft = clamp(listOrigin + offset, -maxScroll, 0); - console.log('---------------'); - console.log('offset: ' + offset); - console.log('target left: ' + targetLeft); - console.log('max: ' + maxScroll); - updateHorizontalPosition(targetLeft); setShadows(targetLeft); diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss index 717770f459fb4988681f68b38a23902cfe02af2d..6c1988684e37b4bf7daea2ee21f0ce14049f2914 100644 --- a/lms/static/sass/_discussion.scss +++ b/lms/static/sass/_discussion.scss @@ -150,10 +150,16 @@ $tag-text-color: #5b614f; //user profile + .user-profile { + @extend .sidebar; + margin-top: 24px; + } + .sidebar-username { font-size: 1.5em; font-weight: bold; line-height: 1.5em; + margin-top: 20px; } .sidebar-user-roles { diff --git a/lms/static/sass/course/_gradebook.scss b/lms/static/sass/course/_gradebook.scss index 5f6c88d918a6aaf76e80ade35ecac5fe6c2073cc..9817188d349b6bc2e95cfe892285b8d8181a5385 100644 --- a/lms/static/sass/course/_gradebook.scss +++ b/lms/static/sass/course/_gradebook.scss @@ -133,18 +133,6 @@ div.gradebook-wrapper { box-shadow: 0 1px 0 $table-border-color inset, 0 2px 0 rgba(255, 255, 255, .7) inset; border-left: 1px solid #ccc; - // &:before { - // content: ''; - // display: block; - // position: absolute; - // left: 0; - // top: 0; - // z-index: 9999; - // width: 1px; - // height: 50px; - // @include linear-gradient(top, rgba(0, 0, 0, 0) 30%, rgba(0, 0, 0, .15)); - // } - &:first-child { border-radius: 5px 0 0 0; box-shadow: 1px 1px 0 $table-border-color inset, 1px 2px 0 rgba(255, 255, 255, .7) inset; @@ -205,6 +193,19 @@ div.gradebook-wrapper { @extend .top-header; } } + + .student-table tr:hover td, + .grade-table tr:hover td, + .student-table tr.highlight td, + .grade-table tr.highlight td { + border-color: #74b7d6; + @include linear-gradient(#8ed6f7, #76cbf4); + color: #333; + + a { + color: #333; + } + } } diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss index 5621396679191aad34fea8b1ee5043493552aaaa..92d8194f240bbdf6fd5b73cf80006e02ecfcaa21 100644 --- a/lms/static/sass/course/courseware/_courseware.scss +++ b/lms/static/sass/course/courseware/_courseware.scss @@ -13,6 +13,7 @@ div.course-wrapper { section.course-content { @extend .content; padding: 40px; + line-height: 1.6; h1 { margin: 0 0 lh(); diff --git a/lms/static/sass/course/courseware/_sidebar.scss b/lms/static/sass/course/courseware/_sidebar.scss index 9f570711a770cd23121e3cbe354537ededd4a92b..52a639d9a6841ecfea9e3b57b4a58610bfaa78fd 100644 --- a/lms/static/sass/course/courseware/_sidebar.scss +++ b/lms/static/sass/course/courseware/_sidebar.scss @@ -75,6 +75,10 @@ section.course-index { @include box-shadow(0 1px 0 #fff inset, 0 -1px 0 rgba(0, 0, 0, .1) inset); @include transition(background-color .1s); + &.is-open { + background: #fff; + } + &:first-child { border-radius: 3px 0 0 0; } diff --git a/lms/static/sass/course/layout/_courseware_header.scss b/lms/static/sass/course/layout/_courseware_header.scss index 95af67473adfd2cb0bf296e5d45d0a9d32e3c765..86b5fe58c5880f7433d87970a0447af8a583aaf3 100644 --- a/lms/static/sass/course/layout/_courseware_header.scss +++ b/lms/static/sass/course/layout/_courseware_header.scss @@ -67,7 +67,39 @@ header.global.slim { @include linear-gradient(top, #fff, #eee); .guest .secondary { + margin-right: 0; + } + + .guest .secondary a { display: none; + + &#login { + display: block; + @include background-image(linear-gradient(-90deg, lighten($blue, 8%), lighten($blue, 5%) 50%, $blue 50%, darken($blue, 10%) 100%)); + border: 1px solid transparent; + border-color: darken($blue, 10%); + @include border-radius(3px); + @include box-sizing(border-box); + @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6)); + color: #fff; + display: inline-block; + font-family: $sans-serif; + font-size: 14px; + font-weight: bold; + @include inline-block; + letter-spacing: 0; + line-height: 1em; + margin: 4px; + padding: 6px 12px 8px; + text-decoration: none; + text-transform: none; + text-shadow: 0 -1px rgba(0, 0, 0, 0.6); + vertical-align: middle; + + &:hover, &.active { + @include background-image(linear-gradient(-90deg, $blue, $blue 50%, $blue 50%, $blue 100%)); + } + } } nav { @@ -86,7 +118,7 @@ header.global.slim { height: 40px; position: absolute; right: 3px; - top: -8px; + top: 0; width: 1px; } @@ -97,7 +129,7 @@ header.global.slim { height: 40px; position: absolute; right: 0px; - top: -12px; + top: 0; width: 1px; } } @@ -129,7 +161,7 @@ header.global.slim { a#signup { position: relative; - margin-top: 4px; + margin-top: 3px; padding: 6px 12px 8px; text-transform: none; font-size: 14px; diff --git a/lms/static/sass/course/wiki/_wiki.scss b/lms/static/sass/course/wiki/_wiki.scss index 912b416ff69508ba870f25645204b1e6ae684927..a58b8fa06c231130ce32ee7a73420b3d51e4ee4b 100644 --- a/lms/static/sass/course/wiki/_wiki.scss +++ b/lms/static/sass/course/wiki/_wiki.scss @@ -954,8 +954,7 @@ section.wiki { .alert { position: relative; - top: -15px; - margin-bottom: 24px; + margin: 24px 40px; padding: 8px 12px; border: 1px solid #EBE8BF; border-radius: 3px; @@ -972,6 +971,10 @@ section.wiki { } } + .main-article .alert { + margin: 0 0 24px; + } + .missing { max-width: 400px; margin: lh(2) auto; diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index 9581f5e01646c4c89ab3cba9b8ad2c88b390f259..f37c772aef54b5d3b4a11986c087846d88451e07 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -68,8 +68,8 @@ @include clearfix; border-bottom: 1px dotted rgb(220,220,220); list-style: none; - margin-bottom: 20px; - padding-bottom: 10px; + margin-bottom: 15px; + padding-bottom: 17px; &:hover { .title .icon { @@ -77,16 +77,20 @@ } } + span { + display: block; + } + span.title { color: $lighter-base-font-color; - float: left; font-family: $sans-serif; + font-size: 13px; .icon { background-size: cover; float: left; height: 19px; - margin: 2px 8px 0 0; + margin: 0 6px 0 0; opacity: 0.6; @include transition(all, 0.15s, linear); width: 19px; @@ -112,7 +116,10 @@ span.data { color: $lighter-base-font-color; font-weight: 700; - margin-left: 12px; + margin-left: 26px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; } } } diff --git a/lms/templates/discussion/_content_renderer.html b/lms/templates/discussion/_content_renderer.html index 54371bf4dd8d328eb23598e8a32a623cf96cb8a2..ac2b0b4897d1a36deb385644c367e0542ea09a7b 100644 --- a/lms/templates/discussion/_content_renderer.html +++ b/lms/templates/discussion/_content_renderer.html @@ -5,7 +5,7 @@ </%def> <%def name="render_content_with_comments(content, *args, **kwargs)"> - <div class="${content['type']}${helpers.show_if(' endorsed', content.get('endorsed'))}" _id="${content['id']}" _discussion_id="${content.get('commentable_id', '')}" _author_id="${helpers.show_if(content['user_id'], not content.get('anonymous'))}"> + <div class="${content['type'] | h}${helpers.show_if(' endorsed', content.get('endorsed')) | h}" _id="${content['id'] | h}" _discussion_id="${content.get('commentable_id', '') | h}" _author_id="${helpers.show_if(content['user_id'], not content.get('anonymous')) | h}"> ${render_content(content, *args, **kwargs)} ${render_comments(content.get('children', []), *args, **kwargs)} </div> diff --git a/lms/templates/discussion/_discussion_module.html b/lms/templates/discussion/_discussion_module.html index 3d92f242a1261482f9dc46fa02a37066a2b05b15..b9e69cc0ad8f97a3b9e34b01fb48908434632e0a 100644 --- a/lms/templates/discussion/_discussion_module.html +++ b/lms/templates/discussion/_discussion_module.html @@ -1,3 +1,3 @@ <div class="discussion-module"> - <a class="discussion-show control-button" href="javascript:void(0)" discussion_id="${discussion_id}">Show Discussion</a> + <a class="discussion-show control-button" href="javascript:void(0)" discussion_id="${discussion_id | h}">Show Discussion</a> </div> diff --git a/lms/templates/discussion/_forum.html b/lms/templates/discussion/_forum.html index 0a812fd1bbb8b79aed57aed0e5ea3996b87e8e6a..b43efae6661e4aa6186730f246d34eb358e3186f 100644 --- a/lms/templates/discussion/_forum.html +++ b/lms/templates/discussion/_forum.html @@ -1,6 +1,6 @@ <%namespace name="renderer" file="_content_renderer.html"/> -<section class="discussion forum-discussion" _id="${discussion_id}"> +<section class="discussion forum-discussion" _id="${discussion_id | h}"> <div class="discussion-non-content local"> <div class="search-wrapper"> diff --git a/lms/templates/discussion/_inline.html b/lms/templates/discussion/_inline.html index aa90e4e6bae989fd524f5cc9262195908e99e96d..abef7f39e8bf05356b8c302ed5c553a1c2b0d55c 100644 --- a/lms/templates/discussion/_inline.html +++ b/lms/templates/discussion/_inline.html @@ -1,6 +1,6 @@ <%namespace name="renderer" file="_content_renderer.html"/> -<section class="discussion inline-discussion" _id="${discussion_id}"> +<section class="discussion inline-discussion" _id="${discussion_id | h}"> <div class="discussion-non-content local"></div> diff --git a/lms/templates/discussion/_paginator.html b/lms/templates/discussion/_paginator.html index dd9bd2d43de853a66daf843a4d826a9c58f8b1fb..bb94b64289d93dc944daea54bb9037562400215f 100644 --- a/lms/templates/discussion/_paginator.html +++ b/lms/templates/discussion/_paginator.html @@ -9,7 +9,7 @@ %> <%def name="link_to_page(_page, text)"> - <a class="discussion-page-link" href="javascript:void(0)" page-url="${url_for_page(_page)}">${text}</a> + <a class="discussion-page-link" href="javascript:void(0)" page-url="${url_for_page(_page) | h}">${text}</a> </%def> <%def name="div_page(_page)"> @@ -36,7 +36,7 @@ % endfor </%def> -<div class="discussion-${discussion_type}-paginator discussion-paginator local"> +<div class="discussion-${discussion_type | h}-paginator discussion-paginator local"> <div class="prev-page"> % if page > 1: ${link_to_page(page - 1, "< Previous page")} diff --git a/lms/templates/discussion/_recent_active_posts.html b/lms/templates/discussion/_recent_active_posts.html index baf505838df689ac50e40130fc43ce84cd0dc87b..b787df2fcf298998d3516257e78d7a27b053d0d1 100644 --- a/lms/templates/discussion/_recent_active_posts.html +++ b/lms/templates/discussion/_recent_active_posts.html @@ -8,7 +8,7 @@ </header> <ol class="discussion-sidebar-following-list"> % for thread in recent_active_threads: - <li><a href="${helpers.permalink(thread)}"><span class="sidebar-following-name">${thread['title']}</span> <span class="sidebar-vote-count">${thread['votes']['point']}</span></a></li> + <li><a href="${helpers.permalink(thread) | h}"><span class="sidebar-following-name">${thread['title'] | h}</span> <span class="sidebar-vote-count">${thread['votes']['point'] | h}</span></a></li> % endfor <ol> </article> diff --git a/lms/templates/discussion/_search_bar.html b/lms/templates/discussion/_search_bar.html index e93b46efb2664682feac782ae1a3b35278e946f9..1f46a8e3c823f3a96f8dd436f502bb1ea92f4a50 100644 --- a/lms/templates/discussion/_search_bar.html +++ b/lms/templates/discussion/_search_bar.html @@ -10,9 +10,9 @@ def base_url_for_search(): <form action="${base_url_for_search()}" method="get" class="discussion-search-form"> % if query_params.get('tags', None): - <input class="search-input" type="text" value="[${tags}]${text}" id="keywords" autocomplete="off"/> + <input class="search-input" type="text" value="[${tags | h}]${text | h}" id="keywords" autocomplete="off"/> % else: - <input class="search-input" type="text" value="${text}" id="keywords" autocomplete="off"/> + <input class="search-input" type="text" value="${text | h}" id="keywords" autocomplete="off"/> % endif <div class="discussion-link discussion-search-link" href="javascript:void(0)">Search posts</div> </form> diff --git a/lms/templates/discussion/_similar_posts.html b/lms/templates/discussion/_similar_posts.html index f68964acec417088f3fbea8bfb60c9d261c73bf7..ef6eedee2ef0105314297201c7b1f8df35448e1d 100644 --- a/lms/templates/discussion/_similar_posts.html +++ b/lms/templates/discussion/_similar_posts.html @@ -3,7 +3,7 @@ <a class="hide-similar-posts" href="javascript:void(0)">Hide</a> <div class="new-post-similar-posts"> % for thread in threads: - <a class="similar-post" href="${thread['permalink']}">${thread['title']}</a> + <a class="similar-post" href="${thread['permalink'] | h}">${thread['title'] | h}</a> % endfor </div> % endif diff --git a/lms/templates/discussion/_single_thread.html b/lms/templates/discussion/_single_thread.html index 395eb72e46738ba7f6e38710f4e7841655477547..bfbf7b069e1d8059634aeb1ec4dbb4a262e110d2 100644 --- a/lms/templates/discussion/_single_thread.html +++ b/lms/templates/discussion/_single_thread.html @@ -1,6 +1,6 @@ <%namespace name="renderer" file="_content_renderer.html"/> -<section class="discussion" _id="${discussion_id}"> +<section class="discussion" _id="${discussion_id | h}"> <a class="discussion-title" href="javascript:void(0)">Discussion</a> <div class="threads"> ${renderer.render_content_with_comments(thread)} diff --git a/lms/templates/discussion/_sort.html b/lms/templates/discussion/_sort.html index 934973a9667a215dc2aa8c1460ab68ad8ecbfc0a..7ba10eab7b3ec351f7beb24f7015fe4339e81a22 100644 --- a/lms/templates/discussion/_sort.html +++ b/lms/templates/discussion/_sort.html @@ -26,7 +26,7 @@ else: return base_url + '?' + urlencode(merge(query_params, {'page': 1, 'sort_key': key, 'sort_order': order})) %> - <a class="discussion-sort-link ${cls}" href="javascript:void(0)" sort-url="${url_for_sort(key, order)}">${title}</a> + <a class="discussion-sort-link ${cls | h}" href="javascript:void(0)" sort-url="${url_for_sort(key, order) | h}">${title}</a> </%def> <div class="discussion-sort local"> diff --git a/lms/templates/discussion/_trending_tags.html b/lms/templates/discussion/_trending_tags.html index fea18c02dc4d571d9f3fe3cbef64fa0a0e563086..509516c2d54791ae4d368e79d804b8a3980ba8cf 100644 --- a/lms/templates/discussion/_trending_tags.html +++ b/lms/templates/discussion/_trending_tags.html @@ -7,7 +7,7 @@ </header> <ol class="discussion-sidebar-tags-list"> % for tag, count in trending_tags: - <li><a href="${helpers.url_for_tags(course.id, tag)}" class="thread-tag">${tag}</a><span class="sidebar-tag-count">×${count}</span></li> + <li><a href="${helpers.url_for_tags(course.id, tag) | h}" class="thread-tag">${tag | h}</a><span class="sidebar-tag-count">×${count | h}</span></li> % endfor <ol> </article> diff --git a/lms/templates/discussion/_user_active_threads.html b/lms/templates/discussion/_user_active_threads.html index ad72ccdd5eecdbe69a7a31e003db5aef7bdd9519..1844009466e15418e6417275f7befca0af28da55 100644 --- a/lms/templates/discussion/_user_active_threads.html +++ b/lms/templates/discussion/_user_active_threads.html @@ -1,6 +1,6 @@ <%namespace name="renderer" file="_content_renderer.html"/> -<section class="discussion user-active-discussion" _id="${user_id}"> +<section class="discussion user-active-discussion" _id="${user_id | h}"> <div class="discussion-non-content local"></div> diff --git a/lms/templates/discussion/_user_profile.html b/lms/templates/discussion/_user_profile.html index a092a97951094ca90ceee0e58eefb996013307f8..8660d8035fa27ee379f564e4d662b2c39ad2c4f7 100644 --- a/lms/templates/discussion/_user_profile.html +++ b/lms/templates/discussion/_user_profile.html @@ -7,12 +7,12 @@ <% role_names = sorted(map(attrgetter('name'), django_user.roles.all())) %> - <div class="sidebar-username">${django_user.username}</div> + <div class="sidebar-username">${django_user.username | h}</div> <div class="sidebar-user-roles"> ${", ".join(role_names)} </div> - <div class="sidebar-threads-count"><span>${profiled_user['threads_count']}</span> ${pluralize('discussion', profiled_user['threads_count'])} started</div> - <div class="sidebar-comments-count"><span>${profiled_user['comments_count']}</span> ${pluralize('comment', profiled_user['comments_count'])}</div> + <div class="sidebar-threads-count"><span>${profiled_user['threads_count'] | h}</span> ${pluralize('discussion', profiled_user['threads_count']) | h} started</div> + <div class="sidebar-comments-count"><span>${profiled_user['comments_count'] | h}</span> ${pluralize('comment', profiled_user['comments_count']) | h}</div> % if check_permissions_by_view(user, course.id, content=None, name='update_moderator_status'): % if "Moderator" in role_names: <a href="javascript:void(0)" class="sidebar-toggle-moderator-button sidebar-revoke-moderator-button">Revoke Moderator provileges</a> diff --git a/lms/templates/discussion/index.html b/lms/templates/discussion/index.html index 43291b6f9be4d6a5e08a8196ab18335d3169103e..1160a14d90e9c827356a0702f6f0a53198bfa6a3 100644 --- a/lms/templates/discussion/index.html +++ b/lms/templates/discussion/index.html @@ -1,7 +1,7 @@ <%inherit file="../main.html" /> <%namespace name='static' file='../static_content.html'/> <%block name="bodyclass">discussion</%block> -<%block name="title"><title>Discussion – ${course.number}</title></%block> +<%block name="title"><title>Discussion – ${course.number | h}</title></%block> <%block name="headextra"> <%static:css group='course'/> diff --git a/lms/templates/discussion/mustache/_content.mustache b/lms/templates/discussion/mustache/_content.mustache index 91330dd0d86943126b722499c461acd44b91c2be..b4f317693117ea9c52f406b7fada1b5a609c69f6 100644 --- a/lms/templates/discussion/mustache/_content.mustache +++ b/lms/templates/discussion/mustache/_content.mustache @@ -7,28 +7,25 @@ </div> <div class="discussion-right-wrapper"> <ul class="admin-actions"> - <li><a href="javascript:void(0)" class="admin-endorse">Endorse</a></li> - <li><a href="javascript:void(0)" class="admin-edit">Edit</a></li> - <li><a href="javascript:void(0)" class="admin-delete">Delete</a></li> + <li style="display: none;"><a href="javascript:void(0)" class="admin-endorse">Endorse</a></li> + <li style="display: none;"><a href="javascript:void(0)" class="admin-edit">Edit</a></li> + <li style="display: none;"><a href="javascript:void(0)" class="admin-delete">Delete</a></li> {{#thread}} - <li><a href="javascript:void(0)" class="admin-openclose">{{close_thread_text}}</a></li> + <li style="display: none;"><a href="javascript:void(0)" class="admin-openclose">{{close_thread_text}}</a></li> {{/thread}} </ul> {{#thread}} <a class="thread-title" name="{{content.id}}" href="javascript:void(0)">{{content.displayed_title}}</a> - <div class="thread-raw-title" style="display: none">{{{content.title}}}</div> {{/thread}} <div class="discussion-content-view"> <a name="{{content.id}}" style="width: 0; height: 0; padding: 0; border: none;"></a> <div class="content-body {{content.type}}-body" id="content-body-{{content.id}}">{{content.displayed_body}}</div> - <div class="content-raw-body {{content.type}}-raw-body" style="display: none">{{{content.body}}}</div> {{#thread}} <div class="thread-tags"> {{#content.tags}} <a class="thread-tag" href="{{##url_for_tags}}{{.}}{{/url_for_tags}}">{{.}}</a> {{/content.tags}} </div> - <div class="thread-raw-tags" style="display: none">{{content.raw_tags}}</div> {{/thread}} <div class="info"> <div class="comment-time"> diff --git a/lms/templates/discussion/user_profile.html b/lms/templates/discussion/user_profile.html index 98d7ddedfd8417bbf1293931d5e613a120e7ec57..4c067db710da398f5c185f00f43970ca05581b33 100644 --- a/lms/templates/discussion/user_profile.html +++ b/lms/templates/discussion/user_profile.html @@ -3,7 +3,7 @@ <%inherit file="../main.html" /> <%namespace name='static' file='../static_content.html'/> <%block name="bodyclass">discussion</%block> -<%block name="title"><title>Discussion – ${course.number}</title></%block> +<%block name="title"><title>Discussion – ${course.number | h}</title></%block> <%block name="headextra"> <%static:css group='course'/> @@ -36,6 +36,6 @@ </section> <script type="text/javascript"> - var $$profiled_user_id = "${user.id | escapejs}"; + var $$profiled_user_id = "${django_user.id | escapejs}"; var $$course_id = "${course.id | escapejs}"; </script> diff --git a/lms/templates/registration/password_reset_email.html b/lms/templates/registration/password_reset_email.html index dba3879281bdc236c98245fd7b544675222ade4e..6d906c84fff44e5d418ff6756c22bd8fcbdb64c8 100644 --- a/lms/templates/registration/password_reset_email.html +++ b/lms/templates/registration/password_reset_email.html @@ -3,7 +3,7 @@ {% trans "Please go to the following page and choose a new password:" %} {% block reset_link %} -https://www.edx.org{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %} +https://{{domain}}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %} {% endblock %} {% trans "Your username, in case you've forgotten:" %} {{ user.username }} diff --git a/lms/templates/seq_module.html b/lms/templates/seq_module.html index 9e26543fbd8f32c19da202bf68ba29545aa5df54..8ff3e096dd6038644f79967952211cab11f4de63 100644 --- a/lms/templates/seq_module.html +++ b/lms/templates/seq_module.html @@ -40,9 +40,6 @@ <script type="text/javascript"> var sequenceNav; $(document).ready(function() { - // console.log($('.sequence-nav')); - sequenceNav = new SequenceNav($('.sequence-nav')); - console.log(sequenceNav); }); </script> diff --git a/lms/templates/wiki/article.html b/lms/templates/wiki/article.html index 0e2a997290e1bcbb8ebd3d1df3301d0c63798d1e..5b12942af53729f27bf7957fa2f41c178b186553 100644 --- a/lms/templates/wiki/article.html +++ b/lms/templates/wiki/article.html @@ -33,11 +33,9 @@ </div> {% if urlpath %} - <!-- <div class="see-children"> <a href="{% url 'wiki:dir' path=urlpath.path %}">See all children</a> </div> - --> {% endif %} </div> </div> diff --git a/lms/urls.py b/lms/urls.py index ca5b01fa2c329238a3726ddfdd85287307bb3cae..86d654eb40d73653f1a354588a7c71e40e016b7f 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -124,6 +124,8 @@ if settings.COURSEWARE_ENABLED: 'courseware.views.course_about', name="about_course"), #Inside the course + url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/$', + 'courseware.views.course_info', name="course_root"), url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$', 'courseware.views.course_info', name="info"), url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/syllabus$', @@ -197,7 +199,6 @@ if settings.QUICKEDIT: if settings.ASKBOT_ENABLED: urlpatterns += (url(r'^%s' % settings.ASKBOT_URL, include('askbot.urls')), \ - url(r'^admin/', include(admin.site.urls)), \ url(r'^settings/', include('askbot.deps.livesettings.urls')), \ url(r'^followit/', include('followit.urls')), \ # url(r'^robots.txt$', include('robots.urls')), @@ -206,8 +207,10 @@ if settings.ASKBOT_ENABLED: if settings.DEBUG: - ## Jasmine - urlpatterns=urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),) + ## Jasmine and admin + urlpatterns=urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')), + url(r'^admin/', include(admin.site.urls)), + ) if settings.MITX_FEATURES.get('AUTH_USE_OPENID'): urlpatterns += ( diff --git a/repo-requirements.txt b/repo-requirements.txt index b93dc2837fb09253a5a6a63e90b44d41a640e517..f98d05ffc9ae71bbc9a5d65b64b9f3933d9aa853 100644 --- a/repo-requirements.txt +++ b/repo-requirements.txt @@ -1,6 +1,6 @@ -e git://github.com/MITx/django-staticfiles.git@6d2504e5c8#egg=django-staticfiles -e git://github.com/MITx/django-pipeline.git#egg=django-pipeline --e git://github.com/benjaoming/django-wiki.git@533c7fc#egg=django-wiki +-e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki -e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev -e common/lib/capa -e common/lib/xmodule