diff options
author | root <root@u15356270.onlinehome-server.com> | 2009-11-13 20:18:55 -0500 |
---|---|---|
committer | root <root@u15356270.onlinehome-server.com> | 2009-11-13 20:31:40 -0500 |
commit | 7e4f1d542e00b4d3121da6ae5524e95867f2371b (patch) | |
tree | 5a3edfb81cc137dbc877f75f9bde80e9720b3ba0 /forum | |
parent | 3a4b6ad43221abc0e0ff345d9368c3348b312128 (diff) | |
download | askbot-7e4f1d542e00b4d3121da6ae5524e95867f2371b.tar.gz askbot-7e4f1d542e00b4d3121da6ae5524e95867f2371b.tar.bz2 askbot-7e4f1d542e00b4d3121da6ae5524e95867f2371b.zip |
better comments, email subscriptions, corrected view counter, some ie7 issues, wiki optional with settings.WIKI_ON, site can be mounted on arbitrary url prefix, english language improvements, added feedback form, versioned css and js files to force browser cache reload when settings.RESOURCE_REVISION is incremented , other fixes
Diffstat (limited to 'forum')
-rw-r--r-- | forum/auth.py | 92 | ||||
-rw-r--r-- | forum/const.py | 2 | ||||
-rw-r--r-- | forum/forms.py | 145 | ||||
-rw-r--r-- | forum/management/commands/send_email_alerts.py | 139 | ||||
-rw-r--r-- | forum/management/commands/subscribe_everyone.py | 31 | ||||
-rw-r--r-- | forum/managers.py | 11 | ||||
-rw-r--r-- | forum/models.py | 141 | ||||
-rw-r--r-- | forum/templatetags/extra_filters.py | 9 | ||||
-rw-r--r-- | forum/templatetags/extra_tags.py | 126 | ||||
-rw-r--r-- | forum/templatetags/smart_if.py | 401 | ||||
-rw-r--r-- | forum/urls.py | 72 | ||||
-rw-r--r-- | forum/user.py | 12 | ||||
-rw-r--r-- | forum/views.py | 362 |
13 files changed, 1299 insertions, 244 deletions
diff --git a/forum/auth.py b/forum/auth.py index 776746e8..1569482f 100644 --- a/forum/auth.py +++ b/forum/auth.py @@ -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..9b9230c0 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')), diff --git a/forum/forms.py b/forum/forms.py index 59d0d620..5b181d48 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 +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 @@ -207,3 +229,70 @@ 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 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', + } + all_questions = forms.ChoiceField(choices=DWN,initial='w', + widget=forms.RadioSelect, + label=_('Entire forum'),) + 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')) + + 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..a25e4343 100644 --- a/forum/management/commands/send_email_alerts.py +++ b/forum/management/commands/send_email_alerts.py @@ -1,9 +1,11 @@ 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 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): @@ -15,27 +17,116 @@ class Command(NoArgsCommand): finally: connection.close() + def get_updated_questions_for_user(self,user): + q_sel = [] + q_ask = [] + q_ans = [] + q_all = [] + 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 + ) + 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: + Q_set = Q_set1.exclude(last_activity_at__gt=cutoff_time) + 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) + q_sel.cutoff_time = cutoff_time + 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': + 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 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() - - + + for user in User.objects.all(): + q_list = self.get_updated_questions_for_user(user) + num_q = len(q_list) + 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>' + 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>' + 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 6ae0ed99..795d382e 100644 --- a/forum/managers.py +++ b/forum/managers.py @@ -70,7 +70,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 +93,11 @@ class QuestionManager(models.Manager): """ #print datetime.datetime.now() from forum.models import Question - questions = list(Question.objects.filter(tagnames = question.tagnames).all()) + questions = list(Question.objects.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)[:50] + extend_questions = Question.objects.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 +110,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 f647ba12..39058bea 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.html import strip_tags from django.core.urlresolvers import reverse from django.contrib.auth.models import User @@ -11,33 +11,61 @@ 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 import django.dispatch import settings 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) @@ -45,7 +73,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) @@ -136,6 +163,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) @@ -155,7 +183,6 @@ class Question(models.Model): comments = generic.GenericRelation(Comment) votes = generic.GenericRelation(Vote) flagged_items = generic.GenericRelation(FlaggedItem) - email_feeds = generic.GenericRelation(EmailFeed) objects = QuestionManager() @@ -212,6 +239,22 @@ class Question(models.Model): def get_latest_revision(self): return self.revisions.all()[0] + 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: @@ -240,7 +283,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}) @@ -274,6 +317,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) @@ -303,7 +351,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.""" @@ -383,6 +432,9 @@ class Answer(models.Model): objects = AnswerManager() + get_comments = get_object_comments + get_last_update_info = post_get_last_update_info + def get_user_vote(self, user): votes = self.votes.filter(user=user) if votes.count() > 0: @@ -397,7 +449,7 @@ class Answer(models.Model): return self.question.title def get_absolute_url(self): - return '%s%s#%s' % (reverse('question', args=[self.question.id]), self.question.title, self.id) + return '%s%s#%s' % (reverse('question', args=[self.question.id]), slugify(self.question.title), self.id) class Meta: db_table = u'answer' @@ -415,7 +467,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 @@ -538,7 +590,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=[self.short_name]) + return reverse('book', args=[self.short_name]) def __unicode__(self): return self.title @@ -577,7 +629,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 = ( @@ -586,11 +637,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')) @@ -608,6 +678,8 @@ 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) # custom signal tags_updated = django.dispatch.Signal(providing_args=["question"]) @@ -630,7 +702,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/templatetags/extra_filters.py b/forum/templatetags/extra_filters.py index d8b8e61f..3644fdc3 100644 --- a/forum/templatetags/extra_filters.py +++ b/forum/templatetags/extra_filters.py @@ -1,5 +1,6 @@ from django import template from forum import auth +import logging register = template.Library() @@ -9,6 +10,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 +22,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): diff --git a/forum/templatetags/extra_tags.py b/forum/templatetags/extra_tags.py index 06a2d27c..8bd0e128 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,20 +236,31 @@ def convert2tagname_list(question): @register.simple_tag def diff_date(date, limen=2): - current_time = datetime.datetime(*time.localtime()[0:6]) - diff = current_time - date - diff_days = diff.days - if diff_days > limen: - return date + 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, @@ -242,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 = out.replace(' ','') + return os.path.normpath(out) + '?v=%d' % settings.RESOURCE_REVISION + +@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..a08fe716 --- /dev/null +++ b/forum/urls.py @@ -0,0 +1,72 @@ +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 django.utils.translation import ugettext as _ + +admin.autodiscover() +feeds = { + 'rss': RssLastestQuestionsFeed +} + +APP_PATH = os.path.dirname(os.path.dirname(__file__)) +urlpatterns = patterns('', + url(r'^$', app.index, name='index'), + (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(?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), + 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 d663a4cb..e4ccaa16 100644 --- a/forum/views.py +++ b/forum/views.py @@ -7,16 +7,18 @@ from django.conf import settings from django.core.files.storage import default_storage from django.shortcuts import render_to_response, get_object_or_404 from django.contrib.auth.decorators import login_required -from django.http import HttpResponseRedirect, HttpResponse,Http404 +from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404 from django.core.paginator import Paginator, EmptyPage, InvalidPage -from django.template import RequestContext +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.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext as _ from django.template.defaultfilters import slugify +from django.core.exceptions import PermissionDenied from utils.html import sanitize_html from markdown2 import Markdown @@ -28,6 +30,7 @@ from forum.auth import * from forum.const import * from forum.user import * from forum import auth +from django_authopenid.util import get_next_url # used in index page INDEX_PAGE_SIZE = 20 @@ -63,24 +66,28 @@ 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 = "-last_activity_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) + view_id, orderby = _get_and_remember_questions_sort_method(request, view_dic, 'latest') + + questions = Question.objects.get_questions_by_pagesize(orderby, INDEX_PAGE_SIZE) # RISK - inner join queries questions = questions.select_related() tags = Tag.objects.get_valid_tags(INDEX_TAGS_SIZE) @@ -98,7 +105,34 @@ 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)) @@ -122,13 +156,8 @@ 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_id, orderby = _get_and_remember_questions_sort_method(request,view_dic,'latest') # check if request is from tagged questions if tagname is not None: @@ -208,16 +237,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 +292,7 @@ def ask(request): if form.is_valid(): added_at = datetime.datetime.now() - title = strip_tags(form.cleaned_data['title']) + title = strip_tags(form.cleaned_data['title'].strip()) wiki = form.cleaned_data['wiki'] tagnames = form.cleaned_data['tags'].strip() text = form.cleaned_data['text'] @@ -302,7 +327,7 @@ 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() @@ -310,6 +335,7 @@ def ask(request): return render_to_response('ask.html', { 'form' : form, 'tags' : tags, + 'email_validation_faq_url':reverse('faq') + '#validate', }, context_instance=RequestContext(request)) def question(request, id): @@ -317,13 +343,26 @@ def question(request, id): page = int(request.GET.get('page', '1')) except ValueError: page = 1 - view_id = request.GET.get('sort', 'votes') + + 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): @@ -349,7 +388,6 @@ def question(request, id): vote_value = 1 user_answer_votes[answer.id] = vote_value - if answers is not None: answers = answers.order_by("-accepted", orderby) @@ -363,8 +401,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, @@ -623,6 +691,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, @@ -631,16 +700,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, @@ -650,16 +718,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, @@ -696,8 +764,7 @@ def answer(request, id): ip_addr = request.META['REMOTE_ADDR'], ) anon.save() - return HttpResponseRedirect('/%s%s%s' % ( _('account/'), - _('signin/'),_('newquestion/'))) + return HttpResponseRedirect(reverse('user_signin_new_answer')) return HttpResponseRedirect(question.get_absolute_url()) @@ -712,7 +779,7 @@ def tags(request): if request.method == "GET": stag = request.GET.get("q", "").strip() - if stag is not None: + 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 == "name": @@ -738,7 +805,7 @@ def tags(request): 'has_next': tags.has_next(), 'previous': tags.previous_page_number(), 'next': tags.next_page_number(), - 'base_url' : '/%s?sort=%s&' % (_('tags/'), sortby) + 'base_url' : reverse('tags') + '?sort=%s&' % sortby } }, context_instance=RequestContext(request)) @@ -930,7 +997,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: @@ -939,13 +1007,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: @@ -955,12 +1029,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.' @@ -991,11 +1061,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) @@ -1028,6 +1098,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: @@ -1058,6 +1148,7 @@ def edit_user(request, id): form = EditUserForm(user) return render_to_response('user_edit.html', { 'form' : form, + 'gravatar_faq_url' : reverse('faq') + '#gravatar', }, context_instance=RequestContext(request)) def user_stats(request, user_id, user_view): @@ -1111,7 +1202,7 @@ def user_stats(request, user_id, user_view): 'comment_count' : 'answer.comment_count' }, tables=['question', 'answer'], - where=['answer.deleted=0 AND answer.author_id=%s AND answer.question_id=question.id'], + 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] @@ -1152,7 +1243,13 @@ def user_stats(request, user_id, user_view): from django.db.models import Count awards = awards.annotate(count = Count('badge__id')) + 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, @@ -1184,10 +1281,9 @@ def user_recent(request, user_id, user_view): self.title = title self.summary = summary 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 = u'/%s%s/%s#%s' %(_('questions/'),question_id, slug_title, answer_id) - else: - self.title_link = u'/%s%s/%s' %(_('questions/'),question_id, slug_title) + self.title_link += '#%s' % answer_id class AwardEvent: def __init__(self, time, type, id): @@ -1207,7 +1303,7 @@ def user_recent(request, user_id, user_view): }, 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'], + '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( @@ -1232,7 +1328,8 @@ def user_recent(request, user_id, user_view): }, 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'], + '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( @@ -1259,7 +1356,8 @@ def user_recent(request, user_id, user_view): 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'], + '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( @@ -1288,7 +1386,8 @@ def user_recent(request, user_id, user_view): 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'], + '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( @@ -1313,8 +1412,9 @@ def user_recent(request, user_id, user_view): 'activity_type' : 'activity.activity_type', 'summary' : 'question_revision.summary' }, - tables=['activity', 'question_revision'], + 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], @@ -1347,6 +1447,7 @@ def user_recent(request, user_id, user_view): 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'] @@ -1375,6 +1476,7 @@ def user_recent(request, user_id, user_view): 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'] @@ -1427,9 +1529,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, title, answer_id) + self.titlelink = reverse('questions') + u'%s/%s#%s' % (question_id, 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] @@ -1715,86 +1817,110 @@ def user_favorites(request, user_id, user_view): }, 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) + if request.method == 'POST': + form = EditUserEmailFeedsForm(request.POST) + if form.is_valid(): + if 'save' in request.POST: + saved = form.save(user) + if saved: + action_status = _('changes saved') + elif 'stop_email' in request.POST: + saved = form.reset().save(user) + initial_values = EditUserEmailFeedsForm.NO_EMAIL_INITIAL + form = EditUserEmailFeedsForm(initial=initial_values) + if saved: + action_status = _('email updates canceled') + if not saved: + action_status = None + else: + form = EditUserEmailFeedsForm() + form.set_initial_values(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, + 'tab_name':user_view.id, + 'tab_description':user_view.tab_description, + 'page_title':user_view.page_title, + 'view_user':user, + 'email_feeds_form':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) + #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, - "add_date" : comment.added_at.strftime('%Y-%m-%d'), + "comment_age" : diff_date(comment.added_at), "text" : comment.comment, "user_display_name" : comment_user.username, - "user_url" : "/%s%s/%s" % (_('users/'), comment_user.id, 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, + 'next' : get_next_url(request), }, context_instance=RequestContext(request)) def badges(request): @@ -1807,6 +1933,7 @@ def badges(request): return render_to_response('badges.html', { 'badges' : badges, 'mybadges' : my_badges, + 'feedback_faq_url' : reverse('feedback'), }, context_instance=RequestContext(request)) def badge(request, id): @@ -1882,7 +2009,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): """ @@ -2007,6 +2134,7 @@ def ask_book(request, short_name): return render_to_response('ask.html', { 'form' : form, 'tags' : tags, + 'email_validation_faq_url': reverse('faq') + '#validate', }, context_instance=RequestContext(request)) def search(request): @@ -2022,11 +2150,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" |