diff --git a/search/tests/test_search_view.py b/search/tests/test_search_view.py index e5622935..35b46b42 100644 --- a/search/tests/test_search_view.py +++ b/search/tests/test_search_view.py @@ -36,7 +36,7 @@ def test_terms_with_spaces(self): self.resource.terms.add(term) resp = self.client.get(reverse("repositories", args=(self.repo.slug,))) self.assertContains(resp, "easy") - self.assertContains(resp, "ancòra") + self.assertContains(resp, "anc\\u00f2ra") self.assertContains(resp, "very difficult") def test_db_hits(self): diff --git a/ui/jstests/test-facets.js b/ui/jstests/test-facets.js deleted file mode 100644 index 8f5effe7..00000000 --- a/ui/jstests/test-facets.js +++ /dev/null @@ -1,59 +0,0 @@ -define(['QUnit', 'jquery', 'facets', 'icheck'], function(QUnit, $, facets) { - 'use strict'; - var testQueryString = 'selected_facets=foo:bar'; - QUnit.test( - 'Test constructFacetQuery with given input element', - function(assert) { - var testData = $( - '' - )[0]; - var facetQuery = facets.constructFacetQuery(testData); - assert.equal(testQueryString, facetQuery); - } - ); - QUnit.test( - 'Test checkFacets with given input element', - function(assert) { - $( - '' - ).appendTo($('body')); - $('#foobar').iCheck(); - facets.checkFacets(testQueryString); - assert.ok($('#foobar').prop('checked')); - $('#foobar').remove(); - } - ); - QUnit.test( - 'Test setupFacets is working', - function(assert) { - $( - '
' - ).appendTo($('body')); - - $( - '' - ).appendTo($('body')); - // Make mock window for location changing - var testWindow = { - location: '' - }; - testWindow.window = testWindow; - facets.setupFacets(testWindow); - assert.notEqual($('#foobar').find('ins')); - // Check and assert - $('#foobar').iCheck('check'); - assert.equal( - testWindow.window.location, - 'foobarselected_facets=undefined:undefined' - ); - $('#foobar').iCheck('uncheck'); - assert.equal( - testWindow.window.location, - 'foobar' - ); - $('#facet-panel').remove(); - $('#foobar').remove(); - } - ); -}); diff --git a/ui/jstests/test_facets.jsx b/ui/jstests/test_facets.jsx new file mode 100644 index 00000000..2bcfa28c --- /dev/null +++ b/ui/jstests/test_facets.jsx @@ -0,0 +1,287 @@ +define(['QUnit', 'jquery', 'listing_resources', 'react', + 'test_utils', 'lodash'], + function (QUnit, $, ListingResources, React, TestUtils, _) { + 'use strict'; + + var facetCounts = { + "1": { + "facet": {"key": "1", "label": "a"}, + "values": [ + {"count": 2, "key": "1", "label": "t1"}, + {"count": 2, "key": "2", "label": "t2"} + ] + }, + "2": { + "facet": { + "key": "2", + "label": "run" + }, + "values": [] + }, + "3": { + "facet": { + "key": "3", + "label": "s" + }, + "values": [] + }, + "4": { + "facet": { + "key": "4", + "label": "allowmulti" + }, + "values": [ + {"count": 4, "key": "6", "label": "term1"}, + {"count": 4, "key": "7", "label": "term2"} + ] + }, + "5": { + "facet": { + "key": "5", + "label": "disallowmultiple" + }, + "values": [ + {"count": 2, "key": "8", "label": "term3"}, + {"count": 2, "key": "9", "label": "term4"} + ] + }, + "6": { + "facet": { + "key": "6", + "label": "one" + }, + "values": [] + }, + "7": { + "facet": { + "key": "7", + "label": "infinite" + }, + "values": [] + }, + "8": { + "facet": { + "key": "8", + "label": "free" + }, + "values": [ + {"count": 1, "key": "12", "label": "tagging"}, + {"count": 1, "key": "13", "label": "tagging with spaces"} + ] + }, + "course": { + "facet": { + "key": "course", + "label": "Course" + }, + "values": [ + {"count": 47, "key": "0.001", "label": "0.001"}, + {"count": 43, "key": "MIT.latex2edx", "label": "MIT.latex2edx"} + ] + }, + "resource_type": { + "facet": { + "key": "resource_type", + "label": "Item Type" + }, + "values": [ + {"count": 35, "key": "problem", "label": "problem"}, + {"count": 30, "key": "vertical", "label": "vertical"}, + {"count": 12, "key": "sequential", "label": "sequential"}, + {"count": 6, "key": "chapter", "label": "chapter"}, + {"count": 3, "key": "html", "label": "html"}, + {"count": 2, "key": "video", "label": "video"}, + {"count": 2, "key": "course", "label": "course"} + ] + }, + "run": { + "facet": { + "key": "run", + "label": "Run" + }, + "values": [ + {"count": 47, "key": "2015_Summer", "label": "2015_Summer"}, + {"count": 43, "key": "2014_Spring", "label": "2014_Spring"} + ] + } + }; + + var emptyFacetCounts = { + "run": { + "facet": {"key": "run", "label": "Run"}, + "values": [] + }, + "1": {"facet": {"key": "1", "label": "a"}, "values": []}, + "course": {"facet": {"key": "course", "label": "Course"}, "values": []}, + "3": {"facet": {"key": "3", "label": "s"}, "values": []}, + "2": {"facet": {"key": "2", "label": "run"}, "values": []}, + "5": {"facet": {"key": "5", "label": "disallowmultiple"}, "values": []}, + "4": {"facet": {"key": "4", "label": "allowmulti"}, "values": []}, + "7": {"facet": {"key": "7", "label": "infinite"}, "values": []}, + "6": {"facet": {"key": "6", "label": "one"}, "values": []}, + "8": {"facet": {"key": "8", "label": "free"}, "values": []}, + "resource_type": { + "facet": {"key": "resource_type", "label": "Item Type"}, + "values": [] + } + }; + + var Facets = ListingResources.Facets; + + QUnit.module('Test listing facets', { + beforeEach: function () { + TestUtils.setup(); + }, + afterEach: function() { + TestUtils.cleanup(); + } + }); + + QUnit.test('Assert that facets render correctly', function(assert) { + var done = assert.async(); + + var selectedFacets = {}; + var updateFacets = function(facetId, id, checked) { + if (!_.has(selectedFacets, facetId)) { + selectedFacets[facetId] = {}; + } + selectedFacets[facetId][id] = checked; + }; + + var afterMount = function (component) { + var $node = $(React.findDOMNode(component)); + + // Assert correct ordering of facet counts + var titles = _.map($node.find(".panel-title"), function(child) { + return $(child).text(); + }); + assert.deepEqual([ + "Course", "Run", "Item Type", + "a", "allowmulti", "disallowmultiple", "free" + ], titles); + + var terms = _.map($node.find("li label"), function(child) { + return $(child).text(); + }); + assert.deepEqual([ + "0.001", "MIT.latex2edx", "2015_Summer", "2014_Spring", "problem", + "vertical", "sequential", "chapter", "html", "video", "course", + "t1", "t2", "term1", "term2", "term3", "term4", "tagging", + "tagging with spaces" + ], terms); + + done(); + }; + + React.addons.TestUtils.renderIntoDocument(); + }); + + QUnit.test('Assert that an empty facet count displays nothing', + function(assert) { + var done = assert.async(); + + var selectedFacets = {}; + var updateFacets = function(facetId, id, checked) { + if (!_.has(selectedFacets, facetId)) { + selectedFacets[facetId] = {}; + } + selectedFacets[facetId][id] = checked; + }; + + var afterMount = function (component) { + var $node = $(React.findDOMNode(component)); + + // Assert correct ordering of facet counts + var titles = _.map($node.find(".panel-title"), function(child) { + return $(child).text(); + }); + assert.deepEqual([], titles); + + var terms = _.map($node.find("li label"), function(child) { + return $(child).text(); + }); + assert.deepEqual([], terms); + + done(); + }; + + React.addons.TestUtils.renderIntoDocument(); + } + ); + + QUnit.test('Assert that facets can be clicked', function(assert) { + var done = assert.async(); + + var selectedFacets = {}; + var updateFacets = function(facetId, id, checked) { + if (!_.has(selectedFacets, facetId)) { + selectedFacets[facetId] = {}; + } + selectedFacets[facetId][id] = checked; + }; + + var afterMountCheck = function (component) { + var $node = $(React.findDOMNode(component)); + + // iCheckbox uses ins elements to contain their checkbox. + var $firstCheckbox = $node.find("ins").first(); + assert.deepEqual(selectedFacets, {}); + $firstCheckbox.click(); + assert.deepEqual(selectedFacets, { + course: { + "0.001": true + } + }); + + var afterMountUncheck = function(component) { + var $node = $(React.findDOMNode(component)); + + var $firstCheckbox = $node.find("ins").first(); + assert.deepEqual(selectedFacets, { + course: { + "0.001": true + } + }); + $firstCheckbox.click(); + assert.deepEqual(selectedFacets, { + course: { + "0.001": false + } + }); + + done(); + }; + + // Unselect that checkbox + React.addons.TestUtils.renderIntoDocument(); + }; + + React.addons.TestUtils.renderIntoDocument(); + }); + + } +); diff --git a/ui/jstests/test_listing_resources.jsx b/ui/jstests/test_listing_resources.jsx index 3b2ed78f..8db59ccc 100644 --- a/ui/jstests/test_listing_resources.jsx +++ b/ui/jstests/test_listing_resources.jsx @@ -11,6 +11,27 @@ define(['QUnit', 'jquery', 'listing_resources', 'react', var waitForAjax = TestUtils.waitForAjax; + // This is mostly filler, we test facets in more detail in test_facets.jsx + var emptyFacetCounts = { + "run": { + "facet": {"key": "run", "label": "Run"}, + "values": [] + }, + "1": {"facet": {"key": "1", "label": "a"}, "values": []}, + "course": {"facet": {"key": "course", "label": "Course"}, "values": []}, + "3": {"facet": {"key": "3", "label": "s"}, "values": []}, + "2": {"facet": {"key": "2", "label": "run"}, "values": []}, + "5": {"facet": {"key": "5", "label": "disallowmultiple"}, "values": []}, + "4": {"facet": {"key": "4", "label": "allowmulti"}, "values": []}, + "7": {"facet": {"key": "7", "label": "infinite"}, "values": []}, + "6": {"facet": {"key": "6", "label": "one"}, "values": []}, + "8": {"facet": {"key": "8", "label": "free"}, "values": []}, + "resource_type": { + "facet": {"key": "resource_type", "label": "Item Type"}, + "values": [] + } + }; + var sampleResources = [{ "run": "2015_Summer", "description": "Description A", @@ -56,7 +77,8 @@ define(['QUnit', 'jquery', 'listing_resources', 'react', sortingOptions: sortingOptions, loggedInUsername: "user", qsPrefix: "qs", - imageDir: "images" + imageDir: "images", + facetCounts: emptyFacetCounts }; // NOTE: these tests will show 404 for /images/*.png due to these diff --git a/ui/static/ui/js/facets.js b/ui/static/ui/js/facets.js deleted file mode 100644 index 2cf2c94a..00000000 --- a/ui/static/ui/js/facets.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Facet handling javascript module - * @module facets - */ -define('facets', ['jquery', 'bootstrap', 'icheck'], function($) { - 'use strict'; - /** - Takes a dom element and returns the string - that makes up a facet querystring parameter - @memberOf module:facets - @param {Element} facetCheckbox - `` checkbox with data about - it's facet. - @returns {string} Returns the querystring parameter for the facet. - */ - var constructFacetQuery = function(facetCheckbox) { - // Takes a dom element and returns the string - // that makes up a facet querystring parameter - var facet = $(facetCheckbox).data('facet-name'); - var facetValue = $(facetCheckbox).data('facet-value'); - return 'selected_facets=' + facet + ':' + facetValue; - }; - /** - Checks all facets for ones that turned on, and sets their state - to checked. - @memberOf module:facets - @param {string} currentQueryset - Current pages querystring - */ - var checkFacets = function(currentQueryset) { - // Check if a facet is enabled and check the box - $('input.icheck-11').each(function() { - if (currentQueryset.indexOf(constructFacetQuery(this)) > -1) { - $(this).iCheck('check'); - } - }); - }; - /** - Add icheck and click handlers for all the checkboxes - @memberOf module:facets - @param {string} currentQueryset - Current pages querystring - */ - var setupFacets = function(window) { - var currentQueryset = $('#facet-panel').data('current-queryset'); - $('input.icheck-11').iCheck({ - checkboxClass: 'icheckbox_square-blue', - radioClass: 'iradio_square-blue' - }); - // Check facets before adding event handlers - checkFacets(currentQueryset); - $('input.icheck-11').on('ifChecked', function() { - $('#progress-modal').modal(); - window.location = currentQueryset + constructFacetQuery(this); - }); - $('input.icheck-11').on('ifUnchecked', function() { - $('#progress-modal').modal(); - window.location = currentQueryset.replace( - constructFacetQuery(this), '' - ); - }); - }; - return { - constructFacetQuery: constructFacetQuery, - checkFacets: checkFacets, - setupFacets: setupFacets - }; -}); diff --git a/ui/static/ui/js/listing.js b/ui/static/ui/js/listing.js index dcfd7b0a..8c2a74d9 100644 --- a/ui/static/ui/js/listing.js +++ b/ui/static/ui/js/listing.js @@ -1,8 +1,8 @@ define('listing', - ['jquery', 'lodash', 'manage_taxonomies', 'facets', + ['jquery', 'lodash', 'manage_taxonomies', 'learning_resources', 'static_assets', 'utils', 'lr_exports', 'listing_resources', 'bootstrap', 'icheck', 'csrf'], - function ($, _, ManageTaxonomies, Facets, LearningResources, StaticAssets, + function ($, _, ManageTaxonomies, LearningResources, StaticAssets, Utils, Exports, ListingResources) { 'use strict'; @@ -10,7 +10,6 @@ define('listing', var repoSlug = listingOptions.repoSlug; var loggedInUsername = listingOptions.loggedInUsername; - Facets.setupFacets(window); var EMAIL_EXTENSION = '@mit.edu'; function formatGroupName(string) { @@ -232,13 +231,55 @@ define('listing', repoSlug, resourceId, $("#tab-3")[0]); }; + var facetIsSelected = function(facetId, valueId) { + var facetQuery = "selected_facets=" + facetId + "_exact:" + valueId; + if (location.search === undefined) { + return false; + } + return location.search.indexOf(facetQuery) !== -1; + }; + + var updateFacets = function(facetId, valueId, selected) { + $("#progress-modal").modal(); + + var currentQueryset = '?'; + if (location.search !== undefined && location.search !== '') { + currentQueryset = location.search; + } + + var facetQuery = "selected_facets=" + facetId + "_exact:" + valueId; + if (!selected) { + window.location = currentQueryset.replace(facetQuery, ''); + } else { + if (currentQueryset === "?") { + window.location = currentQueryset + facetQuery; + } else { + window.location = currentQueryset + "&" + facetQuery; + } + } + }; + + var selectedFacets = {}; + _.each(listingOptions.facetCounts, function(counts) { + var facetId = counts.facet.key; + selectedFacets[facetId] = {}; + + _.each(counts.values, function(value) { + var valueId = value.key; + if (facetIsSelected(facetId, valueId)) { + selectedFacets[facetId][valueId] = true; + } + }); + }); + /** * Rerender listing resources * @returns {ReactComponent} */ renderListingResources = function() { return ListingResources.loader(listingOptions, - container, openExportsPanel, openResourcePanel); + container, openExportsPanel, openResourcePanel, + updateFacets, selectedFacets); }; // Listing resources React object. We need this variable to allow diff --git a/ui/static/ui/js/listing_resources.jsx b/ui/static/ui/js/listing_resources.jsx index b3898f2e..84d4518f 100644 --- a/ui/static/ui/js/listing_resources.jsx +++ b/ui/static/ui/js/listing_resources.jsx @@ -1,7 +1,138 @@ -define('listing_resources', ['react', 'jquery', 'lodash'], - function (React, $, _) { +define('listing_resources', ['react', 'jquery', 'lodash', 'utils'], + function (React, $, _, Utils) { 'use strict'; + var ICheckbox = Utils.ICheckbox; + + var getImageFile = function(resourceType) { + if (resourceType === 'chapter') { + return 'ic-book.png'; + } else if (resourceType === 'sequential') { + return 'ic-sequential.png'; + } else if (resourceType === 'vertical') { + return 'ic-vertical.png'; + } else if (resourceType === 'problem') { + return 'ic-pieces.png'; + } else if (resourceType === 'video') { + return 'ic-video.png'; + } else { + return 'ic-code.png'; + } + }; + + var Facet = React.createClass({ + render: function() { + var icon = null; + if (this.props.facetId === 'resource_type') { + var src = this.props.imageDir + "/" + getImageFile(this.props.id); + icon = + + ; + } + + return
  • + + {icon} + + + {this.props.count} + +
  • ; + }, + handleChange: function(e) { + this.props.updateFacets( + this.props.facetId, this.props.id, e.target.checked); + } + }); + + var FacetGroup = React.createClass({ + render: function() { + var thiz = this; + var facets = _.map(this.props.values, function(value) { + var selected = thiz.props.selectedFacets[value.key]; + + return ; + }); + + var collapseId = "collapse-" + this.props.id; + + return
    + +
    +
    +
      + {facets} +
    +
    +
    +
    ; + } + }); + + var Facets = React.createClass({ + render: function() { + var thiz = this; + var facets = []; + + var makeFacetGroup = function(values) { + if (!values.values.length) { + return null; + } + var selectedFacets = {}; + if (thiz.props.selectedFacets[values.facet.key] !== undefined) { + selectedFacets = thiz.props.selectedFacets[values.facet.key]; + } + + return ; + }; + + _.each(["course", "run", "resource_type"], function(key) { + facets.push(makeFacetGroup(thiz.props.facetCounts[key])); + }); + + _.each(this.props.facetCounts, function(values) { + var key = values.facet.key; + if (key !== "course" && key !== "run" && key !== "resource_type") { + facets.push(makeFacetGroup(values)); + } + }); + + return
    {facets}
    ; + } + }); + var SortingDropdown = React.createClass({ render: function() { var thiz = this; @@ -65,22 +196,6 @@ define('listing_resources', ['react', 'jquery', 'lodash'], } }); - var getImageFile = function(resourceType) { - if (resourceType === 'chapter') { - return 'ic-book.png'; - } else if (resourceType === 'sequential') { - return 'ic-sequential.png'; - } else if (resourceType === 'vertical') { - return 'ic-vertical.png'; - } else if (resourceType === 'problem') { - return 'ic-pieces.png'; - } else if (resourceType === 'video') { - return 'ic-video.png'; - } else { - return 'ic-code.png'; - } - }; - var ListingResource = React.createClass({ render: function() { // Select image based on type. @@ -266,13 +381,33 @@ define('listing_resources', ['react', 'jquery', 'lodash'], } }); + var ListingPage = React.createClass({ + render: function() { + return
    +
    +
    + +
    +
    +
    + +
    +
    ; + } + }); + return { loader: function(listingOptions, container, openExportsPanel, - openResourcePanel) { + openResourcePanel, updateFacets, selectedFacets) { return React.render( - , container ); @@ -282,7 +417,11 @@ define('listing_resources', ['react', 'jquery', 'lodash'], ExportLink: ExportLink, SortingDropdown: SortingDropdown, ListingResource: ListingResource, - Listing: Listing + Listing: Listing, + ListingPage: ListingPage, + FacetGroup: FacetGroup, + Facet: Facet, + Facets: Facets }; } ); diff --git a/ui/static/ui/js/require_config.js b/ui/static/ui/js/require_config.js index 4d9cbd3f..373ed647 100644 --- a/ui/static/ui/js/require_config.js +++ b/ui/static/ui/js/require_config.js @@ -10,7 +10,6 @@ var REQUIRE_PATHS = { lodash: "lodash/lodash", select2: "select2/dist/js/select2.full", csrf: "../ui/js/csrf", - facets: "../ui/js/facets", listing: "../ui/js/listing", manage_taxonomies: "../ui/js/manage_taxonomies.jsx?noext", learning_resources: "../ui/js/learning_resources.jsx?noext", diff --git a/ui/templates/includes/facet_panel.html b/ui/templates/includes/facet_panel.html deleted file mode 100644 index b46a538e..00000000 --- a/ui/templates/includes/facet_panel.html +++ /dev/null @@ -1,141 +0,0 @@ -{% load static %} -{% if facets.fields.course %} - -
    -
    -

    - - Course - -

    -
    -
    -
    -
      - {% for course in facets.fields.course %} -
    • - - - {{ course.1 }} -
    • - {% endfor %} -
    - {% endif %} -
    -
    -
    -{% if facets.fields.run %} -
    -
    -

    - - Run - -

    -
    -
    -
    -
      - {% for run in facets.fields.run %} -
    • - - - {{ run.1 }} -
    • - {% endfor %} -
    -
    -
    -
    -{% endif %} -{% if facets.fields.resource_type %} -
    - -
    -
    -
      - {% for resource_type in facets.fields.resource_type %} -
    • - - - {# Select image based on type #} - {% if resource_type.0 == 'chapter' %} - - {% elif resource_type.0 == 'sequential' %} - - {% elif resource_type.0 == 'vertical' %} - - {% elif resource_type.0 == 'problem' %} - - {% elif resource_type.0 == 'video' %} - - {% else %} - - {% endif %} - - - {{ resource_type.1 }} -
    • - {% endfor %} -
    -
    -
    -
    -{% for vocab, terms in vocabularies.items %} -{% if terms %} -
    - -
    -
    -
      - {% for term in terms %} -
    • - - - {{ term.2 }} -
    • - {% endfor %} -
    -
    -
    -
    -{% endif %} -{% endfor %} -{% endif %} diff --git a/ui/templates/repository.html b/ui/templates/repository.html index 1a4d6e9c..86010df4 100644 --- a/ui/templates/repository.html +++ b/ui/templates/repository.html @@ -22,14 +22,8 @@ -
    -
    - {% include "includes/facet_panel.html" %} -
    -
    -
    -
    -
    +
    +
    @@ -96,7 +90,8 @@ sortingOptions: {{ sorting_options_json|safe }}, loggedInUsername: "{{ request.user.username|escapejs }}", qsPrefix: "{{ qs_prefix|escapejs }}", - imageDir: "{% static "ui/images"|escapejs %}" + imageDir: "{% static "ui/images"|escapejs %}", + facetCounts: {{ facet_counts_json|safe }} }; $(function() { Listing.loader(listingOptions, $("#listing")[0]); diff --git a/ui/views.py b/ui/views.py index 58898b0a..b1500844 100644 --- a/ui/views.py +++ b/ui/views.py @@ -210,6 +210,7 @@ def dispatch(self, *args, **kwargs): """Override for the purpose of having decorators in views.py.""" super(RepositoryView, self).dispatch(*args, **kwargs) + # pylint: disable=too-many-locals def extra_context(self): """Add to the context.""" context = super(RepositoryView, self).extra_context() @@ -264,15 +265,53 @@ def make_dict(result): self.sortby) } + vocabularies = get_vocabularies(context["facets"]) + facet_counts_map = dict(vocabularies) + if "fields" in context["facets"]: + for key, label in ( + ("course", "Course"), + ("run", "Run"), + ("resource_type", "Item Type") + ): + if key in context["facets"]["fields"]: + values = [] + for pair in context["facets"]["fields"][key]: + # This is id, name, count where id == name + # for run, course, resource_type. + values.append((pair[0], pair[0], pair[1])) + + facet_counts_map[(key, label)] = values + + def reformat(key, values): + """ + Convert tuples into dictionaries. + """ + return { + "facet": {"key": str(key[0]), "label": key[1]}, + "values": [ + { + "label": value_label, + "key": str(value_key), + "count": count + } for value_key, value_label, count in values + ] + } + + facet_counts = { + str(key[0]): reformat(key, values) + for key, values in facet_counts_map.items() + } + context.update({ "repo": self.repo, "perms_on_cur_repo": get_perms(self.request.user, self.repo), - "vocabularies": get_vocabularies(context["facets"]), + "vocabularies": vocabularies, "qs_prefix": qs_prefix, "sorting_options": sorting_options, "sorting_options_json": json.dumps(sorting_options), "resources_json": json.dumps(resources), - "exports_json": json.dumps(exports) + "exports_json": json.dumps(exports), + "facet_counts_json": json.dumps(facet_counts) }) return context