diff options
author | Adolfo Fitoria <fitoria@fitoria-laptop.(none)> | 2010-02-09 14:12:05 -0600 |
---|---|---|
committer | Adolfo Fitoria <fitoria@fitoria-laptop.(none)> | 2010-02-09 14:12:05 -0600 |
commit | 8de2b9131ddcef647799cf8e1e79921284523073 (patch) | |
tree | 81e17d84530990e35a0accba3a7886266a601482 /forum | |
parent | 7e95e6481d1e81e43d4b442cbcf3fe37f20d89cc (diff) | |
parent | 9d1fb9890b97beb55461ca34f9757bc685461130 (diff) | |
download | askbot-8de2b9131ddcef647799cf8e1e79921284523073.tar.gz askbot-8de2b9131ddcef647799cf8e1e79921284523073.tar.bz2 askbot-8de2b9131ddcef647799cf8e1e79921284523073.zip |
Merge branch 'evgenyfadeev/master'
Conflicts:
.gitignore
INSTALL
TODO
cnprog.wsgi
django_authopenid/urls.py
django_authopenid/views.py
drop-all-tables.sh
forum/auth.py
forum/managers.py
forum/models.py
forum/templatetags/extra_tags.py
forum/views.py
locale/es/LC_MESSAGES/django.mo
locale/es/LC_MESSAGES/django.po
settings.py
settings_local.py.dist
sql_scripts/update_2009_01_25_001.sql
sql_scripts/update_2009_02_26_001.sql
sql_scripts/update_2009_04_10_001.sql
templates/authopenid/confirm_email.txt
templates/authopenid/sendpw_email.txt
templates/content/js/compress.bat
templates/content/js/flot-build.bat
templates/content/style/style.css
templates/footer.html
templates/question.html
templates/user_reputation.html
templates/user_stats.html
templates/user_votes.html
templates/users_questions.html
urls.py
Diffstat (limited to 'forum')
-rw-r--r-- | forum/auth.py | 94 | ||||
-rw-r--r-- | forum/const.py | 7 | ||||
-rw-r--r-- | forum/feed.py | 4 | ||||
-rw-r--r-- | forum/forms.py | 164 | ||||
-rw-r--r-- | forum/management/commands/send_email_alerts.py | 287 | ||||
-rw-r--r-- | forum/management/commands/subscribe_everyone.py | 31 | ||||
-rw-r--r-- | forum/managers.py | 30 | ||||
-rw-r--r-- | forum/models.py | 207 | ||||
-rw-r--r-- | forum/sitemap.py | 11 | ||||
-rw-r--r-- | forum/templatetags/extra_filters.py | 20 | ||||
-rw-r--r-- | forum/templatetags/extra_tags.py | 127 | ||||
-rw-r--r-- | forum/templatetags/smart_if.py | 401 | ||||
-rw-r--r-- | forum/urls.py | 91 | ||||
-rw-r--r-- | forum/user.py | 12 | ||||
-rw-r--r-- | forum/views.py | 1204 |
15 files changed, 2062 insertions, 628 deletions
diff --git a/forum/auth.py b/forum/auth.py index 776746e8..eb81f853 100644 --- a/forum/auth.py +++ b/forum/auth.py @@ -1,4 +1,4 @@ -""" + """ Authorisation related functions. The actions a User is authorised to perform are dependent on their reputation @@ -6,18 +6,20 @@ and superuser status. """ import datetime from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext as _ from django.db import transaction from models import Repute from models import Question from models import Answer from const import TYPE_REPUTATION +import logging question_type = ContentType.objects.get_for_model(Question) answer_type = ContentType.objects.get_for_model(Answer) VOTE_UP = 15 FLAG_OFFENSIVE = 15 POST_IMAGES = 15 -LEAVE_COMMENTS = 50 +LEAVE_COMMENTS = 50 UPLOAD_FILES = 60 VOTE_DOWN = 100 CLOSE_OWN_QUESTIONS = 250 @@ -58,6 +60,9 @@ REPUTATION_RULES = { 'lose_by_upvote_canceled' : -10, } +def can_moderate_users(user): + return user.is_superuser + def can_vote_up(user): """Determines if a User can vote Questions and Answers up.""" return user.is_authenticated() and ( @@ -70,11 +75,18 @@ def can_flag_offensive(user): user.reputation >= FLAG_OFFENSIVE or user.is_superuser) -def can_add_comments(user): +def can_add_comments(user,subject): """Determines if a User can add comments to Questions and Answers.""" - return user.is_authenticated() and ( - user.reputation >= LEAVE_COMMENTS or - user.is_superuser) + if user.is_authenticated(): + if user.id == subject.author.id: + return True + if user.reputation >= LEAVE_COMMENTS: + return True + if user.is_superuser: + return True + if isinstance(subject,Answer) and subject.question.author.id == user.id: + return True + return False def can_vote_down(user): """Determines if a User can vote Questions and Answers down.""" @@ -139,8 +151,21 @@ def can_reopen_question(user, question): user.reputation >= REOPEN_OWN_QUESTIONS) or user.is_superuser def can_delete_post(user, post): - return (user.is_authenticated() and - user.id == post.author_id) or user.is_superuser + if user.is_superuser: + return True + elif user.is_authenticated() and user == post.author: + if isinstance(post,Answer): + return True + elif isinstance(post,Question): + answers = post.answers.all() + for answer in answers: + if user != answer.author and answer.deleted == False: + return False + return True + else: + return False + else: + return False def can_view_deleted_post(user, post): return user.is_superuser @@ -422,15 +447,20 @@ def onDownVotedCanceled(vote, post, user): def onDeleteCanceled(post, user): post.deleted = False - post.deleted_by = None - post.deleted_at = None + post.deleted_by = None + post.deleted_at = None post.save() - for tag in list(post.tags.all()): - if tag.used_count == 1 and tag.deleted: - tag.deleted = False - tag.deleted_by = None - tag.deleted_at = None - tag.save() + logging.debug('now restoring something') + if isinstance(post,Answer): + logging.debug('updated answer count on undelete, have %d' % post.question.answer_count) + Question.objects.update_answer_count(post.question) + elif isinstance(post,Question): + for tag in list(post.tags.all()): + if tag.used_count == 1 and tag.deleted: + tag.deleted = False + tag.deleted_by = None + tag.deleted_at = None + tag.save() def onDeleted(post, user): post.deleted = True @@ -438,9 +468,31 @@ def onDeleted(post, user): post.deleted_at = datetime.datetime.now() post.save() - for tag in list(post.tags.all()): - if tag.used_count == 1: - tag.deleted = True - tag.deleted_by = user - tag.deleted_at = datetime.datetime.now() + if isinstance(post, Question): + for tag in list(post.tags.all()): + if tag.used_count == 1: + tag.deleted = True + tag.deleted_by = user + tag.deleted_at = datetime.datetime.now() + else: + tag.used_count = tag.used_count - 1 tag.save() + + answers = post.answers.all() + if user == post.author: + if len(answers) > 0: + msg = _('Your question and all of it\'s answers have been deleted') + else: + msg = _('Your question has been deleted') + else: + if len(answers) > 0: + msg = _('The question and all of it\'s answers have been deleted') + else: + msg = _('The question has been deleted') + user.message_set.create(message=msg) + logging.debug('posted a message %s' % msg) + for answer in answers: + onDeleted(answer, user) + elif isinstance(post, Answer): + Question.objects.update_answer_count(post.question) + logging.debug('updated answer count to %d' % post.question.answer_count) diff --git a/forum/const.py b/forum/const.py index f6649cc4..76fd4a24 100644 --- a/forum/const.py +++ b/forum/const.py @@ -6,7 +6,7 @@ For reasons that models, views can't have unicode text in this project, all unic """ CLOSE_REASONS = ( (1, _('duplicate question')), - (2, _('question if off-topic or not relevant')), + (2, _('question is off-topic or not relevant')), (3, _('too subjective and argumentative')), (4, _('is not an answer to the question')), (5, _('the question is answered, right answer was accepted')), @@ -49,6 +49,7 @@ TYPE_ACTIVITY_MARK_OFFENSIVE=14 TYPE_ACTIVITY_UPDATE_TAGS=15 TYPE_ACTIVITY_FAVORITE=16 TYPE_ACTIVITY_USER_FULL_UPDATED = 17 +TYPE_ACTIVITY_QUESTION_EMAIL_UPDATE_SENT = 18 #TYPE_ACTIVITY_EDIT_QUESTION=17 #TYPE_ACTIVITY_EDIT_ANSWER=18 @@ -70,6 +71,7 @@ TYPE_ACTIVITY = ( (TYPE_ACTIVITY_UPDATE_TAGS, _('updated tags')), (TYPE_ACTIVITY_FAVORITE, _('selected favorite')), (TYPE_ACTIVITY_USER_FULL_UPDATED, _('completed user profile')), + (TYPE_ACTIVITY_QUESTION_EMAIL_UPDATE_SENT, _('email update sent to user')), ) TYPE_RESPONSE = { @@ -85,3 +87,6 @@ CONST = { 'default_version' : _('initial version'), 'retagged' : _('retagged'), } + +#how to filter questions by tags in email digests? +TAG_EMAIL_FILTER_CHOICES = (('ignored', _('exclude ignored tags')),('interesting',_('allow only selected tags'))) diff --git a/forum/feed.py b/forum/feed.py index 22a075a5..59983161 100644 --- a/forum/feed.py +++ b/forum/feed.py @@ -16,7 +16,7 @@ from models import Question import settings class RssLastestQuestionsFeed(Feed): title = settings.APP_TITLE + _(' - ')+ _('latest questions') - link = settings.APP_URL + '/' + _('questions/') + link = settings.APP_URL + '/' + _('question/') description = settings.APP_DESCRIPTION #ttl = 10 copyright = settings.APP_COPYRIGHT @@ -34,7 +34,7 @@ class RssLastestQuestionsFeed(Feed): return item.added_at def items(self, item): - return Question.objects.filter(deleted=False).order_by('-added_at')[:30] + return Question.objects.filter(deleted=False).order_by('-last_activity_at')[:30] def main(): pass diff --git a/forum/forms.py b/forum/forms.py index 98ae3cbb..ad8a676a 100644 --- a/forum/forms.py +++ b/forum/forms.py @@ -4,6 +4,8 @@ from django import forms from models import * from const import * from django.utils.translation import ugettext as _ +from django_authopenid.forms import NextUrlField, UserNameField +import settings class TitleField(forms.CharField): def __init__(self, *args, **kwargs): @@ -47,26 +49,28 @@ class TagNamesField(forms.CharField): self.help_text = _('Tags are short keywords, with no spaces within. Up to five tags can be used.') self.initial = '' - def clean(self, value): - value = super(TagNamesField, self).clean(value) - data = value.strip() - if len(data) < 1: - raise forms.ValidationError(_('tags are required')) - list = data.split(' ') - list_temp = [] - if len(list) > 5: - raise forms.ValidationError(_('please use 5 tags or less')) - for tag in list: - if len(tag) > 20: - raise forms.ValidationError(_('tags must be shorter than 20 characters')) - #take tag regex from settings - tagname_re = re.compile(r'[a-z0-9]+') - if not tagname_re.match(tag): - raise forms.ValidationError(_('please use following characters in tags: letters \'a-z\', numbers, and characters \'.-_#\'')) - # only keep one same tag - if tag not in list_temp and len(tag.strip()) > 0: - list_temp.append(tag) - return u' '.join(list_temp) + def clean(self, value): + value = super(TagNamesField, self).clean(value) + data = value.strip() + if len(data) < 1: + raise forms.ValidationError(_('tags are required')) + + split_re = re.compile(r'[ ,]+') + list = split_re.split(data) + list_temp = [] + if len(list) > 5: + raise forms.ValidationError(_('please use 5 tags or less')) + for tag in list: + if len(tag) > 20: + raise forms.ValidationError(_('tags must be shorter than 20 characters')) + #take tag regex from settings + tagname_re = re.compile(r'[a-z0-9]+') + if not tagname_re.match(tag): + raise forms.ValidationError(_('please use following characters in tags: letters \'a-z\', numbers, and characters \'.-_#\'')) + # only keep one same tag + if tag not in list_temp and len(tag.strip()) > 0: + list_temp.append(tag) + return u' '.join(list_temp) class WikiField(forms.BooleanField): def __init__(self, *args, **kwargs): @@ -74,11 +78,14 @@ class WikiField(forms.BooleanField): self.required = False self.label = _('community wiki') self.help_text = _('if you choose community wiki option, the question and answer do not generate points and name of author will not be shown') + def clean(self,value): + return value and settings.WIKI_ON class EmailNotifyField(forms.BooleanField): def __init__(self, *args, **kwargs): super(EmailNotifyField, self).__init__(*args, **kwargs) self.required = False + self.widget.attrs['class'] = 'nomargin' class SummaryField(forms.CharField): def __init__(self, *args, **kwargs): @@ -89,6 +96,25 @@ class SummaryField(forms.CharField): self.label = _('update summary:') self.help_text = _('enter a brief summary of your revision (e.g. fixed spelling, grammar, improved style, this field is optional)') +class ModerateUserForm(forms.ModelForm): + is_approved = forms.BooleanField(label=_("Automatically accept user's contributions for the email updates"), + required=False) + + def clean_is_approved(self): + if 'is_approved' not in self.cleaned_data: + self.cleaned_data['is_approved'] = False + return self.cleaned_data['is_approved'] + + class Meta: + model = User + fields = ('is_approved',) + +class FeedbackForm(forms.Form): + name = forms.CharField(label=_('Your name:'), required=False) + email = forms.EmailField(label=_('Email (not shared with anyone):'), required=False) + message = forms.CharField(label=_('Your message:'), max_length=800,widget=forms.Textarea(attrs={'cols':60})) + next = NextUrlField() + class AskForm(forms.Form): title = TitleField() text = EditorField() @@ -109,16 +135,12 @@ class AnswerForm(forms.Form): def __init__(self, question, user, *args, **kwargs): super(AnswerForm, self).__init__(*args, **kwargs) self.fields['email_notify'].widget.attrs['id'] = 'question-subscribe-updates'; - if question.wiki: + if question.wiki and settings.WIKI_ON: self.fields['wiki'].initial = True if user.is_authenticated(): - try: - feed = EmailFeed.objects.get(feed_id=question.id, subscriber_id=user.id) - if feed.subscriber == user and feed.content == question: - self.fields['email_notify'].initial = True - return - except EmailFeed.DoesNotExist: - pass + if user in question.followed_by.all(): + self.fields['email_notify'].initial = True + return self.fields['email_notify'].initial = False @@ -174,6 +196,7 @@ class EditAnswerForm(forms.Form): class EditUserForm(forms.Form): email = forms.EmailField(label=u'Email', help_text=_('this email does not have to be linked to gravatar'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) + username = UserNameField(label=_('Screen name')) realname = forms.CharField(label=_('Real name'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) website = forms.URLField(label=_('Website'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) city = forms.CharField(label=_('Location'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) @@ -182,6 +205,7 @@ class EditUserForm(forms.Form): def __init__(self, user, *args, **kwargs): super(EditUserForm, self).__init__(*args, **kwargs) + self.fields['username'].initial = user.username self.fields['email'].initial = user.email self.fields['realname'].initial = user.real_name self.fields['website'].initial = user.website @@ -208,3 +232,87 @@ class EditUserForm(forms.Form): raise forms.ValidationError(_('this email has already been registered, please use another one')) raise forms.ValidationError(_('this email has already been registered, please use another one')) return self.cleaned_data['email'] + +class TagFilterSelectionForm(forms.ModelForm): + tag_filter_setting = forms.ChoiceField(choices=TAG_EMAIL_FILTER_CHOICES, #imported from forum/const.py + initial='ignored', + label=_('Choose email tag filter'), + widget=forms.RadioSelect) + class Meta: + model = User + fields = ('tag_filter_setting',) + + def save(self): + before = self.instance.tag_filter_setting + super(TagFilterSelectionForm, self).save() + after = self.instance.tag_filter_setting #User.objects.get(pk=self.instance.id).tag_filter_setting + if before != after: + return True + return False + +class EditUserEmailFeedsForm(forms.Form): + WN = (('w',_('weekly')),('n',_('no email'))) + DWN = (('d',_('daily')),('w',_('weekly')),('n',_('no email'))) + FORM_TO_MODEL_MAP = { + 'all_questions':'q_all', + 'asked_by_me':'q_ask', + 'answered_by_me':'q_ans', + 'individually_selected':'q_sel', + } + NO_EMAIL_INITIAL = { + 'all_questions':'n', + 'asked_by_me':'n', + 'answered_by_me':'n', + 'individually_selected':'n', + } + asked_by_me = forms.ChoiceField(choices=DWN,initial='w', + widget=forms.RadioSelect, + label=_('Asked by me')) + answered_by_me = forms.ChoiceField(choices=DWN,initial='w', + widget=forms.RadioSelect, + label=_('Answered by me')) + individually_selected = forms.ChoiceField(choices=DWN,initial='w', + widget=forms.RadioSelect, + label=_('Individually selected')) + all_questions = forms.ChoiceField(choices=DWN,initial='w', + widget=forms.RadioSelect, + label=_('Entire forum (tag filtered)'),) + + def set_initial_values(self,user=None): + KEY_MAP = dict([(v,k) for k,v in self.FORM_TO_MODEL_MAP.iteritems()]) + if user != None: + settings = EmailFeedSetting.objects.filter(subscriber=user) + initial_values = {} + for setting in settings: + feed_type = setting.feed_type + form_field = KEY_MAP[feed_type] + frequency = setting.frequency + initial_values[form_field] = frequency + self.initial = initial_values + return self + + def reset(self): + self.cleaned_data['all_questions'] = 'n' + self.cleaned_data['asked_by_me'] = 'n' + self.cleaned_data['answered_by_me'] = 'n' + self.cleaned_data['individually_selected'] = 'n' + self.initial = self.NO_EMAIL_INITIAL + return self + + def save(self,user): + changed = False + for form_field, feed_type in self.FORM_TO_MODEL_MAP.items(): + s, created = EmailFeedSetting.objects.get_or_create(subscriber=user,\ + feed_type=feed_type) + new_value = self.cleaned_data[form_field] + if s.frequency != new_value: + s.frequency = self.cleaned_data[form_field] + s.save() + changed = True + else: + if created: + s.save() + if form_field == 'individually_selected': + feed_type = ContentType.objects.get_for_model(Question) + user.followed_questions.clear() + return changed diff --git a/forum/management/commands/send_email_alerts.py b/forum/management/commands/send_email_alerts.py index 3c37aaa3..283d5683 100644 --- a/forum/management/commands/send_email_alerts.py +++ b/forum/management/commands/send_email_alerts.py @@ -1,10 +1,21 @@ from django.core.management.base import NoArgsCommand from django.db import connection +from django.db.models import Q, F from forum.models import * -import collections +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py +======= +from forum import const +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py from django.core.mail import EmailMessage from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext +import datetime import settings +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py +======= +import logging +from utils.odict import OrderedDict +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py class Command(NoArgsCommand): def handle_noargs(self,**options): @@ -15,27 +26,257 @@ class Command(NoArgsCommand): finally: connection.close() + def get_updated_questions_for_user(self,user): +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py + q_sel = [] + q_ask = [] + q_ans = [] + q_all = [] +======= + q_sel = None + q_ask = None + q_ans = None + q_all = None +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py + now = datetime.datetime.now() + Q_set1 = Question.objects.exclude( + last_activity_by=user, + ).exclude( + last_activity_at__lt=user.date_joined + ).filter( + Q(viewed__who=user,viewed__when__lt=F('last_activity_at')) | \ + ~Q(viewed__who=user) + ).exclude( + deleted=True + ).exclude( + closed=True + ) +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py +======= + +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py + user_feeds = EmailFeedSetting.objects.filter(subscriber=user).exclude(frequency='n') + for feed in user_feeds: + cutoff_time = now - EmailFeedSetting.DELTA_TABLE[feed.frequency] + if feed.reported_at == None or feed.reported_at <= cutoff_time: +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py + Q_set = Q_set1.exclude(last_activity_at__gt=cutoff_time) +======= + Q_set = Q_set1.exclude(last_activity_at__gt=cutoff_time)#report these excluded later +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py + feed.reported_at = now + feed.save()#may not actually report anything, depending on filters below + if feed.feed_type == 'q_sel': + q_sel = Q_set.filter(followed_by=user) +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py + q_sel.cutoff_time = cutoff_time +======= + q_sel.cutoff_time = cutoff_time #store cutoff time per query set +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py + elif feed.feed_type == 'q_ask': + q_ask = Q_set.filter(author=user) + q_ask.cutoff_time = cutoff_time + elif feed.feed_type == 'q_ans': + q_ans = Q_set.filter(answers__author=user) + q_ans.cutoff_time = cutoff_time + elif feed.feed_type == 'q_all': +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py + q_all = Q_set + q_all.cutoff_time = cutoff_time + #build list in this order + q_tbl = {} + def extend_question_list(src, dst): + if isinstance(src,list): + return + cutoff_time = src.cutoff_time + for q in src: + if q in dst: + if cutoff_time < dst[q]: + dst[q] = cutoff_time + else: + dst[q] = cutoff_time + + extend_question_list(q_sel, q_tbl) + extend_question_list(q_ask, q_tbl) + extend_question_list(q_ans, q_tbl) + extend_question_list(q_all, q_tbl) + + ctype = ContentType.objects.get_for_model(Question) + out = {} + for q, cutoff_time in q_tbl.items(): + #todo use Activity, but first start keeping more Activity records + #act = Activity.objects.filter(content_type=ctype, object_id=q.id) + #get info on question edits, answer edits, comments + out[q] = {} + q_rev = QuestionRevision.objects.filter(question=q,revised_at__lt=cutoff_time) + q_rev = q_rev.exclude(author=user) + out[q]['q_rev'] = len(q_rev) + if len(q_rev) > 0 and q.added_at == q_rev[0].revised_at: + out[q]['q_rev'] = 0 + out[q]['new_q'] = True + else: + out[q]['new_q'] = False + + new_ans = Answer.objects.filter(question=q,added_at__lt=cutoff_time) + new_ans = new_ans.exclude(author=user) + out[q]['new_ans'] = len(new_ans) + ans_rev = AnswerRevision.objects.filter(answer__question=q,revised_at__lt=cutoff_time) + ans_rev = ans_rev.exclude(author=user) + out[q]['ans_rev'] = len(ans_rev) + return out + + def __act_count(self,string,number,output): +======= + if user.tag_filter_setting == 'ignored': + ignored_tags = Tag.objects.filter(user_selections___reason='bad',user_selections__user=user) + q_all = Q_set.exclude( tags__in=ignored_tags ) + else: + selected_tags = Tag.objects.filter(user_selections___reason='good',user_selections__user=user) + q_all = Q_set.filter( tags__in=selected_tags ) + q_all.cutoff_time = cutoff_time + #build list in this order + q_list = OrderedDict() + def extend_question_list(src, dst): + """src is a query set with questions + or an empty list + dst - is an ordered dictionary + """ + if src is None: + return #will not do anything if subscription of this type is not used + cutoff_time = src.cutoff_time + for q in src: + if q in dst: + if cutoff_time < dst[q]['cutoff_time']: + dst[q]['cutoff_time'] = cutoff_time + else: + #initialise a questions metadata dictionary to use for email reporting + dst[q] = {'cutoff_time':cutoff_time} + + extend_question_list(q_sel, q_list) + extend_question_list(q_ask, q_list) + extend_question_list(q_ans, q_list) + extend_question_list(q_all, q_list) + + ctype = ContentType.objects.get_for_model(Question) + EMAIL_UPDATE_ACTIVITY = const.TYPE_ACTIVITY_QUESTION_EMAIL_UPDATE_SENT + for q, meta_data in q_list.items(): + #todo use Activity, but first start keeping more Activity records + #act = Activity.objects.filter(content_type=ctype, object_id=q.id) + #because currently activity is not fully recorded to through + #revision records to see what kind modifications were done on + #the questions and answers + try: + update_info = Activity.objects.get(content_type=ctype, + object_id=q.id, + activity_type=EMAIL_UPDATE_ACTIVITY) + emailed_at = update_info.active_at + except Activity.DoesNotExist: + update_info = Activity(user=user, content_object=q, activity_type=EMAIL_UPDATE_ACTIVITY) + emailed_at = datetime.datetime(1970,1,1)#long time ago + except Activity.MultipleObjectsReturned: + raise Exception('server error - multiple question email activities found per user-question pair') + + q_rev = QuestionRevision.objects.filter(question=q,\ + revised_at__lt=cutoff_time,\ + revised_at__gt=emailed_at) + q_rev = q_rev.exclude(author=user) + meta_data['q_rev'] = len(q_rev) + if len(q_rev) > 0 and q.added_at == q_rev[0].revised_at: + meta_data['q_rev'] = 0 + meta_data['new_q'] = True + else: + meta_data['new_q'] = False + + new_ans = Answer.objects.filter(question=q,\ + added_at__lt=cutoff_time,\ + added_at__gt=emailed_at) + new_ans = new_ans.exclude(author=user) + meta_data['new_ans'] = len(new_ans) + ans_rev = AnswerRevision.objects.filter(answer__question=q,\ + revised_at__lt=cutoff_time,\ + revised_at__gt=emailed_at) + ans_rev = ans_rev.exclude(author=user) + meta_data['ans_rev'] = len(ans_rev) + if len(q_rev) == 0 and len(new_ans) == 0 and len(ans_rev) == 0: + meta_data['nothing_new'] = True + else: + meta_data['nothing_new'] = False + update_info.active_at = now + update_info.save() #save question email update activity + return q_list + + def __action_count(self,string,number,output): +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py + if number > 0: + output.append(_(string) % {'num':number}) + def send_email_alerts(self): - report_time = datetime.datetime.now() - feeds = EmailFeed.objects.all() - user_ctype = ContentType.objects.get_for_model(User) - - #lists of update messages keyed by email address - update_collection = collections.defaultdict(list) - for feed in feeds: - update_summary = feed.get_update_summary() - if update_summary != None: - email = feed.get_email() - update_collection[email].append(update_summary) - feed.reported_at = report_time - feed.save() - - for email, updates in update_collection.items(): - text = '\n'.join(updates) - subject = _('updates from website') - print 'sent %s to %s' % (updates,email) - msg = EmailMessage(subject, text, settings.DEFAULT_FROM_EMAIL, [email]) - msg.content_subtype = 'html' - msg.send() - +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py + for user in User.objects.all(): + q_list = self.get_updated_questions_for_user(user) + num_q = len(q_list) +======= + #todo: move this to template + for user in User.objects.all(): + q_list = self.get_updated_questions_for_user(user) + num_q = 0 + num_moot = 0 + for meta_data in q_list.values(): + if meta_data['nothing_new'] == False: + num_q += 1 + else: + num_moot += 1 +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py + if num_q > 0: + url_prefix = settings.APP_URL + subject = _('email update message subject') + text = ungettext('%(name)s, this is an update message header for a question', + '%(name)s, this is an update message header for %(num)d questions',num_q) \ + % {'num':num_q, 'name':user.username} + + text += '<ul>' +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py + for q, act in q_list.items(): + act_list = [] + if act['new_q']: + act_list.append(_('new question')) + self.__act_count('%(num)d rev', act['q_rev'],act_list) + self.__act_count('%(num)d ans', act['new_ans'],act_list) + self.__act_count('%(num)d ans rev',act['ans_rev'],act_list) + act_token = ', '.join(act_list) + text += '<li><a href="%s?sort=latest">%s</a> <font color="#777777">(%s)</font></li>' \ + % (url_prefix + q.get_absolute_url(), q.title, act_token) + text += '</ul>' +======= + for q, meta_data in q_list.items(): + act_list = [] + if meta_data['nothing_new']: + continue + else: + if meta_data['new_q']: + act_list.append(_('new question')) + self.__action_count('%(num)d rev', meta_data['q_rev'],act_list) + self.__action_count('%(num)d ans', meta_data['new_ans'],act_list) + self.__action_count('%(num)d ans rev',meta_data['ans_rev'],act_list) + act_token = ', '.join(act_list) + text += '<li><a href="%s?sort=latest">%s</a> <font color="#777777">(%s)</font></li>' \ + % (url_prefix + q.get_absolute_url(), q.title, act_token) + text += '</ul>' + if num_moot > 0: + text += '<p></p>' + text += ungettext('There is also one question which was recently '\ + +'updated but you might not have seen its latest version.', + 'There are also %(num)d more questions which were recently updated '\ + +'but you might not have seen their latest version.',num_moot) \ + % {'num':num_moot,} + text += _('Perhaps you could look up previously sent forum reminders in your mailbox.') + text += '</p>' + +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py + link = url_prefix + user.get_profile_url() + '?sort=email_subscriptions' + text += _('go to %(link)s to change frequency of email updates or %(email)s administrator') \ + % {'link':link, 'email':settings.ADMINS[0][1]} + msg = EmailMessage(subject, text, settings.DEFAULT_FROM_EMAIL, [user.email]) + msg.content_subtype = 'html' + msg.send() diff --git a/forum/management/commands/subscribe_everyone.py b/forum/management/commands/subscribe_everyone.py new file mode 100644 index 00000000..3f8da9ec --- /dev/null +++ b/forum/management/commands/subscribe_everyone.py @@ -0,0 +1,31 @@ +from django.core.management.base import NoArgsCommand +from django.db import connection +from django.db.models import Q, F +from forum.models import * +from django.core.mail import EmailMessage +from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext +import datetime +import settings + +class Command(NoArgsCommand): + def handle_noargs(self,**options): + try: + self.subscribe_everyone() + except Exception, e: + print e + finally: + connection.close() + + def subscribe_everyone(self): + + feed_type_info = EmailFeedSetting.FEED_TYPES + for user in User.objects.all(): + for feed_type in feed_type_info: + try: + feed_setting = EmailFeedSetting.objects.get(subscriber=user,feed_type = feed_type[0]) + except EmailFeedSetting.DoesNotExist: + feed_setting = EmailFeedSetting(subscriber=user,feed_type=feed_type[0]) + feed_setting.frequency = 'w' + feed_setting.reported_at = None + feed_setting.save() diff --git a/forum/managers.py b/forum/managers.py index 31528428..1504491a 100644 --- a/forum/managers.py +++ b/forum/managers.py @@ -7,25 +7,6 @@ from forum.models import * from urllib import quote, unquote class QuestionManager(models.Manager): - def get_translation_questions(self, orderby, page_size): - questions = self.filter(deleted=False, author__id__in=[28,29]).order_by(orderby)[:page_size] - return questions - - def get_questions_by_pagesize(self, orderby, page_size): - questions = self.filter(deleted=False).order_by(orderby)[:page_size] - return questions - - def get_questions_by_tag(self, tagname, orderby): - questions = self.filter(deleted=False, tags__name = unquote(tagname)).order_by(orderby) - return questions - - def get_unanswered_questions(self, orderby): - questions = self.filter(deleted=False, answer_accepted=False).order_by(orderby) - return questions - - def get_questions(self, orderby): - questions = self.filter(deleted=False).order_by(orderby) - return questions def update_tags(self, question, tagnames, user): """ @@ -70,7 +51,7 @@ class QuestionManager(models.Manager): # although we have imported all classes from models on top. from forum.models import Answer self.filter(id=question.id).update( - answer_count=Answer.objects.get_answers_from_question(question).count()) + answer_count=Answer.objects.get_answers_from_question(question).filter(deleted=False).count()) def update_view_count(self, question): """ @@ -93,11 +74,11 @@ class QuestionManager(models.Manager): """ #print datetime.datetime.now() from forum.models import Question - questions = list(Question.objects.filter(tagnames = question.tagnames).exclude(id=question.id).all()) + questions = list(self.filter(tagnames = question.tagnames, deleted=False).all()) tags_list = question.tags.all() for tag in tags_list: - extend_questions = Question.objects.filter(tags__id = tag.id).exclude(id=question.id)[:50] + extend_questions = self.filter(tags__id = tag.id, deleted=False)[:50] for item in extend_questions: if item not in questions and len(questions) < 10: questions.append(item) @@ -110,10 +91,11 @@ class TagManager(models.Manager): 'UPDATE tag ' 'SET used_count = (' 'SELECT COUNT(*) FROM question_tags ' - 'WHERE tag_id = tag.id' + 'INNER JOIN question ON question_id=question.id ' + 'WHERE tag_id = tag.id AND question.deleted=0' ') ' 'WHERE id IN (%s)') - + def get_valid_tags(self, page_size): from forum.models import Tag tags = Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("-id")[:page_size] diff --git a/forum/models.py b/forum/models.py index 3d752db0..3e1e6543 100644 --- a/forum/models.py +++ b/forum/models.py @@ -2,7 +2,7 @@ import datetime import hashlib from urllib import quote_plus, urlencode -from django.db import models +from django.db import models, IntegrityError from django.utils.http import urlquote as django_urlquote from django.utils.html import strip_tags from django.core.urlresolvers import reverse @@ -12,33 +12,66 @@ from django.contrib.contenttypes.models import ContentType from django.template.defaultfilters import slugify from django.db.models.signals import post_delete, post_save, pre_save from django.utils.translation import ugettext as _ +from django.utils.safestring import mark_safe +from django.contrib.sitemaps import ping_google import django.dispatch import settings +import logging + +if settings.USE_SPHINX_SEARCH == True: + from djangosphinx.models import SphinxSearch from forum.managers import * from const import * -class EmailFeed(models.Model): - #subscription key for unsubscribe by visiting emailed link - key = models.CharField(max_length=32) - #generic relation with feed content (i.e. question or tags) - feed_content_type = models.ForeignKey(ContentType,related_name='content_emailfeed') - feed_id = models.PositiveIntegerField() - content = generic.GenericForeignKey('feed_content_type','feed_id') - #generic relation with owner - either nameless email or User - subscriber_content_type = models.ForeignKey(ContentType,related_name='subscriber_emailfeed') - subscriber_id = models.PositiveIntegerField() - subscriber = generic.GenericForeignKey('subscriber_content_type','subscriber_id') - added_at = models.DateTimeField(default=datetime.datetime.now) - reported_at = models.DateTimeField(default=datetime.datetime.now) - - #getter functions rely on implementations of similar functions in content - #of subscriber objects - def get_update_summary(self): - return self.content.get_update_summary(last_reported_at = self.reported_at,recipient_email = self.get_email()) - - def get_email(self): - return self.subscriber.email +def get_object_comments(self): + comments = self.comments.all().order_by('id') + return comments + +def post_get_last_update_info(self): + when = self.added_at + who = self.author + if self.last_edited_at and self.last_edited_at > when: + when = self.last_edited_at + who = self.last_edited_by + comments = self.comments.all() + if len(comments) > 0: + for c in comments: + if c.added_at > when: + when = c.added_at + who = c.user + return when, who + +class EmailFeedSetting(models.Model): + DELTA_TABLE = { + 'w':datetime.timedelta(7), + 'd':datetime.timedelta(1), + 'n':datetime.timedelta(-1), + } + FEED_TYPES = ( + ('q_all',_('Entire forum')), + ('q_ask',_('Questions that I asked')), + ('q_ans',_('Questions that I answered')), + ('q_sel',_('Individually selected questions')), + ) + UPDATE_FREQUENCY = ( + ('w',_('Weekly')), + ('d',_('Daily')), + ('n',_('No email')), + ) + subscriber = models.ForeignKey(User) + feed_type = models.CharField(max_length=16,choices=FEED_TYPES) + frequency = models.CharField(max_length=8,choices=UPDATE_FREQUENCY,default='n') + added_at = models.DateTimeField(auto_now_add=True) + reported_at = models.DateTimeField(null=True) + + def save(self,*args,**kwargs): + type = self.feed_type + subscriber = self.subscriber + similar = self.__class__.objects.filter(feed_type=type,subscriber=subscriber).exclude(pk=self.id) + if len(similar) > 0: + raise IntegrityError('email feed setting already exists') + super(EmailFeedSetting,self).save(*args,**kwargs) class Tag(models.Model): name = models.CharField(max_length=255, unique=True) @@ -46,7 +79,6 @@ class Tag(models.Model): deleted = models.BooleanField(default=False) deleted_at = models.DateTimeField(null=True, blank=True) deleted_by = models.ForeignKey(User, null=True, blank=True, related_name='deleted_tags') - email_feeds = generic.GenericRelation(EmailFeed) # Denormalised data used_count = models.PositiveIntegerField(default=0) @@ -70,6 +102,14 @@ class Comment(models.Model): class Meta: ordering = ('-added_at',) db_table = u'comment' + + def save(self,**kwargs): + super(Comment,self).save(**kwargs) + try: + ping_google() + except Exception: + logging.debug('problem pinging google did you register you sitemap with google?') + def __unicode__(self): return self.comment @@ -137,6 +177,7 @@ class Question(models.Model): locked = models.BooleanField(default=False) locked_by = models.ForeignKey(User, null=True, blank=True, related_name='locked_questions') locked_at = models.DateTimeField(null=True, blank=True) + followed_by = models.ManyToManyField(User, related_name='followed_questions') # Denormalised data score = models.IntegerField(default=0) vote_up_count = models.IntegerField(default=0) @@ -156,10 +197,23 @@ class Question(models.Model): comments = generic.GenericRelation(Comment) votes = generic.GenericRelation(Vote) flagged_items = generic.GenericRelation(FlaggedItem) - email_feeds = generic.GenericRelation(EmailFeed) + + if settings.USE_SPHINX_SEARCH == True: + search = SphinxSearch( + index=' '.join(settings.SPHINX_SEARCH_INDICES), + mode='SPH_MATCH_ALL', + ) + logging.debug('have sphinx search') objects = QuestionManager() + def delete(self): + super(Question, self).delete() + try: + ping_google() + except Exception: + logging.debug('problem pinging google did you register you sitemap with google?') + def save(self, **kwargs): """ Overridden to manually manage addition of tags when the object @@ -170,6 +224,10 @@ class Question(models.Model): """ initial_addition = (self.id is None) super(Question, self).save(**kwargs) + try: + ping_google() + except Exception: + logging.debug('problem pinging google did you register you sitemap with google?') if initial_addition: tags = Tag.objects.get_or_create_multiple(self.tagname_list(), self.author) @@ -184,7 +242,7 @@ class Question(models.Model): return u','.join([unicode(tag) for tag in self.tagname_list()]) def get_absolute_url(self): - return '%s%s' % (reverse('question', args=[self.id]), django_urlquote(self.title.replace(' ', '-'))) + return '%s%s' % (reverse('question', args=[self.id]), django_urlquote(slugify(self.title))) def has_favorite_by_user(self, user): if not user.is_authenticated(): @@ -212,17 +270,23 @@ class Question(models.Model): def get_latest_revision(self): return self.revisions.all()[0] - - def get_user_votes_in_answers(self, user): - content_type = ContentType.objects.get_for_model(Answer) - query_set = Vote.objects.extra( - tables = ['question', 'answer'], - where = ['question.id = answer.question_id AND question.id = %s AND vote.object_id = answer.id AND vote.content_type_id = %s AND vote.user_id = %s'], - params = [self.id, content_type.id, user.id] - ) - - return query_set - + + get_comments = get_object_comments + + def get_last_update_info(self): + + when, who = post_get_last_update_info(self) + + answers = self.answers.all() + if len(answers) > 0: + for a in answers: + a_when, a_who = a.get_last_update_info() + if a_when > when: + when = a_when + who = a_who + + return when, who + def get_update_summary(self,last_reported_at=None,recipient_email=''): edited = False if self.last_edited_at and self.last_edited_at > last_reported_at: @@ -251,7 +315,7 @@ class Question(models.Model): answer_comments.append(comment) #create the report - if edited or comments or new_answers or modified_answers or answer_comments: + if edited or new_answers or modified_answers or answer_comments: out = [] if edited: out.append(_('%(author)s modified the question') % {'author':self.last_edited_by.username}) @@ -285,6 +349,11 @@ class Question(models.Model): class Meta: db_table = u'question' +class QuestionView(models.Model): + question = models.ForeignKey(Question, related_name='viewed') + who = models.ForeignKey(User, related_name='question_views') + when = models.DateTimeField() + class FavoriteQuestion(models.Model): """A favorite Question of a User.""" question = models.ForeignKey(Question) @@ -295,6 +364,12 @@ class FavoriteQuestion(models.Model): def __unicode__(self): return '[%s] favorited at %s' %(self.user, self.added_at) +class MarkedTag(models.Model): + TAG_MARK_REASONS = (('good',_('interesting')),('bad',_('ignored'))) + tag = models.ForeignKey(Tag, related_name='user_selections') + user = models.ForeignKey(User, related_name='tag_selections') + reason = models.CharField(max_length=16, choices=TAG_MARK_REASONS) + class QuestionRevision(models.Model): """A revision of a Question.""" question = models.ForeignKey(Question, related_name='revisions') @@ -314,7 +389,8 @@ class QuestionRevision(models.Model): return self.question.title def get_absolute_url(self): - return '/%s%s/%s' % (_('questions/'),self.question.id,_('revisions')) + print 'in QuestionRevision.get_absolute_url()' + return reverse('question_revisions', args=[self.question.id]) def save(self, **kwargs): """Looks up the next available revision number.""" @@ -394,6 +470,16 @@ class Answer(models.Model): objects = AnswerManager() + get_comments = get_object_comments + get_last_update_info = post_get_last_update_info + + def save(self,**kwargs): + super(Answer,self).save(**kwargs) + try: + ping_google() + except Exception: + logging.debug('problem pinging google did you register you sitemap with google?') + def get_user_vote(self, user): votes = self.votes.filter(user=user) if votes.count() > 0: @@ -408,7 +494,7 @@ class Answer(models.Model): return self.question.title def get_absolute_url(self): - return '%s%s#%s' % (reverse('question', args=[self.question.id]), django_urlquote(self.question.title), self.id) + return '%s%s#%s' % (reverse('question', args=[self.question.id]), django_urlquote(slugify(self.question.title)), self.id) class Meta: db_table = u'answer' @@ -426,7 +512,7 @@ class AnswerRevision(models.Model): text = models.TextField() def get_absolute_url(self): - return '/%s%s/%s' % (_('answers/'),self.answer.id,_('revisions')) + return reverse('answer_revisions', kwargs={'id':self.answer.id}) def get_question_title(self): return self.answer.question.title @@ -549,7 +635,7 @@ class Book(models.Model): questions = models.ManyToManyField(Question, related_name='book', db_table='book_question') def get_absolute_url(self): - return '%s' % reverse('book', args=[django_urlquote(self.short_name)]) + return reverse('book', args=[django_urlquote(slugify(self.short_name))]) def __unicode__(self): return self.title @@ -588,7 +674,6 @@ class AnonymousEmail(models.Model): key = models.CharField(max_length=32) email = models.EmailField(null=False,unique=True) isvalid = models.BooleanField(default=False) - feeds = generic.GenericRelation(EmailFeed) # User extend properties QUESTIONS_PER_PAGE_CHOICES = ( @@ -597,11 +682,30 @@ QUESTIONS_PER_PAGE_CHOICES = ( (50, u'50'), ) +def user_is_username_taken(cls,username): + try: + cls.objects.get(username=username) + return True + except cls.MultipleObjectsReturned: + return True + except cls.DoesNotExist: + return False + +def user_get_q_sel_email_feed_frequency(self): + print 'looking for frequency for user %s' % self + try: + feed_setting = EmailFeedSetting.objects.get(subscriber=self,feed_type='q_sel') + except Exception, e: + print 'have error %s' % e.message + raise e + print 'have freq=%s' % feed_setting.frequency + return feed_setting.frequency + +User.add_to_class('is_approved', models.BooleanField(default=False)) User.add_to_class('email_isvalid', models.BooleanField(default=False)) User.add_to_class('email_key', models.CharField(max_length=32, null=True)) User.add_to_class('reputation', models.PositiveIntegerField(default=1)) User.add_to_class('gravatar', models.CharField(max_length=32)) -User.add_to_class('email_feeds', generic.GenericRelation(EmailFeed)) User.add_to_class('favorite_questions', models.ManyToManyField(Question, through=FavoriteQuestion, related_name='favorited_by')) @@ -619,6 +723,16 @@ User.add_to_class('website', models.URLField(max_length=200, blank=True)) User.add_to_class('location', models.CharField(max_length=100, blank=True)) User.add_to_class('date_of_birth', models.DateField(null=True, blank=True)) User.add_to_class('about', models.TextField(blank=True)) +User.add_to_class('is_username_taken',classmethod(user_is_username_taken)) +User.add_to_class('get_q_sel_email_feed_frequency',user_get_q_sel_email_feed_frequency) +User.add_to_class('hide_ignored_questions', models.BooleanField(default=False)) +User.add_to_class('tag_filter_setting', + models.CharField( + max_length=16, + choices=TAG_EMAIL_FILTER_CHOICES, + default='ignored' + ) + ) # custom signal tags_updated = django.dispatch.Signal(providing_args=["question"]) @@ -641,7 +755,14 @@ def delete_messages(self): def get_profile_url(self): """Returns the URL for this User's profile.""" return '%s%s/' % (reverse('user', args=[self.id]), slugify(self.username)) + +def get_profile_link(self): + profile_link = u'<a href="%s">%s</a>' % (self.get_profile_url(),self.username) + logging.debug('in get profile link %s' % profile_link) + return mark_safe(profile_link) + User.add_to_class('get_profile_url', get_profile_url) +User.add_to_class('get_profile_link', get_profile_link) User.add_to_class('get_messages', get_messages) User.add_to_class('delete_messages', delete_messages) diff --git a/forum/sitemap.py b/forum/sitemap.py new file mode 100644 index 00000000..dc97a009 --- /dev/null +++ b/forum/sitemap.py @@ -0,0 +1,11 @@ +from django.contrib.sitemaps import Sitemap +from forum.models import Question + +class QuestionsSitemap(Sitemap): + changefreq = 'daily' + priority = 0.5 + def items(self): + return Question.objects.exclude(deleted=True) + + def lastmod(self, obj): + return obj.last_activity_at diff --git a/forum/templatetags/extra_filters.py b/forum/templatetags/extra_filters.py index d8b8e61f..865cd33d 100644 --- a/forum/templatetags/extra_filters.py +++ b/forum/templatetags/extra_filters.py @@ -1,5 +1,10 @@ from django import template +<<<<<<< HEAD:forum/templatetags/extra_filters.py +======= +from django.core import serializers +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/templatetags/extra_filters.py from forum import auth +import logging register = template.Library() @@ -9,6 +14,10 @@ def collapse(input): return ' '.join(input.split()) @register.filter +def can_moderate_users(user): + return auth.can_moderate_users(user) + +@register.filter def can_vote_up(user): return auth.can_vote_up(user) @@ -17,8 +26,8 @@ def can_flag_offensive(user): return auth.can_flag_offensive(user) @register.filter -def can_add_comments(user): - return auth.can_add_comments(user) +def can_add_comments(user,subject): + return auth.can_add_comments(user,subject) @register.filter def can_vote_down(user): @@ -86,3 +95,10 @@ def cnprog_intword(number): return number except: return number +<<<<<<< HEAD:forum/templatetags/extra_filters.py +======= + +@register.filter +def json_serialize(object): + return serializers.serialize('json',object) +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/templatetags/extra_filters.py diff --git a/forum/templatetags/extra_tags.py b/forum/templatetags/extra_tags.py index 90ebb65b..b2199284 100644 --- a/forum/templatetags/extra_tags.py +++ b/forum/templatetags/extra_tags.py @@ -1,4 +1,5 @@ import time +import os import datetime import math import re @@ -6,13 +7,15 @@ import logging from django import template from django.utils.encoding import smart_unicode from django.utils.safestring import mark_safe -from django.utils.timesince import timesince from forum.const import * +from forum.models import Question, Answer, QuestionRevision, AnswerRevision from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext +from django.conf import settings register = template.Library() -GRAVATAR_TEMPLATE = ('<img width="%(size)s" height="%(size)s" ' +GRAVATAR_TEMPLATE = ('<img class="gravatar" width="%(size)s" height="%(size)s" ' 'src="http://www.gravatar.com/avatar/%(gravatar_hash)s' '?s=%(size)s&d=identicon&r=PG" ' 'alt="%(username)s\'s gravatar image" />') @@ -115,6 +118,23 @@ def cnprog_pagesize(context): "pagesize" : context["pagesize"], "is_paginated": context["is_paginated"] } + +@register.inclusion_tag("post_contributor_info.html") +def post_contributor_info(post,contributor_type='original_author'): + """contributor_type: original_author|last_updater + """ + if isinstance(post,Question): + post_type = 'question' + elif isinstance(post,Answer): + post_type = 'answer' + elif isinstance(post,AnswerRevision) or isinstance(post,QuestionRevision): + post_type = 'revision' + return { + 'post':post, + 'post_type':post_type, + 'wiki_on':settings.WIKI_ON, + 'contributor_type':contributor_type + } @register.simple_tag def get_score_badge(user): @@ -216,21 +236,31 @@ def convert2tagname_list(question): @register.simple_tag def diff_date(date, limen=2): - meses = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sept', 'Oct', 'Nov', 'Dic'] - current_time = datetime.datetime(*time.localtime()[0:6]) - diff = current_time - date - diff_days = diff.days - if diff_days > limen: - return "%s %s - %s @ %s:%s" % (meses[date.month-1], date.day, date.year, date.hour, date.minute) + now = datetime.datetime.now()#datetime(*time.localtime()[0:6])#??? + diff = now - date + days = diff.days + hours = int(diff.seconds/3600) + minutes = int(diff.seconds/60) + + if days > 2: + if date.year == now.year: + return date.strftime(_("%b %d at %H:%M")) + else: + return date.strftime(_("%b %d '%y at %H:%M")) + elif days == 2: + return _('2 days ago') + elif days == 1: + return _('yesterday') + elif minutes >= 60: + return ungettext('%(hr)d hour ago','%(hr)d hours ago',hours) % {'hr':hours} else: - return timesince(date) + _(' ago') - + return ungettext('%(min)d min ago','%(min)d mins ago',minutes) % {'min':minutes} + @register.simple_tag def get_latest_changed_timestamp(): try: from time import localtime, strftime from os import path - from django.conf import settings root = settings.SITE_SRC_ROOT dir = ( root, @@ -243,3 +273,78 @@ def get_latest_changed_timestamp(): except: timestr = '' return timestr + +@register.simple_tag +def href(url): + url = '///' + settings.FORUM_SCRIPT_ALIAS + '/' + url + return os.path.normpath(url) + '?v=%d' % settings.RESOURCE_REVISION + +class ItemSeparatorNode(template.Node): + def __init__(self,separator): + sep = separator.strip() + if sep[0] == sep[-1] and sep[0] in ('\'','"'): + sep = sep[1:-1] + else: + raise template.TemplateSyntaxError('separator in joinitems tag must be quoted') + self.content = sep + def render(self,context): + return self.content + +class JoinItemListNode(template.Node): + def __init__(self,separator=ItemSeparatorNode("''"), items=()): + self.separator = separator + self.items = items + def render(self,context): + out = [] + empty_re = re.compile(r'^\s*$') + for item in self.items: + bit = item.render(context) + if not empty_re.search(bit): + out.append(bit) + return self.separator.render(context).join(out) + +@register.tag(name="joinitems") +def joinitems(parser,token): + try: + tagname,junk,sep_token = token.split_contents() + except ValueError: + raise template.TemplateSyntaxError("joinitems tag requires 'using \"separator html\"' parameters") + if junk == 'using': + sep_node = ItemSeparatorNode(sep_token) + else: + raise template.TemplateSyntaxError("joinitems tag requires 'using \"separator html\"' parameters") + nodelist = [] + while True: + nodelist.append(parser.parse(('separator','endjoinitems'))) + next = parser.next_token() + if next.contents == 'endjoinitems': + break + + return JoinItemListNode(separator=sep_node,items=nodelist) + +class BlockResourceNode(template.Node): + def __init__(self,nodelist): + self.items = nodelist + def render(self,context): + out = '///' + settings.FORUM_SCRIPT_ALIAS + if self.items: + out += '/' + for item in self.items: + bit = item.render(context) + out += bit + out = os.path.normpath(out) + '?v=%d' % settings.RESOURCE_REVISION + return out.replace(' ','') + +@register.tag(name='blockresource') +def blockresource(parser,token): + try: + tagname = token.split_contents() + except ValueError: + raise template.TemplateSyntaxError("blockresource tag does not use arguments") + nodelist = [] + while True: + nodelist.append(parser.parse(('endblockresource'))) + next = parser.next_token() + if next.contents == 'endblockresource': + break + return BlockResourceNode(nodelist) diff --git a/forum/templatetags/smart_if.py b/forum/templatetags/smart_if.py new file mode 100644 index 00000000..a8fc1944 --- /dev/null +++ b/forum/templatetags/smart_if.py @@ -0,0 +1,401 @@ +"""
+A smarter {% if %} tag for django templates.
+
+While retaining current Django functionality, it also handles equality,
+greater than and less than operators. Some common case examples::
+
+ {% if articles|length >= 5 %}...{% endif %}
+ {% if "ifnotequal tag" != "beautiful" %}...{% endif %}
+"""
+import unittest
+from django import template
+
+
+register = template.Library()
+
+
+#==============================================================================
+# Calculation objects
+#==============================================================================
+
+class BaseCalc(object):
+ def __init__(self, var1, var2=None, negate=False):
+ self.var1 = var1
+ self.var2 = var2
+ self.negate = negate
+
+ def resolve(self, context):
+ try:
+ var1, var2 = self.resolve_vars(context)
+ outcome = self.calculate(var1, var2)
+ except:
+ outcome = False
+ if self.negate:
+ return not outcome
+ return outcome
+
+ def resolve_vars(self, context):
+ var2 = self.var2 and self.var2.resolve(context)
+ return self.var1.resolve(context), var2
+
+ def calculate(self, var1, var2):
+ raise NotImplementedError()
+
+
+class Or(BaseCalc):
+ def calculate(self, var1, var2):
+ return var1 or var2
+
+
+class And(BaseCalc):
+ def calculate(self, var1, var2):
+ return var1 and var2
+
+
+class Equals(BaseCalc):
+ def calculate(self, var1, var2):
+ return var1 == var2
+
+
+class Greater(BaseCalc):
+ def calculate(self, var1, var2):
+ return var1 > var2
+
+
+class GreaterOrEqual(BaseCalc):
+ def calculate(self, var1, var2):
+ return var1 >= var2
+
+
+class In(BaseCalc):
+ def calculate(self, var1, var2):
+ return var1 in var2
+
+
+#==============================================================================
+# Tests
+#==============================================================================
+
+class TestVar(object):
+ """
+ A basic self-resolvable object similar to a Django template variable. Used
+ to assist with tests.
+ """
+ def __init__(self, value):
+ self.value = value
+
+ def resolve(self, context):
+ return self.value
+
+
+class SmartIfTests(unittest.TestCase):
+ def setUp(self):
+ self.true = TestVar(True)
+ self.false = TestVar(False)
+ self.high = TestVar(9000)
+ self.low = TestVar(1)
+
+ def assertCalc(self, calc, context=None):
+ """
+ Test a calculation is True, also checking the inverse "negate" case.
+ """
+ context = context or {}
+ self.assert_(calc.resolve(context))
+ calc.negate = not calc.negate
+ self.assertFalse(calc.resolve(context))
+
+ def assertCalcFalse(self, calc, context=None):
+ """
+ Test a calculation is False, also checking the inverse "negate" case.
+ """
+ context = context or {}
+ self.assertFalse(calc.resolve(context))
+ calc.negate = not calc.negate
+ self.assert_(calc.resolve(context))
+
+ def test_or(self):
+ self.assertCalc(Or(self.true))
+ self.assertCalcFalse(Or(self.false))
+ self.assertCalc(Or(self.true, self.true))
+ self.assertCalc(Or(self.true, self.false))
+ self.assertCalc(Or(self.false, self.true))
+ self.assertCalcFalse(Or(self.false, self.false))
+
+ def test_and(self):
+ self.assertCalc(And(self.true, self.true))
+ self.assertCalcFalse(And(self.true, self.false))
+ self.assertCalcFalse(And(self.false, self.true))
+ self.assertCalcFalse(And(self.false, self.false))
+
+ def test_equals(self):
+ self.assertCalc(Equals(self.low, self.low))
+ self.assertCalcFalse(Equals(self.low, self.high))
+
+ def test_greater(self):
+ self.assertCalc(Greater(self.high, self.low))
+ self.assertCalcFalse(Greater(self.low, self.low))
+ self.assertCalcFalse(Greater(self.low, self.high))
+
+ def test_greater_or_equal(self):
+ self.assertCalc(GreaterOrEqual(self.high, self.low))
+ self.assertCalc(GreaterOrEqual(self.low, self.low))
+ self.assertCalcFalse(GreaterOrEqual(self.low, self.high))
+
+ def test_in(self):
+ list_ = TestVar([1,2,3])
+ invalid_list = TestVar(None)
+ self.assertCalc(In(self.low, list_))
+ self.assertCalcFalse(In(self.low, invalid_list))
+
+ def test_parse_bits(self):
+ var = IfParser([True]).parse()
+ self.assert_(var.resolve({}))
+ var = IfParser([False]).parse()
+ self.assertFalse(var.resolve({}))
+
+ var = IfParser([False, 'or', True]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser([False, 'and', True]).parse()
+ self.assertFalse(var.resolve({}))
+
+ var = IfParser(['not', False, 'and', 'not', False]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser(['not', 'not', True]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser([1, '=', 1]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser([1, 'not', '=', 1]).parse()
+ self.assertFalse(var.resolve({}))
+
+ var = IfParser([1, 'not', 'not', '=', 1]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser([1, '!=', 1]).parse()
+ self.assertFalse(var.resolve({}))
+
+ var = IfParser([3, '>', 2]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser([1, '<', 2]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser([2, 'not', 'in', [2, 3]]).parse()
+ self.assertFalse(var.resolve({}))
+
+ var = IfParser([1, 'or', 1, '=', 2]).parse()
+ self.assert_(var.resolve({}))
+
+ def test_boolean(self):
+ var = IfParser([True, 'and', True, 'and', True]).parse()
+ self.assert_(var.resolve({}))
+ var = IfParser([False, 'or', False, 'or', True]).parse()
+ self.assert_(var.resolve({}))
+ var = IfParser([True, 'and', False, 'or', True]).parse()
+ self.assert_(var.resolve({}))
+ var = IfParser([False, 'or', True, 'and', True]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser([True, 'and', True, 'and', False]).parse()
+ self.assertFalse(var.resolve({}))
+ var = IfParser([False, 'or', False, 'or', False]).parse()
+ self.assertFalse(var.resolve({}))
+ var = IfParser([False, 'or', True, 'and', False]).parse()
+ self.assertFalse(var.resolve({}))
+ var = IfParser([False, 'and', True, 'or', False]).parse()
+ self.assertFalse(var.resolve({}))
+
+ def test_invalid(self):
+ self.assertRaises(ValueError, IfParser(['not']).parse)
+ self.assertRaises(ValueError, IfParser(['==']).parse)
+ self.assertRaises(ValueError, IfParser([1, 'in']).parse)
+ self.assertRaises(ValueError, IfParser([1, '>', 'in']).parse)
+ self.assertRaises(ValueError, IfParser([1, '==', 'not', 'not']).parse)
+ self.assertRaises(ValueError, IfParser([1, 2]).parse)
+
+
+OPERATORS = {
+ '=': (Equals, True),
+ '==': (Equals, True),
+ '!=': (Equals, False),
+ '>': (Greater, True),
+ '>=': (GreaterOrEqual, True),
+ '<=': (Greater, False),
+ '<': (GreaterOrEqual, False),
+ 'or': (Or, True),
+ 'and': (And, True),
+ 'in': (In, True),
+}
+BOOL_OPERATORS = ('or', 'and')
+
+
+class IfParser(object):
+ error_class = ValueError
+
+ def __init__(self, tokens):
+ self.tokens = tokens
+
+ def _get_tokens(self):
+ return self._tokens
+
+ def _set_tokens(self, tokens):
+ self._tokens = tokens
+ self.len = len(tokens)
+ self.pos = 0
+
+ tokens = property(_get_tokens, _set_tokens)
+
+ def parse(self):
+ if self.at_end():
+ raise self.error_class('No variables provided.')
+ var1 = self.get_bool_var()
+ while not self.at_end():
+ op, negate = self.get_operator()
+ var2 = self.get_bool_var()
+ var1 = op(var1, var2, negate=negate)
+ return var1
+
+ def get_token(self, eof_message=None, lookahead=False):
+ negate = True
+ token = None
+ pos = self.pos
+ while token is None or token == 'not':
+ if pos >= self.len:
+ if eof_message is None:
+ raise self.error_class()
+ raise self.error_class(eof_message)
+ token = self.tokens[pos]
+ negate = not negate
+ pos += 1
+ if not lookahead:
+ self.pos = pos
+ return token, negate
+
+ def at_end(self):
+ return self.pos >= self.len
+
+ def create_var(self, value):
+ return TestVar(value)
+
+ def get_bool_var(self):
+ """
+ Returns either a variable by itself or a non-boolean operation (such as
+ ``x == 0`` or ``x < 0``).
+
+ This is needed to keep correct precedence for boolean operations (i.e.
+ ``x or x == 0`` should be ``x or (x == 0)``, not ``(x or x) == 0``).
+ """
+ var = self.get_var()
+ if not self.at_end():
+ op_token = self.get_token(lookahead=True)[0]
+ if isinstance(op_token, basestring) and (op_token not in
+ BOOL_OPERATORS):
+ op, negate = self.get_operator()
+ return op(var, self.get_var(), negate=negate)
+ return var
+
+ def get_var(self):
+ token, negate = self.get_token('Reached end of statement, still '
+ 'expecting a variable.')
+ if isinstance(token, basestring) and token in OPERATORS:
+ raise self.error_class('Expected variable, got operator (%s).' %
+ token)
+ var = self.create_var(token)
+ if negate:
+ return Or(var, negate=True)
+ return var
+
+ def get_operator(self):
+ token, negate = self.get_token('Reached end of statement, still '
+ 'expecting an operator.')
+ if not isinstance(token, basestring) or token not in OPERATORS:
+ raise self.error_class('%s is not a valid operator.' % token)
+ if self.at_end():
+ raise self.error_class('No variable provided after "%s".' % token)
+ op, true = OPERATORS[token]
+ if not true:
+ negate = not negate
+ return op, negate
+
+
+#==============================================================================
+# Actual templatetag code.
+#==============================================================================
+
+class TemplateIfParser(IfParser):
+ error_class = template.TemplateSyntaxError
+
+ def __init__(self, parser, *args, **kwargs):
+ self.template_parser = parser
+ return super(TemplateIfParser, self).__init__(*args, **kwargs)
+
+ def create_var(self, value):
+ return self.template_parser.compile_filter(value)
+
+
+class SmartIfNode(template.Node):
+ def __init__(self, var, nodelist_true, nodelist_false=None):
+ self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
+ self.var = var
+
+ def render(self, context):
+ if self.var.resolve(context):
+ return self.nodelist_true.render(context)
+ if self.nodelist_false:
+ return self.nodelist_false.render(context)
+ return ''
+
+ def __repr__(self):
+ return "<Smart If node>"
+
+ def __iter__(self):
+ for node in self.nodelist_true:
+ yield node
+ if self.nodelist_false:
+ for node in self.nodelist_false:
+ yield node
+
+ def get_nodes_by_type(self, nodetype):
+ nodes = []
+ if isinstance(self, nodetype):
+ nodes.append(self)
+ nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype))
+ if self.nodelist_false:
+ nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype))
+ return nodes
+
+
+@register.tag('if')
+def smart_if(parser, token):
+ """
+ A smarter {% if %} tag for django templates.
+
+ While retaining current Django functionality, it also handles equality,
+ greater than and less than operators. Some common case examples::
+
+ {% if articles|length >= 5 %}...{% endif %}
+ {% if "ifnotequal tag" != "beautiful" %}...{% endif %}
+
+ Arguments and operators _must_ have a space between them, so
+ ``{% if 1>2 %}`` is not a valid smart if tag.
+
+ All supported operators are: ``or``, ``and``, ``in``, ``=`` (or ``==``),
+ ``!=``, ``>``, ``>=``, ``<`` and ``<=``.
+ """
+ bits = token.split_contents()[1:]
+ var = TemplateIfParser(parser, bits).parse()
+ nodelist_true = parser.parse(('else', 'endif'))
+ token = parser.next_token()
+ if token.contents == 'else':
+ nodelist_false = parser.parse(('endif',))
+ parser.delete_first_token()
+ else:
+ nodelist_false = None
+ return SmartIfNode(var, nodelist_true, nodelist_false)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/forum/urls.py b/forum/urls.py new file mode 100644 index 00000000..62e70161 --- /dev/null +++ b/forum/urls.py @@ -0,0 +1,91 @@ +import os.path +from django.conf.urls.defaults import * +from django.contrib import admin +from forum import views as app +from forum.feed import RssLastestQuestionsFeed +from forum.sitemap import QuestionsSitemap +from django.utils.translation import ugettext as _ + +admin.autodiscover() +feeds = { + 'rss': RssLastestQuestionsFeed +} +sitemaps = { + 'questions': QuestionsSitemap +} + +APP_PATH = os.path.dirname(os.path.dirname(__file__)) +urlpatterns = patterns('', + url(r'^$', app.index, name='index'), + url(r'^sitemap.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps}), + (r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/content/images/favicon.ico'}), + (r'^favicon\.gif$', 'django.views.generic.simple.redirect_to', {'url': '/content/images/favicon.gif'}), + (r'^content/(?P<path>.*)$', 'django.views.static.serve', + {'document_root': os.path.join(APP_PATH, 'templates/content').replace('\\','/')} + ), + (r'^%s(?P<path>.*)$' % _('upfiles/'), 'django.views.static.serve', + {'document_root': os.path.join(APP_PATH, 'templates/upfiles').replace('\\','/')} + ), + (r'^%s/$' % _('signin/'), 'django_authopenid.views.signin'), + url(r'^%s$' % _('about/'), app.about, name='about'), + url(r'^%s$' % _('faq/'), app.faq, name='faq'), + url(r'^%s$' % _('privacy/'), app.privacy, name='privacy'), + url(r'^%s$' % _('logout/'), app.logout, name='logout'), + url(r'^%s(?P<id>\d+)/%s$' % (_('answers/'), _('comments/')), app.answer_comments, name='answer_comments'), + url(r'^%s(?P<id>\d+)/%s$' % (_('answers/'), _('edit/')), app.edit_answer, name='edit_answer'), + url(r'^%s(?P<id>\d+)/%s$' % (_('answers/'), _('revisions/')), app.answer_revisions, name='answer_revisions'), + url(r'^%s$' % _('questions/'), app.questions, name='questions'), + url(r'^%s%s$' % (_('questions/'), _('ask/')), app.ask, name='ask'), + url(r'^%s%s$' % (_('questions/'), _('unanswered/')), app.unanswered, name='unanswered'), + url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('edit/')), app.edit_question, name='edit_question'), + url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('close/')), app.close, name='close'), + url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('reopen/')), app.reopen, name='reopen'), + url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('answer/')), app.answer, name='answer'), + url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('vote/')), app.vote, name='vote'), + url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('revisions/')), app.question_revisions, name='question_revisions'), + url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('comments/')), app.question_comments, name='question_comments'), + url(r'^%s$' % _('command/'), app.ajax_command, name='call_ajax'), + + url(r'^%s(?P<object_id>\d+)/%s(?P<comment_id>\d+)/%s$' % (_('questions/'), _('comments/'),_('delete/')), \ + app.delete_comment, kwargs={'commented_object_type':'question'},\ + name='delete_question_comment'), + + url(r'^%s(?P<object_id>\d+)/%s(?P<comment_id>\d+)/%s$' % (_('answers/'), _('comments/'),_('delete/')), \ + app.delete_comment, kwargs={'commented_object_type':'answer'}, \ + name='delete_answer_comment'), \ + #place general question item in the end of other operations + url(r'^%s(?P<id>\d+)//*' % _('question/'), app.question, name='question'), + url(r'^%s$' % _('tags/'), app.tags, name='tags'), + url(r'^%s(?P<tag>[^/]+)/$' % _('tags/'), app.tag, name='tag_questions'), + + url(r'^%s%s(?P<tag>[^/]+)/$' % (_('mark-tag/'),_('interesting/')), app.mark_tag, \ + kwargs={'reason':'good','action':'add'}, \ + name='mark_interesting_tag'), + + url(r'^%s%s(?P<tag>[^/]+)/$' % (_('mark-tag/'),_('ignored/')), app.mark_tag, \ + kwargs={'reason':'bad','action':'add'}, \ + name='mark_ignored_tag'), + + url(r'^%s(?P<tag>[^/]+)/$' % _('unmark-tag/'), app.mark_tag, \ + kwargs={'action':'remove'}, \ + name='mark_ignored_tag'), + + url(r'^%s$' % _('users/'),app.users, name='users'), + url(r'^%s(?P<id>\d+)/$' % _('moderate-user/'), app.moderate_user, name='moderate_user'), + url(r'^%s(?P<id>\d+)/%s$' % (_('users/'), _('edit/')), app.edit_user, name='edit_user'), + url(r'^%s(?P<id>\d+)//*' % _('users/'), app.user, name='user'), + url(r'^%s$' % _('badges/'),app.badges, name='badges'), + url(r'^%s(?P<id>\d+)//*' % _('badges/'), app.badge, name='badge'), + url(r'^%s%s$' % (_('messages/'), _('markread/')),app.read_message, name='read_message'), + # (r'^admin/doc/' % _('admin/doc'), include('django.contrib.admindocs.urls')), + (r'^%s(.*)' % _('nimda/'), admin.site.root), + url(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feeds}), + (r'^%s$' % _('upload/'), app.upload), + url(r'^%s$' % _('books/'), app.books, name='books'), + url(r'^%s%s(?P<short_name>[^/]+)/$' % (_('books/'), _('ask/')), app.ask_book, name='ask_book'), + url(r'^%s(?P<short_name>[^/]+)/$' % _('books/'), app.book, name='book'), + url(r'^%s$' % _('search/'), app.search, name='search'), + url(r'^%s$' % _('feedback/'), app.feedback, name='feedback'), + (r'^%s' % _('account/'), include('django_authopenid.urls')), + (r'^i18n/', include('django.conf.urls.i18n')), +) diff --git a/forum/user.py b/forum/user.py index 41811db9..40bf6a89 100644 --- a/forum/user.py +++ b/forum/user.py @@ -64,11 +64,11 @@ USER_TEMPLATE_VIEWS = ( data_size = 50 ), UserView( - id = 'preferences', - tab_title = _('preferences'), - tab_description = _('user preference settings'), - page_title = _('profile - user preferences'), - view_name = 'user_preferences', - template_file = 'user_preferences.html' + id = 'email_subscriptions', + tab_title = _('email subscriptions'), + tab_description = _('email subscription settings'), + page_title = _('profile - email subscriptions'), + view_name = 'user_email_subscriptions', + template_file = 'user_email_subscriptions.html' ) ) diff --git a/forum/views.py b/forum/views.py index 6c79bfbd..65b80d0e 100644 --- a/forum/views.py +++ b/forum/views.py @@ -2,20 +2,23 @@ import calendar from django.conf import settings from django.contrib.auth.decorators import login_required -from django.contrib.contenttypes.models import ContentType -from django.core.files.storage import default_storage -from django.core.paginator import EmptyPage -from django.core.paginator import InvalidPage -from django.core.paginator import Paginator -from django.http import Http404 -from django.http import HttpResponse -from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404 -from django.shortcuts import render_to_response -from django.template import RequestContext -from django.utils import simplejson +from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404 +from django.core.paginator import Paginator, EmptyPage, InvalidPage +from django.template import RequestContext, loader from django.utils.html import * +from django.utils import simplejson +from django.core import serializers +from django.core.mail import mail_admins +from django.db import transaction +from django.db.models import Count, Q +from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext as _ +from django.utils.datastructures import SortedDict +from django.template.defaultfilters import slugify +from django.core.exceptions import PermissionDenied + +from utils.html import sanitize_html +from utils.decorators import ajax_method, ajax_login_required from markdown2 import Markdown import os.path import random @@ -29,7 +32,8 @@ from forum.diff import textDiff as htmldiff from forum.forms import * from forum.models import * from forum.user import * -from utils.html import sanitize_html +from forum import auth +from django_authopenid.util import get_next_url # used in index page INDEX_PAGE_SIZE = 20 @@ -65,42 +69,85 @@ def _get_tags_cache_json(): tags = simplejson.dumps(tags_list) return tags +def _get_and_remember_questions_sort_method(request, view_dic, default): + if default not in view_dic: + raise Exception('default value must be in view_dic') + + q_sort_method = request.REQUEST.get('sort', None) + if q_sort_method == None: + q_sort_method = request.session.get('questions_sort_method', default) + + if q_sort_method not in view_dic: + q_sort_method = default + request.session['questions_sort_method'] = q_sort_method + return q_sort_method, view_dic[q_sort_method] + def index(request): - view_id = request.GET.get('sort', None) view_dic = { - "latest":"-last_activity_at", - "hottest":"-answer_count", - "mostvoted":"-score", - "trans": "-last_activity_at" - } - try: - orderby = view_dic[view_id] - except KeyError: - view_id = "latest" - orderby = "-added_at" - # group questions by author_id of 28,29 - if view_id == 'trans': - questions = Question.objects.get_translation_questions(orderby, INDEX_PAGE_SIZE) - else: - questions = Question.objects.get_questions_by_pagesize(orderby, INDEX_PAGE_SIZE) + "latest":"-last_activity_at", + "hottest":"-answer_count", + "mostvoted":"-score", + } + view_id, orderby = _get_and_remember_questions_sort_method(request, view_dic, 'latest') + + page_size = request.session.get('pagesize', QUESTIONS_PAGE_SIZE) + questions = Question.objects.exclude(deleted=True).order_by(orderby)[:page_size] # RISK - inner join queries questions = questions.select_related() tags = Tag.objects.get_valid_tags(INDEX_TAGS_SIZE) awards = Award.objects.get_recent_awards() + (interesting_tag_names, ignored_tag_names) = (None, None) + if request.user.is_authenticated(): + pt = MarkedTag.objects.filter(user=request.user) + interesting_tag_names = pt.filter(reason='good').values_list('tag__name', flat=True) + ignored_tag_names = pt.filter(reason='bad').values_list('tag__name', flat=True) + + tags_autocomplete = _get_tags_cache_json() + return render_to_response('index.html', { - "questions": questions, - "tab_id": view_id, - "tags": tags, - "awards": awards[:INDEX_AWARD_SIZE], - }, context_instance=RequestContext(request)) + 'interesting_tag_names': interesting_tag_names, + 'tags_autocomplete': tags_autocomplete, + 'ignored_tag_names': ignored_tag_names, + "questions" : questions, + "tab_id" : view_id, + "tags" : tags, + "awards" : awards[:INDEX_AWARD_SIZE], + }, context_instance=RequestContext(request)) def about(request): return render_to_response('about.html', context_instance=RequestContext(request)) def faq(request): - return render_to_response('faq.html', context_instance=RequestContext(request)) + data = { + 'gravatar_faq_url': reverse('faq') + '#gravatar', + 'send_email_key_url': reverse('send_email_key'), + 'ask_question_url': reverse('ask'), + } + return render_to_response('faq.html', data, context_instance=RequestContext(request)) + +def feedback(request): + data = {} + form = None + if request.method == "POST": + form = FeedbackForm(request.POST) + if form.is_valid(): + if not request.user.is_authenticated: + data['email'] = form.cleaned_data.get('email',None) + data['message'] = form.cleaned_data['message'] + data['name'] = form.cleaned_data.get('name',None) + message = render_to_response('feedback_email.txt',data,context_instance=RequestContext(request)) + mail_admins(_('Q&A forum feedback'), message) + msg = _('Thanks for the feedback!') + request.user.message_set.create(message=msg) + return HttpResponseRedirect(get_next_url(request)) + else: + form = FeedbackForm(initial={'next':get_next_url(request)}) + + data['form'] = form + return render_to_response('feedback.html', data, context_instance=RequestContext(request)) +feedback.CANCEL_MESSAGE=_('We look forward to hearing your feedback! Please, give it next time :)') def privacy(request): return render_to_response('privacy.html', context_instance=RequestContext(request)) @@ -113,7 +160,7 @@ def questions(request, tagname=None, unanswered=False): List of Questions, Tagged questions, and Unanswered questions. """ # template file - # "questions.html" or "unanswered.html" + # "questions.html" or maybe index.html in the future template_file = "questions.html" # get pagesize from session, if failed then get default value pagesize = request.session.get("pagesize", 10) @@ -122,27 +169,65 @@ def questions(request, tagname=None, unanswered=False): except ValueError: page = 1 - 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" + view_dic = {"latest":"-added_at", "active":"-last_activity_at", "hottest":"-answer_count", "mostvoted":"-score" } + view_id, orderby = _get_and_remember_questions_sort_method(request,view_dic,'latest') # check if request is from tagged questions + qs = Question.objects.exclude(deleted=True) + if tagname is not None: - objects = Question.objects.get_questions_by_tag(tagname, orderby) - elif unanswered: - #check if request is from unanswered questions - template_file = "unanswered.html" - objects = Question.objects.get_unanswered_questions(orderby) - else: - objects = Question.objects.get_questions(orderby) + qs = qs.filter(tags__name = unquote(tagname)) - # RISK - inner join queries - objects = objects.select_related(depth=1); - objects_list = Paginator(objects, pagesize) + if unanswered: + qs = qs.exclude(answer_accepted=True) + + author_name = None + #user contributed questions & answers + if 'user' in request.GET: + try: + author_name = request.GET['user'] + u = User.objects.get(username=author_name) + qs = qs.filter(Q(author=u) | Q(answers__author=u)) + except User.DoesNotExist: + author_name = None + + if request.user.is_authenticated(): + uid_str = str(request.user.id) + qs = qs.extra( + select = SortedDict([ + ( + 'interesting_score', + 'SELECT COUNT(1) FROM forum_markedtag, question_tags ' + + 'WHERE forum_markedtag.user_id = %s ' + + 'AND forum_markedtag.tag_id = question_tags.tag_id ' + + 'AND forum_markedtag.reason = "good" ' + + 'AND question_tags.question_id = question.id' + ), + ]), + select_params = (uid_str,), + ) + if request.user.hide_ignored_questions: + ignored_tags = Tag.objects.filter(user_selections__reason='bad', + user_selections__user = request.user) + qs = qs.exclude(tags__in=ignored_tags) + else: + qs = qs.extra( + select = SortedDict([ + ( + 'ignored_score', + 'SELECT COUNT(1) FROM forum_markedtag, question_tags ' + + 'WHERE forum_markedtag.user_id = %s ' + + 'AND forum_markedtag.tag_id = question_tags.tag_id ' + + 'AND forum_markedtag.reason = "bad" ' + + 'AND question_tags.question_id = question.id' + ) + ]), + select_params = (uid_str, ) + ) + + qs = qs.select_related(depth=1).order_by(orderby) + + objects_list = Paginator(qs, pagesize) questions = objects_list.page(page) # Get related tags from this page objects @@ -150,28 +235,41 @@ def questions(request, tagname=None, unanswered=False): related_tags = Tag.objects.get_tags_by_questions(questions.object_list) else: related_tags = None - return render_to_response(template_file, { - "questions": questions, - "tab_id": view_id, - "questions_count": objects_list.count, - "tags": related_tags, - "searchtag": tagname, - "is_unanswered": unanswered, - "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 + '?sort=%s&' % view_id, - 'pagesize': pagesize - }}, context_instance=RequestContext(request)) + tags_autocomplete = _get_tags_cache_json() + + # get the list of interesting and ignored tags + (interesting_tag_names, ignored_tag_names) = (None, None) + if request.user.is_authenticated(): + pt = MarkedTag.objects.filter(user=request.user) + interesting_tag_names = pt.filter(reason='good').values_list('tag__name', flat=True) + ignored_tag_names = pt.filter(reason='bad').values_list('tag__name', flat=True) -def create_new_answer(question=None, author=None, \ - added_at=None, wiki=False, \ - text='', email_notify=False): + return render_to_response(template_file, { + "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" : { + '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 + '?sort=%s&' % view_id, + 'pagesize' : pagesize + }}, context_instance=RequestContext(request)) + +def create_new_answer( question=None, author=None,\ + added_at=None, wiki=False,\ + text='', email_notify=False): html = sanitize_html(markdowner.convert(text)) @@ -208,16 +306,12 @@ def create_new_answer(question=None, author=None, \ #set notification/delete if email_notify: - try: - EmailFeed.objects.get(feed_id=question.id, subscriber_id=author.id, feed_content_type=question_type) - except EmailFeed.DoesNotExist: - feed = EmailFeed(content=question, subscriber=author) - feed.save() + if author not in question.followed_by.all(): + question.followed_by.add(author) else: #not sure if this is necessary. ajax should take care of this... try: - feed = Email.objects.get(feed_id=question.id, subscriber_id=author.id, feed_content_type=question_type) - feed.delete() + question.followed_by.remove(author) except: pass @@ -267,7 +361,7 @@ def ask(request): if form.is_valid(): added_at = datetime.datetime.now() - title = strip_tags(form.cleaned_data['title']).strip() + title = strip_tags(form.cleaned_data['title'].strip()) wiki = form.cleaned_data['wiki'] tagnames = form.cleaned_data['tags'].strip() text = form.cleaned_data['text'] @@ -301,28 +395,42 @@ def ask(request): ip_addr=request.META['REMOTE_ADDR'], ) question.save() - return HttpResponseRedirect('%s%s%s' % (_('/account/'), _('signin/'), ('newquestion/'))) + return HttpResponseRedirect(reverse('user_signin_new_question')) else: form = AskForm() tags = _get_tags_cache_json() return render_to_response('ask.html', { - 'form': form, - 'tags': tags, - }, context_instance=RequestContext(request)) + 'form' : form, + 'tags' : tags, + 'email_validation_faq_url':reverse('faq') + '#validate', + }, context_instance=RequestContext(request)) def question(request, id): try: page = int(request.GET.get('page', '1')) except ValueError: page = 1 - view_id = request.GET.get('sort', 'votes') - view_dic = {"latest":"-added_at", "oldest":"added_at", "votes":"-score"} + + view_id = request.GET.get('sort', None) + view_dic = {"latest":"-added_at", "oldest":"added_at", "votes":"-score" } try: orderby = view_dic[view_id] except KeyError: - view_id = "votes" - orderby = "-score" + qsm = request.session.get('questions_sort_method',None) + if qsm in ('mostvoted','latest'): + logging.debug('loaded from session ' + qsm) + if qsm == 'mostvoted': + view_id = 'votes' + orderby = '-score' + else: + view_id = 'latest' + orderby = '-added_at' + else: + view_id = "votes" + orderby = "-score" + + logging.debug('view_id=' + str(view_id)) question = get_object_or_404(Question, id=id) if question.deleted and not can_view_deleted_post(request.user, question): @@ -345,8 +453,11 @@ def question(request, id): vote_value = -1 if vote.is_upvote(): vote_value = 1 - user_answer_votes[vote.object_id] = vote_value - + user_answer_votes[answer.id] = vote_value + + if answers is not None: + answers = answers.order_by("-accepted", orderby) + filtered_answers = [] for answer in answers: if answer.deleted == True: @@ -357,8 +468,38 @@ def question(request, id): objects_list = Paginator(filtered_answers, ANSWERS_PAGE_SIZE) page_objects = objects_list.page(page) - # update view count - Question.objects.update_view_count(question) + + #todo: merge view counts per user and per session + #1) view count per session + update_view_count = False + if 'question_view_times' not in request.session: + request.session['question_view_times'] = {} + + last_seen = request.session['question_view_times'].get(question.id,None) + updated_when, updated_who = question.get_last_update_info() + + if updated_who != request.user: + if last_seen: + if last_seen < updated_when: + update_view_count = True + else: + update_view_count = True + + request.session['question_view_times'][question.id] = datetime.datetime.now() + + if update_view_count: + question.view_count += 1 + question.save() + + #2) question view count per user + if request.user.is_authenticated(): + try: + question_view = QuestionView.objects.get(who=request.user, question=question) + except QuestionView.DoesNotExist: + question_view = QuestionView(who=request.user, question=question) + question_view.when = datetime.datetime.now() + question_view.save() + return render_to_response('question.html', { "question": question, "question_vote": question_vote, @@ -472,7 +613,6 @@ def _retag_question(request, question): 'tags': _get_tags_cache_json(), }, context_instance=RequestContext(request)) - def _edit_question(request, question): latest_revision = question.get_latest_revision() revision_form = None @@ -617,6 +757,7 @@ QUESTION_REVISION_TEMPLATE = ('<h1>%(title)s</h1>\n' def question_revisions(request, id): post = get_object_or_404(Question, id=id) revisions = list(post.revisions.all()) + revisions.reverse() for i, revision in enumerate(revisions): revision.html = QUESTION_REVISION_TEMPLATE % { 'title': revision.title, @@ -625,16 +766,15 @@ def question_revisions(request, id): for tag in revision.tagnames.split(' ')]), } if i > 0: - revisions[i - 1].diff = htmldiff(revision.html, - revisions[i - 1].html) + revisions[i].diff = htmldiff(revisions[i-1].html, revision.html) else: - revisions[i - 1].diff = QUESTION_REVISION_TEMPLATE % { + revisions[i].diff = QUESTION_REVISION_TEMPLATE % { 'title': revisions[0].title, 'html': sanitize_html(markdowner.convert(revisions[0].text)), 'tags': ' '.join(['<a class="post-tag">%s</a>' % tag for tag in revisions[0].tagnames.split(' ')]), } - revisions[i - 1].summary = None + revisions[i].summary = _('initial version') return render_to_response('revisions_question.html', { 'post': post, 'revisions': revisions, @@ -644,16 +784,16 @@ ANSWER_REVISION_TEMPLATE = ('<div class="text">%(html)s</div>') def answer_revisions(request, id): post = get_object_or_404(Answer, id=id) revisions = list(post.revisions.all()) + revisions.reverse() for i, revision in enumerate(revisions): revision.html = ANSWER_REVISION_TEMPLATE % { 'html': sanitize_html(markdowner.convert(revision.text)) } if i > 0: - revisions[i - 1].diff = htmldiff(revision.html, - revisions[i - 1].html) + revisions[i].diff = htmldiff(revisions[i-1].html, revision.html) else: - revisions[i - 1].diff = revisions[i-1].text - revisions[i - 1].summary = None + revisions[i].diff = revisions[i].text + revisions[i].summary = _('initial version') return render_to_response('revisions_answer.html', { 'post': post, 'revisions': revisions, @@ -691,8 +831,7 @@ def answer(request, id): ip_addr=request.META['REMOTE_ADDR'], ) anon.save() - return HttpResponseRedirect('/%s%s%s%s' % ( _('account/'), - _('signin/'),'?next=', question.get_absolute_url())) + return HttpResponseRedirect(reverse('user_signin_new_answer')) return HttpResponseRedirect(question.get_absolute_url()) @@ -707,7 +846,7 @@ def tags(request): if request.method == "GET": stag = request.GET.get("q", "").strip() - if len(stag) > 0: + if stag != '': objects_list = Paginator(Tag.objects.filter(deleted=False).exclude(used_count=0).extra(where=['name like %s'], params=['%' + stag + '%']), DEFAULT_PAGE_SIZE) else: if sortby == "used": @@ -721,22 +860,21 @@ def tags(request): tags = objects_list.page(objects_list.num_pages) return render_to_response('tags.html', { - "tags": tags, - "stag": stag, - "tab_id": sortby, - "keywords": stag, - "context": { - 'is_paginated': is_paginated, - 'pages': objects_list.num_pages, - 'page': page, - 'has_previous': tags.has_previous(), - 'has_next': tags.has_next(), - 'previous': tags.previous_page_number(), - 'next': tags.next_page_number(), - 'base_url': '/tags/?sort=%s&' % sortby - } - - }, context_instance=RequestContext(request)) + "tags" : tags, + "stag" : stag, + "tab_id" : sortby, + "keywords" : stag, + "context" : { + 'is_paginated' : is_paginated, + 'pages': objects_list.num_pages, + 'page': page, + 'has_previous': tags.has_previous(), + 'has_next': tags.has_next(), + 'previous': tags.previous_page_number(), + 'next': tags.next_page_number(), + 'base_url' : reverse('tags') + '?sort=%s&' % sortby + } + }, context_instance=RequestContext(request)) def tag(request, tag): return questions(request, tagname=tag) @@ -925,7 +1063,8 @@ def vote(request, id): if not can_delete_post(request.user, post): response_data['allowed'] = -2 - elif post.deleted: + elif post.deleted == True: + logging.debug('debug restoring post in view') onDeleteCanceled(post, request.user) response_data['status'] = 1 else: @@ -934,13 +1073,19 @@ def vote(request, id): elif vote_type == '11':#subscribe q updates user = request.user if user.is_authenticated(): - try: - EmailFeed.objects.get(feed_id=question.id, subscriber_id=user.id, feed_content_type=question_type) - except EmailFeed.DoesNotExist: - feed = EmailFeed(subscriber=user, content=question) - feed.save() + if user not in question.followed_by.all(): + question.followed_by.add(user) if settings.EMAIL_VALIDATION == 'on' and user.email_isvalid == False: - response_data['message'] = _('subscription saved, %(email)s needs validation') % {'email':user.email} + response_data['message'] = \ + _('subscription saved, %(email)s needs validation, see %(details_url)s') \ + % {'email':user.email,'details_url':reverse('faq') + '#validate'} + feed_setting = EmailFeedSetting.objects.get(subscriber=user,feed_type='q_sel') + if feed_setting.frequency == 'n': + feed_setting.frequency = 'd' + feed_setting.save() + if 'message' in response_data: + response_data['message'] += '<br/>' + response_data['message'] = _('email update frequency has been set to daily') #response_data['status'] = 1 #responst_data['allowed'] = 1 else: @@ -950,12 +1095,8 @@ def vote(request, id): elif vote_type == '12':#unsubscribe q updates user = request.user if user.is_authenticated(): - try: - feed = EmailFeed.objects.get(feed_id=question.id, subscriber_id=user.id) - feed.delete() - except EmailFeed.DoesNotExist: - pass - + if user in question.followed_by.all(): + question.followed_by.remove(user) else: response_data['success'] = 0 response_data['message'] = u'Request mode is not supported. Please try again.' @@ -967,6 +1108,42 @@ def vote(request, id): data = simplejson.dumps(response_data) return HttpResponse(data, mimetype="application/json") +@ajax_login_required +def mark_tag(request, tag=None, **kwargs): + action = kwargs['action'] + ts = MarkedTag.objects.filter(user=request.user, tag__name=tag) + if action == 'remove': + logging.debug('deleting tag %s' % tag) + ts.delete() + else: + reason = kwargs['reason'] + if len(ts) == 0: + try: + t = Tag.objects.get(name=tag) + mt = MarkedTag(user=request.user, reason=reason, tag=t) + mt.save() + except: + pass + else: + ts.update(reason=reason) + return HttpResponse(simplejson.dumps(''), mimetype="application/json") + +@ajax_login_required +def ajax_toggle_ignored_questions(request): + if request.user.hide_ignored_questions: + new_hide_setting = False + else: + new_hide_setting = True + request.user.hide_ignored_questions = new_hide_setting + request.user.save() + +@ajax_method +def ajax_command(request): + if 'command' not in request.POST: + return HttpResponseForbidden(mimetype="application/json") + if request.POST['command'] == 'toggle-ignored-questions': + return ajax_toggle_ignored_questions(request) + def users(request): is_paginated = True sortby = request.GET.get('sort', 'reputation') @@ -986,11 +1163,11 @@ def users(request): # default else: objects_list = Paginator(User.objects.all().order_by('-reputation'), USERS_PAGE_SIZE) - base_url = '/%s?sort=%s&' % (_('users/'), sortby) + base_url = reverse('users') + '?sort=%s&' % sortby else: sortby = "reputation" objects_list = Paginator(User.objects.extra(where=['username like %s'], params=['%' + suser + '%']).order_by('-reputation'), USERS_PAGE_SIZE) - base_url = '/%s?name=%s&sort=%s&' % (_('users/'), suser, sortby) + base_url = reverse('users') + '?name=%s&sort=%s&' % (suser, sortby) try: users = objects_list.page(page) @@ -998,22 +1175,22 @@ def users(request): users = objects_list.page(objects_list.num_pages) return render_to_response('users.html', { - "users": users, - "suser": suser, - "keywords": suser, - "tab_id": sortby, - "context": { - 'is_paginated': is_paginated, - 'pages': objects_list.num_pages, - 'page': page, - 'has_previous': users.has_previous(), - 'has_next': users.has_next(), - 'previous': users.previous_page_number(), - 'next': users.next_page_number(), - 'base_url': base_url - } - - }, context_instance=RequestContext(request)) + "users" : users, + "suser" : suser, + "keywords" : suser, + "tab_id" : sortby, + "context" : { + 'is_paginated' : is_paginated, + 'pages': objects_list.num_pages, + 'page': page, + 'has_previous': users.has_previous(), + 'has_next': users.has_next(), + 'previous': users.previous_page_number(), + 'next': users.next_page_number(), + 'base_url' : base_url + } + + }, context_instance=RequestContext(request)) def user(request, id): sort = request.GET.get('sort', 'stats') @@ -1023,6 +1200,26 @@ def user(request, id): return func(request, id, user_view) @login_required +def moderate_user(request, id): + """ajax handler of user moderation + """ + if not auth.can_moderate_users(request.user) or request.method != 'POST': + raise Http404 + if not request.is_ajax(): + return HttpResponseForbidden(mimetype="application/json") + + user = get_object_or_404(User, id=id) + form = ModerateUserForm(request.POST, instance=user) + + if form.is_valid(): + form.save() + logging.debug('data saved') + response = HttpResponse(simplejson.dumps(''), mimetype="application/json") + else: + response = HttpResponseForbidden(mimetype="application/json") + return response + +@login_required def edit_user(request, id): user = get_object_or_404(User, id=id) if request.user != user: @@ -1035,6 +1232,7 @@ def edit_user(request, id): from django_authopenid.views import set_new_email set_new_email(user, new_email) + user.username = sanitize_html(form.cleaned_data['username']) user.real_name = sanitize_html(form.cleaned_data['realname']) user.website = sanitize_html(form.cleaned_data['website']) user.location = sanitize_html(form.cleaned_data['city']) @@ -1052,8 +1250,9 @@ def edit_user(request, id): else: form = EditUserForm(user) return render_to_response('user_edit.html', { - 'form': form, - }, context_instance=RequestContext(request)) + 'form' : form, + 'gravatar_faq_url' : reverse('faq') + '#gravatar', + }, context_instance=RequestContext(request)) def user_stats(request, user_id, user_view): user = get_object_or_404(User, id=user_id) @@ -1097,74 +1296,99 @@ def user_stats(request, user_id, user_view): 'la_user_reputation')[:100] answered_questions = Question.objects.extra( - select={ - 'vote_up_count': 'answer.vote_up_count', - 'vote_down_count': 'answer.vote_down_count', - 'answer_id': 'answer.id', - 'accepted': 'answer.accepted', - 'vote_count': 'answer.score', - 'comment_count': 'answer.comment_count' - }, - tables=['question', 'answer'], - where=['answer.deleted=0 AND answer.author_id=%s AND answer.question_id=question.id'], - params=[user_id], - order_by=['-vote_count', '-answer_id'], - select_params=[user_id] - ).distinct().values('comment_count', - 'id', - 'answer_id', - 'title', - 'author_id', - 'accepted', - 'vote_count', - 'answer_count', - 'vote_up_count', - 'vote_down_count')[:100] + select={ + 'vote_up_count' : 'answer.vote_up_count', + 'vote_down_count' : 'answer.vote_down_count', + 'answer_id' : 'answer.id', + 'accepted' : 'answer.accepted', + 'vote_count' : 'answer.score', + 'comment_count' : 'answer.comment_count' + }, + tables=['question', 'answer'], + where=['answer.deleted=0 AND question.deleted=0 AND answer.author_id=%s AND answer.question_id=question.id'], + params=[user_id], + order_by=['-vote_count', '-answer_id'], + select_params=[user_id] + ).distinct().values('comment_count', + 'id', + 'answer_id', + 'title', + 'author_id', + 'accepted', + 'vote_count', + 'answer_count', + 'vote_up_count', + 'vote_down_count')[:100] + up_votes = Vote.objects.get_up_vote_count_from_user(user) down_votes = Vote.objects.get_down_vote_count_from_user(user) votes_today = Vote.objects.get_votes_count_today_from_user(user) votes_total = VOTE_RULES['scope_votes_per_user_per_day'] - tags = user.created_tags.all().order_by('-used_count')[:50] + question_id_set = set(map(lambda v: v['id'], list(questions))) \ + | set(map(lambda v: v['id'], list(answered_questions))) + + user_tags = Tag.objects.filter(questions__id__in = question_id_set) try: from django.db.models import Count awards = Award.objects.extra( - select={'id': 'badge.id', 'name':'badge.name', 'description': 'badge.description', 'type': 'badge.type'}, - tables=['award', 'badge'], - order_by=['-awarded_at'], - where=['user_id=%s AND badge_id=badge.id'], - params=[user.id] - ).values('id', 'name', 'description', 'type') + select={'id': 'badge.id', + 'name':'badge.name', + 'description': 'badge.description', + 'type': 'badge.type'}, + tables=['award', 'badge'], + order_by=['-awarded_at'], + where=['user_id=%s AND badge_id=badge.id'], + params=[user.id] + ).values('id', 'name', 'description', 'type') total_awards = awards.count() - awards = awards.annotate(count=Count('badge__id')) + awards = awards.annotate(count = Count('badge__id')) + user_tags = user_tags.annotate(user_tag_usage_count=Count('name')) + except ImportError: awards = Award.objects.extra( - select={'id': 'badge.id', 'count': 'count(badge_id)', 'name':'badge.name', 'description': 'badge.description', 'type': 'badge.type'}, - tables=['award', 'badge'], - order_by=['-awarded_at'], - where=['user_id=%s AND badge_id=badge.id'], - params=[user.id] - ).values('id', 'count', 'name', 'description', 'type') + select={'id': 'badge.id', + 'count': 'count(badge_id)', + 'name':'badge.name', + 'description': 'badge.description', + 'type': 'badge.type'}, + tables=['award', 'badge'], + order_by=['-awarded_at'], + where=['user_id=%s AND badge_id=badge.id'], + params=[user.id] + ).values('id', 'count', 'name', 'description', 'type') total_awards = awards.count() awards.query.group_by = ['badge_id'] + user_tags = user_tags.extra( + select={'user_tag_usage_count': 'COUNT(1)',}, + order_by=['-user_tag_usage_count'], + ) + user_tags.query.group_by = ['name'] - return render_to_response(user_view.template_file, { - "tab_name": user_view.id, - "tab_description": user_view.tab_description, - "page_title": user_view.page_title, - "view_user": user, - "questions": questions, - "answered_questions": answered_questions, - "up_votes": up_votes, - "down_votes": down_votes, - "total_votes": up_votes + down_votes, - "votes_today_left": votes_total-votes_today, - "votes_total_per_day": votes_total, - "tags": tags, - "awards": awards, - "total_awards": total_awards, - }, context_instance=RequestContext(request)) + if auth.can_moderate_users(request.user): + moderate_user_form = ModerateUserForm(instance=user) + else: + moderate_user_form = None + + return render_to_response(user_view.template_file,{ + 'moderate_user_form': moderate_user_form, + "tab_name" : user_view.id, + "tab_description" : user_view.tab_description, + "page_title" : user_view.page_title, + "view_user" : user, + "questions" : questions, + "answered_questions" : answered_questions, + "up_votes" : up_votes, + "down_votes" : down_votes, + "total_votes": up_votes + down_votes, + "votes_today_left": votes_total-votes_today, + "votes_total_per_day": votes_total, + "user_tags" : user_tags[:50], + "tags" : tags, + "awards": awards, + "total_awards" : total_awards, + }, context_instance=RequestContext(request)) def user_recent(request, user_id, user_view): user = get_object_or_404(User, id=user_id) @@ -1180,8 +1404,10 @@ def user_recent(request, user_id, user_view): self.type_id = type self.title = title self.summary = summary - self.title_link = u'/questions/%s/%s#%s' % (question_id, title, answer_id)\ - if int(answer_id) > 0 else u'/questions/%s/%s' % (question_id, title) + slug_title = slugify(title) + self.title_link = reverse('question', kwargs={'id':question_id}) + u'%s' % slug_title + if int(answer_id) > 0: + self.title_link += '#%s' % answer_id class AwardEvent: def __init__(self, time, type, id): @@ -1193,23 +1419,23 @@ def user_recent(request, user_id, user_view): activities = [] # ask questions questions = Activity.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'question.id', - 'active_at': 'activity.active_at', - 'activity_type': 'activity.activity_type' - }, - tables=['activity', 'question'], - where=['activity.content_type_id = %s AND activity.object_id = ' + - 'question.id AND activity.user_id = %s AND activity.activity_type = %s'], - params=[question_type_id, user_id, TYPE_ACTIVITY_ASK_QUESTION], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'active_at', - 'activity_type' - ) + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'active_at' : 'activity.active_at', + 'activity_type' : 'activity.activity_type' + }, + tables=['activity', 'question'], + where=['activity.content_type_id = %s AND activity.object_id = ' + + 'question.id AND question.deleted=0 AND activity.user_id = %s AND activity.activity_type = %s'], + params=[question_type_id, user_id, TYPE_ACTIVITY_ASK_QUESTION], + order_by=['-activity.active_at'] + ).values( + 'title', + 'question_id', + 'active_at', + 'activity_type' + ) if len(questions) > 0: questions = [(Event(q['active_at'], q['activity_type'], q['title'], '', '0', \ q['question_id'])) for q in questions] @@ -1217,25 +1443,26 @@ def user_recent(request, user_id, user_view): # answers answers = Activity.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'question.id', - 'answer_id': 'answer.id', - 'active_at': 'activity.active_at', - 'activity_type': 'activity.activity_type' - }, - tables=['activity', 'answer', 'question'], - where=['activity.content_type_id = %s AND activity.object_id = answer.id AND ' + - 'answer.question_id=question.id AND activity.user_id=%s AND activity.activity_type=%s'], - params=[answer_type_id, user_id, TYPE_ACTIVITY_ANSWER], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'answer_id', - 'active_at', - 'activity_type' - ) + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'answer_id' : 'answer.id', + 'active_at' : 'activity.active_at', + 'activity_type' : 'activity.activity_type' + }, + tables=['activity', 'answer', 'question'], + where=['activity.content_type_id = %s AND activity.object_id = answer.id AND ' + + 'answer.question_id=question.id AND answer.deleted=0 AND activity.user_id=%s AND '+ + 'activity.activity_type=%s AND question.deleted=0'], + params=[answer_type_id, user_id, TYPE_ACTIVITY_ANSWER], + order_by=['-activity.active_at'] + ).values( + 'title', + 'question_id', + 'answer_id', + 'active_at', + 'activity_type' + ) if len(answers) > 0: answers = [(Event(q['active_at'], q['activity_type'], q['title'], '', q['answer_id'], \ q['question_id'])) for q in answers] @@ -1243,25 +1470,26 @@ def user_recent(request, user_id, user_view): # question comments comments = Activity.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'comment.object_id', - 'added_at': 'comment.added_at', - 'activity_type': 'activity.activity_type' - }, - tables=['activity', 'question', 'comment'], - - where=['activity.content_type_id = %s AND activity.object_id = comment.id AND ' + - 'activity.user_id = comment.user_id AND comment.object_id=question.id AND ' + - 'comment.content_type_id=%s AND activity.user_id = %s AND activity.activity_type=%s'], - params=[comment_type_id, question_type_id, user_id, TYPE_ACTIVITY_COMMENT_QUESTION], - order_by=['-comment.added_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'activity_type' - ) + select={ + 'title' : 'question.title', + 'question_id' : 'comment.object_id', + 'added_at' : 'comment.added_at', + 'activity_type' : 'activity.activity_type' + }, + tables=['activity', 'question', 'comment'], + + where=['activity.content_type_id = %s AND activity.object_id = comment.id AND '+ + 'activity.user_id = comment.user_id AND comment.object_id=question.id AND '+ + 'comment.content_type_id=%s AND activity.user_id = %s AND activity.activity_type=%s AND ' + + 'question.deleted=0'], + params=[comment_type_id, question_type_id, user_id, TYPE_ACTIVITY_COMMENT_QUESTION], + order_by=['-comment.added_at'] + ).values( + 'title', + 'question_id', + 'added_at', + 'activity_type' + ) if len(comments) > 0: comments = [(Event(q['added_at'], q['activity_type'], q['title'], '', '0', \ @@ -1270,28 +1498,29 @@ def user_recent(request, user_id, user_view): # answer comments comments = Activity.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'question.id', - 'answer_id': 'answer.id', - 'added_at': 'comment.added_at', - 'activity_type': 'activity.activity_type' - }, - tables=['activity', 'question', 'answer', 'comment'], - - where=['activity.content_type_id = %s AND activity.object_id = comment.id AND ' + - 'activity.user_id = comment.user_id AND comment.object_id=answer.id AND ' + - 'comment.content_type_id=%s AND question.id = answer.question_id AND ' + - 'activity.user_id = %s AND activity.activity_type=%s'], - params=[comment_type_id, answer_type_id, user_id, TYPE_ACTIVITY_COMMENT_ANSWER], - order_by=['-comment.added_at'] - ).values( - 'title', - 'question_id', - 'answer_id', - 'added_at', - 'activity_type' - ) + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'answer_id' : 'answer.id', + 'added_at' : 'comment.added_at', + 'activity_type' : 'activity.activity_type' + }, + tables=['activity', 'question', 'answer', 'comment'], + + where=['activity.content_type_id = %s AND activity.object_id = comment.id AND '+ + 'activity.user_id = comment.user_id AND comment.object_id=answer.id AND '+ + 'comment.content_type_id=%s AND question.id = answer.question_id AND '+ + 'activity.user_id = %s AND activity.activity_type=%s AND '+ + 'answer.deleted=0 AND question.deleted=0'], + params=[comment_type_id, answer_type_id, user_id, TYPE_ACTIVITY_COMMENT_ANSWER], + order_by=['-comment.added_at'] + ).values( + 'title', + 'question_id', + 'answer_id', + 'added_at', + 'activity_type' + ) if len(comments) > 0: comments = [(Event(q['added_at'], q['activity_type'], q['title'], '', q['answer_id'], \ @@ -1300,26 +1529,27 @@ def user_recent(request, user_id, user_view): # question revisions revisions = Activity.objects.extra( - select={ - 'title': 'question_revision.title', - 'question_id': 'question_revision.question_id', - 'added_at': 'activity.active_at', - 'activity_type': 'activity.activity_type', - 'summary': 'question_revision.summary' - }, - tables=['activity', 'question_revision'], - where=['activity.content_type_id = %s AND activity.object_id = question_revision.id AND ' + - 'activity.user_id = question_revision.author_id AND activity.user_id = %s AND ' + - 'activity.activity_type=%s'], - params=[question_revision_type_id, user_id, TYPE_ACTIVITY_UPDATE_QUESTION], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'activity_type', - 'summary' - ) + select={ + 'title' : 'question_revision.title', + 'question_id' : 'question_revision.question_id', + 'added_at' : 'activity.active_at', + 'activity_type' : 'activity.activity_type', + 'summary' : 'question_revision.summary' + }, + tables=['activity', 'question_revision', 'question'], + where=['activity.content_type_id = %s AND activity.object_id = question_revision.id AND '+ + 'question_revision.id=question.id AND question.deleted=0 AND '+ + 'activity.user_id = question_revision.author_id AND activity.user_id = %s AND '+ + 'activity.activity_type=%s'], + params=[question_revision_type_id, user_id, TYPE_ACTIVITY_UPDATE_QUESTION], + order_by=['-activity.active_at'] + ).values( + 'title', + 'question_id', + 'added_at', + 'activity_type', + 'summary' + ) if len(revisions) > 0: revisions = [(Event(q['added_at'], q['activity_type'], q['title'], q['summary'], '0', \ @@ -1328,30 +1558,31 @@ def user_recent(request, user_id, user_view): # answer revisions revisions = Activity.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'question.id', - 'answer_id': 'answer.id', - 'added_at': 'activity.active_at', - 'activity_type': 'activity.activity_type', - 'summary': 'answer_revision.summary' - }, - tables=['activity', 'answer_revision', 'question', 'answer'], - - where=['activity.content_type_id = %s AND activity.object_id = answer_revision.id AND ' + - 'activity.user_id = answer_revision.author_id AND activity.user_id = %s AND ' + - 'answer_revision.answer_id=answer.id AND answer.question_id = question.id AND ' + - 'activity.activity_type=%s'], - params=[answer_revision_type_id, user_id, TYPE_ACTIVITY_UPDATE_ANSWER], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'answer_id', - 'activity_type', - 'summary' - ) + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'answer_id' : 'answer.id', + 'added_at' : 'activity.active_at', + 'activity_type' : 'activity.activity_type', + 'summary' : 'answer_revision.summary' + }, + tables=['activity', 'answer_revision', 'question', 'answer'], + + where=['activity.content_type_id = %s AND activity.object_id = answer_revision.id AND '+ + 'activity.user_id = answer_revision.author_id AND activity.user_id = %s AND '+ + 'answer_revision.answer_id=answer.id AND answer.question_id = question.id AND '+ + 'question.deleted=0 AND answer.deleted=0 AND '+ + 'activity.activity_type=%s'], + params=[answer_revision_type_id, user_id, TYPE_ACTIVITY_UPDATE_ANSWER], + order_by=['-activity.active_at'] + ).values( + 'title', + 'question_id', + 'added_at', + 'answer_id', + 'activity_type', + 'summary' + ) if len(revisions) > 0: revisions = [(Event(q['added_at'], q['activity_type'], q['title'], q['summary'], \ @@ -1360,24 +1591,25 @@ def user_recent(request, user_id, user_view): # accepted answers accept_answers = Activity.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'question.id', - 'added_at': 'activity.active_at', - 'activity_type': 'activity.activity_type', - }, - tables=['activity', 'answer', 'question'], - where=['activity.content_type_id = %s AND activity.object_id = answer.id AND ' + - 'activity.user_id = question.author_id AND activity.user_id = %s AND ' + - 'answer.question_id=question.id AND activity.activity_type=%s'], - params=[answer_type_id, user_id, TYPE_ACTIVITY_MARK_ANSWER], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'activity_type', - ) + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'added_at' : 'activity.active_at', + 'activity_type' : 'activity.activity_type', + }, + tables=['activity', 'answer', 'question'], + where=['activity.content_type_id = %s AND activity.object_id = answer.id AND '+ + 'activity.user_id = question.author_id AND activity.user_id = %s AND '+ + 'answer.deleted=0 AND question.deleted=0 AND '+ + 'answer.question_id=question.id AND activity.activity_type=%s'], + params=[answer_type_id, user_id, TYPE_ACTIVITY_MARK_ANSWER], + order_by=['-activity.active_at'] + ).values( + 'title', + 'question_id', + 'added_at', + 'activity_type', + ) if len(accept_answers) > 0: accept_answers = [(Event(q['added_at'], q['activity_type'], q['title'], '', '0', \ q['question_id'])) for q in accept_answers] @@ -1405,13 +1637,13 @@ def user_recent(request, user_id, user_view): activities.sort(lambda x, y: cmp(y.time, x.time)) - return render_to_response(user_view.template_file, { - "tab_name": user_view.id, - "tab_description": user_view.tab_description, - "page_title": user_view.page_title, - "view_user": user, - "activities": activities[:user_view.data_size] - }, context_instance=RequestContext(request)) + return render_to_response(user_view.template_file,{ + "tab_name" : user_view.id, + "tab_description" : user_view.tab_description, + "page_title" : user_view.page_title, + "view_user" : user, + "activities" : activities[:user_view.data_size] + }, context_instance=RequestContext(request)) def user_responses(request, user_id, user_view): """ @@ -1421,9 +1653,9 @@ def user_responses(request, user_id, user_view): def __init__(self, type, title, question_id, answer_id, time, username, user_id, content): self.type = type self.title = title - self.titlelink = u'/%s%s/%s#%s' % (_('questions/'), question_id, slugify(title), answer_id) + self.titlelink = reverse('question', args=[question_id]) + u'%s#%s' % (slugify(title), answer_id) self.time = time - self.userlink = u'/%s%s/%s/' % (_('users/'), user_id, username) + self.userlink = reverse('users') + u'%s/%s/' % (user_id, username) self.username = username self.content = u'%s ...' % strip_tags(content)[:300] @@ -1433,30 +1665,31 @@ def user_responses(request, user_id, user_view): user = get_object_or_404(User, id=user_id) responses = [] answers = Answer.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'question.id', - 'answer_id': 'answer.id', - 'added_at': 'answer.added_at', - 'html': 'answer.html', - 'username': 'auth_user.username', - 'user_id': 'auth_user.id' - }, - select_params=[user_id], - tables=['answer', 'question', 'auth_user'], - where=['answer.question_id = question.id AND answer.deleted=0 AND question.deleted = 0 AND ' + - 'question.author_id = %s AND answer.author_id <> %s AND answer.author_id=auth_user.id'], - params=[user_id, user_id], - order_by=['-answer.id'] - ).values( - 'title', - 'question_id', - 'answer_id', - 'added_at', - 'html', - 'username', - 'user_id' - ) + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'answer_id' : 'answer.id', + 'added_at' : 'answer.added_at', + 'html' : 'answer.html', + 'username' : 'auth_user.username', + 'user_id' : 'auth_user.id' + }, + select_params=[user_id], + tables=['answer', 'question', 'auth_user'], + where=['answer.question_id = question.id AND answer.deleted=0 AND question.deleted = 0 AND '+ + 'question.author_id = %s AND answer.author_id <> %s AND answer.author_id=auth_user.id'], + params=[user_id, user_id], + order_by=['-answer.id'] + ).values( + 'title', + 'question_id', + 'answer_id', + 'added_at', + 'html', + 'username', + 'user_id' + ) + if len(answers) > 0: answers = [(Response(TYPE_RESPONSE['QUESTION_ANSWERED'], a['title'], a['question_id'], a['answer_id'], a['added_at'], a['username'], a['user_id'], a['html'])) for a in answers] @@ -1465,27 +1698,27 @@ def user_responses(request, user_id, user_view): # question comments comments = Comment.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'comment.object_id', - 'added_at': 'comment.added_at', - 'comment': 'comment.comment', - 'username': 'auth_user.username', - 'user_id': 'auth_user.id' - }, - tables=['question', 'auth_user', 'comment'], - where=['question.deleted = 0 AND question.author_id = %s AND comment.object_id=question.id AND ' + - 'comment.content_type_id=%s AND comment.user_id <> %s AND comment.user_id = auth_user.id'], - params=[user_id, question_type_id, user_id], - order_by=['-comment.added_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'comment', - 'username', - 'user_id' - ) + select={ + 'title' : 'question.title', + 'question_id' : 'comment.object_id', + 'added_at' : 'comment.added_at', + 'comment' : 'comment.comment', + 'username' : 'auth_user.username', + 'user_id' : 'auth_user.id' + }, + tables=['question', 'auth_user', 'comment'], + where=['question.deleted = 0 AND question.author_id = %s AND comment.object_id=question.id AND '+ + 'comment.content_type_id=%s AND comment.user_id <> %s AND comment.user_id = auth_user.id'], + params=[user_id, question_type_id, user_id], + order_by=['-comment.added_at'] + ).values( + 'title', + 'question_id', + 'added_at', + 'comment', + 'username', + 'user_id' + ) if len(comments) > 0: comments = [(Response(TYPE_RESPONSE['QUESTION_COMMENTED'], c['title'], c['question_id'], @@ -1719,88 +1952,117 @@ def user_favorites(request, user_id, user_view): "view_user": user }, context_instance=RequestContext(request)) - -def user_preferences(request, user_id, user_view): +def user_email_subscriptions(request, user_id, user_view): user = get_object_or_404(User, id=user_id) - return render_to_response(user_view.template_file, { - "tab_name": user_view.id, - "tab_description": user_view.tab_description, - "page_title": user_view.page_title, - "view_user": user, - }, context_instance=RequestContext(request)) + if request.method == 'POST': + email_feeds_form = EditUserEmailFeedsForm(request.POST) + tag_filter_form = TagFilterSelectionForm(request.POST, instance=user) + if email_feeds_form.is_valid() and tag_filter_form.is_valid(): + + action_status = None + tag_filter_saved = tag_filter_form.save() + if tag_filter_saved: + action_status = _('changes saved') + if 'save' in request.POST: + feeds_saved = email_feeds_form.save(user) + if feeds_saved: + action_status = _('changes saved') + elif 'stop_email' in request.POST: + email_stopped = email_feeds_form.reset().save(user) + initial_values = EditUserEmailFeedsForm.NO_EMAIL_INITIAL + email_feeds_form = EditUserEmailFeedsForm(initial=initial_values) + if email_stopped: + action_status = _('email updates canceled') + else: + email_feeds_form = EditUserEmailFeedsForm() + email_feeds_form.set_initial_values(user) + tag_filter_form = TagFilterSelectionForm(instance=user) + action_status = None + return render_to_response(user_view.template_file,{ + 'tab_name':user_view.id, + 'tab_description':user_view.tab_description, + 'page_title':user_view.page_title, + 'view_user':user, + 'email_feeds_form':email_feeds_form, + 'tag_filter_selection_form':tag_filter_form, + 'action_status':action_status, + }, context_instance=RequestContext(request)) def question_comments(request, id): question = get_object_or_404(Question, id=id) user = request.user - return __comments(request, question, 'question', user) + return __comments(request, question, 'question') def answer_comments(request, id): answer = get_object_or_404(Answer, id=id) user = request.user - return __comments(request, answer, 'answer', user) + return __comments(request, answer, 'answer') -def __comments(request, obj, type, user): +def __comments(request, obj, type): # only support get comments by ajax now + user = request.user if request.is_ajax(): if request.method == "GET": - return __generate_comments_json(obj, type, user) + response = __generate_comments_json(obj, type, user) elif request.method == "POST": - comment_data = request.POST.get('comment') - comment = Comment(content_object=obj, comment=comment_data, user=request.user) - comment.save() - obj.comment_count = obj.comment_count + 1 - obj.save() - return __generate_comments_json(obj, type, user) + if auth.can_add_comments(user,obj): + comment_data = request.POST.get('comment') + comment = Comment(content_object=obj, comment=comment_data, user=request.user) + comment.save() + obj.comment_count = obj.comment_count + 1 + obj.save() + response = __generate_comments_json(obj, type, user) + else: + response = HttpResponseForbidden(mimetype="application/json") + return response def __generate_comments_json(obj, type, user): - comments = obj.comments.all().order_by('-id') + comments = obj.comments.all().order_by('id') # {"Id":6,"PostId":38589,"CreationDate":"an hour ago","Text":"hello there!","UserDisplayName":"Jarrod Dixon","UserUrl":"/users/3/jarrod-dixon","DeleteUrl":null} json_comments = [] + from forum.templatetags.extra_tags import diff_date for comment in comments: comment_user = comment.user delete_url = "" if user != None and auth.can_delete_comment(user, comment): #/posts/392845/comments/219852/delete - delete_url = "/" + type + "s/%s/comments/%s/delete/" % (obj.id, comment.id) - json_comments.append({"id": comment.id, - "object_id": obj.id, - "add_date": comment.added_at.strftime('%Y-%m-%d'), - "text": comment.comment, - "user_display_name": comment_user.username, - "user_url": "/users/%s/%s" % (comment_user.id, comment_user.username), - "delete_url": delete_url - }) + #todo translate this url + delete_url = reverse(index) + type + "s/%s/comments/%s/delete/" % (obj.id, comment.id) + json_comments.append({"id" : comment.id, + "object_id" : obj.id, + "comment_age" : diff_date(comment.added_at), + "text" : comment.comment, + "user_display_name" : comment_user.username, + "user_url" : comment_user.get_profile_url(), + "delete_url" : delete_url + }) data = simplejson.dumps(json_comments) return HttpResponse(data, mimetype="application/json") -def delete_question_comment(request, question_id, comment_id): - if request.is_ajax(): - question = get_object_or_404(Question, id=question_id) - comment = get_object_or_404(Comment, id=comment_id) - - question.comments.remove(comment) - question.comment_count = question.comment_count - 1 - question.save() - user = request.user - return __generate_comments_json(question, 'question', user) +def delete_comment(request, object_id='', comment_id='', commented_object_type=None): + response = None + commented_object = None + if commented_object_type == 'question': + commented_object = Question + elif commented_object_type == 'answer': + commented_object = Answer -def delete_answer_comment(request, answer_id, comment_id): if request.is_ajax(): - answer = get_object_or_404(Answer, id=answer_id) comment = get_object_or_404(Comment, id=comment_id) - - answer.comments.remove(comment) - answer.comment_count = answer.comment_count - 1 - answer.save() - user = request.user - return __generate_comments_json(answer, 'answer', user) + if auth.can_delete_comment(request.user, comment): + obj = get_object_or_404(commented_object, id=object_id) + obj.comments.remove(comment) + obj.comment_count = obj.comment_count - 1 + obj.save() + user = request.user + return __generate_comments_json(obj, commented_object_type, user) + raise PermissionDenied() def logout(request): - url = request.GET.get('next') return render_to_response('logout.html', { - 'next': url, - }, context_instance=RequestContext(request)) + 'next' : get_next_url(request), + }, context_instance=RequestContext(request)) def badges(request): badges = Badge.objects.all().order_by('type') @@ -1810,9 +2072,10 @@ def badges(request): my_badges.query.group_by = ['badge_id'] return render_to_response('badges.html', { - 'badges': badges, - 'mybadges': my_badges, - }, context_instance=RequestContext(request)) + 'badges' : badges, + 'mybadges' : my_badges, + 'feedback_faq_url' : reverse('feedback'), + }, context_instance=RequestContext(request)) def badge(request, id): badge = get_object_or_404(Badge, id=id) @@ -1863,8 +2126,8 @@ def upload(request): if not file_name_suffix in settings.ALLOW_FILE_TYPES: raise FileTypeNotAllow - # genetate new file name - new_file_name = str(time.time()).replace('.', str(random.randint(0, 100000))) + file_name_suffix + # generate new file name + new_file_name = str(time.time()).replace('.', str(random.randint(0,100000))) + file_name_suffix # use default storage to store file default_storage.save(new_file_name, f) # check file size @@ -1887,7 +2150,7 @@ def upload(request): return HttpResponse(result, mimetype="application/xml") def books(request): - return HttpResponseRedirect("/books/mysql-zhaoyang") + return HttpResponseRedirect(reverse('books') + '/mysql-zhaoyang') def book(request, short_name, unanswered=False): """ @@ -2010,9 +2273,10 @@ def ask_book(request, short_name): tags = _get_tags_cache_json() return render_to_response('ask.html', { - 'form': form, - 'tags': tags, - }, context_instance=RequestContext(request)) + 'form' : form, + 'tags' : tags, + 'email_validation_faq_url': reverse('faq') + '#validate', + }, context_instance=RequestContext(request)) def search(request): """ @@ -2027,11 +2291,11 @@ def search(request): except ValueError: page = 1 if keywords is None: - return HttpResponseRedirect('/') + return HttpResponseRedirect(reverse(index)) if search_type == 'tag': - return HttpResponseRedirect('/%s?q=%s&page=%s' % (_('tags/'), keywords.strip(), page)) + return HttpResponseRedirect(reverse('tags') + '?q=%s&page=%s' % (keywords.strip(), page)) elif search_type == "user": - return HttpResponseRedirect('/%s?q=%s&page=%s' % (_('users/'), keywords.strip(), page)) + return HttpResponseRedirect(reverse('users') + '?q=%s&page=%s' % (keywords.strip(), page)) elif search_type == "question": template_file = "questions.html" @@ -2070,10 +2334,16 @@ def search(request): view_id = "latest" orderby = "-added_at" - objects = Question.objects.filter(deleted=False).extra(where=['title like %s'], params=['%' + keywords + '%']).order_by(orderby) + if settings.USE_SPHINX_SEARCH == True: + #search index is now free of delete questions and answers + #so there is not "antideleted" filtering here + objects = Question.search.query(keywords) + #no related selection either because we're relying on full text search here + else: + objects = Question.objects.filter(deleted=False).extra(where=['title like %s'], params=['%' + keywords + '%']).order_by(orderby) + # RISK - inner join queries + objects = objects.select_related(); - # RISK - inner join queries - objects = objects.select_related(); objects_list = Paginator(objects, pagesize) questions = objects_list.page(page) |