diff options
-rwxr-xr-x | django_authopenid/views.py | 18 | ||||
-rwxr-xr-x | forum/const.py | 30 | ||||
-rwxr-xr-x | forum/forms.py | 73 | ||||
-rw-r--r-- | forum/middleware/view_log.py | 63 | ||||
-rwxr-xr-x | forum/models/question.py | 61 | ||||
-rwxr-xr-x | forum/models/tag.py | 4 | ||||
-rw-r--r-- | forum/search/__init__.py | 0 | ||||
-rw-r--r-- | forum/search/state_manager.py | 127 | ||||
-rwxr-xr-x | forum/skins/default/media/style/style.css | 20 | ||||
-rw-r--r-- | forum/skins/default/templates/header.html | 2 | ||||
-rw-r--r-- | forum/skins/default/templates/questions.html | 126 | ||||
-rw-r--r-- | forum/views/readers.py | 202 | ||||
-rw-r--r-- | locale/en/LC_MESSAGES/django.mo | bin | 27146 -> 27175 bytes | |||
-rw-r--r-- | locale/en/LC_MESSAGES/django.po | 2 | ||||
-rw-r--r-- | settings.py | 1 |
15 files changed, 537 insertions, 192 deletions
diff --git a/django_authopenid/views.py b/django_authopenid/views.py index 3c794a0d..e2d8b67c 100755 --- a/django_authopenid/views.py +++ b/django_authopenid/views.py @@ -71,6 +71,7 @@ from forum.utils.forms import get_next_url EXTERNAL_LOGIN_APP = settings.LOAD_EXTERNAL_LOGIN_APP() +#todo: decouple from forum def login(request,user): from django.contrib.auth import login as _login from forum.models import user_logged_in #custom signal @@ -80,14 +81,27 @@ def login(request,user): #1) get old session key session_key = request.session.session_key - #2) login and get new session key + #2) get old search state + search_state = None + if 'search_state' in request.session: + search_state = request.session['search_state'] + + #3) login and get new session key _login(request,user) - #3) send signal with old session key as argument + #4) transfer search_state to new session if found + if search_state: + search_session.set_logged_in() + request.session['search_state'] = search_state + #5) send signal with old session key as argument logging.debug('logged in user %s with session key %s' % (user.username, session_key)) user_logged_in.send(user=user,session_key=session_key,sender=None) +#todo: uncouple this from forum def logout(request): from django.contrib.auth import logout as _logout#for login I've added wrapper below - called login + if 'search_state' in request.session: + request.session['search_state'].set_logged_out() + request.session.modified = True _logout(request) if settings.USE_EXTERNAL_LEGACY_LOGIN == True: EXTERNAL_LOGIN_APP.api.logout(request) diff --git a/forum/const.py b/forum/const.py index 7b78032a..6f6086f0 100755 --- a/forum/const.py +++ b/forum/const.py @@ -32,19 +32,34 @@ TYPE_REPUTATION = ( (-8, 'lose_by_upvote_canceled'), ) +#do not translate these!!! POST_SORT_METHODS = ( - _('newest'), _('oldest'),_('active'), - _('inactive'),_('hot'),_('cold'), - _('popular'),_('unpopular'),_('relevance') + ('latest', _('newest')), + ('oldest', _('oldest')), + ('active', _('active')), + ('inactive', _('inactive')), + ('hottest', _('hottest')), + ('coldest', _('coldest')), + ('mostvoted', _('most voted')), + ('leastvoted', _('least voted')), + ('relevant', _('relevance')), ) #todo: add assertion here that all sort methods are unique #because they are keys to the hash used in implementations of Q.run_advanced_search -DEFAULT_POST_SORT_METHOD = _('newest') -POST_SCOPES = (_('all'),_('unanswered'),_('favorite')) -DEFAULT_POST_SCOPE = _('all') +DEFAULT_POST_SORT_METHOD = 'latest' +POST_SCOPE_LIST = ( + ('all', _('all')), + ('unanswered', _('unanswered')), + ('favorite', _('favorite')), + ) +DEFAULT_POST_SCOPE = 'all' +DEFAULT_QUESTIONS_PAGE_SIZE = 30 +PAGE_SIZES = (10,30,50) -QUESTIONS_PAGE_SIZE = 30 +UNANSWERED_MEANING_LIST = ('NO_ANSWERS','NO_UPVOTED_ANSWERS','NO_ACCEPTED_ANSWERS') +UNANSWERED_MEANING = 'NO_ANSWERS' +assert(UNANSWERED_MEANING in UNANSWERED_MEANING_LIST) #todo: #this probably needs to be language-specific @@ -53,6 +68,7 @@ QUESTIONS_PAGE_SIZE = 30 #correct regexes - plus this must be an anchored regex #to do full string match TAG_REGEX = r'^[a-z0-9\+\.\-]+$' +TAG_SPLIT_REGEX = r'[ ,]+' MAX_TAG_LENGTH = 20 #default 20 chars MAX_TAGS_PER_POST = 5 #no more than five tags diff --git a/forum/forms.py b/forum/forms.py index dbdc4e9f..c32ce6c0 100755 --- a/forum/forms.py +++ b/forum/forms.py @@ -61,7 +61,7 @@ class TagNamesField(forms.CharField): if len(data) < 1: raise forms.ValidationError(_('tags are required')) - split_re = re.compile(r'[ ,]+') + split_re = re.compile(const.TAG_SPLIT_REGEX) tag_strings = split_re.split(data) out_tag_list = [] tag_count = len(tag_strings) @@ -79,7 +79,7 @@ class TagNamesField(forms.CharField): msg = ungettext('each tag must be shorter than %(max_chars)d character',#odd but added for completeness 'each tag must be shorter than %(max_shars)d characters', tag_length) % {'max_chars':tag_length} - raise forms.ValidationError(msg)_ + raise forms.ValidationError(msg) #todo - this needs to come from settings tagname_re = re.compile(const.TAG_REGEX, re.UNICODE) @@ -127,6 +127,75 @@ class ModerateUserForm(forms.ModelForm): model = User fields = ('is_approved',) +class AdvancedSearchForm(forms.Form): + #nothing must be required in this form + #it is used by the main questions view + scope = forms.ChoiceField(choices=const.POST_SCOPE_LIST, required=False) + sort = forms.ChoiceField(choices=const.POST_SORT_METHODS, required=False) + query = forms.CharField(max_length=256, required=False) + reset_tags = forms.BooleanField(required=False) + reset_author = forms.BooleanField(required=False) + reset_query = forms.BooleanField(required=False) + tags = forms.CharField(max_length=256,required=False) + author = forms.IntegerField(required=False) + page_size = forms.ChoiceField(choices=const.PAGE_SIZES, required=False) + page = forms.IntegerField(required=False) + + def clean_tags(self): + if 'tags' in self.cleaned_data: + tags_input = self.cleaned_data['tags'].strip() + split_re = re.compile(TAG_SPLIT_REGEX) + tag_strings = split_re.split(tags_input) + tagname_re = re.compile(const.TAG_REGEX, re.UNICODE) + out = set() + for s in tag_strings: + if tagname_re.search(s): + out.add(s) + if len(out) > 0: + self.cleaned_data['tags'] = out + else: + self.cleaned_data['tags'] = None + return self.cleaned_data['tags'] + + def clean_query(self): + if 'query' in self.cleaned_data: + q = self.cleaned_data['query'].strip() + if q == '': + q = None + self.cleaned_data['query'] = q + return self.cleaned_data['query'] + + def clean_page_size(self): + if 'page_size' in self.cleaned_data: + if self.cleaned_data['page_size'] == '': + self.cleaned_data['page_size'] = None + return self.cleaned_data['page_size'] + + def clean(self): + #todo rewrite + print self.cleaned_data + if self.cleaned_data['scope'] == '': + del self.cleaned_data['scope'] + if self.cleaned_data['tags'] is None: + del self.cleaned_data['tags'] + if self.cleaned_data['sort'] == '': + del self.cleaned_data['sort'] + if self.cleaned_data['query'] == None: + del self.cleaned_data['query'] + if self.cleaned_data['reset_tags'] == False: + del self.cleaned_data['reset_tags'] + if self.cleaned_data['reset_author'] == False: + del self.cleaned_data['reset_author'] + if self.cleaned_data['reset_query'] == False: + del self.cleaned_data['reset_query'] + if self.cleaned_data['author'] is None: + del self.cleaned_data['author'] + if self.cleaned_data['page'] is None: + del self.cleaned_data['page'] + if self.cleaned_data['page_size'] is None: + del self.cleaned_data['page_size'] + return self.cleaned_data + class NotARobotForm(forms.Form): recaptcha = ReCaptchaField() diff --git a/forum/middleware/view_log.py b/forum/middleware/view_log.py new file mode 100644 index 00000000..6f59568a --- /dev/null +++ b/forum/middleware/view_log.py @@ -0,0 +1,63 @@ +import logging +from django.conf import settings +from forum.views.readers import questions as questions_view +from forum.views.commands import vote as vote_view +from django.views.static import serve as django_serve_view + +class ViewLog(object): + """must be modified only in this middlware + however, can be read anywhere else + """ + def __init__(self): + self.views = [] + self.depth = 3 #todo maybe move this to const.py + def set_current(self, view_name): + thi + + def get_previous(self, num): + if num > self.depth - 1: + raise Exception("view log depth exceeded") + elif num < 0: + raise Exception("num must be positive"); + elif num <= len(self.views) - 1: + return self.views[num] + else: + return None + + def set_current(self, view_name): + self.views.insert(0, view_name) + if len(self.views) > self.depth: + self.views.pop() + + def __str__(self): + return str(self.views) + ' depth=%d' % self.depth + +class ViewLogMiddleware(object): + def process_view(self, request, view_func, view_args, view_kwargs): + if view_func == questions_view: + view_str = 'questions' + elif view_func in (django_serve_view, vote_view): + return + elif settings.DEBUG == True: + #todo: dependency! + from debug_toolbar.views import debug_media_view + if view_func == debug_media_view: + return + else: + view_str = view_func.__name__ + else: + view_str = view_func.__name__ + + if request.user.is_authenticated(): + user_name = request.user.username + else: + user_name = request.META['REMOTE_ADDR'] + logging.debug('user %s, view %s' % (request.user.username, view_str)) + + if 'view_log' in request.session: + view_log = request.session['view_log'] + else: + view_log = ViewLog() + + view_log.set_current(view_str) + request.session['view_log'] = view_log diff --git a/forum/models/question.py b/forum/models/question.py index 71da0396..98b50490 100755 --- a/forum/models/question.py +++ b/forum/models/question.py @@ -1,24 +1,31 @@ -from base import * +from base import * #todo maybe remove * from tag import Tag +#todo: make uniform import for consts from forum.const import CONST +from forum import const from forum.utils.html import sanitize_html from markdown2 import Markdown from django.utils.html import strip_tags import datetime +from django.conf import settings +from django.utils.datastructures import SortedDict +from forum.models.tag import MarkedTag + markdowner = Markdown(html4tags=True) from forum.utils.lists import LazyList +#todo: too bad keys are duplicated see const sort methods QUESTION_ORDER_BY_MAP = { - _('newest'): '-added_at', - _('oldest'): 'added_at', - _('active'): '-last_activity_at', - _('inactive'): 'last_activity_at', - _('hot'): '-answer_count', - _('cold'): 'answer_count' - _('popular'): '-score', - _('unpopular'): 'score', - _('relevance'):None #this is a special case + 'latest': '-added_at', + 'oldest': 'added_at', + 'active': '-last_activity_at', + 'inactive': 'last_activity_at', + 'hottest': '-answer_count', + 'coldest': 'answer_count', + 'mostvoted': '-score', + 'leastvoted': 'score', + 'relevant': None #this is a special case } class QuestionManager(models.Manager): @@ -54,7 +61,7 @@ class QuestionManager(models.Manager): def run_advanced_search( self, request_user = None, - scope_selector = scope_selector,#unanswered/all/favorite (for logged in) + scope_selector = const.DEFAULT_POST_SCOPE,#unanswered/all/favorite (for logged in) search_query = None, tag_selector = None, author_selector = None,#???question or answer author or just contributor @@ -65,16 +72,32 @@ class QuestionManager(models.Manager): a relvant filter will be silently dropped """ + qs = self.filter(deleted=False)#todo - add a possibility to see deleted questions + #return metadata meta_data = {} if tag_selector: qs = qs.filter(tags__name__in = tag_selector) + if search_query: + qs = qs.filter(deleted=False).extra( + where=['title like %s'], + params=['%' + search_query + '%'] + ) + if scope_selector: + if scope_selector == 'unanswered': + if const.UNANSWERED_MEANING == 'NO_ANSWERS': + qs = qs.filter(answer_count=0)#todo: expand for different meanings of this + elif const.UNANSWERED_MEANING == 'NO_ACCEPTED_ANSWERS': + qs = qs.filter(answer_accepted=False) + elif const.UNANSWERED_MEANING == 'NO_UPVOTED_ANSWERS': + raise NotImplementedError() + else: + raise Exception('UNANSWERED_MEANING setting is wrong') + elif scope_selector == 'favorite': + qs = qs.filter(favorited_by = request_user) - if unanswered: - qs = qs.exclude(answer_accepted=True) - #user contributed questions & answers if author_selector: try: @@ -126,9 +149,13 @@ class QuestionManager(models.Manager): meta_data['interesting_tag_names'] = pt.filter(reason='good').values_list('tag__name', flat=True) meta_data['ignored_tag_names'] = pt.filter(reason='bad').values_list('tag__name', flat=True) - #todo: fix orderby here - qs = qs.select_related(depth=1).order_by(orderby) - return qs, meta_data + qs = qs.select_related(depth=1) + #todo: fix orderby here + orderby = QUESTION_ORDER_BY_MAP[sort_method] + if orderby: + #relevance will be ignored here + qs = qs.order_by(orderby) + return qs, meta_data def update_tags(self, question, tagnames, user): """ diff --git a/forum/models/tag.py b/forum/models/tag.py index 8d26d6f4..e13baf9b 100755 --- a/forum/models/tag.py +++ b/forum/models/tag.py @@ -49,6 +49,8 @@ class TagManager(models.Manager): def get_tags_by_questions(self, questions): question_ids = [] + if len(questions) == 0: + return [] for question in questions: question_ids.append(question.id) @@ -82,4 +84,4 @@ class MarkedTag(models.Model): reason = models.CharField(max_length=16, choices=TAG_MARK_REASONS) class Meta: - app_label = 'forum'
\ No newline at end of file + app_label = 'forum' diff --git a/forum/search/__init__.py b/forum/search/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/forum/search/__init__.py diff --git a/forum/search/state_manager.py b/forum/search/state_manager.py new file mode 100644 index 00000000..b36c5e53 --- /dev/null +++ b/forum/search/state_manager.py @@ -0,0 +1,127 @@ +#search state manager object +#that lives in the session and takes care of the state +#persistece during the search session +from forum import const +import logging + +class SearchState(object): + def __init__(self): + self.scope= const.DEFAULT_POST_SCOPE + self.query = None + self.tags = None + self.author = None + self.sort = const.DEFAULT_POST_SORT_METHOD + self.page_size = const.DEFAULT_QUESTIONS_PAGE_SIZE + self.page = 1 + self.logged_in = False + logging.debug('new search state initialized') + print 'new search state' + + def __str__(self): + out = 'scope=%s\n' % self.scope + out += 'query=%s\n' % self.query + if self.tags: + out += 'tags=%s\n' % ','.join(self.tags) + out += 'author=%s\n' % self.author + out += 'sort=%s\n' % self.sort + out += 'page_size=%d\n' % self.page_size + out += 'page=%d\n' % self.page + out += 'logged_in=%s\n' % str(self.logged_in) + return out + + def set_logged_out(self): + if self.scope == 'favorite': + self.scope = None + self.logged_in = False + + def set_logged_in(self): + self.logged_in = True + + def reset(self): + #re-initialize, but keep login state + is_logged_in = self.logged_in + self.__init__() + self.logged_in = is_logged_in + + def update_value(self, key, store): + if key in store: + old_value = getattr(self, key) + new_value = store[key] + if new_value != old_value: + setattr(self, key, new_value) + self.reset_page() + + def update_from_user_input(self,input): + #todo: this function will probably not + #fit the case of multiple parameters entered at the same tiem + print ','.join(input.keys()) + if 'page' in input: + print input['page'] + self.page = input['page'] + #special case - on page flip no other input is accepted + return + + if 'page_size' in input: + self.update_value('page_size',input) + self.reset_page()#todo may be smarter here - start with ~same q + #same as with page - return right away + return + + if 'scope' in input: + if input['scope'] == 'favorite' and self.logged_in == False: + self.reset_scope() + else: + self.update_value('scope',input) + + if 'tags' in input: + if self.tags: + old_tags = self.tags.copy() + self.tags.union(input['tags']) + if self.tags != old_tags: + self.reset_page() + else: + self.tags = input['tags'] + + #all resets just return + if 'reset_tags' in input: + if self.tags: + self.tags = None + self.reset_page() + return + + #todo: handle case of deleting tags one-by-one + if 'reset_author' in input: + if self.author: + self.author = None + self.reset_page() + return + + if 'reset_query' in input: + if self.query: + self.query = None + self.reset_page() + if self.sort == 'relevant': + self.reset_sort() + return + + self.update_value('author',input) + + if 'query' in input: + self.update_value('query',input) + self.sort = 'relevant' + + + if 'sort' in input: + if input['sort'] == 'relevant' and self.query is None: + self.reset_sort() + else: + self.update_value('sort',input) + + def reset_page(self): + self.page = 1 + + def reset_sort(self): + self.sort = const.DEFAULT_POST_SORT_METHOD + + def reset_scope(self): + self.scope = const.DEFAULT_POST_SCOPE diff --git a/forum/skins/default/media/style/style.css b/forum/skins/default/media/style/style.css index 93c979ce..8315839d 100755 --- a/forum/skins/default/media/style/style.css +++ b/forum/skins/default/media/style/style.css @@ -866,7 +866,16 @@ a:hover.medal { height: 20px; } -.tabsA a.on, .tabsA a:hover, .tabsB a.on, .tabsB a:hover { +.tabsC { + background-color: #FFF; + float: left; + position: relative; + display: block; + font-weight: bold; + height: 20px; +} + +.tabsA a.on, .tabsA a:hover, .tabsB a.on, .tabsB a:hover , .tabsC a.on, tabsC a:hover { background: #fff; color: #a40000; border-top: 1px solid #babdb6; @@ -879,7 +888,7 @@ a:hover.medal { padding: 0px 11px 0px 11px; } -.tabsA a { +.tabsA a, .tabsC a{ background: #f9f7eb; border-top: 1px solid #eeeeec; border-left: 1px solid #eeeeec; @@ -895,6 +904,12 @@ a:hover.medal { text-decoration: none; } +.tabsA .label, .tabsC .label { + float: left; + font-weight: bold; + margin: 8px 4px 0 4px; +} + .tabsB a { background: #eee; border: 1px solid #eee; @@ -920,7 +935,6 @@ a:hover.medal { } .headQuestions { - float: left; height: 23px; line-height: 23px; margin: 5px 0 0 5px; diff --git a/forum/skins/default/templates/header.html b/forum/skins/default/templates/header.html index 099bfb85..aaf19874 100644 --- a/forum/skins/default/templates/header.html +++ b/forum/skins/default/templates/header.html @@ -31,7 +31,9 @@ <a id="nav_books" href="{% url books %}">{% trans "books" %}</a> {% endif %} <a id="nav_badges" href="{% url badges %}">{% trans "badges" %}</a> + {% comment %} <a id="nav_unanswered" href="{% url unanswered %}">{% trans "unanswered questions" %}</a> + {% endcomment %} <div class="focus"> <a id="nav_ask" href="{% url ask %}" class="special">{% trans "ask a question" %}</a> </div> diff --git a/forum/skins/default/templates/questions.html b/forum/skins/default/templates/questions.html index 366727d1..0a7c0c96 100644 --- a/forum/skins/default/templates/questions.html +++ b/forum/skins/default/templates/questions.html @@ -10,9 +10,11 @@ <script type="text/javascript">
var tags = {{ tags_autocomplete|safe }};
$().ready(function(){
- var tab_id = "{{ tab_id }}";
- $("#"+tab_id).attr('className',"on");
- var on_tab = {% if is_unanswered %}'#nav_unanswered'{% else %}'#nav_questions'{% endif %};
+ var sort_tab_id = "{{ sort }}";
+ $("#"+sort_tab_id).attr('className',"on");
+ var scope_tab_id = "{{ scope }}";
+ $("#"+scope_tab_id).attr('className',"on");
+ var on_tab = '#nav_questions';
$(on_tab).attr('className','on');
Hilite.exact = false;
Hilite.elementid = "listA";
@@ -24,6 +26,7 @@ {% endblock %}
{% block content %}
<div class="tabBar">
+ {% comment %}
<div class="headQuestions">
{% if searchtag %}
{% trans "Found by tags" %}
@@ -42,27 +45,116 @@ {% endif %}
{% endif %}
{% endif %}
+ </div><br/>
+ {% endcomment %}
+ <div class="tabsC">
+ <span class="label">{% trans "Pick:" %}</span>
+ <a id="all" class="off" href="?scope=all" title="{% trans "see all questions" %}">{% trans "all" %}</a>
+ <a id="unanswered" class="off" href="?scope=unanswered" title="{% trans "see unanswered questions" %}">{% trans "unanswered" %}</a>
+ {% if request.user.is_authenticated %}
+ <a id="favorite" class="off" href="?scope=favorite" title="{% trans "see your favorite questions" %}">{% trans "favorite" %}</a>
+ {% endif %}
</div>
<div class="tabsA">
- <a id="latest" href="{% if is_search %}{{ search_uri }}&{% else %}?{% endif %}sort=latest" class="off" title="{% trans "most recently asked questions" %}">{% trans "newest" %}</a>
- <a id="active" href="{% if is_search %}{{ search_uri }}&{% else %}?{% endif %}sort=active" class="off" title="{% trans "most recently updated questions" %}">{% trans "active" %}</a>
- <a id="hottest" href="{% if is_search %}{{ search_uri }}&{% else %}?{% endif %}sort=hottest" class="off" title="{% trans "hottest questions" %}">{% trans "hottest" %}</a>
- <a id="mostvoted" href="{% if is_search %}{{ search_uri }}&{% else %}?{% endif %}sort=mostvoted" class="off" title="{% trans "most voted questions" %}">{% trans "most voted" %}</a>
+ <span class="label">{% trans "Sort by:" %}</span>
+ {% if sort == "oldest" %}
+ <a id="oldest"
+ href="?sort=latest"
+ class="off"
+ title="{% trans "click to see the newest questions" %}">{% trans "oldest" %}</a>
+ {% else %}
+ {% if sort == "latest" %}
+ <a id="latest"
+ href="?sort=oldest"
+ class="off"
+ title="{% trans "click to see the oldest questions" %}">{% trans "newest" %}</a>
+ {% else %}
+ <a id="latest"
+ href="?sort=latest"
+ class="off"
+ title="{% trans "click to see the newest questions" %}">{% trans "newest" %}</a>
+ {% endif %}
+ {% endif %}
+
+ {% if sort == "inactive" %}
+ <a id="inactive"
+ href="?sort=active"
+ class="off"
+ title="{% trans "click to see the most recently updated questions" %}">{% trans "inactive" %}</a>
+ {% else %}
+ {% if sort == "active" %}
+ <a id="active"
+ href="?sort=inactive"
+ class="off"
+ title="{% trans "click to see the least recently updated questions" %}">{% trans "active" %}</a>
+ {% else %}
+ <a id="active"
+ href="?sort=active"
+ class="off"
+ title="{% trans "click to see the most recently updated questions" %}">{% trans "active" %}</a>
+ {% endif %}
+ {% endif %}
+
+ {% if sort == "coldest" %}
+ <a id="coldest"
+ href="?sort=hottest"
+ class="off"
+ title="{% trans "click to see hottest questions" %}">{% trans "coldest" %}</a>
+ {% else %}
+ {% if sort == "hottest" %}
+ <a id="hottest"
+ href="?sort=coldest"
+ class="off"
+ title="{% trans "click to see coldest questions" %}">{% trans "hottest" %}</a>
+ {% else %}
+ <a id="hottest"
+ href="?sort=hottest"
+ class="off"
+ title="{% trans "click to see hottest questions" %}">{% trans "hottest" %}</a>
+ {% endif %}
+ {% endif %}
+
+ {% if sort == "leastvoted" %}
+ <a id="leastvoted"
+ href="?sort=mostvoted"
+ class="off"
+ title="{% trans "click to see most voted questions" %}">{% trans "unpopular" %}</a>
+ {% else %}
+ {% if sort == "mostvoted" %}
+ <a id="mostvoted"
+ href="?sort=leastvoted"
+ class="off"
+ title="{% trans "click to see least voted questions" %}">{% trans "popular" %}</a>
+ {% else %}
+ <a id="mostvoted"
+ href="?sort=mostvoted"
+ class="off"
+ title="{% trans "click to see most voted questions" %}">{% trans "popular" %}</a>
+ {% endif %}
+ {% endif %}
</div>
</div>
<div id="listA">
{% include "question_list.html" %}
- {% if searchtitle %}
{% if questions_count == 0 %}
- <p class="evenMore" style="padding-top:30px;text-align:center;">
- {% trans "Did not find anything?" %}
+ <p class="evenMore" style="padding-top:30px;text-align:center;">
+ {% if scope == "unanswered" %}
+ {% trans "There are no unanswered questions in this selection" %}
+ {% endif %}
+ {% if scope == "favorite" %}
+ {% trans "No favorite questions here. " %}
+ {% trans "Please bookmark some questions with a star and find them here later." %}
+ {% endif %}
+ {% if scope == "all" %}
+ {% trans "Did not find anything? Please feel free to ask your question!" %}
+ {% endif %}
+ </p>
{% else %}
- <p class="evenMore" style="padding-left:9px">
- {% trans "Did not find what you were looking for?" %}
+ <p class="evenMore" style="padding-left:9px">
+ {% trans "Did not find what you were looking for?" %}
+ <a href="{% url ask %}">{% trans "Please, post your question!" %}</a>
+ </p>
{% endif %}
- <a href="{% url ask %}">{% trans "Please, post your question!" %}</a>
- </p>
- {% endif %}
</div>
{% endblock %}
@@ -84,9 +176,9 @@ {% endif %}
{% else %}
{% if is_unanswered %}
- {% blocktrans count questions as cnt with questions_count|intcomma as q_num %} have total {{q_num}} unanswered questions {% plural %} have total {{q_num}} unanswered questions {% endblocktrans %}
+ {% blocktrans count questions_count as cnt with questions_count|intcomma as q_num %} have total {{q_num}} unanswered questions {% plural %} have total {{q_num}} unanswered questions {% endblocktrans %}
{% else %}
- {% blocktrans count questions as cnt with questions_count|intcomma as q_num %} have total {{q_num}} questions {% plural %} have total {{q_num}} questions {% endblocktrans %}
+ {% blocktrans count questions_count as cnt with questions_count|intcomma as q_num %} have total {{q_num}} questions {% plural %} have total {{q_num}} questions {% endblocktrans %}
{% endif %}
{% endif %}
{% endif %}
diff --git a/forum/views/readers.py b/forum/views/readers.py index 8c15ffe7..9e9662dd 100644 --- a/forum/views/readers.py +++ b/forum/views/readers.py @@ -13,7 +13,6 @@ from django.db.models import Q from django.utils.translation import ugettext as _ from django.template.defaultfilters import slugify from django.core.urlresolvers import reverse -from django.utils.datastructures import SortedDict from django.views.decorators.cache import cache_page from forum.utils.html import sanitize_html @@ -27,6 +26,7 @@ from forum.const import * from forum import const from forum import auth from forum.utils.forms import get_next_url +from forum.search.state_manager import SearchState # used in index page #refactor - move these numbers somewhere? @@ -69,25 +69,7 @@ def index(request):#generates front page - shows listing of questions sorted in def unanswered(request):#generates listing of unanswered questions return questions(request, unanswered=True) -def _get_and_stick(key, storage, defaults, data=None): - """storage is request session in this case - if a new value is coming from data, then storage will be updated - this function assumes that data is clean - """ - if key in storage: - value = storage[key] - else: - value = defaults[key] - - if data and key in data: - new_value = data[key] - if new_value != value: - storage[key] = new_value - return new_value - else: - return value - -def questions(request, tagname=None, unanswered=False):#a view generating listing of questions, used by 'unanswered' too +def questions(request):#a view generating listing of questions, used by 'unanswered' too """ List of Questions, Tagged questions, and Unanswered questions. """ @@ -96,147 +78,84 @@ def questions(request, tagname=None, unanswered=False):#a view generating listin if request.method == 'POST': raise Http404 - #default values - DV = { - 'scope_selector': const.DEFAULT_POST_SCOPE, - 'search_query': None, - 'tag_selector': None, - 'author_selector': None, - 'question_sort_method': const.DEFAULT_POST_SORT_METHOD, - 'page_size': const.QUESTIONS_PAGE_SIZE, - 'page_number': 1, - } - - form = AdvancedSearchForm(request) + search_state = request.session.get('search_state', SearchState()) + + view_log = request.session['view_log'] + print view_log + if view_log.get_previous(1) != 'questions': + if view_log.get_previous(2) != 'questions': + print 'user stepped too far, resetting search state' + search_state.reset() + + if request.user.is_authenticated(): + search_state.set_logged_in() + + print 'before: ', search_state + + form = AdvancedSearchForm(request.GET) if form.is_valid(): - - d = form.cleaned_data - s = request.session - - scope_selector = _get_and_stick('scope_selector', s, DV, d) - search_query = _get_and_stick('search_query', s, DV, d) - tag_selector = _get_and_stick('tag_selector', s, DV, d) - author_selector = _get_and_stick('author_selector', s, DV, d) - sort_method = _get_and_stick('question_sort_method', s, DV, d) - page_size = _get_and_stick('page_size', s, DV, d) - else: - s = request.session - scope_selector = _get_and_stick('scope_selector', s, DV) - search_query = _get_and_stick('search_query', s, DV) - tag_selector = _get_and_stick('tag_selector', s, DV) - author_selector = _get_and_stick('author_selector', s, DV) - sort_method = _get_and_stick('question_sort_method', s, DV) - page_size = _get_and_stick('page_size', s, DV) + print 'form is valid' + search_state.update_from_user_input(form.cleaned_data) + request.session['search_state'] = search_state + request.session.modified = True + + print 'after: ', search_state + + print 'going into the search' #have this call implemented for sphinx, mysql and pgsql - (questions, meta_data) = Question.objects.run_advanced_search( + (qs, meta_data) = Question.objects.run_advanced_search( request_user = request.user, - scope_selector = scope_selector,#unanswered/all/favorite (for logged in) - search_query = search_query, - tag_selector = tags, - author_selector = author_selector,#???question or answer author or just contributor - sort_method = sort_method# + scope_selector = search_state.scope,#unanswered/all/favorite (for logged in) + search_query = search_state.query, + tag_selector = search_state.tags, + author_selector = search_state.author, + sort_method = search_state.sort ) - #maybe have this inside? - questions = questions.select_related(depth=1).order_by(orderby) - page = Paginator(questions).get_page(page_number) - related_tags = Tag.objects.get_tags_by_questions(questions) + print 'got out of the search' - objects_list = Paginator(qs, pagesize) - questions = objects_list.page(page)#todo: is this right? + logging.debug('search state is %s' % search_state) + + objects_list = Paginator(qs, search_state.page_size) + questions = objects_list.page(search_state.page) + + #todo maybe do this search on query the set instead + related_tags = Tag.objects.get_tags_by_questions(questions.object_list) tags_autocomplete = _get_tags_cache_json() + + #todo!!!! #contributors = #User.objects.get_related_to_questions - #todo make above form compatible with the template data - #take whatever is missing from meta_data returned above + print 'rendering template!!!' + print 'have %d' % objects_list.count + return render_to_response('questions.html', { - "questions" : questions, - "author_name" : author_name, - "tab_id" : view_id, - "questions_count" : objects_list.count, - "tags" : related_tags, - "tags_autocomplete" : tags_autocomplete, - "searchtag" : tagname, - "is_unanswered" : unanswered, - "interesting_tag_names": interesting_tag_names, - 'ignored_tag_names': ignored_tag_names, - "context" : { + 'questions' : questions, + 'author_name' : meta_data.get('author_name',None), + 'tab_id' : search_state.sort, + 'questions_count' : objects_list.count, + 'tags' : related_tags, + 'tags_autocomplete' : tags_autocomplete, + 'searchtag' : search_state.tags, + 'is_unanswered' : False,#remove this from template + 'interesting_tag_names': meta_data.get('interesting_tag_names',None), + 'ignored_tag_names': meta_data.get('ignored_tag_names',None), + 'sort': search_state.sort, + 'scope': search_state.scope, + 'context' : { 'is_paginated' : True, 'pages': objects_list.num_pages, - 'page': page, + 'page': search_state.page, 'has_previous': questions.has_previous(), 'has_next': questions.has_next(), 'previous': questions.previous_page_number(), 'next': questions.next_page_number(), - 'base_url' : request.path + '?sort=%s&' % view_id, - 'pagesize' : pagesize + 'base_url' : request.path + '?sort=%s&' % search_state.sort,#todo in T sort=>sort_method + 'pagesize' : search_state.page_size,#todo in T pagesize -> page_size }}, context_instance=RequestContext(request)) -def fulltext(request): - if request.method == "GET": - keywords = request.GET.get("q") - - view_id = request.GET.get('sort', None) - view_dic = {"latest":"-added_at", "active":"-last_activity_at", "hottest":"-answer_count", "mostvoted":"-score" } - try: - orderby = view_dic[view_id] - except KeyError: - view_id = "latest" - orderby = "-added_at" - - def question_search(keywords, orderby): - objects = Question.objects.filter(deleted=False).extra(where=['title like %s'], params=['%' + keywords + '%']).order_by(orderby) - # RISK - inner join queries - return objects.select_related(); - - from forum.modules import get_handler - - question_search = get_handler('question_search', question_search) - - objects = question_search(keywords, orderby) - - objects_list = Paginator(objects, pagesize) - questions = objects_list.page(page) - - # Get related tags from this page objects - related_tags = [] - for question in questions.object_list: - tags = list(question.tags.all()) - for tag in tags: - if tag not in related_tags: - related_tags.append(tag) - - #if is_search is true in the context, prepend this string to soting tabs urls - search_uri = "?q=%s&page=%d&t=question" % ("+".join(keywords.split()), page) - - return render_to_response(template_file, { - "questions" : questions, - "tab_id" : view_id, - "questions_count" : objects_list.count, - "tags" : related_tags, - "searchtag" : None, - "searchtitle" : keywords, - "keywords" : keywords, - "is_unanswered" : False, - "is_search": True, - "search_uri": search_uri, - "context" : { - 'is_paginated' : True, - 'pages': objects_list.num_pages, - 'page': page, - 'has_previous': questions.has_previous(), - 'has_next': questions.has_next(), - 'previous': questions.previous_page_number(), - 'next': questions.next_page_number(), - 'base_url' : request.path + '?t=question&q=%s&sort=%s&' % (keywords, view_id), - 'pagesize' : pagesize - }}, context_instance=RequestContext(request)) - - else: - raise Http404 - def search(request): #generates listing of questions matching a search query - including tags and just words """redirects to people and tag search pages todo: eliminate this altogether and instead make @@ -258,7 +177,6 @@ def search(request): #generates listing of questions matching a search query - i else: raise Http404 - def tag(request, tag):#stub generates listing of questions tagged with a single tag return questions(request, tagname=tag) diff --git a/locale/en/LC_MESSAGES/django.mo b/locale/en/LC_MESSAGES/django.mo Binary files differindex 65af2080..cac204e1 100644 --- a/locale/en/LC_MESSAGES/django.mo +++ b/locale/en/LC_MESSAGES/django.mo diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 23e1d410..e773b46f 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -1690,7 +1690,7 @@ msgstr "" #: forum/skins/default/templates/header.html:29 #: forum/skins/default/templates/header.html:57 msgid "users" -msgstr "" +msgstr "people" #: forum/skins/default/templates/header.html:31 msgid "books" diff --git a/settings.py b/settings.py index c8098df9..4d799a13 100644 --- a/settings.py +++ b/settings.py @@ -31,6 +31,7 @@ MIDDLEWARE_CLASSES = ( #'recaptcha_django.middleware.ReCaptchaMiddleware', 'django.middleware.transaction.TransactionMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware', + 'forum.middleware.view_log.ViewLogMiddleware', ) TEMPLATE_CONTEXT_PROCESSORS = ( |