Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cms/envs/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,5 +227,5 @@

FEATURES['ENABLE_EDXNOTES'] = True
EDXNOTES_INTERFACE = {
'url': 'http://localhost:8042/',
'url': 'http://localhost:8042/api/v1',
}
13 changes: 12 additions & 1 deletion common/djangoapps/terrain/stubs/edxnotes.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ def _search(self):
user = self.get_params.get("user", None)
usage_id = self.get_params.get("usage_id", None)
course_id = self.get_params.get("course_id", None)
text = self.get_params.get("text", None)

if user is None:
self.respond(400, "Bad Request")
Expand All @@ -209,6 +210,8 @@ def _search(self):
results = self.server.filter_by_course_id(results, course_id)
if usage_id is not None:
results = self.server.filter_by_usage_id(results, usage_id)
if text:
results = self.server.search(results, text)
self.respond(content={
"total": len(results),
"rows": results,
Expand Down Expand Up @@ -247,7 +250,9 @@ def get_notes(self):
"""
Returns a list of all notes.
"""
return deepcopy(self.notes)
notes = deepcopy(self.notes)
notes.reverse()
return notes

def add_notes(self, notes):
"""
Expand Down Expand Up @@ -318,3 +323,9 @@ def filter_by(self, data, field_name, value):
Filters provided `data(list)` by the `field_name(str)` with `value`.
"""
return [note for note in data if note.get(field_name) == value]

def search(self, data, query):
"""
Search the `query(str)` text in the provided `data(list)`.
"""
return [note for note in data if unicode(query).strip() in note.get("text")]
108 changes: 108 additions & 0 deletions common/static/js/vendor/jquery.highlight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* jQuery Highlight plugin
*
* Based on highlight v3 by Johann Burkard
* http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html
*
* Code a little bit refactored and cleaned (in my humble opinion).
* Most important changes:
* - has an option to highlight only entire words (wordsOnly - false by default),
* - has an option to be case sensitive (caseSensitive - false by default)
* - highlight element tag and class names can be specified in options
*
* Usage:
* // wrap every occurrance of text 'lorem' in content
* // with <span class='highlight'> (default options)
* $('#content').highlight('lorem');
*
* // search for and highlight more terms at once
* // so you can save some time on traversing DOM
* $('#content').highlight(['lorem', 'ipsum']);
* $('#content').highlight('lorem ipsum');
*
* // search only for entire word 'lorem'
* $('#content').highlight('lorem', { wordsOnly: true });
*
* // don't ignore case during search of term 'lorem'
* $('#content').highlight('lorem', { caseSensitive: true });
*
* // wrap every occurrance of term 'ipsum' in content
* // with <em class='important'>
* $('#content').highlight('ipsum', { element: 'em', className: 'important' });
*
* // remove default highlight
* $('#content').unhighlight();
*
* // remove custom highlight
* $('#content').unhighlight({ element: 'em', className: 'important' });
*
*
* Copyright (c) 2009 Bartek Szopka
*
* Licensed under MIT license.
*
*/

jQuery.extend({
highlight: function (node, re, nodeName, className) {
if (node.nodeType === 3) {
var match = node.data.match(re);
if (match) {
var highlight = document.createElement(nodeName || 'span');
highlight.className = className || 'highlight';
var wordNode = node.splitText(match.index);
wordNode.splitText(match[0].length);
var wordClone = wordNode.cloneNode(true);
highlight.appendChild(wordClone);
wordNode.parentNode.replaceChild(highlight, wordNode);
return 1; //skip added node in parent
}
} else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children
!/(script|style)/i.test(node.tagName) && // ignore script and style nodes
!(node.tagName === nodeName.toUpperCase() && node.className === className)) { // skip if already highlighted
for (var i = 0; i < node.childNodes.length; i++) {
i += jQuery.highlight(node.childNodes[i], re, nodeName, className);
}
}
return 0;
}
});

jQuery.fn.unhighlight = function (options) {
var settings = { className: 'highlight', element: 'span' };
jQuery.extend(settings, options);

return this.find(settings.element + "." + settings.className).each(function () {
var parent = this.parentNode;
parent.replaceChild(this.firstChild, this);
parent.normalize();
}).end();
};

jQuery.fn.highlight = function (words, options) {
var settings = { className: 'highlight', element: 'span', caseSensitive: false, wordsOnly: false };
jQuery.extend(settings, options);

if (words.constructor === String) {
words = [words];
}
words = jQuery.grep(words, function(word, i){
return word != '';
});
words = jQuery.map(words, function(word, i) {
return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
});
if (words.length == 0) { return this; };

var flag = settings.caseSensitive ? "" : "i";
var pattern = "(" + words.join("|") + ")";
if (settings.wordsOnly) {
pattern = "\\b" + pattern + "\\b";
}
var re = new RegExp(pattern, flag);

return this.each(function () {
jQuery.highlight(this, re, settings.element, settings.className);
});
};

2 changes: 1 addition & 1 deletion common/templates/edxnotes_wrapper.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</div>
<script type="text/javascript">
(function (require) {
require(['js/edxnotes/views/notes'], function (Notes) {
require(['js/edxnotes/views/notes_factory'], function (Notes) {
var element = document.getElementById('edx-notes-wrapper-${uid}');
Notes.factory(element, ${json.dumps(params)});
});
Expand Down
158 changes: 137 additions & 21 deletions common/test/acceptance/pages/lms/edxnotes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from bok_choy.page_object import PageObject
from bok_choy.page_object import PageObject, PageLoadError, unguarded
from bok_choy.promise import BrokenPromise
from .course_page import CoursePage
from ...tests.helpers import disable_animations
from selenium.webdriver.common.action_chains import ActionChains
Expand Down Expand Up @@ -26,51 +27,166 @@ def _bounded_selector(self, selector):
)


class EdxNotesPageView(PageObject):
"""
Base class for EdxNotes views: Recent Activity, Course Structure, Search Results.
"""
url = None
BODY_SELECTOR = ".edx-notes-page-items-list"
TAB_SELECTOR = ".tab-item"
CHILD_SELECTOR = ".edx-notes-page-item"

@unguarded
def visit(self):
"""
Open the page containing this page object in the browser.

Raises:
PageLoadError: The page did not load successfully.

Returns:
PageObject
"""
self.q(css=self.TAB_SELECTOR).first.click()
try:
return self.wait_for_page()
except (BrokenPromise):
raise PageLoadError("Timed out waiting to load page '{!r}'".format(self))

def is_browser_on_page(self):
return all([
self.q(css="{}".format(self.BODY_SELECTOR)).present,
self.q(css="{}.is-active".format(self.TAB_SELECTOR)).present,
not self.q(css=".ui-loading").visible,
])

@property
def is_closable(self):
"""
Indicates if tab is closable or not.
"""
return self.q(css="{} .btn-close".format(self.TAB_SELECTOR)).present

def close(self):
"""
Closes the tab.
"""
self.q(css="{} .btn-close".format(self.TAB_SELECTOR)).first.click()

@property
def children(self):
"""
Returns all notes on the page.
"""
children = self.q(css=self.CHILD_SELECTOR)
return [EdxNotesPageItem(self.browser, child.get_attribute("id")) for child in children]


class RecentActivityView(EdxNotesPageView):
"""
Helper class for Recent Activity view.
"""
BODY_SELECTOR = "#edx-notes-page-recent-activity"
TAB_SELECTOR = ".tab-item.tab-recent-activity"


class SearchResultsView(EdxNotesPageView):
"""
Helper class for Search Results view.
"""
BODY_SELECTOR = "#edx-notes-page-search-results"
TAB_SELECTOR = ".tab-item.tab-search-results"


class EdxNotesPage(CoursePage):
"""
EdxNotes page.
"""
url_path = "edxnotes"
MAPPING = {
"recent": RecentActivityView,
"search": SearchResultsView,
}

def __init__(self, *args, **kwargs):
super(EdxNotesPage, self).__init__(*args, **kwargs)
self.current_view = EdxNotesPageView(self.browser, "edx-notes-page-recent-activity")
self.current_view = self.MAPPING["recent"](self.browser)

def is_browser_on_page(self):
return self.q(css=".edx-notes-page-wrapper").present

def switch_to_tab(self, tab_name):
"""
Switches to the appropriate tab `tab_name(str)`.
"""
self.current_view = self.MAPPING[tab_name](self.browser)
self.current_view.visit()

def close_tab(self, tab_name):
"""
Closes the tab `tab_name(str)`.
"""
self.current_view.close()
self.current_view = self.MAPPING["recent"](self.browser)

def search(self, text):
"""
Runs search with `text(str)` query.
"""
self.q(css=".search-box #search-field").first.fill(text)
self.q(css='.search-box button').first.click()
# Frontend will automatically switch to Search results tab when search
# is running, so the view also needs to be changed.
self.current_view = self.MAPPING["search"](self.browser)

@property
def tabs(self):
"""
Returns all tabs on the page.
"""
tabs = self.q(css=".tabs .tab-item-name")
if tabs:
return tabs.text
else:
return None

@property
def is_error_visible(self):
"""
Indicates whether error message is visible or not.
"""
return self.q(css=".inline-error").visible

@property
def error_text(self):
"""
Returns error message.
"""
element = self.q(css=".inline-error").first
if element and self.is_error_visible:
return element.text[0]
else:
return None

@property
def children(self):
"""
Returns all notes on the page.
"""
return self.current_view.children

@property
def no_content_text(self):
"""
Returns no content message.
"""
element = self.q(css=".no-content").first
if element:
return element.text[0]
else:
return None


class EdxNotesPageView(NoteChild):
"""
Base class for EdxNotes views: Recent Activity, Course Structure.
"""
BODY_SELECTOR = ".edx-notes-page-items-list"
CHILD_SELECTOR = ".edx-notes-page-item"

def is_browser_on_page(self):
return all([
self.q(css="{}#{}".format(self.BODY_SELECTOR, self.item_id)).present,
not self.q(css=".ui-loading").visible,
])

@property
def children(self):
children = self.q(css=self._bounded_selector(self.CHILD_SELECTOR))
return [EdxNotesPageItem(self.browser, child.get_attribute("id")) for child in children]


class EdxNotesPageItem(NoteChild):
"""
Helper class that works with note items on Note page of the course.
Expand Down
Loading