summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--askbot/forms.py6
-rw-r--r--askbot/media/js/live_search.js1037
-rw-r--r--askbot/media/js/live_search_new_thread.js4
-rw-r--r--askbot/media/js/utils.js76
-rw-r--r--askbot/media/style/style.css364
-rw-r--r--askbot/media/style/style.less475
-rw-r--r--askbot/migrations/0157_add_title_search_indices_for_postgresql_and_mysql.py405
-rw-r--r--askbot/migrations_api/__init__.py20
-rw-r--r--askbot/models/__init__.py2
-rw-r--r--askbot/models/question.py20
-rw-r--r--askbot/search/mysql.py11
-rw-r--r--askbot/search/postgresql/__init__.py24
-rw-r--r--askbot/search/postgresql/thread_and_post_models_27112012.plsql231
-rw-r--r--askbot/search/state_manager.py6
-rw-r--r--askbot/skins/utils.py2
-rw-r--r--askbot/templates/answer_edit.html2
-rw-r--r--askbot/templates/ask.html7
-rw-r--r--askbot/templates/base.html1
-rw-r--r--askbot/templates/embed/ask_by_widget.html12
-rw-r--r--askbot/templates/macros.html12
-rw-r--r--askbot/templates/main_page/javascript.html7
-rw-r--r--askbot/templates/main_page/tab_bar.html4
-rw-r--r--askbot/templates/meta/bottom_scripts.html35
-rw-r--r--askbot/templates/user_profile/user_stats.html3
-rw-r--r--askbot/templates/widgets/edit_post.html2
-rw-r--r--askbot/templates/widgets/scope_nav.html2
-rw-r--r--askbot/templates/widgets/search_bar.html25
-rw-r--r--askbot/templates/widgets/secondary_header.html45
-rw-r--r--askbot/templates/widgets/user_navigation.html6
-rw-r--r--askbot/tests/page_load_tests.py14
-rw-r--r--askbot/urls.py8
-rw-r--r--askbot/views/commands.py36
32 files changed, 2257 insertions, 647 deletions
diff --git a/askbot/forms.py b/askbot/forms.py
index ba52a891..5168e354 100644
--- a/askbot/forms.py
+++ b/askbot/forms.py
@@ -1157,11 +1157,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..ef1b6e43 100644
--- a/askbot/media/js/live_search.js
+++ b/askbot/media/js/live_search.js
@@ -1,3 +1,190 @@
+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) {
+ //count is 0 with no title matches, curItem is 0 when none is selected
+ 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
+ if (curItem === 0) {
+ return true;
+ } else {
+ 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 +193,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 +237,602 @@ 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();
+};
- var render_related_tags = function(tags, query_string){
- if (tags.length === 0) return;
+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 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">&#215; ');
- html_list.push(tags[i]['used_count']);
- html_list.push('</span>');
- html_list.push('<br />');
+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
+ );
+};
+
+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">&#215; ');
+ 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' ? ' &#9650;':' &#9660;');
- 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' ? ' &#9650;':' &#9660;');
+ 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.makeFormSubmitHandler = function() {
+ var me = this;
+ var baseUrl = me._baseUrl;
+ return function(evt) {
+ // if user clicks the button the s(h)e probably wants page reload,
+ // so provide that experience but first update the query string
+ me.updateQueryString();
+ var searchUrl = me.getSearchUrl();
+ evt.preventDefault();
+ window.location.href = baseUrl + searchUrl;
+ };
+};
+
+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());
+ //below is called after getElement, b/c element must be defined
+ if (this._prevText !== '') {
+ toolTip.hide();//hide if search query is not empty
+ }
+ this._toolTip = toolTip;
- // Wire search tags
+ var dropMenu = new SearchDropMenu();
+ dropMenu.setAskHandler(this.makeAskHandler());
+ dropMenu.setAskButtonEnabled(this._askButtonEnabled);
+ this._dropMenu = dropMenu;
+ element.parent().after(this._dropMenu.getElement());
+
+ $(element).click(function(e) { return false });
+ $(document).click(function() { dropMenu.reset(); });
+
+ //the tag search input is optional in askbot
+ $('#ab-tag-search').parent().before(
+ this._tag_warning_box.getElement()
+ );
+
+ // 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();
- $("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;
- });
-
- /* *********************************** */
-
- // Hook for tag_selector.js
- liveSearch.refresh = function () {
- send_query();
- };
+ $("form#searchForm").submit(me.makeFormSubmitHandler());
};
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 3c2a478b..cf748ee6 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)){
@@ -116,15 +128,16 @@ var setupButtonEventHandlers = function(button, callback){
var putCursorAtEnd = function(element){
- var el = element.get()[0];
+ var el = $(element).get()[0];
+ var jEl = $(el);
if (el.setSelectionRange){
- var len = element.val().length * 2;
+ var len = jEl.val().length * 2;
el.setSelectionRange(len, len);
}
else{
- element.val(element.val());
+ jEl.val(jEl.val());
}
- element.scrollTop = 999999;
+ jEl.scrollTop(999999);
};
var setCheckBoxesIn = function(selector, value){
@@ -201,7 +214,8 @@ QSutils.patch_query_string = function (query_string, patch, remove) {
var mapping = {};
if(!remove) {
- mapping[patch_split[0]] = patch_split[1]; // prepopulate the patched selector if it's not meant to be removed
+ // prepopulate the patched selector if it's not meant to be removed
+ mapping[patch_split[0]] = patch_split[1];
}
for (var i = 0; i < params.length; i++) {
@@ -411,6 +425,30 @@ OneShotForm.prototype.decorate = function(element) {
/**
* @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() {
@@ -1538,10 +1576,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;
};
@@ -1572,7 +1614,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();
@@ -1697,6 +1753,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.css b/askbot/media/style/style.css
index 1f5aabc5..7c3f6f68 100644
--- a/askbot/media/style/style.css
+++ b/askbot/media/style/style.css
@@ -163,6 +163,9 @@ html {
a:hover {
text-decoration: underline;
}
+.avatar-box {
+ text-decoration: none;
+}
.badge-context-toggle.active {
cursor: pointer;
text-decoration: underline;
@@ -380,7 +383,7 @@ body.user-messages {
margin-bottom: 10px;
font-family: 'Open Sans Condensed', Arial, sans-serif;
}
-#secondaryHeader #homeButton {
+#homeButton {
border-right: #afaf9e 1px solid;
background: -6px -36px url(../images/sprites.png) no-repeat;
height: 55px;
@@ -388,28 +391,22 @@ body.user-messages {
display: block;
float: left;
}
-#secondaryHeader #homeButton:hover {
+#homeButton:hover {
background: -51px -36px url(../images/sprites.png) no-repeat;
}
-#secondaryHeader #scopeWrapper {
- width: 688px;
- float: left;
-}
-#secondaryHeader #scopeWrapper a {
+.scope-selector {
display: block;
float: left;
-}
-#secondaryHeader #scopeWrapper .scope-selector {
font-size: 20px;
color: #7a7a6b;
height: 55px;
line-height: 55px;
margin-left: 16px;
}
-#secondaryHeader #scopeWrapper .on {
+.scope-selector.on {
background: url(../images/scopearrow.png) no-repeat center bottom;
}
-#secondaryHeader #scopeWrapper .ask-message {
+.scope-selector.ask-message {
font-size: 24px;
}
.validate-email-page label {
@@ -431,78 +428,122 @@ 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;
}
-#searchBar .searchInput,
-#searchBar .searchInputCancelable {
- font-size: 26px;
- height: 39px;
- line-height: 39px;
+#searchBar 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: Arial;
- vertical-align: top;
+ width: 100%;
+ margin: 8px 0 6px 0;
+ padding: 0;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
}
-#searchBar .searchInput {
- width: 340px;
+#searchBar div.input-tool-tip {
+ margin-top: -40px;
+ padding-top: 10px;
+ height: 30px;
+ line-height: 20px;
+ font-size: 20px;
+ font-style: italic;
}
-#searchBar .searchInputCancelable {
- width: 305px;
+.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;
}
-#searchBar .logoutsearch {
- width: 337px;
+.search-drop-menu ul {
+ list-style: none;
+ overflow: auto;
+ padding: 0 0 10px 0;
+ margin: 0 0 20px 0;
+ position: relative;
}
-#searchBar .searchBtn {
+.search-drop-menu ul li {
+ padding: 5px 10px;
+ position: relative;
+}
+.search-drop-menu ul li a {
+ text-decoration: none;
+}
+.search-drop-menu ul li.selected {
+ background: #08c;
+}
+.search-drop-menu ul li.selected a {
+ color: whitesmoke;
+}
+.search-drop-menu ul.empty {
+ padding: 5px;
+ margin: 0;
+}
+.search-drop-menu .footer {
+ text-align: center;
+ padding-bottom: 10px;
+}
+.input-tool-tip {
+ color: #999;
+}
+.input-tool-tip.dimmed {
+ color: #ccc;
+}
+input[type="submit"].searchBtn {
font-size: 10px;
color: #666;
background-color: #eee;
- height: 42px;
+ height: 41px;
border: #FFF 1px solid;
line-height: 22px;
text-align: center;
float: right;
- margin: 0px;
+ margin: 7px 28px 0 0;
width: 48px;
background: -98px -36px url(../images/sprites.png) no-repeat;
cursor: pointer;
+ position: relative;
+ z-index: 10001;
+}
+.ask-page input[type="submit"].searchBtn {
+ display: none;
}
-#searchBar .searchBtn:hover {
+.searchBtn:hover {
background: -146px -36px url(../images/sprites.png) no-repeat;
}
-#searchBar .cancelSearchBtn {
+.cancelSearchBtn {
font-size: 30px;
color: #ce8888;
background: #fff;
- height: 42px;
+ 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;
}
-#searchBar .cancelSearchBtn:hover {
+.cancelSearchBtn:hover {
color: #d84040;
}
-body.anon #searchBar {
- width: 500px;
-}
-body.anon #searchBar .searchInput {
- width: 435px;
-}
-body.anon #searchBar .searchInputCancelable {
- width: 405px;
-}
#askButton {
/* check blocks/secondary_header.html and widgets/ask_button.html*/
@@ -555,6 +596,35 @@ body.anon #searchBar .searchInputCancelable {
-moz-text-shadow: 0px 1px 0px #c6d9dd;
-webkit-text-shadow: 0px 1px 0px #c6d9dd;
}
+/*
+ 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,
+body.anon .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 {
width: 730px;
@@ -1425,8 +1495,9 @@ ul#related-tags li {
}
#askFormBar {
display: inline-block;
- padding: 4px 7px 0px 0px;
+ padding: 4px 0 0 0;
margin-top: 0px;
+ width: 100%;
}
#askFormBar p {
margin: 0 0 5px 0;
@@ -1438,10 +1509,14 @@ 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;*/
+
}
.ask-page div#question-list,
.edit-question-page div#question-list {
@@ -1487,11 +1562,13 @@ ul#related-tags li {
}
.ask-page #id_tags,
.edit-question-page #id_tags {
+ box-sizing: border-box;
border: #cce6ec 3px solid;
- height: 25px;
+ height: 31px;
padding-left: 5px;
font-size: 14px;
- width: 395px;
+ width: 100%;
+ max-width: 395px;
}
.ask-page #id_post_author_username,
.question-page #id_post_author_username,
@@ -1762,12 +1839,17 @@ ul#related-tags li {
.edit-question-page .wmd-container,
.edit-answer-page .wmd-container {
width: 723px;
+ width: 100%;
}
.ask-page #editor,
.question-page #editor,
.edit-question-page #editor,
.edit-answer-page #editor {
- width: 710px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 100%;
+ height: 100%;
padding: 6px;
}
.ask-page .retagger-buttons button,
@@ -1891,6 +1973,7 @@ ul#related-tags li {
font-size: 14px;
margin-top: 5px;
margin-bottom: 5px;
+ width: 100%;
}
.edit-question-page #id_title,
#fmedit #id_title,
@@ -1901,7 +1984,7 @@ ul#related-tags li {
margin: 0px;
padding: 0px 0 0 5px;
border: #cce6ec 3px solid;
- width: 719px;
+ width: 100%;
margin-bottom: 10px;
}
.edit-question-page #id_summary,
@@ -1980,6 +2063,7 @@ ul#related-tags li {
margin-top: 10px;
font-family: Arial;
color: #4b4b4b;
+ word-wrap: break-word;
}
.question-page .question-body p,
.question-page .answer-body p {
@@ -3922,22 +4006,12 @@ pre.prettyprint {
}
/* language-specific fixes */
body.lang-es #searchBar {
- width: 398px;
-}
-body.lang-es #searchBar .searchInput {
- width: 337px;
-}
-body.lang-es #searchBar .searchInputCancelable {
- width: 302px;
+ /* need special left padding */
+
}
body.anon.lang-es #searchBar {
- width: 485px;
-}
-body.anon.lang-es #searchBar .searchInput {
- width: 425px;
-}
-body.anon.lang-es #searchBar .searchInputCancelable {
- width: 390px;
+ /* need special left padding */
+
}
/* user groups */
#user-groups ul {
@@ -4145,6 +4219,12 @@ textarea.tipped-input {
/* modifications for small screens */
@media screen and (max-width: 960px) {
/* content margins touch viewport */
+ #homeButton {
+ background: 1px -36px url(../images/sprites.png) no-repeat;
+ }
+ #homeButton:hover {
+ background: -44px -36px url(../images/sprites.png) no-repeat;
+ }
}
@media screen and (max-width: 480px) {
.content-wrapper {
@@ -4156,24 +4236,148 @@ 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;
+ .ask-page input[type="submit"].searchBtn,
+ .edit-question-page input[type="submit"].searchBtn {
+ display: none;
+ }
+ .ask-page .preview-toggle,
+ .edit-answer-page .preview-toggle,
+ .edit-question-page .preview-toggle,
+ .ask-page .proxy-user-info,
+ .edit-answer-page .proxy-user-info,
+ .edit-question-page .proxy-user-info,
+ .ask-page .answer-options,
+ .edit-answer-page .answer-options,
+ .edit-question-page .answer-options,
+ .ask-page .question-options,
+ .edit-answer-page .question-options,
+ .edit-question-page .question-options,
+ .ask-page .revision-comment,
+ .edit-answer-page .revision-comment,
+ .edit-question-page .revision-comment,
+ .ask-page .wmd-preview,
+ .edit-answer-page .wmd-preview,
+ .edit-question-page .wmd-preview,
+ .ask-page #wmd-hr-button,
+ .edit-answer-page #wmd-hr-button,
+ .edit-question-page #wmd-hr-button,
+ .ask-page #wmd-heading-button,
+ .edit-answer-page #wmd-heading-button,
+ .edit-question-page #wmd-heading-button {
+ display: none;
+ }
+ .edit-answer-page label[for="id_title"],
+ .edit-question-page label[for="id_title"],
+ .edit-answer-page label[for="id_revision"],
+ .edit-question-page label[for="id_revision"],
+ .edit-answer-page #id_revision,
+ .edit-question-page #id_revision {
+ display: none;
+ }
+ .edit-answer-page #fmedit #id_title,
+ .edit-question-page #fmedit #id_title {
+ margin: 15px 0 0 0;
+ }
+ .question-page .comment-votes {
+ display: none;
+ }
+ .question-page .comments form.post-comments {
+ margin: 0 10px 0 0;
+ }
+ .question-page .comments .comment .comment-body {
+ margin-left: 5px;
+ }
+ .question-page .post-update-info-container {
float: none;
- margin: auto;
- margin-top: 5px;
+ width: 100%;
}
- .main-page h1,
- .main-page .counts .views,
- .main-page .counts .votes,
- .main-page .rss,
- .main-page .tabBar,
- .main-page .tags,
- .main-page .userinfo {
+ .question-page .post-update-info {
+ float: none;
+ margin-left: 0;
+ width: 95%;
+ }
+ .question-page .post-update-info br,
+ .question-page .post-update-info .badge1,
+ .question-page .post-update-info .badge2,
+ .question-page .post-update-info .badge3,
+ .question-page .post-update-info .gravatar,
+ .question-page .post-update-info .reputation-score,
+ .question-page .post-update-info .badge-count {
display: none;
}
+ .question-page .question-content,
+ .question-page ul.post-retag input {
+ width: 88%;
+ }
+ .question-page .answer-table,
+ .question-page #question-table {
+ width: 85%;
+ }
+ .users-page .userList td {
+ display: block;
+ width: 100%;
+ }
+ .users-page .userList td .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;
+ }
+ #metaNav a#navUsers,
+ #metaNav a#navTags,
+ #metaNav 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/media/style/style.less b/askbot/media/style/style.less
index 2ad46f23..515e9e57 100644
--- a/askbot/media/style/style.less
+++ b/askbot/media/style/style.less
@@ -152,6 +152,10 @@ a:hover {
text-decoration: underline;
}
+.avatar-box {
+ text-decoration: none;
+}
+
.badge-context-toggle.active {
cursor: pointer;
text-decoration: underline;
@@ -395,45 +399,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 +452,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 +593,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 +1493,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 +1507,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 +1554,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 +1719,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 +1862,7 @@ ul#related-tags li {
font-size:14px;
margin-top:5px;
margin-bottom:5px;
+ width: 100%;
}
#id_title{
font-size: 24px;
@@ -1792,7 +1871,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 +1917,7 @@ ul#related-tags li {
vertical-align: top;
}
- .question-content{
+ .question-content {
float:right;
width:682px;
margin-bottom:10px;
@@ -2512,7 +2591,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;
@@ -3757,24 +3839,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 */
}
}
@@ -4008,6 +4078,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 {
@@ -4019,27 +4095,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/migrations/0157_add_title_search_indices_for_postgresql_and_mysql.py b/askbot/migrations/0157_add_title_search_indices_for_postgresql_and_mysql.py
new file mode 100644
index 00000000..f2351a02
--- /dev/null
+++ b/askbot/migrations/0157_add_title_search_indices_for_postgresql_and_mysql.py
@@ -0,0 +1,405 @@
+# -*- coding: utf-8 -*-
+import askbot
+import datetime
+import os
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+from askbot.migrations_api import mysql_table_supports_full_text_search
+from askbot.migrations_api import get_drop_index_sql
+from askbot.migrations_api import get_create_full_text_index_sql
+from askbot.search import postgresql
+
+INDEX_NAME = 'askbot_thread_search_index'
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ "Write your forwards methods here."
+ # Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..."
+ if db.backend_name == 'mysql':
+ table_name = orm['askbot.Thread']._meta.db_table
+ if mysql_table_supports_full_text_search(table_name):
+ columns = ('title', 'tagnames')
+ sql = get_create_full_text_index_sql(INDEX_NAME, table_name, columns)
+ db.execute(sql)
+ elif db.backend_name == 'postgres':
+ script_path = os.path.join(
+ askbot.get_install_directory(),
+ 'search',
+ 'postgresql',
+ 'thread_and_post_models_27112012.plsql'
+ )
+ postgresql.setup_full_text_search(script_path)
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+ if db.backend_name == 'mysql':
+ table_name = orm['askbot.Thread']._meta.db_table
+ if mysql_table_supports_full_text_search(table_name):
+ sql = get_drop_index_sql(INDEX_NAME, table_name)
+ db.execute(sql)
+
+ models = {
+ 'askbot.activity': {
+ 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"},
+ 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True'}),
+ 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'symmetrical': 'False', 'through': "orm['askbot.ActivityAuditStatus']", 'to': "orm['auth.User']"}),
+ 'summary': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.activityauditstatus': {
+ 'Meta': {'unique_together': "(('user', 'activity'),)", 'object_name': 'ActivityAuditStatus'},
+ 'activity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Activity']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.anonymousanswer': {
+ 'Meta': {'object_name': 'AnonymousAnswer'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Post']"}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.anonymousquestion': {
+ 'Meta': {'object_name': 'AnonymousQuestion'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.askwidget': {
+ 'Meta': {'object_name': 'AskWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'include_text_field': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'inner_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'outer_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Tag']", 'null': 'True', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.award': {
+ 'Meta': {'object_name': 'Award', 'db_table': "u'award'"},
+ 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.BadgeData']"}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.badgedata': {
+ 'Meta': {'ordering': "('slug',)", 'object_name': 'BadgeData'},
+ 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'symmetrical': 'False', 'through': "orm['askbot.Award']", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
+ },
+ 'askbot.draftanswer': {
+ 'Meta': {'object_name': 'DraftAnswer'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['askbot.Thread']"})
+ },
+ 'askbot.draftquestion': {
+ 'Meta': {'object_name': 'DraftQuestion'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125', 'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300', 'null': 'True'})
+ },
+ 'askbot.emailfeedsetting': {
+ 'Meta': {'unique_together': "(('subscriber', 'feed_type'),)", 'object_name': 'EmailFeedSetting'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.favoritequestion': {
+ 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.group': {
+ 'Meta': {'object_name': 'Group', '_ormbases': ['auth.Group']},
+ 'description': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'described_group'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
+ 'is_vip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}),
+ 'moderate_answers_to_enquirers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'openness': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}),
+ 'preapproved_email_domains': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'preapproved_emails': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.groupmembership': {
+ 'Meta': {'object_name': 'GroupMembership', '_ormbases': ['auth.AuthUserGroups']},
+ 'authusergroups_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.AuthUserGroups']", 'unique': 'True', 'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.markedtag': {
+ 'Meta': {'object_name': 'MarkedTag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.post': {
+ 'Meta': {'object_name': 'Post'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}),
+ 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_posts'", 'symmetrical': 'False', 'through': "orm['askbot.PostToGroup']", 'to': "orm['askbot.Group']"}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'old_answer_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_comment_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_question_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'summary': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'posts'", 'null': 'True', 'blank': 'True', 'to': "orm['askbot.Thread']"}),
+ 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.postflagreason': {
+ 'Meta': {'object_name': 'PostFlagReason'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'details': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'post_reject_reasons'", 'to': "orm['askbot.Post']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'askbot.postrevision': {
+ 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'},
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'approved_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'approved_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}),
+ 'by_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'revised_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '125', 'blank': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'})
+ },
+ 'askbot.posttogroup': {
+ 'Meta': {'unique_together': "(('post', 'group'),)", 'object_name': 'PostToGroup', 'db_table': "'askbot_post_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']"})
+ },
+ 'askbot.questionview': {
+ 'Meta': {'object_name': 'QuestionView'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Post']"}),
+ 'when': ('django.db.models.fields.DateTimeField', [], {}),
+ 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.questionwidget': {
+ 'Meta': {'object_name': 'QuestionWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'order_by': ('django.db.models.fields.CharField', [], {'default': "'-added_at'", 'max_length': '18'}),
+ 'question_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '7'}),
+ 'search_query': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'style': ('django.db.models.fields.TextField', [], {'default': '"\\n@import url(\'http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:300,400,700\');\\nbody {\\n overflow: hidden;\\n}\\n\\n#container {\\n width: 200px;\\n height: 350px;\\n}\\nul {\\n list-style: none;\\n padding: 5px;\\n margin: 5px;\\n}\\nli {\\n border-bottom: #CCC 1px solid;\\n padding-bottom: 5px;\\n padding-top: 5px;\\n}\\nli:last-child {\\n border: none;\\n}\\na {\\n text-decoration: none;\\n color: #464646;\\n font-family: \'Yanone Kaffeesatz\', sans-serif;\\n font-size: 15px;\\n}\\n"', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.replyaddress': {
+ 'Meta': {'object_name': 'ReplyAddress'},
+ 'address': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}),
+ 'allowed_from_email': ('django.db.models.fields.EmailField', [], {'max_length': '150'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reply_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'reply_action': ('django.db.models.fields.CharField', [], {'default': "'auto_answer_or_comment'", 'max_length': '32'}),
+ 'response_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'edit_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.repute': {
+ 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.tag': {
+ 'Meta': {'ordering': "('-used_count', 'name')", 'object_name': 'Tag', 'db_table': "u'tag'"},
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': "orm['auth.User']"}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'suggested_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'suggested_tags'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'tag_wiki': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'described_tag'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.thread': {
+ 'Meta': {'object_name': 'Thread'},
+ 'accepted_answer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'answer_accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'unused_favorite_threads'", 'symmetrical': 'False', 'through': "orm['askbot.FavoriteQuestion']", 'to': "orm['auth.User']"}),
+ 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_threads'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_threads'", 'symmetrical': 'False', 'through': "orm['askbot.ThreadToGroup']", 'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'unused_last_active_in_threads'", 'to': "orm['auth.User']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'threads'", 'symmetrical': 'False', 'to': "orm['askbot.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.threadtogroup': {
+ 'Meta': {'unique_together': "(('thread', 'group'),)", 'object_name': 'ThreadToGroup', 'db_table': "'askbot_thread_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'visibility': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.vote': {
+ 'Meta': {'unique_together': "(('user', 'voted_post'),)", 'object_name': 'Vote', 'db_table': "u'vote'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['auth.User']"}),
+ 'vote': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'voted_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['askbot.Post']"})
+ },
+ 'auth.authusergroups': {
+ 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'AuthUserGroups', 'db_table': "'auth_user_groups'", 'managed': 'False'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
+ 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
+ 'email_signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_fake': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
+ 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'show_marked_tags': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['askbot']
+ symmetrical = True
diff --git a/askbot/migrations_api/__init__.py b/askbot/migrations_api/__init__.py
index 586c02d9..0260f51b 100644
--- a/askbot/migrations_api/__init__.py
+++ b/askbot/migrations_api/__init__.py
@@ -5,6 +5,7 @@ since models change with time, this api is implemented in different
versions. Different versions do not need to have all the same functions.
"""
from south.db import db
+from django.db import connection
def safe_add_column(table, column, column_data, keep_default = False):
"""when user calls syncdb with askbot the first time
@@ -22,6 +23,25 @@ def safe_add_column(table, column, column_data, keep_default = False):
return False
+def mysql_table_supports_full_text_search(table_name):
+ """true, if engine is MyISAM"""
+ cursor = connection.cursor()
+ cursor.execute("SHOW CREATE TABLE %s" % table_name)
+ data = cursor.fetchone()
+ return 'ENGINE=MyISAM' in data[1]
+
+
+def get_drop_index_sql(index_name, table_name):
+ """returns sql for dropping index by name on table"""
+ return 'ALTER TABLE %s DROP INDEX %s' % (table_name, index_name)
+
+
+def get_create_full_text_index_sql(index_name, table_name, column_list):
+ column_sql = '(%s)' % ','.join(column_list)
+ query_template = 'CREATE FULLTEXT INDEX %s on %s %s'
+ return query_template % (index_name, table_name, column_sql)
+
+
class BaseAPI(object):
def __init__(self, orm):
self.orm = orm
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py
index 279998a9..cfca46a5 100644
--- a/askbot/models/__init__.py
+++ b/askbot/models/__init__.py
@@ -109,7 +109,7 @@ def get_users_by_text_query(search_query, users_query_set = None):
users_query_set = User.objects.all()
if 'postgresql_psycopg2' in askbot.get_database_engine_name():
from askbot.search import postgresql
- return postgresql.run_full_text_search(users_query_set, search_query)
+ return postgresql.run_thread_search(users_query_set, search_query)
else:
return users_query_set.filter(
models.Q(username__icontains=search_query) |
diff --git a/askbot/models/question.py b/askbot/models/question.py
index 5c0d5732..361ea8ad 100644
--- a/askbot/models/question.py
+++ b/askbot/models/question.py
@@ -42,6 +42,23 @@ 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
+ """
+ db_engine_name = askbot.get_database_engine_name()
+ if 'postgresql_psycopg2' in db_engine_name:
+ from askbot.search import postgresql
+ return postgresql.run_title_search(
+ self, search_query
+ ).order_by('-relevance')
+ elif 'mysql' in db_engine_name and mysql.supports_full_text_search():
+ return self.filter(title__search=search_query)
+ else:
+ return self.filter(title__icontains=search_query)
+
+
class ThreadManager(BaseQuerySetManager):
def get_query_set(self):
@@ -174,6 +191,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
@@ -195,7 +213,7 @@ class ThreadManager(BaseQuerySetManager):
)
elif 'postgresql_psycopg2' in askbot.get_database_engine_name():
from askbot.search import postgresql
- return postgresql.run_full_text_search(qs, search_query)
+ return postgresql.run_thread_search(qs, search_query)
else:
return qs.filter(
models.Q(title__icontains=search_query) |
diff --git a/askbot/search/mysql.py b/askbot/search/mysql.py
index 055b30ca..e6d2e21e 100644
--- a/askbot/search/mysql.py
+++ b/askbot/search/mysql.py
@@ -26,14 +26,3 @@ def supports_full_text_search():
else:
SUPPORTS_FTS = False
return SUPPORTS_FTS
-
-def get_create_full_text_index_sql(index_name, table_name, column_list):
- cursor = connection.cursor()
- column_sql = '(%s)' % ','.join(column_list)
- sql = 'CREATE FULLTEXT INDEX %s on %s %s' % (index_name, table_name, column_sql)
- cursor.execute(sql)
- return sql
-
-def get_drop_index_sql(index_name, table_name):
- return 'ALTER TABLE %s DROP INDEX %s' % (table_name, index_name)
-
diff --git a/askbot/search/postgresql/__init__.py b/askbot/search/postgresql/__init__.py
index 5b893129..42e10c5f 100644
--- a/askbot/search/postgresql/__init__.py
+++ b/askbot/search/postgresql/__init__.py
@@ -20,7 +20,7 @@ def setup_full_text_search(script_path):
finally:
cursor.close()
-def run_full_text_search(query_set, query_text):
+def run_full_text_search(query_set, query_text, text_search_vector_name):
"""runs full text search against the query set and
the search text. All words in the query text are
added to the search with the & operator - i.e.
@@ -29,14 +29,17 @@ def run_full_text_search(query_set, query_text):
It is also assumed that we ar searching in the same
table as the query set was built against, also
it is assumed that the table has text search vector
- stored in the column called `text_search_vector`.
+ stored in the column called with value of`text_search_vector_name`.
"""
table_name = query_set.model._meta.db_table
-
+
rank_clause = 'ts_rank(' + table_name + \
- '.text_search_vector, plainto_tsquery(%s))'
-
- where_clause = table_name + '.text_search_vector @@ plainto_tsquery(%s)'
+ '.' + text_search_vector_name + \
+ ', plainto_tsquery(%s))'
+
+ where_clause = table_name + '.' + \
+ text_search_vector_name + \
+ ' @@ plainto_tsquery(%s)'
search_query = '&'.join(query_text.split())#apply "AND" operator
extra_params = (search_query,)
@@ -46,5 +49,12 @@ def run_full_text_search(query_set, query_text):
'params': extra_params,
'select_params': extra_params,
}
-
return query_set.extra(**extra_kwargs)
+
+def run_thread_search(query_set, query):
+ """runs search for full thread content"""
+ return run_full_text_search(query_set, query, 'text_search_vector');
+
+def run_title_search(query_set, query):
+ """runs search for title and tags"""
+ return run_full_text_search(query_set, query, 'title_search_vector')
diff --git a/askbot/search/postgresql/thread_and_post_models_27112012.plsql b/askbot/search/postgresql/thread_and_post_models_27112012.plsql
new file mode 100644
index 00000000..9e90522d
--- /dev/null
+++ b/askbot/search/postgresql/thread_and_post_models_27112012.plsql
@@ -0,0 +1,231 @@
+/* function testing for existence of a column in a table
+ if table does not exists, function will return "false" */
+CREATE OR REPLACE FUNCTION column_exists(colname text, tablename text)
+RETURNS boolean AS
+$$
+DECLARE
+ q text;
+ onerow record;
+BEGIN
+
+ q = 'SELECT attname FROM pg_attribute WHERE attrelid = ( SELECT oid FROM pg_class WHERE relname = '''||tablename||''') AND attname = '''||colname||'''';
+
+ FOR onerow IN EXECUTE q LOOP
+ RETURN true;
+ END LOOP;
+
+ RETURN false;
+END;
+$$ LANGUAGE plpgsql;
+
+/* function adding tsvector column to table if it does not exists */
+CREATE OR REPLACE FUNCTION add_tsvector_column(colname text, tablename text)
+RETURNS boolean AS
+$$
+DECLARE
+ q text;
+BEGIN
+ IF NOT column_exists(colname, tablename) THEN
+ q = 'ALTER TABLE ' || tablename || ' ADD COLUMN ' || colname || ' tsvector';
+ EXECUTE q;
+ RETURN true;
+ ELSE
+ q = 'UPDATE ' || tablename || ' SET ' || colname || '=NULL';
+ EXECUTE q;
+ RETURN false;
+ END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+/* aggregate function that concatenates tsvectors */
+CREATE OR REPLACE FUNCTION tsv_add(tsv1 tsvector, tsv2 tsvector)
+RETURNS tsvector AS
+$$
+BEGIN
+ RETURN tsv1 || tsv2;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION setup_aggregates() RETURNS boolean AS
+$$
+DECLARE
+ onerow record;
+BEGIN
+ FOR onerow IN SELECT * FROM pg_proc WHERE proname = 'concat_tsvectors' AND proisagg LOOP
+ DROP AGGREGATE concat_tsvectors(tsvector);
+ END LOOP;
+ CREATE AGGREGATE concat_tsvectors (
+ BASETYPE = tsvector,
+ SFUNC = tsv_add,
+ STYPE = tsvector,
+ INITCOND = ''
+ );
+ RETURN true;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT setup_aggregates();
+
+/* calculates text search vector for the individual thread row
+DOES not include question body post, answers or comments */
+CREATE OR REPLACE FUNCTION get_thread_tsv(title text, tagnames text)
+RETURNS tsvector AS
+$$
+BEGIN
+ /* todo add weight depending on votes */
+ RETURN setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
+ setweight(to_tsvector('english', coalesce(tagnames, '')), 'A');
+END;
+$$ LANGUAGE plpgsql;
+
+/* calculates text seanch vector for the individual question row */
+CREATE OR REPLACE FUNCTION get_post_tsv(text text, post_type text)
+RETURNS tsvector AS
+$$
+BEGIN
+ /* todo adjust weights to reflect votes */
+ IF post_type='question' THEN
+ RETURN setweight(to_tsvector('english', coalesce(text, '')), 'B');
+ ELSIF post_type='answer' THEN
+ /* todo reflect whether the answer acepted or has many points */
+ RETURN setweight(to_tsvector('english', coalesce(text, '')), 'C');
+ ELSIF post_type='comment' THEN
+ RETURN setweight(to_tsvector('english', coalesce(text, '')), 'D');
+ ELSE
+ RETURN to_tsvector('');
+ END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+/* calculates text search vector for the question body part by thread id
+here we extract question title and the text by thread_id and then
+calculate the text search vector. In the future question
+title will be moved to the askbot_thread table and this function
+will be simpler.
+*/
+CREATE OR REPLACE FUNCTION get_thread_question_tsv(thread_id integer)
+RETURNS tsvector AS
+$$
+DECLARE
+ query text;
+ onerow record;
+BEGIN
+ query = 'SELECT text FROM askbot_post WHERE thread_id=' || thread_id ||
+ ' AND post_type=''question'' AND deleted=false';
+ FOR onerow in EXECUTE query LOOP
+ RETURN get_post_tsv(onerow.text, 'question');
+ END LOOP;
+ RETURN to_tsvector('');
+END;
+$$ LANGUAGE plpgsql;
+
+DROP FUNCTION IF EXISTS get_dependent_comments_tsv(object_id integer, tablename text);
+CREATE OR REPLACE FUNCTION get_dependent_comments_tsv(parent_id integer)
+RETURNS tsvector AS
+$$
+DECLARE
+ query text;
+ onerow record;
+BEGIN
+ query = 'SELECT concat_tsvectors(text_search_vector) FROM askbot_post' ||
+ ' WHERE parent_id=' || parent_id ||
+ ' AND post_type=''comment'' AND deleted=false';
+ FOR onerow IN EXECUTE query LOOP
+ RETURN onerow.concat_tsvectors;
+ END LOOP;
+ RETURN to_tsvector('');
+END;
+$$ LANGUAGE plpgsql;
+
+DROP FUNCTION IF EXISTS get_dependent_answers_tsv(question_id integer);
+CREATE OR REPLACE FUNCTION get_dependent_answers_tsv(thread_id integer)
+RETURNS tsvector AS
+$$
+DECLARE
+ query text;
+ onerow record;
+BEGIN
+ query = 'SELECT concat_tsvectors(text_search_vector) ' ||
+ 'FROM askbot_post WHERE thread_id = ' || thread_id ||
+ ' AND deleted=false';
+ FOR onerow IN EXECUTE query LOOP
+ RETURN onerow.concat_tsvectors;
+ END LOOP;
+ RETURN to_tsvector('');
+END;
+$$ LANGUAGE plpgsql;
+
+/* create tsvector columns in the content tables */
+SELECT add_tsvector_column('text_search_vector', 'askbot_thread');
+SELECT add_tsvector_column('text_search_vector', 'askbot_post');
+SELECT add_tsvector_column('title_search_vector', 'askbot_thread');
+
+/* populate tsvectors with data */
+-- post tsvectors
+UPDATE askbot_post set text_search_vector = get_post_tsv(text, 'comment') WHERE post_type='comment';
+UPDATE askbot_post SET text_search_vector = get_post_tsv(text, 'answer') WHERE post_type='answer';
+UPDATE askbot_post SET text_search_vector = get_post_tsv(text, 'question') WHERE post_type='question';
+UPDATE askbot_post as q SET text_search_vector = text_search_vector ||
+ get_dependent_comments_tsv(q.id) WHERE post_type IN ('question', 'answer');
+
+--thread tsvector
+UPDATE askbot_thread SET text_search_vector = get_thread_tsv(title, tagnames);
+UPDATE askbot_thread as t SET text_search_vector = text_search_vector ||
+ get_dependent_answers_tsv(t.id) ||
+ get_thread_question_tsv(t.id);
+UPDATE askbot_thread SET title_search_vector = get_thread_tsv(title, tagnames);
+
+/* one trigger per table for tsv updates */
+
+/* set up update triggers */
+CREATE OR REPLACE FUNCTION thread_update_trigger() RETURNS trigger AS
+$$
+DECLARE
+ title_tsv tsvector;
+BEGIN
+ title_tsv = get_thread_tsv(new.title, new.tagnames);
+ new.title_search_vector = title_tsv;
+ new.text_search_vector = title_tsv ||
+ get_thread_question_tsv(new.id) ||
+ get_dependent_answers_tsv(new.id);
+ RETURN new;
+END;
+$$ LANGUAGE plpgsql;
+DROP TRIGGER IF EXISTS thread_search_vector_update_trigger on askbot_thread;
+CREATE TRIGGER thread_search_vector_update_trigger
+BEFORE UPDATE ON askbot_thread FOR EACH ROW EXECUTE PROCEDURE thread_update_trigger();
+
+CREATE OR REPLACE FUNCTION thread_insert_trigger() RETURNS trigger AS
+$$
+BEGIN
+ new.text_search_vector = get_thread_tsv(new.title, new.tagnames);
+ RETURN new;
+END;
+$$ LANGUAGE plpgsql;
+DROP TRIGGER IF EXISTS thread_search_vector_insert_trigger on askbot_thread;
+CREATE TRIGGER thread_search_vector_insert_trigger
+BEFORE INSERT ON askbot_thread FOR EACH ROW EXECUTE PROCEDURE thread_insert_trigger();
+
+/* post trigger */
+CREATE OR REPLACE FUNCTION post_trigger() RETURNS trigger AS
+$$
+BEGIN
+ IF new.post_type = 'question' THEN
+ new.text_search_vector = get_post_tsv(new.text, 'question') ||
+ get_dependent_comments_tsv(new.id);
+ ELSIF new.post_type = 'answer' THEN
+ new.text_search_vector = get_post_tsv(new.text, 'answer') ||
+ get_dependent_comments_tsv(new.id);
+ ELSIF new.post_type = 'comment' THEN
+ new.text_search_vector = get_post_tsv(new.text, 'comment');
+ END IF;
+ UPDATE askbot_thread SET id=new.thread_id WHERE id=new.thread_id;
+ return new;
+END;
+$$ LANGUAGE plpgsql;
+DROP TRIGGER IF EXISTS post_search_vector_update_trigger on askbot_post;
+CREATE TRIGGER post_search_vector_update_trigger
+BEFORE INSERT OR UPDATE ON askbot_post FOR EACH ROW EXECUTE PROCEDURE post_trigger();
+
+DROP INDEX IF EXISTS askbot_search_idx;
+CREATE INDEX askbot_search_idx ON askbot_thread USING gin(text_search_vector);
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/skins/utils.py b/askbot/skins/utils.py
index e2a815b7..b04f9bda 100644
--- a/askbot/skins/utils.py
+++ b/askbot/skins/utils.py
@@ -169,7 +169,7 @@ def get_media_url(url, ignore_missing = False):
#print after - before
return url
-def update_media_revision(skin = None):
+def update_media_revision(skin=None):
"""update skin media revision number based on the contents
of the skin media directory"""
from askbot.conf import settings as askbot_settings
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" />&nbsp;
<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 7000c0ac..4646a3a0 100644
--- a/askbot/templates/base.html
+++ b/askbot/templates/base.html
@@ -10,6 +10,7 @@
{% if settings.GOOGLE_SITEMAP_CODE %}
<meta name="google-site-verification" content="{{settings.GOOGLE_SITEMAP_CODE}}" />
{% endif %}
+ <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<link rel="shortcut icon" href="{{ settings.SITE_FAVICON|media }}" />
{% block before_css %}{% endblock %}
{% include "meta/html_head_stylesheets.html" %}
diff --git a/askbot/templates/embed/ask_by_widget.html b/askbot/templates/embed/ask_by_widget.html
index af6da32c..dc3db806 100644
--- a/askbot/templates/embed/ask_by_widget.html
+++ b/askbot/templates/embed/ask_by_widget.html
@@ -209,13 +209,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 55113377..02a97cc7 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 %}
@@ -631,7 +631,7 @@ answer {% if answer.accepted() %}accepted-answer{% endif %} {% if answer.author_
{#todo: rename this to avatar #}
{%- macro gravatar(user, size) -%}
{% spaceless %}
-<a style="text-decoration:none"
+<a class="avatar-box"
href="{{ user.get_absolute_url() }}"
><img class="gravatar"
width="{{size}}" height="{{size}}"
diff --git a/askbot/templates/main_page/javascript.html b/askbot/templates/main_page/javascript.html
index 80eb39ca..7ad02e29 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;
@@ -21,7 +18,6 @@
askbot['urls']['mark_subscribed_tag'] = '{% url mark_subscribed_tag %}';
askbot['urls']['unmark_tag'] = '{% url unmark_tag %}';
askbot['urls']['set_tag_filter_strategy'] = '{% url "set_tag_filter_strategy" %}';
- askbot['urls']['questions'] = '{% url "questions" %}';
askbot['urls']['question_url_template'] = scriptUrl + '{{'question/'|transurl}}{{ "{{QuestionID}}/" }}';
if (Modernizr.history) {
@@ -48,4 +44,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..c0b37955 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'] = {
+ 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 9b754caa..c55760be 100644
--- a/askbot/templates/meta/bottom_scripts.html
+++ b/askbot/templates/meta/bottom_scripts.html
@@ -24,8 +24,12 @@
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['urls']['questions'] = '{% url "questions" %}';
+ askbot['settings']['static_url'] = '{{ settings.STATIC_URL }}';
+ askbot['settings']['minSearchWordLength'] = {{settings.MIN_SEARCH_WORD_LENGTH}};
</script>
<script
type="text/javascript"
@@ -39,6 +43,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 +57,35 @@
/*<![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();
+ putCursorAtEnd(searchInput);
+ }
+
+ {% 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/user_profile/user_stats.html b/askbot/templates/user_profile/user_stats.html
index dc3d97e0..aa5aa07e 100644
--- a/askbot/templates/user_profile/user_stats.html
+++ b/askbot/templates/user_profile/user_stats.html
@@ -158,8 +158,5 @@
});
</script>
<script type='text/javascript' src='{{"/js/tag_selector.js"|media}}'></script>
- <script type="text/javascript">
- askbot['urls']['questions'] = '{% url "questions" %}';
- </script>
{% endblock %}
<!-- end user_stats.html -->
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 1e5ce903..91b8015d 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..9bb00406 100644
--- a/askbot/urls.py
+++ b/askbot/urls.py
@@ -13,7 +13,7 @@ from askbot.sitemap import QuestionsSitemap
from askbot.skins.utils import update_media_revision
admin.autodiscover()
-update_media_revision()#needs to be run once, so put it here
+#update_media_revision()#needs to be run once, so put it here
if getattr(settings, "ASKBOT_TRANSLATE_URL", False):
from django.utils.translation import ugettext as _
@@ -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 2f60c88b..1663a049 100644
--- a/askbot/views/commands.py
+++ b/askbot/views/commands.py
@@ -696,31 +696,35 @@ 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 conf.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]
+
+ thread_list = list()
+ for thread in threads:#todo: this is a temp hack until thread model is fixed
+ try:
+ thread_list.append({
+ 'title': escape(thread.title),
+ 'url': thread.get_absolute_url(),
+ 'answer_count': thread.get_answer_count(request.user)
+ })
+ except:
+ continue
+
json_data = simplejson.dumps(thread_list)
return HttpResponse(json_data, mimetype = "application/json")