diff options
23 files changed, 1247 insertions, 525 deletions
diff --git a/askbot/forms.py b/askbot/forms.py index ed47e20e..55d864c3 100644 --- a/askbot/forms.py +++ b/askbot/forms.py @@ -1156,11 +1156,7 @@ class RevisionForm(forms.Form): """ Lists revisions of a Question or Answer """ - revision = forms.ChoiceField( - widget=forms.Select( - attrs={'style': 'width:520px'} - ) - ) + revision = forms.ChoiceField(widget=forms.Select()) def __init__(self, post, latest_revision, *args, **kwargs): super(RevisionForm, self).__init__(*args, **kwargs) diff --git a/askbot/media/js/live_search.js b/askbot/media/js/live_search.js index f7d89c2a..5dffb677 100644 --- a/askbot/media/js/live_search.js +++ b/askbot/media/js/live_search.js @@ -1,3 +1,185 @@ +var SearchDropMenu = function() { + WrappedElement.call(this); + this._data = undefined; + this._selectedItemIndex = 0; + this._askButtonEnabled = true; +} +inherits(SearchDropMenu, WrappedElement); + +SearchDropMenu.prototype.setData = function(data) { + this._data = data; +}; + +SearchDropMenu.prototype.setAskHandler = function(handler) { + this._askHandler = handler; +}; + +SearchDropMenu.prototype.setAskButtonEnabled = function(isEnabled) { + this._askButtonEnabled = isEnabled; +}; + +/** + * assumes that data is already set + */ +SearchDropMenu.prototype.render = function() { + var list = this._resultsList; + list.empty(); + var me = this; + $.each(this._data, function(idx, item) { + var listItem = me.makeElement('li'); + var link = me.makeElement('a'); + link.attr('href', item['url']); + link.html(item['title']); + listItem.append(link); + list.append(listItem); + }); + if (this._data.length === 0) { + list.addClass('empty'); + } else { + list.removeClass('empty'); + } +}; + +/** + * @param {number} idx position of item starting from 1 for the topmost + * Selects item inentified by position. + * Scrolls the list to make top of the item visible. + */ +SearchDropMenu.prototype.selectItem = function(idx) { + //idx is 1-based index + this._selectedItemIndex = idx; + var list = this._resultsList; + list.find('li').removeClass('selected'); + var item = this.getItem(idx); + if (item && idx > 0) { + item.addClass('selected'); + var itemTopY = item.position().top;//relative to visible area + var curScrollTop = list.scrollTop(); + + /* if item is clipped on top, scroll down */ + if (itemTopY < 0) { + list.scrollTop(curScrollTop + itemTopY); + return; + } + + var listHeight = list.outerHeight(); + /* pixels above the lower border of the list */ + var itemPeepHeight = listHeight - itemTopY; + /* pixels below the lower border */ + var itemSinkHeight = item.outerHeight() - itemPeepHeight; + if (itemSinkHeight > 0) { + list.scrollTop(curScrollTop + itemSinkHeight); + } + } + +}; + +SearchDropMenu.prototype.getItem = function(idx) { + return $(this._resultsList.find('li')[idx - 1]); +}; + +SearchDropMenu.prototype.getItemCount = function() { + return this._resultsList.find('li').length; +}; + +SearchDropMenu.prototype.getSelectedItemIndex = function() { + return this._selectedItemIndex; +}; + +SearchDropMenu.prototype.navigateToItem = function(idx) { + var item = this.getItem(idx); + if (item) { + window.location.href = item.find('a').attr('href'); + } +}; + +SearchDropMenu.prototype.makeKeyHandler = function() { + var me = this; + return function(e) { + var keyCode = getKeyCode(e); + if (keyCode === 27) {//escape + me.hide(); + return false; + } + if (keyCode !== 38 && keyCode !== 40 && keyCode !== 13) { + return; + } + var itemCount = me.getItemCount(); + if (itemCount > 0) { + var curItem = me.getSelectedItemIndex(); + if (keyCode === 38) {//upArrow + if (curItem > 0) { + curItem = curItem - 1; + } + } else if (keyCode === 40) {//downArrow + if (curItem < itemCount) { + curItem = curItem + 1; + } + } else if (keyCode === 13) {//enter + me.navigateToItem(curItem); + return false; + } + me.selectItem(curItem); + return false + } + }; +}; + +SearchDropMenu.prototype.createDom = function() { + this._element = this.makeElement('div'); + this._element.addClass('search-drop-menu'); + this._element.hide(); + + this._resultsList = this.makeElement('ul'); + this._element.append(this._resultsList); + this._element.addClass('empty'); + + //add ask button, @todo: make into separate class? + var footer = this.makeElement('div'); + this._element.append(footer); + this._footer = footer; + + if (this._askButtonEnabled) { + footer.addClass('footer'); + var button = this.makeElement('button'); + button.addClass('submit'); + button.html(gettext('Ask Your Question')) + footer.append(button); + var handler = this._askHandler; + setupButtonEventHandlers(button, handler); + } + + $(document).keydown(this.makeKeyHandler()); +}; + +SearchDropMenu.prototype.isOpen = function() { + return this._element.is(':visible'); +}; + +SearchDropMenu.prototype.show = function() { + var searchBar = this._element.prev(); + var searchBarHeight = searchBar.outerHeight(); + var topOffset = searchBar.offset().top + searchBarHeight; + this._element.show();//show so that size calcs work + var footerHeight = this._footer.outerHeight(); + var windowHeight = $(window).height(); + this._resultsList.css( + 'max-height', + windowHeight - topOffset - footerHeight - 40 //what is this number? + ); +}; + +SearchDropMenu.prototype.hide = function() { + this._element.hide(); +}; + +SearchDropMenu.prototype.reset = function() { + this._data = undefined; + this._resultsList.empty(); + this._selectedItemIndex = 0; + this._element.hide(); +}; + var TagWarningBox = function(){ WrappedElement.call(this); this._tags = []; @@ -6,9 +188,8 @@ inherits(TagWarningBox, WrappedElement); TagWarningBox.prototype.createDom = function(){ this._element = this.makeElement('div'); - this._element - .css('display', 'block') - .css('margin', '0 0 13px 2px'); + this._element.css('display', 'block'); + this._element.css('margin', '0 0 13px 2px'); this._element.addClass('non-existing-tags'); this._warning = this.makeElement('p'); this._element.append(this._warning); @@ -51,355 +232,596 @@ TagWarningBox.prototype.showWarning = function(){ this._warning.show(); }; -var liveSearch = function(query_string) { - var query = $('input#keywords'); - var query_val = function () {return $.trim(query.val());}; - var prev_text = query_val(); - var running = false; - var q_list_sel = 'question-list';//id of question listing div - var search_url = askbot['urls']['questions']; - var x_button = $('input[name=reset_query]'); - var tag_warning_box = new TagWarningBox(); +/** + * @constructor + * tool tip to be shown on top of the search input + */ +var InputToolTip = function() { + WrappedElement.call(this); +}; +inherits(InputToolTip, WrappedElement); - //the tag search input is optional in askbot - $('#ab-tag-search').parent().before( - tag_warning_box.getElement() - ); +InputToolTip.prototype.show = function() { + this._element.removeClass('dimmed'); + this._element.show(); +}; - var run_tag_search = function(){ - var search_tags = $('#ab-tag-search').val().split(/\s+/); - if (search_tags.length === 0) { - return; - } - /** @todo: the questions/ might need translation... */ - query_string = '/questions/scope:all/sort:activity-desc/page:1/' - $.each(search_tags, function(idx, tag) { - query_string = QSutils.add_search_tag(query_string, search_tags); - }); - var url = search_url + query_string; - $.ajax({ - url: url, - dataType: 'json', - success: function(data, text_status, xhr){ - render_result(data, text_status, xhr); - $('#ab-tag-search').val(''); - }, - }); - updateHistory(url); - }; +InputToolTip.prototype.hide = function() { + this._element.removeClass('dimmed'); + this._element.hide(); +}; - var activate_tag_search_input = function(){ - //the autocomplete is set up in tag_selector.js - var button = $('#ab-tag-search-add'); - if (button.length === 0){//may be absent - return; - } - var ac = new AutoCompleter({ - url: askbot['urls']['get_tag_list'], - preloadData: true, - minChars: 1, - useCache: true, - matchInside: true, - maxCacheLength: 100, - maxItemsToShow: 20, - onItemSelect: run_tag_search, - delay: 10 - }); - ac.decorate($('#ab-tag-search')); - setupButtonEventHandlers(button, run_tag_search); - //var tag_search_input = $('#ab-tag-search'); - //tag_search_input.keydown( - // makeKeyHandler(13, run_tag_search) - //); - }; +InputToolTip.prototype.dim = function() { + this._element.addClass('dimmed'); +}; - var render_tag_warning = function(tag_list){ - if ( !tag_list ) { - return; - } - tag_warning_box.clear(); - $.each(tag_list, function(idx, tag_name){ - tag_warning_box.addTag(tag_name); - }); - tag_warning_box.showWarning(); - }; +InputToolTip.prototype.setClickHandler = function(handler) { + this._clickHandler = handler; +}; - var refresh_x_button = function(){ - if(query_val().length > 0){ - if (query.hasClass('searchInput')){ - query.attr('class', 'searchInputCancelable'); - x_button.show(); - } - } else { - x_button.hide(); - query.attr('class', 'searchInput'); - } - }; +InputToolTip.prototype.createDom = function() { + var element = this.makeElement('div'); + this._element = element; - var restart_query = function() { - sortMethod = 'activity-desc'; - query.val(''); - refresh_x_button(); - send_query(); - }; + element.html(gettext('search or ask your question')); + element.addClass('input-tool-tip'); - var eval_query = function(){ - cur_query = query_val(); - if (cur_query !== prev_text && running === false){ - if (cur_query.length >= minSearchWordLength){ - send_query(cur_query); - } else if (cur_query.length === 0){ - restart_query(); - } + var handler = this._clickHandler; + var me = this; + element.click(function() { + handler(); + me.dim(); + return false; + }); + $(document).click(function() { + if (element.css('display') === 'block') { + element.removeClass('dimmed'); } - }; + }); +}; + + +/** + * @constructor + * provides full text search functionality + * which re-draws contents of the main page + * in response to the search query + */ +var FullTextSearch = function() { + WrappedElement.call(this); + this._running = false; + this._baseUrl = askbot['urls']['questions']; + this._q_list_sel = 'question-list';//id of question listing div + /** @todo: the questions/ needs translation... */ + this._searchUrl = '/scope:all/sort:activity-desc/page:1/' + this._askButtonEnabled = true; +}; +inherits(FullTextSearch, WrappedElement); + +/** + * @param {{boolean=}} optional, if given then function is setter + * otherwise it is a getter + * isRunning returns `true` when search is running + */ +FullTextSearch.prototype.isRunning = function(val) { + if (val === undefined) { + return this._running; + } else { + this._running = val; + } +}; - var update_query_string = function(query_text){ - if(query_text === undefined) { // handle missing parameter - query_text = query_val(); - } - query_string = QSutils.patch_query_string( - query_string, - 'query:' + encodeURIComponent(query_text), - query_text === '' // remove if empty - ); - return query_text; - }; +FullTextSearch.prototype.setAskButtonEnabled = function(isEnabled) { + this._askButtonEnabled = isEnabled; +} - var send_query = function(query_text){ - running = true; - if(!prev_text && query_text && showSortByRelevance) { - // If there was no query but there is some query now - and we support relevance search - then switch to it */ - query_string = QSutils.patch_query_string(query_string, 'sort:relevance-desc'); - } - prev_text = update_query_string(query_text); - query_string = QSutils.patch_query_string(query_string, 'page:1'); /* if something has changed, then reset the page no. */ - var url = search_url + query_string; - $.ajax({ - url: url, - dataType: 'json', - success: render_result, - complete: function(){ - running = false; - eval_query(); - }, - cache: false - }); - updateHistory(url); - }; +/** + * @param {{string}} url for the page displaying search results + */ +FullTextSearch.prototype.setSearchUrl = function(url) { + this._searchUrl = url; +}; - var updateHistory = function(url) { - var context = { state:1, rand:Math.random() }; - History.pushState( context, "Questions", url ); - setTimeout(function (){ - /* HACK: For some weird reson, sometimes something overrides the above pushState so we re-aplly it - This might be caused by some other JS plugin. - The delay of 10msec allows the other plugin to override the URL. - */ - History.replaceState( context, "Questions", url ); - }, 10); - }; +FullTextSearch.prototype.getSearchUrl = function() { + return this._searchUrl; +}; + +FullTextSearch.prototype.renderTagWarning = function(tag_list){ + if ( !tag_list ) { + return; + } + var tagWarningBox = this._tag_warning_box; + tagWarningBox.clear(); + $.each(tag_list, function(idx, tag_name){ + tagWarningBox.addTag(tag_name); + }); + tagWarningBox.showWarning(); +}; - /* *********************************** */ +FullTextSearch.prototype.runTagSearch = function() { + var search_tags = $('#ab-tag-search').val().split(/\s+/); + if (search_tags.length === 0) { + return; + } + var searchUrl = this.getSearchUrl(); + //add all tags to the url + searchUrl = QSutils.add_search_tag(searchUrl, search_tags); + var url = this._baseUrl + searchUrl; + var me = this; + $.ajax({ + url: url, + dataType: 'json', + success: function(data, text_status, xhr){ + me.renderFullTextResult(data, text_status, xhr); + $('#ab-tag-search').val(''); + }, + }); + this.updateHistory(url); +}; - var render_related_tags = function(tags, query_string){ - if (tags.length === 0) return; +FullTextSearch.prototype.updateHistory = function(url) { + var context = { state:1, rand:Math.random() }; + History.pushState( context, "Questions", url ); + setTimeout(function(){ + /* HACK: For some weird reson, sometimes + * overrides the above pushState so we re-aplly it + * This might be caused by some other JS plugin. + * The delay of 10msec allows the other plugin to override the URL. + */ + History.replaceState(context, "Questions", url); + }, + 10 + ); +}; - var html_list = []; - for (var i=0; i<tags.length; i++){ - var tag = new Tag(); - tag.setName(tags[i]['name']); - tag.setDeletable(false); - tag.setLinkable(true); - tag.setUrlParams(query_string); - - html_list.push(tag.getElement().outerHTML()); - html_list.push('<span class="tag-number">× '); - html_list.push(tags[i]['used_count']); - html_list.push('</span>'); - html_list.push('<br />'); +FullTextSearch.prototype.activateTagSearchInput = function() { + //the autocomplete is set up in tag_selector.js + var button = $('#ab-tag-search-add'); + if (button.length === 0){//may be absent + return; + } + var me = this; + var ac = new AutoCompleter({ + url: askbot['urls']['get_tag_list'], + preloadData: true, + minChars: 1, + useCache: true, + matchInside: true, + maxCacheLength: 100, + maxItemsToShow: 20, + onItemSelect: function(){ this.runTagSearch(); }, + delay: 10 + }); + ac.decorate($('#ab-tag-search')); + setupButtonEventHandlers( + button, + function() { me.runTagSearch() } + ); +}; + +FullTextSearch.prototype.sendTitleSearchQuery = function(query_text) { + this.isRunning(true); + this._prevText = query_text; + var data = {query_text: query_text}; + var me = this; + $.ajax({ + url: askbot['urls']['titleSearch'], + data: data, + dataType: 'json', + success: function(data, text_status, xhr){ + me.renderTitleSearchResult(data, text_status, xhr); + }, + complete: function(){ + me.isRunning(false); + me.evalTitleSearchQuery(); + }, + cache: false + }); +}; + + +FullTextSearch.prototype.sendFullTextSearchQuery = function(query_text) { + this.isRunning(true); + var searchUrl = this.getSearchUrl(); + var prevText = this._prevText; + if(!prevText && query_text && askbot['settings']['showSortByRelevance']) { + /* If there was no query but there is some + * query now - and we support relevance search + * - then switch to it + */ + searchUrl = QSutils.patch_query_string( + searchUrl, 'sort:relevance-desc' + ); + } + this._prevText = this.updateQueryString(query_text); + + /* if something has changed, then reset the page no. */ + searchUrl = QSutils.patch_query_string(searchUrl, 'page:1'); + var url = this._baseUrl + searchUrl; + var me = this; + $.ajax({ + url: url, + dataType: 'json', + success: function(data, text_status, xhr){ + me.renderFullTextSearchResult(data, text_status, xhr); + }, + complete: function(){ + me.isRunning(false); + }, + cache: false + }); + this.updateHistory(url); +}; + +FullTextSearch.prototype.refresh = function() { + this.sendFullTextSearchQuery();/* used for tag search, maybe not necessary */ +}; + +FullTextSearch.prototype.getSearchQuery = function() { + return $.trim(this._query.val()); +}; + +/** + * renders title search result in the dropdown under the search input + */ +FullTextSearch.prototype.renderTitleSearchResult = function(data) { + var menu = this._dropMenu; + menu.setData(data); + menu.render(); + menu.show(); +}; + +FullTextSearch.prototype.renderFullTextSearchResult = function(data) { + if (data['questions'].length === 0) { + return; + } + + $('#pager').toggle(data['paginator'] !== '').html(data['paginator']); + $('#questionCount').html(data['question_counter']); + this.renderSearchTags(data['query_data']['tags'], data['query_string']); + if(data['faces'].length > 0) { + $('#contrib-users > a').remove(); + $('#contrib-users').append(data['faces'].join('')); + } + this.renderRelatedTags(data['related_tags'], data['query_string']); + this.renderRelevanceSortTab(data['query_string']); + this.renderTagWarning(data['non_existing_tags']); + this.setActiveSortTab( + data['query_data']['sort_order'], + data['query_string'] + ); + if(data['feed_url']){ + // Change RSS URL + $("#ContentLeft a.rss:first").attr("href", data['feed_url']); + } + + // Patch scope selectors + var baseUrl = this._baseUrl; + $('#scopeWrapper > a.scope-selector').each(function(index) { + var old_qs = $(this).attr('href').replace(baseUrl, ''); + var scope = QSutils.get_query_string_selector_value(old_qs, 'scope'); + qs = QSutils.patch_query_string(data['query_string'], 'scope:' + scope); + $(this).attr('href', baseUrl + qs); + }); + + // Patch "Ask your question" + var askButton = $('#askButton'); + var askHrefBase = askButton.attr('href').split('?')[0]; + askButton.attr( + 'href', + askHrefBase + data['query_data']['ask_query_string'] + ); /* INFO: ask_query_string should already be URL-encoded! */ + + this._query.focus(); + + var old_list = $('#' + this._q_list_sel); + var new_list = $('<div></div>').hide().html(data['questions']); + new_list.find('.timeago').timeago(); + + var q_list_sel = this._q_list_sel; + old_list.stop(true).after(new_list).fadeOut(200, function() { + //show new div with a fadeIn effect + old_list.remove(); + new_list.attr('id', q_list_sel); + new_list.fadeIn(400); + }); +}; + +FullTextSearch.prototype.evalTitleSearchQuery = function() { + var cur_query = this.getSearchQuery(); + var prevText = this._prevText; + if (cur_query !== prevText && this.isRunning() === false){ + if (cur_query.length >= askbot['settings']['minSearchWordLength']){ + this.sendTitleSearchQuery(cur_query); + } else if (cur_query.length === 0){ + this.reset(); } - $('#related-tags').html(html_list.join('')); - }; + } +}; - var render_search_tags = function(tags, query_string){ - var search_tags = $('#searchTags'); - search_tags.empty(); - if (tags.length === 0){ - $('#listSearchTags').hide(); - $('#search-tips').hide();//wrong - if there are search users - } else { - $('#listSearchTags').show(); - $('#search-tips').show(); - $.each(tags, function(idx, tag_name){ - var tag = new Tag(); - tag.setName(tag_name); - tag.setLinkable(false); - tag.setDeletable(true); - tag.setDeleteHandler( - function(){ - remove_search_tag(tag_name, query_string); - } - ); - search_tags.append(tag.getElement()); - }); +FullTextSearch.prototype.reset = function() { + this._prevText = ''; + this._dropMenu.reset(); + this._element.val(''); + this._element.focus(); + this._xButton.hide(); + this._toolTip.show(); +}; + +FullTextSearch.prototype.refreshXButton = function() { + if(this.getSearchQuery().length > 0){ + if (this._query.hasClass('searchInput')){ + $('#searchBar').attr('class', 'cancelable'); + this._xButton.show(); } - }; + } else { + this._xButton.hide(); + $('#searchBar').removeClass('cancelable'); + } +}; - var create_relevance_tab = function(query_string){ - relevance_tab = $('<a></a>'); - href = search_url + QSutils.patch_query_string(query_string, 'sort:relevance-desc'); - relevance_tab.attr('href', href); - relevance_tab.attr('id', 'by_relevance'); - relevance_tab.html('<span>' + sortButtonData['relevance']['label'] + '</span>'); - return relevance_tab; - }; +FullTextSearch.prototype.updateQueryString = function(query_text) { + if (query_text === undefined) { // handle missing parameter + query_text = this.getSearchQuery(); + } + var newUrl = QSutils.patch_query_string( + this._searchUrl, + 'query:' + encodeURIComponent(query_text), + query_text === '' // remove if empty + ); + this.setSearchUrl(newUrl); + return query_text; +}; - /* *************************************** */ +FullTextSearch.prototype.renderRelatedTags = function(tags, query_string){ + if (tags.length === 0) return; - var remove_search_tag = function(tag){ - query_string = QSutils.remove_search_tag(query_string, tag); - send_query(); - }; + var html_list = []; + for (var i=0; i<tags.length; i++){ + var tag = new Tag(); + tag.setName(tags[i]['name']); + tag.setDeletable(false); + tag.setLinkable(true); + tag.setUrlParams(query_string); + + html_list.push(tag.getElement().outerHTML()); + html_list.push('<span class="tag-number">× '); + html_list.push(tags[i]['used_count']); + html_list.push('</span>'); + html_list.push('<br />'); + } + $('#related-tags').html(html_list.join('')); +}; - var set_active_sort_tab = function(sort_method, query_string){ - var tabs = $('#sort_tabs > a'); - tabs.attr('class', 'off'); - tabs.each(function(index, element){ - var tab = $(element); - if ( tab.attr('id') ) { - var tab_name = tab.attr('id').replace(/^by_/,''); - if (tab_name in sortButtonData){ - href = search_url + QSutils.patch_query_string( - query_string, - 'sort:' + tab_name + '-desc' - ); - tab.attr('href', href); - tab.attr('title', sortButtonData[tab_name]['desc_tooltip']); - tab.html(sortButtonData[tab_name]['label']); +FullTextSearch.prototype.renderSearchTags = function(tags, query_string){ + var search_tags = $('#searchTags'); + search_tags.empty(); + var me = this; + if (tags.length === 0){ + $('#listSearchTags').hide(); + $('#search-tips').hide();//wrong - if there are search users + } else { + $('#listSearchTags').show(); + $('#search-tips').show(); + $.each(tags, function(idx, tag_name){ + var tag = new Tag(); + tag.setName(tag_name); + tag.setLinkable(false); + tag.setDeletable(true); + tag.setDeleteHandler( + function(){ + this.removeSearchTag(tag_name, query_string); } - } + ); + search_tags.append(tag.getElement()); }); - var bits = sort_method.split('-', 2); - var name = bits[0]; - var sense = bits[1];//sense of sort - var antisense = (sense == 'asc' ? 'desc':'asc'); - var arrow = (sense == 'asc' ? ' ▲':' ▼'); - var active_tab = $('#by_' + name); - active_tab.attr('class', 'on'); - active_tab.attr('title', sortButtonData[name][antisense + '_tooltip']); - active_tab.html(sortButtonData[name]['label'] + arrow); - }; + } +}; - var render_relevance_sort_tab = function(query_string){ - if (showSortByRelevance === false){ - return; - } - var relevance_tab = $('#by_relevance'); - if (prev_text && prev_text.length > 0){ - if (relevance_tab.length == 0){ - relevance_tab = create_relevance_tab(query_string); - $('#sort_tabs>span').after(relevance_tab); +FullTextSearch.prototype.createRelevanceTab = function(query_string){ + var relevance_tab = $('<a></a>'); + href = this._baseUrl + QSutils.patch_query_string(query_string, 'sort:relevance-desc'); + relevance_tab.attr('href', href); + relevance_tab.attr('id', 'by_relevance'); + relevance_tab.html( + '<span>' + askbot['data']['sortButtonData']['relevance']['label'] + '</span>' + ); + return relevance_tab; +}; + +FullTextSearch.prototype.removeSearchTag = function(tag) { + var searchUrl = this.getSearchUrl() + searchUrl = QSutils.remove_search_tag(searchUrl, tag); + this.setSearchUrl(searchUrl); + this.sendFullTextSearchQuery(); +}; + +FullTextSearch.prototype.setActiveSortTab = function(sort_method, query_string){ + var tabs = $('#sort_tabs > a'); + tabs.attr('class', 'off'); + var baseUrl = this._baseUrl; + tabs.each(function(index, element){ + var tab = $(element); + if ( tab.attr('id') ) { + var tab_name = tab.attr('id').replace(/^by_/,''); + if (tab_name in askbot['data']['sortButtonData']){ + href = baseUrl + QSutils.patch_query_string( + query_string, + 'sort:' + tab_name + '-desc' + ); + tab.attr('href', href); + tab.attr( + 'title', + askbot['data']['sortButtonData'][tab_name]['desc_tooltip'] + ); + tab.html( + askbot['data']['sortButtonData'][tab_name]['label'] + ); } } - else { - if (relevance_tab.length > 0){ - relevance_tab.remove(); - } + }); + var bits = sort_method.split('-', 2); + var name = bits[0]; + var sense = bits[1];//sense of sort + var antisense = (sense == 'asc' ? 'desc':'asc'); + var arrow = (sense == 'asc' ? ' ▲':' ▼'); + var active_tab = $('#by_' + name); + active_tab.attr('class', 'on'); + active_tab.attr( + 'title', + askbot['data']['sortButtonData'][name][antisense + '_tooltip'] + ); + active_tab.html( + askbot['data']['sortButtonData'][name]['label'] + arrow + ); +}; + +FullTextSearch.prototype.renderRelevanceSortTab = function(query_string) { + if (askbot['settings']['showSortByRelevance'] === false){ + return; + } + var relevance_tab = $('#by_relevance'); + var prevText = this._prevText; + if (prevText && prevText.length > 0){ + if (relevance_tab.length == 0){ + relevance_tab = this.createRelevanceTab(query_string); + $('#sort_tabs>span').after(relevance_tab); } + } else { + if (relevance_tab.length > 0){ + relevance_tab.remove(); + } + } +}; + +FullTextSearch.prototype.makeAskHandler = function() { + var me = this; + return function() { + var query = me.getSearchQuery(); + window.location.href = askbot['urls']['ask'] + '?title=' + query; + return false; }; +}; - var render_result = function(data, text_status, xhr){ - if (data['questions'].length > 0){ - $('#pager').toggle(data['paginator'] !== '').html(data['paginator']); - $('#questionCount').html(data['question_counter']); - render_search_tags(data['query_data']['tags'], data['query_string']); - if(data['faces'].length > 0) { - $('#contrib-users > a').remove(); - $('#contrib-users').append(data['faces'].join('')); - } - render_related_tags(data['related_tags'], data['query_string']); - render_relevance_sort_tab(data['query_string']); - render_tag_warning(data['non_existing_tags']); - set_active_sort_tab(data['query_data']['sort_order'], data['query_string']); - if(data['feed_url']){ - // Change RSS URL - $("#ContentLeft a.rss:first").attr("href", data['feed_url']); +FullTextSearch.prototype.updateToolTip = function() { + var query = this.getSearchQuery(); + if (query === '') { + this._toolTip.show(); + } else { + this._toolTip.hide(); + } +}; + +/** + * keydown handler operates on the tooltip and the X button + * keyup is not good enough, because in that case + * tooltip will be displayed with the input box simultaneously + */ +FullTextSearch.prototype.makeKeyDownHandler = function() { + var me = this; + var toolTip = this._toolTip; + var xButton = this._xButton; + var dropMenu = this._dropMenu; + return function(e) {//don't like the keyup delay to + var keyCode = getKeyCode(e); + + if (keyCode === 27) {//escape key + if (dropMenu.isOpen() === false) { + me.reset(); + return false; } + } - // Patch scope selectors - $('#scopeWrapper > a.scope-selector').each(function(index) { - var old_qs = $(this).attr('href').replace(search_url, ''); - var scope = QSutils.get_query_string_selector_value(old_qs, 'scope'); - qs = QSutils.patch_query_string(data['query_string'], 'scope:' + scope); - $(this).attr('href', search_url + qs); - }); - - // Patch "Ask your question" - var askButton = $('#askButton'); - var askHrefBase = askButton.attr('href').split('?')[0]; - askButton.attr('href', askHrefBase + data['query_data']['ask_query_string']); /* INFO: ask_query_string should already be URL-encoded! */ - - query.focus(); - - var old_list = $('#' + q_list_sel); - var new_list = $('<div></div>').hide().html(data['questions']); - new_list.find('.timeago').timeago(); - old_list.stop(true).after(new_list).fadeOut(200, function() { - //show new div with a fadeIn effect - old_list.remove(); - new_list.attr('id', q_list_sel); - new_list.fadeIn(400); - }); + var query = me.getSearchQuery(); + if (query.length === 0) { + if (keyCode !== 8 && keyCode !== 48) {//del and backspace + toolTip.hide(); + //xButton.show();//causes a jump of search input... + } + } else { + me.updateToolTip(); + me.refreshXButton(); } }; +}; + +FullTextSearch.prototype.decorate = function(element) { + this._element = element;/* this is a bit artificial we don't use _element */ + this._query = element; + this._xButton = $('input[name=reset_query]'); + this._prevText = this.getSearchQuery(); + this._tag_warning_box = new TagWarningBox(); + + var toolTip = new InputToolTip(); + toolTip.setClickHandler(function() { + element.focus(); + }); + this._element.after(toolTip.getElement()); + this._toolTip = toolTip; + + var dropMenu = new SearchDropMenu(); + dropMenu.setAskHandler(this.makeAskHandler()); + dropMenu.setAskButtonEnabled(this._askButtonEnabled); + this._dropMenu = dropMenu; + element.parent().after(this._dropMenu.getElement()); + + var menuCloser = function(){ + dropMenu.reset(); + }; + $(element).click(function(e){ return false }); + $(document).click(menuCloser); - /* *********************************** */ + //the tag search input is optional in askbot + $('#ab-tag-search').parent().before( + this._tag_warning_box.getElement() + ); - // Wire search tags + // make search tags functional var search_tags = $('#searchTags .tag-left'); + var searchUrl = this.getSearchUrl(); + var me = this; $.each(search_tags, function(idx, element){ var tag = new Tag(); tag.decorate($(element)); //todo: setDeleteHandler and setHandler //must work after decorate & must have getName tag.setDeleteHandler( - function(){ - remove_search_tag(tag.getName(), query_string); - } + function(){ + me.removeSearchTag(tag.getName(), searchUrl); + } ); }); - - // Wire X button - x_button.click(function () { - restart_query(); /* wrapped in closure because it's not yet defined at this point */ + // enable x button (search reset) + this._xButton.click(function () { + /* wrapped in closure because it's not yet defined at this point */ + me.reset(); }); - refresh_x_button(); + this.refreshXButton(); - // Wire query box + // enable query box var main_page_eval_handle; - query.keyup(function(e){ - refresh_x_button(); - if (running === false){ + this._query.keydown(this.makeKeyDownHandler()); + this._query.keyup(function(e){ + me.updateToolTip(); + me.refreshXButton(); + if (me.isRunning() === false){ clearTimeout(main_page_eval_handle); - main_page_eval_handle = setTimeout(eval_query, 400); + main_page_eval_handle = setTimeout( + function() { me.evalTitleSearchQuery() }, + 400 + ); } }); - activate_tag_search_input(); + this.activateTagSearchInput(); + var baseUrl = this._baseUrl; + var searchUrl = this.getSearchUrl(); $("form#searchForm").submit(function(event) { // if user clicks the button the s(h)e probably wants page reload, // so provide that experience but first update the query string event.preventDefault(); - update_query_string(); - window.location.href = search_url + query_string; + me.updateQueryString(); + window.location.href = baseUrl + searchUrl; }); - - /* *********************************** */ - - // Hook for tag_selector.js - liveSearch.refresh = function () { - send_query(); - }; }; diff --git a/askbot/media/js/live_search_new_thread.js b/askbot/media/js/live_search_new_thread.js index eedd5fe8..d17951eb 100644 --- a/askbot/media/js/live_search_new_thread.js +++ b/askbot/media/js/live_search_new_thread.js @@ -2,7 +2,7 @@ var liveSearchNewThreadInit = function(auto_focus_out) { var query = $('input#id_title.questionTitleInput'); var prev_text = $.trim(query.val()); - var search_url = askbot['urls']['api_get_questions']; + var search_url = askbot['urls']['titleSearch']; var running = false; var q_list_sel = 'question-list'; //id of question listing div @@ -32,7 +32,7 @@ var liveSearchNewThreadInit = function(auto_focus_out) { var eval_query = function(){ cur_text = $.trim(query.val()); if (cur_text !== prev_text && running === false){ - if (cur_text.length >= minSearchWordLength){ + if (cur_text.length >= askbot['settings']['minSearchWordLength']){ send_query(cur_text); } else if (cur_text.length === 0){ restart_query(); diff --git a/askbot/media/js/utils.js b/askbot/media/js/utils.js index 2534fb21..6ea23566 100644 --- a/askbot/media/js/utils.js +++ b/askbot/media/js/utils.js @@ -97,6 +97,18 @@ var showMessage = function(element, msg, where) { }; })(jQuery); +/** + * @return {number} key code of the event or `undefined` + */ +var getKeyCode = function(e) { + if (e.which) { + return e.which; + } else if (e.keyCode) { + return e.keyCode; + } + return undefined; +}; + var makeKeyHandler = function(key, callback){ return function(e){ if ((e.which && e.which == key) || (e.keyCode && e.keyCode == key)){ @@ -370,6 +382,30 @@ WrappedElement.prototype.dispose = function(){ /** * @constructor + * a simple link + */ +var Link = function() { + WrappedElement.call(this); +}; +inherits(Link, WrappedElement); + +Link.prototype.setUrl = function(url) { + this._url = url; +}; + +Link.prototype.setText = function(text) { + this._text = text; +}; + +Link.prototype.createDom = function() { + var link = this.makeElement('a'); + this._element = link; + link.attr('href', this._url); + link.html(this._text); +}; + +/** + * @constructor * Widget is a Wrapped element with state */ var Widget = function() { @@ -1497,10 +1533,14 @@ var SelectBox = function(){ this._items = []; this._select_handler = function(){};//empty default this._is_editable = false; - this._item_class = SelectBoxItem; + this._item_class = this.setItemClass(SelectBoxItem); }; inherits(SelectBox, Widget); +SelectBox.prototype.setItemClass = function(itemClass) { + this._item_class = itemClass; +}; + SelectBox.prototype.setEditable = function(is_editable) { this._is_editable = is_editable; }; @@ -1531,7 +1571,21 @@ SelectBox.prototype.getItemByIndex = function(idx) { return this._items[idx]; }; -//why do we have these two almost identical methods? +/** + * removes all items + */ +SelectBox.prototype.empty = function() { + var items = this._items; + $.each(items, function(idx, item){ + item.dispose(); + }); + this._items = []; +}; + +/* + * why do we have these two almost identical methods? + * the difference seems to be remove/vs fade out + */ SelectBox.prototype.removeItem = function(id){ var item = this.getItem(id); item.getElement().fadeOut(); @@ -1656,6 +1710,12 @@ SelectBox.prototype.decorate = function(element){ }); }; +SelectBox.prototype.createDom = function() { + var element = this.makeElement('ul'); + this._element = element; + element.addClass('select-box'); +}; + /** * This is a dropdown list elment */ diff --git a/askbot/media/style/style.less b/askbot/media/style/style.less index 69f4daf7..2f6d601e 100644 --- a/askbot/media/style/style.less +++ b/askbot/media/style/style.less @@ -395,45 +395,37 @@ body.user-messages { border-top:#fcfcfc 1px solid; margin-bottom:10px; font-family:@main-font; +} - #homeButton{ - border-right:#afaf9e 1px solid; - .sprites(-6px,-36px); - height:55px; - width:43px; - display:block; - float:left; - } +#homeButton{ + border-right: #afaf9e 1px solid; + .sprites(-6px,-36px); + height:55px; + width:43px; + display:block; + float:left; +} - #homeButton:hover{ - .sprites(-6px-45,-36px); - } - - #scopeWrapper{ - width:688px; - float:left; - - a{ - display:block; - float:left; - } +#homeButton:hover{ + .sprites(-51px,-36px); +} - .scope-selector{ - font-size:20px; - color:#7a7a6b; - height:55px; - line-height:55px; - margin-left:16px - } +.scope-selector { + display:block; + float:left; + font-size:20px; + color:#7a7a6b; + height:55px; + line-height:55px; + margin-left:16px +} - .on{ - background:url(../images/scopearrow.png) no-repeat center bottom; - } +.scope-selector.on { + background:url(../images/scopearrow.png) no-repeat center bottom; +} - .ask-message{ - font-size:24px; - } - } +.scope-selector.ask-message { + font-size:24px; } .validate-email-page { @@ -456,90 +448,133 @@ body.user-messages { } #searchBar { /* Main search form , check widgets/search_bar.html */ - display: inline-block; + display: block; background-color: #fff; - width: 400px; border: 1px solid #c9c9b5; - float:right; - height:42px; - margin:6px 0px 0px 15px; + height: 41px; + z-index: 10000; + position: relative; - .searchInput, .searchInputCancelable{ - font-size: 26px; - height: 39px; - line-height: 39px; + input.searchInput { + font-size: 22px; + height: 26px; + line-height: 26px; font-weight:300; background:#FFF; border:0px; color:#484848; - padding-left:10px; - padding-top: 1px; font-family:@body-font; - vertical-align: top; - } - - .searchInput,{ - width: 340px; + width: 100%; + margin: 8px 0 6px 0; + padding: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; } - .searchInputCancelable { - width: 305px; + div.input-tool-tip { + margin-top: -40px; + padding-top: 10px; + height: 30px; + line-height: 20px; + font-size: 20px; + font-style: italic; } +} - .logoutsearch { - width: 337px; - } +.search-drop-menu { + box-sizing: border-box; + background: whitesmoke; + border: 1px solid #c9c9b5; + border-top: none; + margin: 0; + position: relative; + top: 0; + z-index: 10000; - .searchBtn { - font-size: 10px; - color: #666; - background-color: #eee; - height: 42px; - border:#FFF 1px solid; - line-height: 22px; - text-align: center; - float:right; - margin: 0px; - width:48px; - .sprites(-98px,-36px); - cursor:pointer; + ul { + list-style: none; + overflow: auto; + padding: 0 0 10px 0; + margin: 0 0 20px 0; + position: relative; + li { + padding: 5px 10px; + position: relative; + a { + text-decoration: none; + } + } + li.selected { + background: #08c; + a { + color: whitesmoke; + } + } } - .searchBtn:hover { - .sprites(-98px-48,-36px); + ul.empty { + padding: 5px; + margin: 0; } - .cancelSearchBtn { - font-size: 30px; - color: #ce8888; - background:#fff; - height: 42px; - line-height: 42px; - border:0px; - border-left:#deded0 1px solid; + .footer { text-align: center; - width: 35px; - cursor:pointer; + padding-bottom: 10px; } +} - .cancelSearchBtn:hover { - color: #d84040; - } +.input-tool-tip { + color: #999; +} +.input-tool-tip.dimmed { + color: #ccc; } -body.anon { - #searchBar { - width: 500px; - .searchInput { - width: 435px; - } +input[type="submit"].searchBtn { + font-size: 10px; + color: #666; + background-color: #eee; + height: 41px; + border:#FFF 1px solid; + line-height: 22px; + text-align: center; + float:right; + margin: 7px 28px 0 0; + width: 48px; + .sprites(-98px,-36px); + cursor:pointer; + position: relative; + z-index: 10001; +} +.ask-page input[type="submit"].searchBtn { + display: none; +} - .searchInputCancelable { - width: 405px; - } - } +.searchBtn:hover { + .sprites(-98px-48,-36px); } +.cancelSearchBtn { + font-size: 30px; + color: #ce8888; + background:#fff; + height: 41px; + line-height: 42px; + border:0px; + border-left:#deded0 1px solid; + text-align: center; + width: 35px; + cursor:pointer; + float: right; + margin-top: 7px; + position: relative; + z-index: 10001; +} + +.cancelSearchBtn:hover { + color: #d84040; +} #askButton{ /* check blocks/secondary_header.html and widgets/ask_button.html*/ line-height:44px; @@ -554,6 +589,37 @@ body.anon { .button-style-hover; } +/* + Put the secondary navigation together: + 1) raise the search bar by 55px + 2) add padding to fit the buttons +*/ +#searchBar { + margin: 0 228px 0 327px; + width: auto; + margin-top: -49px; + padding: 0 49px 0 8px; +} +/* line up drop menu the same way as the search bar */ +.search-drop-menu { + margin: 0 228px 0 327px; + width: auto; +} +.ask-page .search-drop-menu, +body.anon.ask-page .search-drop-menu { + margin: 0; +} +body.anon { + #searchBar, + .search-drop-menu { + margin-left: 227px;/* we don't have the "followed" scope */ + } +} +#searchBar.cancelable { + padding-right: 82px; +} + + /* ----- Content layout, check two_column_body.html or one_column_body.html ----- */ #ContentLeft { @@ -1423,10 +1489,11 @@ ul#related-tags li { #askFormBar { display:inline-block; - padding: 4px 7px 0px 0px; + padding: 4px 0 0 0; margin-top:0px; + width: 100%; - p{ + p { margin:0 0 5px 0; font-size:14px; color:@info-text-dark; @@ -1436,10 +1503,10 @@ ul#related-tags li { font-size: 24px; height: 36px; line-height: 36px; - margin: 0px; - padding: 0px 0 0 5px; + margin: 0; + padding: 0;/*-left: 5px;*/ border:#cce6ec 3px solid; - width:719px; + width: 100%;/*719px;*/ } } @@ -1483,11 +1550,13 @@ ul#related-tags li { } #id_tags { - border:#cce6ec 3px solid; - height:25px; - padding-left:5px; - font-size:14px; - width:395px; + box-sizing: border-box; + border: #cce6ec 3px solid; + height: 31px; + padding-left: 5px; + font-size: 14px; + width: 100%; + max-width: 395px; } } @@ -1646,9 +1715,14 @@ ul#related-tags li { .edit-answer-page { .wmd-container { width: 723px; + width: 100%; } #editor { - width: 710px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 100%; + height: 100%; padding: 6px; } .retagger-buttons { @@ -1784,6 +1858,7 @@ ul#related-tags li { font-size:14px; margin-top:5px; margin-bottom:5px; + width: 100%; } #id_title{ font-size: 24px; @@ -1792,7 +1867,7 @@ ul#related-tags li { margin: 0px; padding: 0px 0 0 5px; border:#cce6ec 3px solid; - width: 719px; + width: 100%; margin-bottom:10px; } #id_summary{ @@ -1838,7 +1913,7 @@ ul#related-tags li { vertical-align: top; } - .question-content{ + .question-content { float:right; width:682px; margin-bottom:10px; @@ -2511,7 +2586,10 @@ ul#related-tags li { form{ margin-bottom:15px; } - input[type="text"],input[type="password"],select{ + + input[type="text"], + input[type="password"], + select{ border:#cce6ec 3px solid; height:25px; line-height: 25px; @@ -3756,24 +3834,12 @@ pre.prettyprint { clear:both;padding: 3px; border: 0px solid #888; } /* language-specific fixes */ body.lang-es { #searchBar { - width: 398px; - .searchInput { - width: 337px; - } - .searchInputCancelable { - width: 302px; - } + /* need special left padding */ } } body.anon.lang-es { #searchBar { - width: 485px; - .searchInput { - width: 425px; - } - .searchInputCancelable { - width: 390px; - } + /* need special left padding */ } } @@ -4007,6 +4073,12 @@ textarea.tipped-input { /* modifications for small screens */ @media screen and (max-width: 960px) {/* content margins touch viewport */ + #homeButton { + .sprites(1px,-36px); + } + #homeButton:hover { + .sprites(-44px,-36px); + } } @media screen and (max-width: 480px) { .content-wrapper { @@ -4018,27 +4090,154 @@ textarea.tipped-input { #ContentLeft { width: 100%; } - #secondaryHeader #scopeWrapper { + .main-page h1, + #askButton, + #metaNav #navBadges, + .user-info, + .copyright, + .counts .views, + .counts .votes, + .help, + .rss, + .scope-selector, + .settings, + .tabBar, + .tags, + .userinfo, + .widgets { display: none; } - #askButton { - display: block; - float: none; - margin: auto; - margin-top: 5px; + + .ask-page, + .edit-question-page { + input[type="submit"].searchBtn { + display: none; + } } - #homeButton { + + .ask-page, + .edit-answer-page, + .edit-question-page { + .preview-toggle, + .proxy-user-info, + .answer-options, + .question-options, + .revision-comment, + .wmd-preview, + #wmd-hr-button, + #wmd-heading-button { + display: none; + } + } + + .edit-answer-page, + .edit-question-page { + label[for="id_title"], + label[for="id_revision"], + #id_revision { + display: none; + } + #fmedit #id_title { + margin: 15px 0 0 0; + } } - .main-page { - h1, - .counts .views, - .counts .votes, - .rss, - .tabBar, - .tags, - .userinfo { + + .question-page { + .comment-votes { display: none; } + .comments { + form.post-comments { + margin: 0 10px 0 0; + } + .comment .comment-body { + margin-left: 5px; + } + } + .post-update-info-container { + float: none; + width: 100%; + } + .post-update-info { + float: none; + margin-left: 0; + width: 100%-5; + + br, + .badge1, + .badge2, + .badge3, + .gravatar, + .reputation-score, + .badge-count { + display: none; + } + } + .question-content, + ul.post-retag input { + width: 100%-12; + } + .answer-table, + #question-table { + width: 100%-15; + } + } + + .users-page { + .userList td { + display: block; + width: 100%; + .user { + width: 100%; + } + } + } + + .user-profile-page { + td { + display: block; + } + } + + .footer-links, + .powered-link { + text-align: center; + width: 100%; + } + + #userToolsNav { + margin-left: 10px; + } + + #metaNav { + float: left; + a#navUsers, + a#navTags, + a#navGroups { + background: none; + color: #d0e296; + font-size: 16px; + text-decoration: underline; + margin-left: 20px; + padding-left: 0; + } + } + .powered-link { + margin-bottom: 15px; + } + .short-summary:first-child { + padding-top: 0; + } + #searchBar, + body.anon #searchBar { + margin: -49px 8px 0 52px; + } + .search-drop-menu, + body.anon .search-drop-menu { + margin: 0 8px 0 52px; + } + input[type="submit"].searchBtn { + margin-right: 8px; } .short-summary { width: 100%; diff --git a/askbot/models/question.py b/askbot/models/question.py index 5c0d5732..3e908167 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -42,6 +42,14 @@ class ThreadQuerySet(models.query.QuerySet): groups = [Group.objects.get_global_group()] return self.filter(groups__in=groups).distinct() + def get_for_title_query(self, search_query): + """returns threads matching title query + todo: possibly add tags + todo: implement full text search on relevant fields + """ + return self.filter(title__icontains=search_query) + + class ThreadManager(BaseQuerySetManager): def get_query_set(self): @@ -174,6 +182,7 @@ class ThreadManager(BaseQuerySetManager): def get_for_query(self, search_query, qs=None): """returns a query set of questions, matching the full text query + todo: move to query set """ if getattr(django_settings, 'ENABLE_HAYSTACK_SEARCH', False): from askbot.search.haystack import AskbotSearchQuerySet diff --git a/askbot/search/state_manager.py b/askbot/search/state_manager.py index a02f1577..7b6b746c 100644 --- a/askbot/search/state_manager.py +++ b/askbot/search/state_manager.py @@ -170,10 +170,16 @@ class SearchState(object): SAFE_CHARS = const.TAG_SEP + '_+.-' def query_string(self): + """returns part of the url to the main page, + responsible to display the full text search results, + taking into account sort method, selected scope + and search tags""" + lst = [ 'scope:' + self.scope, 'sort:' + self.sort ] + if self.query: lst.append('query:' + urllib.quote(smart_str(self.query), safe=self.SAFE_CHARS)) if self.tags: diff --git a/askbot/templates/answer_edit.html b/askbot/templates/answer_edit.html index 20c0684d..887714cb 100644 --- a/askbot/templates/answer_edit.html +++ b/askbot/templates/answer_edit.html @@ -25,6 +25,7 @@ editor_type = settings.EDITOR_TYPE ) }} + <div class="answer-options"> {% if settings.WIKI_ON and answer.wiki == False %} {{ macros.checkbox_in_div(form.wiki) }} {% endif %} @@ -34,6 +35,7 @@ %} {{ macros.checkbox_in_div(form.post_privately) }} {% endif %} + </div> <div class="after-editor"> <input id="edit_post_form_submit_button" type="submit" value="{% trans %}Save edit{% endtrans %}" class="submit" /> <input type="button" value="{% trans %}Cancel{% endtrans %}" class="submit" onclick="history.back(-1);" /> diff --git a/askbot/templates/ask.html b/askbot/templates/ask.html index 27434f83..5e54ad6f 100644 --- a/askbot/templates/ask.html +++ b/askbot/templates/ask.html @@ -20,11 +20,6 @@ <script type='text/javascript' src='{{"/js/wmd/showdown.js"|media}}'></script> <script type='text/javascript' src='{{"/js/wmd/wmd.js"|media}}'></script> {% endif %} - <script type='text/javascript'> - var sortMethod = undefined;//need for live_search - var minSearchWordLength = {{settings.MIN_SEARCH_WORD_LENGTH}}; - </script> - <script type='text/javascript' src='{{"/js/live_search_new_thread.js"|media}}'></script> {% include "meta/editor_data.html" %} {% if mandatory_tags %} {% include "meta/mandatory_tags_js.html" %} @@ -33,7 +28,6 @@ {% include "meta/category_tree_js.html" %} {% endif %} <script type='text/javascript'> - askbot['urls']['api_get_questions'] = '{% url api_get_questions %}'; askbot['urls']['saveDraftQuestion'] = '{% url save_draft_question %}'; {% if settings.ENABLE_MATHJAX or settings.MARKUP_CODE_FRIENDLY %} var codeFriendlyMarkdown = true; @@ -41,7 +35,6 @@ var codeFriendlyMarkdown = false; {% endif %} $().ready(function(){ - liveSearchNewThreadInit(); //set current module button style $('#editor').TextAreaResizer(); //highlight code synctax when editor has new text diff --git a/askbot/templates/base.html b/askbot/templates/base.html index 63d7115f..ffe3bf84 100644 --- a/askbot/templates/base.html +++ b/askbot/templates/base.html @@ -9,6 +9,7 @@ <meta name="keywords" content="{%block keywords%}{%endblock%},{{settings.APP_KEYWORDS|escape}}" /> {% if settings.GOOGLE_SITEMAP_CODE %} <meta name="google-site-verification" content="{{settings.GOOGLE_SITEMAP_CODE}}" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> {% endif %} <link rel="shortcut icon" href="{{ settings.SITE_FAVICON|media }}" /> {% block before_css %}{% endblock %} diff --git a/askbot/templates/embed/ask_by_widget.html b/askbot/templates/embed/ask_by_widget.html index 4cec5f6d..ef139d1c 100644 --- a/askbot/templates/embed/ask_by_widget.html +++ b/askbot/templates/embed/ask_by_widget.html @@ -213,13 +213,15 @@ <script type="text/javascript" src='{{"/js/live_search_new_thread.js"|media}}'></script> <script type="text/javascript" charset="utf-8"> - var minSearchWordLength = {{settings.MIN_SEARCH_WORD_LENGTH}}; - askbot['urls']['api_get_questions'] = '{% url api_get_questions %}'; + askbot['settings']['minSearchWordLength'] = {{settings.MIN_SEARCH_WORD_LENGTH}}; + askbot['urls']['titleSearch'] = '{% url title_search %}'; askbot['urls']['upload'] = '{% url upload %}'; $(document).ready(function(){ - $("#id_title").focus(); - $("#id_title").addClass('questionTitleInput'); - liveSearchNewThreadInit(true); + var searchInput = $('#id_title'); + searchInput.addClass('questionTitleInput'); + var search = new FullTextSearch(); + search.decorate(searchInput); + searchInput.focus(); }); </script> {% endblock %} diff --git a/askbot/templates/macros.html b/askbot/templates/macros.html index 8e578dec..f2194a9d 100644 --- a/askbot/templates/macros.html +++ b/askbot/templates/macros.html @@ -498,11 +498,11 @@ for the purposes of the AJAX comment editor #} title="{{desc_tooltip}}"><span>{{label}}</span></a> {% endif %} <script type="text/javascript">{# need to pass on text translations to js #} - var sortButtonData = sortButtonData || {}; - sortButtonData["{{key_name}}"] = { - label: "{{label}}", - asc_tooltip: "{{asc_tooltip}}", - desc_tooltip: "{{desc_tooltip}}" + askbot['data']['sortButtonData'] = askbot['data']['sortButtonData'] || {}; + askbot['data']['sortButtonData']['{{key_name}}'] = { + label: '{{label}}', + asc_tooltip: '{{asc_tooltip}}', + desc_tooltip: '{{desc_tooltip}}' }; </script> {%- endmacro %} diff --git a/askbot/templates/main_page/javascript.html b/askbot/templates/main_page/javascript.html index f1e0fb44..bb7190ce 100644 --- a/askbot/templates/main_page/javascript.html +++ b/askbot/templates/main_page/javascript.html @@ -1,12 +1,9 @@ <script type="text/javascript"> /*<![CDATA[*/ - var sortMethod = '{{sort}}'; - var showSortByRelevance = {% if show_sort_by_relevance %}true{% else %}false{% endif %}; - var minSearchWordLength = {{settings.MIN_SEARCH_WORD_LENGTH}}; + askbot['settings']['showSortByRelevance'] = {% if show_sort_by_relevance %}true{% else %}false{% endif %}; $(document).ready(function(){ /*var on_tab = '#nav_questions'; $(on_tab).attr('className','on');*/ - liveSearch('{{ search_state.query_string()|escapejs }}'); Hilite.exact = false; Hilite.elementid = "question-list"; Hilite.debug_referrer = location.href; @@ -48,4 +45,3 @@ {% if request.user.is_authenticated() %} <script type='text/javascript' src='{{"/js/tag_selector.js"|media}}'></script> {% endif %} -<script type="text/javascript" src='{{"/js/live_search.js"|media}}'></script> diff --git a/askbot/templates/main_page/tab_bar.html b/askbot/templates/main_page/tab_bar.html index 37f63012..c86abd89 100644 --- a/askbot/templates/main_page/tab_bar.html +++ b/askbot/templates/main_page/tab_bar.html @@ -33,8 +33,8 @@ </a> {% endif %} <script type="text/javascript"> - var sortButtonData = sortButtonData || {}; - sortButtonData['relevance'] = { + var askbot['data']['sortButtonData'] = askbot['data']['sortButtonData'] || {}; + askbot['data']['sortButtonData']['relevance'] = { asc_tooltip: "{{asc_relevance_tooltip}}", desc_tooltip: "{{desc_relevance_tooltip}}", label: "{{relevance_label}}" diff --git a/askbot/templates/meta/bottom_scripts.html b/askbot/templates/meta/bottom_scripts.html index b3fcd815..24c2ba10 100644 --- a/askbot/templates/meta/bottom_scripts.html +++ b/askbot/templates/meta/bottom_scripts.html @@ -24,8 +24,11 @@ askbot['urls']['follow_user'] = '/followit/follow/user/{{'{{'}}userId{{'}}'}}/'; askbot['urls']['unfollow_user'] = '/followit/unfollow/user/{{'{{'}}userId{{'}}'}}/'; askbot['urls']['user_signin'] = '{{ settings.LOGIN_URL }}'; - askbot['settings']['static_url'] = '{{ settings.STATIC_URL }}'; askbot['urls']['getEditor'] = '{% url "get_editor" %}'; + askbot['urls']['titleSearch'] = '{% url "title_search" %}'; + askbot['urls']['ask'] = '{% url "ask" %}'; + askbot['settings']['static_url'] = '{{ settings.STATIC_URL }}'; + askbot['settings']['minSearchWordLength'] = {{settings.MIN_SEARCH_WORD_LENGTH}}; </script> <script type="text/javascript" @@ -39,6 +42,7 @@ <!-- History.js --> <script type='text/javascript' src="{{"/js/jquery.history.js"|media }}"></script> <script type='text/javascript' src="{{"/js/utils.js"|media }}"></script> +<script type="text/javascript" src="{{'/js/live_search.js'|media}}"></script> {% if settings.ENABLE_MATHJAX %} <script type='text/javascript' src="{{settings.MATHJAX_BASE_URL}}/MathJax.js"> MathJax.Hub.Config({ @@ -52,13 +56,34 @@ /*<![CDATA[*/ $(document).ready(function(){ // focus input on the search bar endcomment - {% if active_tab in ('users', 'questions', 'tags') %} - $('#keywords').focus(); + {% if active_tab in ('users', 'questions', 'tags', 'badges') %} + var searchInput = $('#keywords'); {% elif active_tab == 'ask' %} - $('#id_title').focus(); + var searchInput = $('#id_title'); {% else %} + var searchInput = undefined; animateHashes(); {% endif %} + + if (searchInput) { + searchInput.focus(); + } + + {% if active_tab in ('questions', 'badges', 'ask') %} + if (searchInput) { + var search = new FullTextSearch(); + {% if search_state %} + search.setSearchUrl('{{ search_state.query_string()|escapejs }}'); + {% else %} + search.setSearchUrl(''); + {% endif %} + {% if active_tab == 'ask' %} + search.setAskButtonEnabled(false); + {% endif %} + search.decorate(searchInput); + } + {% endif %} + if (askbot['data']['userIsAdminOrMod']) { $('body').addClass('admin'); } diff --git a/askbot/templates/widgets/edit_post.html b/askbot/templates/widgets/edit_post.html index b9bfa1e3..57770570 100644 --- a/askbot/templates/widgets/edit_post.html +++ b/askbot/templates/widgets/edit_post.html @@ -64,7 +64,7 @@ </div> {% endif %} {% if 'summary' in post_form['fields'] %} - <div class="form-item"> + <div class="form-item revision-comment"> <strong>{{ post_form.summary.label_tag() }}</strong> <br/> {{ post_form.summary }} <div class="title-desc"> diff --git a/askbot/templates/widgets/scope_nav.html b/askbot/templates/widgets/scope_nav.html index a6bda630..b68d899c 100644 --- a/askbot/templates/widgets/scope_nav.html +++ b/askbot/templates/widgets/scope_nav.html @@ -1,3 +1,4 @@ +<div id="scopeNav"> {% if active_tab != "ask" %} {% if not search_state %} {# get empty SearchState() if there's none #} {% set search_state=search_state|get_empty_search_state %} @@ -13,3 +14,4 @@ {% else %} <div class="scope-selector ask-message">{% trans %}Please ask your question here{% endtrans %}</div> {% endif %} +</div> diff --git a/askbot/templates/widgets/search_bar.html b/askbot/templates/widgets/search_bar.html index 59c4fd58..8c485c73 100644 --- a/askbot/templates/widgets/search_bar.html +++ b/askbot/templates/widgets/search_bar.html @@ -1,17 +1,7 @@ {% if active_tab != "ask" %} {% spaceless %} -<div id="searchBar"> +<div id="searchBar" {% if query %}class="cancelable"{% endif %}> {# url action depends on which tab is active #} - <form - {% if active_tab == "tags" %} - action="{% url tags %}" - {% elif active_tab == "users" %} - action="{% url users %}" - {% else %} - action="{% url questions %}" id="searchForm" - {% endif %} - method="get"> - <input type="submit" value="" name="search" class="searchBtn" /> {% if active_tab == "tags" %} <input type="hidden" name="t" value="tag"/> {% else %} @@ -21,26 +11,13 @@ {% endif %} {# class was searchInput #} <input - {% if query %} - class="searchInputCancelable" - {% else %} class="searchInput" - {% endif %} type="text" autocomplete="off" value="{{ query|default_if_none('') }}" name="query" id="keywords" /> - <input type="button" - value="X" - name="reset_query" - class="cancelSearchBtn" - {% if not query %}{# query is only defined by questions view (active_tab) #} - style="display: none;" - {% endif %} - /> - </form> </div> {% endspaceless %} {% endif %} diff --git a/askbot/templates/widgets/secondary_header.html b/askbot/templates/widgets/secondary_header.html index caf190bc..f0aca706 100644 --- a/askbot/templates/widgets/secondary_header.html +++ b/askbot/templates/widgets/secondary_header.html @@ -1,12 +1,45 @@ <!-- template secondary_header.html --> <div id="secondaryHeader"> <div class="content-wrapper"> - <a id="homeButton" href="{% url questions %}"></a> - <div id="scopeWrapper"> - {% include "widgets/scope_nav.html" %} + {# form is wrapping search buttons and the search bar inputs #} + <form + {% if active_tab == "tags" %} + action="{% url tags %}" + {% elif active_tab == "users" %} + action="{% url users %}" + {% else %} + action="{% url questions %}" id="searchForm" + {% endif %} + method="get"> + <div> + {# + Some or all contents of this div may be dropped + over the search bar via negative margins, + to make sure that the search bar can occupy 100% + of the content width. + Search bar may have padding on the left and right + to accomodate the buttons. + #} + <a id="homeButton" href="{% url questions %}"></a> + {% include "widgets/scope_nav.html" %} + {# + three buttons below are in the opposite order because + they are floated at the right + #} + {% include "widgets/ask_button.html" %} + <input type="submit" value="" name="search" class="searchBtn" /> + <input type="button" + value="X" + name="reset_query" + class="cancelSearchBtn" + {% if not query %}{# query is only defined by questions view (active_tab) #} + style="display: none;" + {% endif %} + /> + {# clears button floats #} + <div class="clearfix"></div> + </div> {% include "widgets/search_bar.html" %} {# include search form widget #} - </div> - {% include "widgets/ask_button.html" %} - <div class="clean"></div> + </form> </div> </div> diff --git a/askbot/templates/widgets/user_navigation.html b/askbot/templates/widgets/user_navigation.html index 06b0cdb9..4cb6314a 100644 --- a/askbot/templates/widgets/user_navigation.html +++ b/askbot/templates/widgets/user_navigation.html @@ -20,7 +20,7 @@ <a href="{{ settings.LOGIN_URL }}?next={{request.path|clean_login_url}}">{% trans %}Hi there! Please sign in{% endtrans %}</a> {% endif %} {% if request.user.is_authenticated() and request.user.is_administrator() %} - <a href="{% url site_settings %}">{% trans %}settings{% endtrans %}</a> - <a href="{% url widgets %}">{% trans %}widgets{% endtrans %}</a> + <a class="settings" href="{% url site_settings %}">{% trans %}settings{% endtrans %}</a> + <a class="widgets" href="{% url widgets %}">{% trans %}widgets{% endtrans %}</a> {% endif %} - <a href="{% url "help" %}" title="{% trans %}help{% endtrans %}">{% trans %}help{% endtrans %}</a> + <a class="help" href="{% url "help" %}" title="{% trans %}help{% endtrans %}">{% trans %}help{% endtrans %}</a> diff --git a/askbot/tests/page_load_tests.py b/askbot/tests/page_load_tests.py index 6c820fef..73a646d6 100644 --- a/askbot/tests/page_load_tests.py +++ b/askbot/tests/page_load_tests.py @@ -153,14 +153,14 @@ class PageLoadTestCase(AskbotTestCase): self.proto_test_ask_page(True, 200) @with_settings(GROUPS_ENABLED=False) - def test_api_get_questions_groups_disabled(self): - data = {'query': 'Question'} - response = self.client.get(reverse('api_get_questions'), data) + def test_title_search_groups_disabled(self): + data = {'query_text': 'Question'} + response = self.client.get(reverse('title_search'), data) data = simplejson.loads(response.content) self.assertTrue(len(data) > 1) @with_settings(GROUPS_ENABLED=True) - def test_api_get_questions_groups_enabled(self): + def test_title_search_groups_enabled(self): group = models.Group(name='secret group', openness=models.Group.OPEN) group.save() @@ -169,14 +169,14 @@ class PageLoadTestCase(AskbotTestCase): question = self.post_question(user=user, title='alibaba', group_id=group.id) #ask for data anonymously - should get nothing - query_data = {'query': 'alibaba'} - response = self.client.get(reverse('api_get_questions'), query_data) + query_data = {'query_text': 'alibaba'} + response = self.client.get(reverse('title_search'), query_data) response_data = simplejson.loads(response.content) self.assertEqual(len(response_data), 0) #log in - should get the question self.client.login(method='force', user_id=user.id) - response = self.client.get(reverse('api_get_questions'), query_data) + response = self.client.get(reverse('title_search'), query_data) response_data = simplejson.loads(response.content) self.assertEqual(len(response_data), 1) diff --git a/askbot/urls.py b/askbot/urls.py index 4694b38c..4fd63658 100644 --- a/askbot/urls.py +++ b/askbot/urls.py @@ -74,9 +74,9 @@ urlpatterns = patterns('', # END main page urls url( - r'^api/get_questions/', - views.commands.api_get_questions, - name='api_get_questions' + r'^api/title_search/', + views.commands.title_search, + name='title_search' ), url( r'^get-thread-shared-users/', diff --git a/askbot/views/commands.py b/askbot/views/commands.py index de5bb12b..c901b6c6 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -696,31 +696,30 @@ def subscribe_for_tags(request): @decorators.get_only -def api_get_questions(request): - """json api for retrieving questions""" - query = request.GET.get('query', '').strip() - if not query: +def title_search(request): + """json api for retrieving questions by title match""" + query = request.GET.get('query_text') + + if query is None: return HttpResponseBadRequest('Invalid query') + query = query.strip() + if askbot_settings.GROUPS_ENABLED: threads = models.Thread.objects.get_visible(user=request.user) else: threads = models.Thread.objects.all() - threads = models.Thread.objects.get_for_query( - search_query=query, - qs=threads - ) - - if should_show_sort_by_relevance(): - threads = threads.extra(order_by = ['-relevance']) + threads = threads.get_for_title_query(query) #todo: filter out deleted threads, for now there is no way threads = threads.distinct()[:30] + thread_list = [{ 'title': escape(thread.title), 'url': thread.get_absolute_url(), 'answer_count': thread.get_answer_count(request.user) } for thread in threads] + json_data = simplejson.dumps(thread_list) return HttpResponse(json_data, mimetype = "application/json") |