Merge pull request #3986 from edx/jsa/search-spell-correction
Add support for search spell corrections to Forums UX.
This commit is contained in:
@@ -17,6 +17,7 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
|
||||
pattern_handlers = {
|
||||
"/api/v1/users/(?P<user_id>\\d+)/active_threads$": self.do_user_profile,
|
||||
"/api/v1/users/(?P<user_id>\\d+)$": self.do_user,
|
||||
"/api/v1/search/threads$": self.do_search_threads,
|
||||
"/api/v1/threads$": self.do_threads,
|
||||
"/api/v1/threads/(?P<thread_id>\\w+)$": self.do_thread,
|
||||
"/api/v1/comments/(?P<comment_id>\\w+)$": self.do_comment,
|
||||
@@ -86,6 +87,9 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
|
||||
def do_threads(self):
|
||||
self.send_json_response({"collection": [], "page": 1, "num_pages": 1})
|
||||
|
||||
def do_search_threads(self):
|
||||
self.send_json_response(self.server.config.get('search_result', {}))
|
||||
|
||||
def do_comment(self, comment_id):
|
||||
# django_comment_client calls GET comment before doing a DELETE, so that's what this is here to support.
|
||||
if comment_id in self.server.config.get('comments', {}):
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
describe "DiscussionThreadListView", ->
|
||||
|
||||
beforeEach ->
|
||||
|
||||
setFixtures """
|
||||
<script type="text/template" id="thread-list-template">
|
||||
<div class="browse-search">
|
||||
<div class="home"></div>
|
||||
<div class="browse is-open"></div>
|
||||
<div class="search">
|
||||
<form class="post-search">
|
||||
<label class="sr" for="search-discussions">Search</label>
|
||||
<input type="text" id="search-discussions" placeholder="Search all discussions" class="post-search-field">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sort-bar"></div>
|
||||
<div class="search-alerts"></div>
|
||||
<div class="post-list-wrapper">
|
||||
<ul class="post-list"></ul>
|
||||
</div>
|
||||
</script>
|
||||
<script aria-hidden="true" type="text/template" id="search-alert-template">
|
||||
<div class="search-alert" id="search-alert-<%- cid %>">
|
||||
<div class="search-alert-content">
|
||||
<p class="message"><%- message %></p>
|
||||
</div>
|
||||
|
||||
<div class="search-alert-controls">
|
||||
<a href="#" class="dismiss control control-dismiss"><i class="icon icon-remove"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<div class="sidebar"></div>
|
||||
"""
|
||||
window.$$course_id = "TestOrg/TestCourse/TestRun"
|
||||
window.user = new DiscussionUser({id: "567", upvoted_ids: []})
|
||||
|
||||
spyOn($, "ajax")
|
||||
|
||||
@discussion = new Discussion([])
|
||||
@view = new DiscussionThreadListView({collection: @discussion, el: $(".sidebar")})
|
||||
@view.render()
|
||||
|
||||
testAlertMessages = (expectedMessages) ->
|
||||
expect($(".search-alert .message").map( ->
|
||||
$(@).html()
|
||||
).get()).toEqual(expectedMessages)
|
||||
|
||||
it "renders and removes search alerts", ->
|
||||
testAlertMessages []
|
||||
foo = @view.addSearchAlert("foo")
|
||||
testAlertMessages ["foo"]
|
||||
bar = @view.addSearchAlert("bar")
|
||||
testAlertMessages ["foo", "bar"]
|
||||
@view.removeSearchAlert(foo.cid)
|
||||
testAlertMessages ["bar"]
|
||||
@view.removeSearchAlert(bar.cid)
|
||||
testAlertMessages []
|
||||
|
||||
it "clears all search alerts", ->
|
||||
@view.addSearchAlert("foo")
|
||||
@view.addSearchAlert("bar")
|
||||
@view.addSearchAlert("baz")
|
||||
testAlertMessages ["foo", "bar", "baz"]
|
||||
@view.clearSearchAlerts()
|
||||
testAlertMessages []
|
||||
|
||||
testCorrection = (view, correctedText) ->
|
||||
spyOn(view, "addSearchAlert")
|
||||
$.ajax.andCallFake(
|
||||
(params) =>
|
||||
params.success(
|
||||
{discussion_data: [], page: 42, num_pages: 99, corrected_text: correctedText}, 'success'
|
||||
)
|
||||
{always: ->}
|
||||
)
|
||||
view.searchFor("dummy")
|
||||
expect($.ajax).toHaveBeenCalled()
|
||||
|
||||
it "adds a search alert when an alternate term was searched", ->
|
||||
testCorrection(@view, "foo")
|
||||
expect(@view.addSearchAlert).toHaveBeenCalled()
|
||||
expect(@view.addSearchAlert.mostRecentCall.args[0]).toMatch(/foo/)
|
||||
|
||||
it "does not add a search alert when no alternate term was searched", ->
|
||||
testCorrection(@view, null)
|
||||
expect(@view.addSearchAlert).not.toHaveBeenCalled()
|
||||
|
||||
it "clears search alerts when a new search is performed", ->
|
||||
spyOn(@view, "clearSearchAlerts")
|
||||
spyOn(DiscussionUtil, "safeAjax")
|
||||
@view.searchFor("dummy")
|
||||
expect(@view.clearSearchAlerts).toHaveBeenCalled()
|
||||
|
||||
it "clears search alerts when the underlying collection changes", ->
|
||||
spyOn(@view, "clearSearchAlerts")
|
||||
spyOn(@view, "renderThread")
|
||||
@view.collection.trigger("change", new Thread({id: 1}))
|
||||
expect(@view.clearSearchAlerts).toHaveBeenCalled()
|
||||
@@ -36,7 +36,36 @@ if Backbone?
|
||||
@current_search = ""
|
||||
@mode = 'all'
|
||||
|
||||
@searchAlertCollection = new Backbone.Collection([], {model: Backbone.Model})
|
||||
|
||||
@searchAlertCollection.on "add", (searchAlert) =>
|
||||
content = _.template(
|
||||
$("#search-alert-template").html(),
|
||||
{'message': searchAlert.attributes.message, 'cid': searchAlert.cid}
|
||||
)
|
||||
@$(".search-alerts").append(content)
|
||||
@$("#search-alert-" + searchAlert.cid + " a.dismiss").bind "click", searchAlert, (event) =>
|
||||
@removeSearchAlert(event.data.cid)
|
||||
|
||||
@searchAlertCollection.on "remove", (searchAlert) =>
|
||||
@$("#search-alert-" + searchAlert.cid).remove()
|
||||
|
||||
@searchAlertCollection.on "reset", =>
|
||||
@$(".search-alerts").empty()
|
||||
|
||||
addSearchAlert: (message) =>
|
||||
m = new Backbone.Model({"message": message})
|
||||
@searchAlertCollection.add(m)
|
||||
m
|
||||
|
||||
removeSearchAlert: (searchAlert) =>
|
||||
@searchAlertCollection.remove(searchAlert)
|
||||
|
||||
clearSearchAlerts: =>
|
||||
@searchAlertCollection.reset()
|
||||
|
||||
reloadDisplayedCollection: (thread) =>
|
||||
@clearSearchAlerts()
|
||||
thread_id = thread.get('id')
|
||||
content = @renderThread(thread)
|
||||
current_el = @$("a[data-id=#{thread_id}]")
|
||||
@@ -405,6 +434,7 @@ if Backbone?
|
||||
@searchFor(text)
|
||||
|
||||
searchFor: (text, callback, value) ->
|
||||
@clearSearchAlerts()
|
||||
@mode = 'search'
|
||||
@current_search = text
|
||||
url = DiscussionUtil.urlFor("search")
|
||||
@@ -429,6 +459,8 @@ if Backbone?
|
||||
Content.loadContentInfos(response.annotated_content_info)
|
||||
@collection.current_page = response.page
|
||||
@collection.pages = response.num_pages
|
||||
if !_.isNull response.corrected_text
|
||||
@addSearchAlert('Showing results for "' + response.corrected_text + '"');
|
||||
# TODO: Perhaps reload user info so that votes can be updated.
|
||||
# In the future we might not load all of a user's votes at once
|
||||
# so this would probably be necessary anyway
|
||||
|
||||
@@ -39,7 +39,6 @@ class Thread(ContentFactory):
|
||||
pinned = False
|
||||
read = False
|
||||
|
||||
|
||||
class Comment(ContentFactory):
|
||||
thread_id = None
|
||||
depth = 0
|
||||
@@ -52,7 +51,34 @@ class Response(Comment):
|
||||
body = "dummy response body"
|
||||
|
||||
|
||||
class SingleThreadViewFixture(object):
|
||||
class SearchResult(factory.Factory):
|
||||
FACTORY_FOR = dict
|
||||
discussion_data = []
|
||||
annotated_content_info = {}
|
||||
num_pages = 1
|
||||
page = 1
|
||||
corrected_text = None
|
||||
|
||||
|
||||
class DiscussionContentFixture(object):
|
||||
|
||||
def push(self):
|
||||
"""
|
||||
Push the data to the stub comments service.
|
||||
"""
|
||||
requests.put(
|
||||
'{}/set_config'.format(COMMENTS_STUB_URL),
|
||||
data=self.get_config_data()
|
||||
)
|
||||
|
||||
def get_config_data(self):
|
||||
"""
|
||||
return a dictionary with the fixture's data serialized for PUTting to the stub server's config endpoint.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class SingleThreadViewFixture(DiscussionContentFixture):
|
||||
|
||||
def __init__(self, thread):
|
||||
self.thread = thread
|
||||
@@ -76,30 +102,27 @@ class SingleThreadViewFixture(object):
|
||||
return res
|
||||
return dict(_visit(self.thread))
|
||||
|
||||
def push(self):
|
||||
"""
|
||||
Push the data to the stub comments service.
|
||||
"""
|
||||
requests.put(
|
||||
'{}/set_config'.format(COMMENTS_STUB_URL),
|
||||
data={
|
||||
"threads": json.dumps({self.thread['id']: self.thread}),
|
||||
"comments": json.dumps(self._get_comment_map())
|
||||
}
|
||||
)
|
||||
def get_config_data(self):
|
||||
return {
|
||||
"threads": json.dumps({self.thread['id']: self.thread}),
|
||||
"comments": json.dumps(self._get_comment_map())
|
||||
}
|
||||
|
||||
class UserProfileViewFixture(object):
|
||||
|
||||
class UserProfileViewFixture(DiscussionContentFixture):
|
||||
|
||||
def __init__(self, threads):
|
||||
self.threads = threads
|
||||
|
||||
def push(self):
|
||||
"""
|
||||
Push the data to the stub comments service.
|
||||
"""
|
||||
requests.put(
|
||||
'{}/set_config'.format(COMMENTS_STUB_URL),
|
||||
data={
|
||||
"active_threads": json.dumps(self.threads),
|
||||
}
|
||||
)
|
||||
def get_config_data(self):
|
||||
return {"active_threads": json.dumps(self.threads)}
|
||||
|
||||
|
||||
class SearchResultFixture(DiscussionContentFixture):
|
||||
|
||||
def __init__(self, result):
|
||||
self.result = result
|
||||
|
||||
def get_config_data(self):
|
||||
return {"search_result": json.dumps(self.result)}
|
||||
|
||||
|
||||
@@ -4,7 +4,13 @@ from bok_choy.promise import EmptyPromise
|
||||
from .course_page import CoursePage
|
||||
|
||||
|
||||
class DiscussionThreadPage(PageObject):
|
||||
class DiscussionPageMixin(object):
|
||||
|
||||
def is_ajax_finished(self):
|
||||
return self.browser.execute_script("return jQuery.active") == 0
|
||||
|
||||
|
||||
class DiscussionThreadPage(PageObject, DiscussionPageMixin):
|
||||
url = None
|
||||
|
||||
def __init__(self, browser, thread_selector):
|
||||
@@ -53,11 +59,8 @@ class DiscussionThreadPage(PageObject):
|
||||
"""Clicks the load more responses button and waits for responses to load"""
|
||||
self._find_within(".load-response-button").click()
|
||||
|
||||
def _is_ajax_finished():
|
||||
return self.browser.execute_script("return jQuery.active") == 0
|
||||
|
||||
EmptyPromise(
|
||||
_is_ajax_finished,
|
||||
self.is_ajax_finished,
|
||||
"Loading more Responses"
|
||||
).fulfill()
|
||||
|
||||
@@ -304,3 +307,44 @@ class DiscussionUserProfilePage(CoursePage):
|
||||
def click_on_page(self, page_number):
|
||||
self._click_pager_with_text(unicode(page_number), page_number)
|
||||
|
||||
|
||||
class DiscussionTabHomePage(CoursePage, DiscussionPageMixin):
|
||||
|
||||
ALERT_SELECTOR = ".discussion-body .sidebar .search-alert"
|
||||
|
||||
def __init__(self, browser, course_id):
|
||||
super(DiscussionTabHomePage, self).__init__(browser, course_id)
|
||||
self.url_path = "discussion/forum/"
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css=".discussion-body section.home-header").present
|
||||
|
||||
def perform_search(self):
|
||||
self.q(css=".discussion-body .sidebar .search").first.click()
|
||||
EmptyPromise(
|
||||
lambda: self.q(css=".discussion-body .sidebar .search.is-open").present,
|
||||
"waiting for search input to be available"
|
||||
).fulfill()
|
||||
self.q(css="#search-discussions").fill("dummy" + chr(10))
|
||||
EmptyPromise(
|
||||
self.is_ajax_finished,
|
||||
"waiting for server to return result"
|
||||
).fulfill()
|
||||
|
||||
def get_search_alert_messages(self):
|
||||
return self.q(css=self.ALERT_SELECTOR + " .message").text
|
||||
|
||||
def dismiss_alert_message(self, text):
|
||||
"""
|
||||
dismiss any search alert message containing the specified text.
|
||||
"""
|
||||
def _match_messages(text):
|
||||
return self.q(css=".search-alert").filter(lambda elem: text in elem.text)
|
||||
|
||||
for alert_id in _match_messages(text).attrs("id"):
|
||||
self.q(css="{}#{} a.dismiss".format(self.ALERT_SELECTOR, alert_id)).click()
|
||||
EmptyPromise(
|
||||
lambda: _match_messages(text).results == [],
|
||||
"waiting for dismissed alerts to disappear"
|
||||
).fulfill()
|
||||
|
||||
|
||||
@@ -11,10 +11,19 @@ from ..pages.lms.discussion import (
|
||||
DiscussionTabSingleThreadPage,
|
||||
InlineDiscussionPage,
|
||||
InlineDiscussionThreadPage,
|
||||
DiscussionUserProfilePage
|
||||
DiscussionUserProfilePage,
|
||||
DiscussionTabHomePage
|
||||
)
|
||||
from ..fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
from ..fixtures.discussion import SingleThreadViewFixture, UserProfileViewFixture, Thread, Response, Comment
|
||||
from ..fixtures.discussion import (
|
||||
SingleThreadViewFixture,
|
||||
UserProfileViewFixture,
|
||||
SearchResultFixture,
|
||||
Thread,
|
||||
Response,
|
||||
Comment,
|
||||
SearchResult
|
||||
)
|
||||
|
||||
|
||||
class DiscussionResponsePaginationTestMixin(object):
|
||||
@@ -405,3 +414,47 @@ class DiscussionUserProfileTest(UniqueCourseTest):
|
||||
|
||||
def test_151_threads(self):
|
||||
self.check_pages(151)
|
||||
|
||||
class DiscussionSearchAlertTest(UniqueCourseTest):
|
||||
"""
|
||||
Tests for spawning and dismissing alerts related to user search actions and their results.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(DiscussionSearchAlertTest, self).setUp()
|
||||
CourseFixture(**self.course_info).install()
|
||||
AutoAuthPage(self.browser, course_id=self.course_id).visit()
|
||||
self.page = DiscussionTabHomePage(self.browser, self.course_id)
|
||||
self.page.visit()
|
||||
|
||||
def setup_corrected_text(self, text):
|
||||
SearchResultFixture(SearchResult(corrected_text=text)).push()
|
||||
|
||||
def check_search_alert_messages(self, expected):
|
||||
actual = self.page.get_search_alert_messages()
|
||||
self.assertTrue(all(map(lambda msg, sub: msg.find(sub) >= 0, actual, expected)))
|
||||
|
||||
def test_no_rewrite(self):
|
||||
self.setup_corrected_text(None)
|
||||
self.page.perform_search()
|
||||
self.check_search_alert_messages([])
|
||||
|
||||
def test_rewrite_dismiss(self):
|
||||
self.setup_corrected_text("foo")
|
||||
self.page.perform_search()
|
||||
self.check_search_alert_messages(["foo"])
|
||||
self.page.dismiss_alert_message("foo")
|
||||
self.check_search_alert_messages([])
|
||||
|
||||
def test_new_search(self):
|
||||
self.setup_corrected_text("foo")
|
||||
self.page.perform_search()
|
||||
self.check_search_alert_messages(["foo"])
|
||||
|
||||
self.setup_corrected_text("bar")
|
||||
self.page.perform_search()
|
||||
self.check_search_alert_messages(["bar"])
|
||||
|
||||
self.setup_corrected_text(None)
|
||||
self.page.perform_search()
|
||||
self.check_search_alert_messages([])
|
||||
|
||||
@@ -1933,19 +1933,31 @@ After a user executes a text search in the navigation sidebar of the Discussion
|
||||
|
||||
**Event Source**: Server
|
||||
|
||||
**History**: Added 16 May 2014.
|
||||
**History**: Added 16 May 2014. The ``corrected_text`` field was added on June 5 2014.
|
||||
|
||||
``event`` **Fields**:
|
||||
|
||||
+---------------------+---------------+----------------------------------------------------------------------------------------------------+
|
||||
| Field | Type | Details |
|
||||
+=====================+===============+====================================================================================================+
|
||||
| ``query`` | string | The text entered into the search box by the user. |
|
||||
+---------------------+---------------+----------------------------------------------------------------------------------------------------+
|
||||
| ``page`` | integer | Results are returned in sets of 20 per page. Identifies the page of results requested by the user. |
|
||||
+---------------------+---------------+----------------------------------------------------------------------------------------------------+
|
||||
| ``total_results`` | integer | The total number of results matching the query. |
|
||||
+---------------------+---------------+----------------------------------------------------------------------------------------------------+
|
||||
.. list-table::
|
||||
:widths: 15 15 60
|
||||
:header-rows: 1
|
||||
|
||||
* - Field
|
||||
- Type
|
||||
- Details
|
||||
* - ``query``
|
||||
- string
|
||||
- The text entered into the search box by the user.
|
||||
* - ``page``
|
||||
- integer
|
||||
- Results are returned in sets of 20 per page. Identifies the page of results requested by the user.
|
||||
* - ``total_results``
|
||||
- integer
|
||||
- The total number of results matching the query.
|
||||
* - ``corrected_text``
|
||||
- string
|
||||
- A re-spelling of the query, suggested by the search engine, which was automatically substituted for the original
|
||||
one. This happens only when there are no results for the original query, but the index contains matches for
|
||||
a similar term or phrase. Otherwise, this field is null.
|
||||
|
||||
.. _Instructor_Event_Types:
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
|
||||
'sort_order', 'text',
|
||||
'commentable_ids', 'flagged'])))
|
||||
|
||||
threads, page, num_pages = cc.Thread.search(query_params)
|
||||
threads, page, num_pages, corrected_text = cc.Thread.search(query_params)
|
||||
|
||||
#now add the group name if the thread has a group id
|
||||
for thread in threads:
|
||||
@@ -103,6 +103,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
|
||||
|
||||
query_params['page'] = page
|
||||
query_params['num_pages'] = num_pages
|
||||
query_params['corrected_text'] = corrected_text
|
||||
|
||||
return threads, query_params
|
||||
|
||||
@@ -198,6 +199,7 @@ def forum_form_discussion(request, course_id):
|
||||
'annotated_content_info': annotated_content_info,
|
||||
'num_pages': query_params['num_pages'],
|
||||
'page': query_params['page'],
|
||||
'corrected_text': query_params['corrected_text'],
|
||||
})
|
||||
else:
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"):
|
||||
|
||||
@@ -63,25 +63,28 @@ class Thread(models.Model):
|
||||
course_id = query_params['course_id']
|
||||
requested_page = params['page']
|
||||
total_results = response.get('total_results')
|
||||
corrected_text = response.get('corrected_text')
|
||||
# Record search result metric to allow search quality analysis.
|
||||
# course_id is already included in the context for the event tracker
|
||||
tracker.emit(
|
||||
'edx.forum.searched',
|
||||
{
|
||||
'query': search_query,
|
||||
'corrected_text': corrected_text,
|
||||
'page': requested_page,
|
||||
'total_results': total_results,
|
||||
}
|
||||
)
|
||||
log.info(
|
||||
'forum_text_search query="{search_query}" course_id={course_id} page={requested_page} total_results={total_results}'.format(
|
||||
'forum_text_search query="{search_query}" corrected_text="{corrected_text}" course_id={course_id} page={requested_page} total_results={total_results}'.format(
|
||||
search_query=search_query,
|
||||
corrected_text=corrected_text,
|
||||
course_id=course_id,
|
||||
requested_page=requested_page,
|
||||
total_results=total_results
|
||||
)
|
||||
)
|
||||
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
|
||||
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1), response.get('corrected_text')
|
||||
|
||||
@classmethod
|
||||
def url_for_threads(cls, params={}):
|
||||
|
||||
@@ -1,480 +0,0 @@
|
||||
@mixin blue-button {
|
||||
display: block;
|
||||
height: 33px;
|
||||
margin: 12px;
|
||||
padding: 0 15px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #4697c1;
|
||||
background: -webkit-linear-gradient(top, #6dccf1, #38a8e5);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 30px;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 0 rgba(0, 0, 0, .4);
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4) inset, 0 1px 1px rgba(0, 0, 0, .15);
|
||||
|
||||
&:hover {
|
||||
border-color: #297095;
|
||||
background: -webkit-linear-gradient(top, #4fbbe4, #2090d0);
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-body {
|
||||
|
||||
.vote-btn {
|
||||
float: right;
|
||||
display: block;
|
||||
height: 27px;
|
||||
padding: 0 8px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #b2b2b2;
|
||||
background: -webkit-linear-gradient(top, #fff 35%, #ebebeb);
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, .15);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 25px;
|
||||
color: #333;
|
||||
|
||||
.plus-icon {
|
||||
float: left;
|
||||
margin-right: 6px;
|
||||
font-size: 18px;
|
||||
color: #17b429;
|
||||
}
|
||||
|
||||
&.is-cast {
|
||||
border-color: #379a42;
|
||||
background: -webkit-linear-gradient(top, #50cc5e, #3db84b);
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 0 rgba(0, 0, 0, .3);
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, .4) inset, 0 1px 2px rgba(0, 0, 0, .2);
|
||||
|
||||
.plus-icon {
|
||||
color: #336a39;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, .4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.new-post-btn {
|
||||
@include blue-button;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.new-post-icon {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 16px;
|
||||
height: 17px;
|
||||
margin: 7px 7px 0 0;
|
||||
font-size: 16px;
|
||||
padding-right: $baseline/2;
|
||||
vertical-align: middle;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.post-search {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.post-search-field {
|
||||
width: 280px;
|
||||
height: 30px;
|
||||
padding: 0 15px 0 30px;
|
||||
margin-top: 14px;
|
||||
border: 1px solid #acacac;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .1) inset, 0 1px 0 rgba(255, 255, 255, .5);
|
||||
background: url(../images/search-icon.png) no-repeat 8px center #fff;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
line-height: 30px;
|
||||
color: #333;
|
||||
outline: 0;
|
||||
-webkit-transition: border-color .1s;
|
||||
|
||||
&:focus {
|
||||
border-color: #4697c1;
|
||||
}
|
||||
}
|
||||
|
||||
h1, ul, li, a, ol {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
ul, li {
|
||||
list-style-type: none;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #009fe2;
|
||||
}
|
||||
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #aaa;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.sidebar {
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
width: 27.7%;
|
||||
background: #f6f6f6;
|
||||
border-radius: 3px 0 0 3px;
|
||||
border-right: 1px solid #bcbcbc;
|
||||
.post-list {
|
||||
background-color: #ddd;
|
||||
|
||||
li:last-child a {
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
margin-bottom: 1px;
|
||||
background: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 34px;
|
||||
color: #333;
|
||||
|
||||
&.read .title {
|
||||
font-weight: 400;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
&.followed:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: url(../images/following-flag.png) no-repeat;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: -webkit-linear-gradient(top, #96e0fd, #61c7fc);
|
||||
border-color: #4697c1;
|
||||
box-shadow: 0 1px 0 #4697c1, 0 -1px 0 #4697c1;
|
||||
|
||||
.title {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.votes-count,
|
||||
.comments-count {
|
||||
background: -webkit-linear-gradient(top, #3994c7, #4da7d3);
|
||||
color: #fff;
|
||||
|
||||
&:after {
|
||||
color: #4da7d3;
|
||||
}
|
||||
}
|
||||
|
||||
&.followed:after {
|
||||
background-position: 0 -12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 70%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.votes-count,
|
||||
.comments-count {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 32px;
|
||||
height: 16px;
|
||||
margin-top: 9px;
|
||||
border-radius: 2px;
|
||||
background: -webkit-linear-gradient(top, #d4d4d4, #dfdfdf);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
color: #767676;
|
||||
}
|
||||
|
||||
.comments-count {
|
||||
position: relative;
|
||||
margin-left: 4px;
|
||||
|
||||
&:after {
|
||||
content: '◥';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
right: 3px;
|
||||
font-size: 6px;
|
||||
color: #dfdfdf;
|
||||
}
|
||||
|
||||
&.new {
|
||||
background: -webkit-linear-gradient(top, #84d7fe, #99e0fe);
|
||||
color: #333;
|
||||
|
||||
&:after {
|
||||
color: #99e0fe;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.board-drop-btn {
|
||||
display: block;
|
||||
height: 60px;
|
||||
border-bottom: 1px solid #a3a3a3;
|
||||
border-radius: 3px 0 0 0;
|
||||
background: -webkit-linear-gradient(top, #ebebeb, #d9d9d9);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 58px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, .8);
|
||||
}
|
||||
|
||||
.sort-bar {
|
||||
height: 27px;
|
||||
border-bottom: 1px solid #a3a3a3;
|
||||
background: -webkit-linear-gradient(top, #cdcdcd, #b6b6b6);
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, .4) inset;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
float: right;
|
||||
height: 27px;
|
||||
margin-right: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
line-height: 23px;
|
||||
color: #333;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
|
||||
|
||||
.sort-label {
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.global-discussion-actions {
|
||||
height: 60px;
|
||||
background: -webkit-linear-gradient(top, #ebebeb, #d9d9d9);
|
||||
border-radius: 0 3px 0 0;
|
||||
border-bottom: 1px solid #bcbcbc;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.discussion-article {
|
||||
position: relative;
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
width: 72.3%;
|
||||
padding: 40px;
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.posted-details {
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
p + p {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.dogear {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -1px;
|
||||
width: 52px;
|
||||
height: 51px;
|
||||
background: url(../images/follow-dog-ear.png) 0 -51px no-repeat;
|
||||
|
||||
&.is-followed {
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-post header,
|
||||
.responses li header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.responses {
|
||||
margin-top: 40px;
|
||||
|
||||
> li {
|
||||
margin: 0 -10px;
|
||||
padding: 30px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #b2b2b2;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
.posted-by {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.endorse-btn {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
margin-right: 10px;
|
||||
border-radius: 27px;
|
||||
border: 1px solid #a0a0a0;
|
||||
background: -webkit-linear-gradient(top, #fff 35%, #ebebeb);
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, .1);
|
||||
|
||||
.check-icon {
|
||||
display: block;
|
||||
width: 13px;
|
||||
height: 12px;
|
||||
margin: 8px auto;
|
||||
background: url(../images/endorse-icon.png) no-repeat;
|
||||
}
|
||||
|
||||
&.is-endorsed {
|
||||
border: 1px solid #4697c1;
|
||||
background: -webkit-linear-gradient(top, #6dccf1, #38a8e5);
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, .4) inset;
|
||||
|
||||
.check-icon {
|
||||
background-position: 0 -12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comments {
|
||||
margin-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
|
||||
li {
|
||||
background: #f6f6f6;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
padding: 10px 20px;
|
||||
|
||||
.posted-details {
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-form {
|
||||
padding: 8px 20px;
|
||||
}
|
||||
|
||||
.comment-form-input {
|
||||
width: 100%;
|
||||
height: 31px;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #b2b2b2;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .1) inset;
|
||||
-webkit-transition: border-color .1s;
|
||||
outline: 0;
|
||||
|
||||
&:focus {
|
||||
border-color: #4697c1;
|
||||
}
|
||||
}
|
||||
|
||||
.moderator-actions {
|
||||
margin-top: 20px;
|
||||
@include clearfix;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
height: 26px;
|
||||
padding: 0 12px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #b2b2b2;
|
||||
background: -webkit-linear-gradient(top, #fff 35%, #ebebeb);
|
||||
font-size: 13px;
|
||||
line-height: 24px;
|
||||
color: #737373;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.delete-icon {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin: 8px 4px 0 0;
|
||||
background: url(../images/moderator-delete-icon.png) no-repeat;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin: 7px 4px 0 0;
|
||||
background: url(../images/moderator-edit-icon.png) no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,8 @@
|
||||
@import 'views/shoppingcart';
|
||||
|
||||
// applications
|
||||
@import 'discussion';
|
||||
@import 'discussion/discussion';
|
||||
@import 'discussion/discussion-developer';
|
||||
@import 'news';
|
||||
|
||||
// temp - shame and developer
|
||||
|
||||
78
lms/static/sass/discussion/_discussion-developer.scss
Normal file
78
lms/static/sass/discussion/_discussion-developer.scss
Normal file
@@ -0,0 +1,78 @@
|
||||
// discussion: - developer
|
||||
// ====================
|
||||
|
||||
// NOTES:
|
||||
// * use this area for any developer-needed or created styling that needs to be refactored into patterns or visually
|
||||
// polished. Please list any template/view that reference your styles when definining them (example below):
|
||||
|
||||
// --------------------
|
||||
// Views: Error
|
||||
// --------------------
|
||||
// .crazy-new-feature {
|
||||
// background: transparent;
|
||||
// }
|
||||
|
||||
// --------------------
|
||||
// Views: forum_form_discussion / single_thread
|
||||
// provisional styling for "search alerts" (messages boxes that appear in the sidebar below the search
|
||||
// input field with notices pertaining to the search result).
|
||||
// --------------------
|
||||
body.discussion {
|
||||
|
||||
.sidebar {
|
||||
|
||||
// wrapper for multiple alerts
|
||||
.search-alerts {
|
||||
|
||||
}
|
||||
|
||||
// a single alert, which can be independently displayed / dismissed
|
||||
.search-alert {
|
||||
@include transition(none);
|
||||
padding: ($baseline/2) 11px ($baseline/2) 18px;
|
||||
background-color: $black;
|
||||
}
|
||||
|
||||
.search-alert-content, .search-alert-controls {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// alert content
|
||||
.search-alert-content {
|
||||
width: 70%;
|
||||
|
||||
// alert copy
|
||||
.message {
|
||||
@include font-size(12);
|
||||
@extend %t-weight5;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
// links to jump to users/content in alerts
|
||||
.link-jump {
|
||||
@include transition(none);
|
||||
@extend %t-weight5;
|
||||
}
|
||||
}
|
||||
|
||||
// alert controls
|
||||
.search-alert-controls {
|
||||
width: 28%;
|
||||
text-align: right;
|
||||
|
||||
.control {
|
||||
@include font-size(14);
|
||||
@include transition(none);
|
||||
@extend %t-weight5;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
|
||||
// reseting poorly globally scoped hover/focus state for this control
|
||||
&:hover, &:focus {
|
||||
color: $link-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@
|
||||
</select>
|
||||
%endif
|
||||
</div>
|
||||
<div class="search-alerts"></div>
|
||||
<div class="post-list-wrapper">
|
||||
<ul class="post-list">
|
||||
</ul>
|
||||
|
||||
@@ -170,14 +170,14 @@
|
||||
<i class="icon icon-remove"></i><span class="sr">${_("Delete Comment")}</span></div>
|
||||
<div class="discussion-edit-comment action-edit" data-tooltip="${_('Edit') | h}" role="button" tabindex="0">
|
||||
<i class="icon icon-pencil"></i><span class="sr">${_("Edit")}</span></div>
|
||||
<%
|
||||
<%
|
||||
js_block = u"""
|
||||
interpolate(
|
||||
'{}',
|
||||
{{'time_ago': '<span class=\"timeago\" title=\"' + created_at + '\">' + created_at + '</span>'}},
|
||||
true
|
||||
)""".format(
|
||||
## Translators: 'timeago' is a placeholder for a fuzzy, relative timestamp (see: https://github.com/rmm5t/jquery-timeago)
|
||||
## Translators: 'timeago' is a placeholder for a fuzzy, relative timestamp (see: https://github.com/rmm5t/jquery-timeago)
|
||||
escapejs(_('-posted %(time_ago)s by'))
|
||||
)
|
||||
%>
|
||||
@@ -207,9 +207,9 @@
|
||||
<script aria-hidden="true" type="text/template" id="thread-list-item-template">
|
||||
<a href="${'<%- id %>'}" data-id="${'<%- id %>'}">
|
||||
<span class="title">${"<%- title %>"}</span>
|
||||
<%
|
||||
<%
|
||||
js_block = u"""
|
||||
var fmt;
|
||||
var fmt;
|
||||
var data = {{
|
||||
'span_sr_open': '<span class=\"sr\">',
|
||||
'span_close': '</span>',
|
||||
@@ -232,7 +232,7 @@
|
||||
<span class="comments-count">
|
||||
${'<%'}${js_block}${'%>'}
|
||||
</span>
|
||||
<%
|
||||
<%
|
||||
js_block = u"""
|
||||
interpolate(
|
||||
'{}',
|
||||
@@ -255,7 +255,7 @@
|
||||
<h1 class="home-title">${course.display_name_with_default}</h1>
|
||||
% endif
|
||||
</section>
|
||||
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_DISCUSSION_HOME_PANEL'):
|
||||
<span class="label label-settings">${_("HOW TO USE EDX DISCUSSIONS")}</span>
|
||||
<table class="home-helpgrid">
|
||||
@@ -307,3 +307,15 @@
|
||||
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script aria-hidden="true" type="text/template" id="search-alert-template">
|
||||
<div class="search-alert" id="search-alert-${'<%- cid %>'}">
|
||||
<div class="search-alert-content">
|
||||
<p class="message">${'<%- message %>'}</p>
|
||||
</div>
|
||||
|
||||
<div class="search-alert-controls">
|
||||
<a href="#" class="dismiss control control-dismiss"><i class="icon icon-remove"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user