summaryrefslogtreecommitdiffstats
path: root/forum
diff options
context:
space:
mode:
authorAdolfo Fitoria <fitoria@fitoria-laptop.(none)>2010-02-09 14:12:05 -0600
committerAdolfo Fitoria <fitoria@fitoria-laptop.(none)>2010-02-09 14:12:05 -0600
commit8de2b9131ddcef647799cf8e1e79921284523073 (patch)
tree81e17d84530990e35a0accba3a7886266a601482 /forum
parent7e95e6481d1e81e43d4b442cbcf3fe37f20d89cc (diff)
parent9d1fb9890b97beb55461ca34f9757bc685461130 (diff)
downloadaskbot-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.py94
-rw-r--r--forum/const.py7
-rw-r--r--forum/feed.py4
-rw-r--r--forum/forms.py164
-rw-r--r--forum/management/commands/send_email_alerts.py287
-rw-r--r--forum/management/commands/subscribe_everyone.py31
-rw-r--r--forum/managers.py30
-rw-r--r--forum/models.py207
-rw-r--r--forum/sitemap.py11
-rw-r--r--forum/templatetags/extra_filters.py20
-rw-r--r--forum/templatetags/extra_tags.py127
-rw-r--r--forum/templatetags/smart_if.py401
-rw-r--r--forum/urls.py91
-rw-r--r--forum/user.py12
-rw-r--r--forum/views.py1204
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&amp;d=identicon&amp;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)