diff options
author | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2010-02-14 23:53:39 -0500 |
---|---|---|
committer | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2010-02-14 23:53:39 -0500 |
commit | 537e839ccf52f3b415852d219a07b9f924e946e1 (patch) | |
tree | 1dbed6e0cfa9920f7f6625b72effb59203710b1b /forum | |
parent | 151da132a6dcec9144847f98a3c6887fb292c890 (diff) | |
download | askbot-537e839ccf52f3b415852d219a07b9f924e946e1.tar.gz askbot-537e839ccf52f3b415852d219a07b9f924e946e1.tar.bz2 askbot-537e839ccf52f3b415852d219a07b9f924e946e1.zip |
split forum/views.py, fixed defect in skins on the javascript side and blockmedia template tag
Diffstat (limited to 'forum')
-rw-r--r-- | forum/__init__.py | 1 | ||||
-rw-r--r-- | forum/forms.py.orig | 352 | ||||
-rw-r--r-- | forum/models.py | 112 | ||||
-rw-r--r-- | forum/models/models.py | 949 | ||||
-rw-r--r-- | forum/skins/__init__.py (renamed from forum/skins.py) | 3 | ||||
-rw-r--r-- | forum/skins/default/media/js/com.cnprog.post.js | 28 | ||||
-rw-r--r-- | forum/skins/default/media/js/com.cnprog.tag_selector.js | 6 | ||||
-rw-r--r-- | forum/skins/default/media/js/com.cnprog.utils.js | 18 | ||||
-rw-r--r-- | forum/skins/default/media/js/wmd/wmd.js | 6 | ||||
-rwxr-xr-x | forum/skins/default/templates/base.html | 24 | ||||
-rw-r--r-- | forum/skins/default/templates/base_content.html | 29 | ||||
-rw-r--r-- | forum/skins/default/templates/question.html | 54 | ||||
-rw-r--r-- | forum/templatetags/extra_tags.py | 4 | ||||
-rw-r--r-- | forum/user.py | 74 | ||||
-rw-r--r-- | forum/views/__init__.py | 4 | ||||
-rw-r--r-- | forum/views/books.py | 139 | ||||
-rw-r--r-- | forum/views/content.py | 1393 | ||||
-rw-r--r-- | forum/views/meta.py | 98 | ||||
-rw-r--r-- | forum/views/users.py | 943 | ||||
-rw-r--r-- | forum/views/views.py (renamed from forum/views.py) | 0 |
20 files changed, 3655 insertions, 582 deletions
diff --git a/forum/__init__.py b/forum/__init__.py index e69de29b..a221a3ad 100644 --- a/forum/__init__.py +++ b/forum/__init__.py @@ -0,0 +1 @@ +__all__ = ['admin','auth','const','diff','feed','forms','managers','models','sitemap','urls','views'] diff --git a/forum/forms.py.orig b/forum/forms.py.orig deleted file mode 100644 index 42becc11..00000000 --- a/forum/forms.py.orig +++ /dev/null @@ -1,352 +0,0 @@ -import re -from datetime import date -from django import forms -from models import * -from const import * -from django.utils.translation import ugettext as _ -from utils.forms import NextUrlField, UserNameField -from recaptcha_django import ReCaptchaField -from django.conf import settings -import logging - -class TitleField(forms.CharField): - def __init__(self, *args, **kwargs): - super(TitleField, self).__init__(*args, **kwargs) - self.required = True - self.widget = forms.TextInput(attrs={'size' : 70, 'autocomplete' : 'off'}) - self.max_length = 255 - self.label = _('title') - self.help_text = _('please enter a descriptive title for your question') - self.initial = '' - - def clean(self, value): - if len(value) < 10: - raise forms.ValidationError(_('title must be > 10 characters')) - - return value - -class EditorField(forms.CharField): - def __init__(self, *args, **kwargs): - super(EditorField, self).__init__(*args, **kwargs) - self.required = True - self.widget = forms.Textarea(attrs={'id':'editor'}) - self.label = _('content') - self.help_text = u'' - self.initial = '' - - def clean(self, value): - if len(value) < 10: - raise forms.ValidationError(_('question content must be > 10 characters')) - - return value - -class TagNamesField(forms.CharField): - def __init__(self, *args, **kwargs): - super(TagNamesField, self).__init__(*args, **kwargs) - self.required = True - self.widget = forms.TextInput(attrs={'size' : 50, 'autocomplete' : 'off'}) - self.max_length = 255 - self.label = _('tags') - #self.help_text = _('please use space to separate tags (this enables autocomplete feature)') - 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')) - - 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): - super(WikiField, self).__init__(*args, **kwargs) - 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): - super(SummaryField, self).__init__(*args, **kwargs) - self.required = False - self.widget = forms.TextInput(attrs={'size' : 50, 'autocomplete' : 'off'}) - self.max_length = 300 - 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 NotARobotForm(forms.Form): - recaptcha = ReCaptchaField() - -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() - tags = TagNamesField() - wiki = WikiField() - - openid = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 40, 'class':'openid-input'})) - user = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) - email = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) - -class AnswerForm(forms.Form): - text = EditorField() - wiki = WikiField() - openid = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 40, 'class':'openid-input'})) - user = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) - email = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) - email_notify = EmailNotifyField() - 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 and settings.WIKI_ON: - self.fields['wiki'].initial = True - if user.is_authenticated(): - if user in question.followed_by.all(): - self.fields['email_notify'].initial = True - return - self.fields['email_notify'].initial = False - - -class CloseForm(forms.Form): - reason = forms.ChoiceField(choices=CLOSE_REASONS) - -class RetagQuestionForm(forms.Form): - tags = TagNamesField() - # initialize the default values - def __init__(self, question, *args, **kwargs): - super(RetagQuestionForm, self).__init__(*args, **kwargs) - self.fields['tags'].initial = question.tagnames - -class RevisionForm(forms.Form): - """ - Lists revisions of a Question or Answer - """ - revision = forms.ChoiceField(widget=forms.Select(attrs={'style' : 'width:520px'})) - - def __init__(self, post, latest_revision, *args, **kwargs): - super(RevisionForm, self).__init__(*args, **kwargs) - revisions = post.revisions.all().values_list( - 'revision', 'author__username', 'revised_at', 'summary') - date_format = '%c' - self.fields['revision'].choices = [ - (r[0], u'%s - %s (%s) %s' % (r[0], r[1], r[2].strftime(date_format), r[3])) - for r in revisions] - self.fields['revision'].initial = latest_revision.revision - -class EditQuestionForm(forms.Form): - title = TitleField() - text = EditorField() - tags = TagNamesField() - summary = SummaryField() - - def __init__(self, question, revision, *args, **kwargs): - super(EditQuestionForm, self).__init__(*args, **kwargs) - self.fields['title'].initial = revision.title - self.fields['text'].initial = revision.text - self.fields['tags'].initial = revision.tagnames - # Once wiki mode is enabled, it can't be disabled - if not question.wiki: - self.fields['wiki'] = WikiField() - -class EditAnswerForm(forms.Form): - text = EditorField() - summary = SummaryField() - - def __init__(self, answer, revision, *args, **kwargs): - super(EditAnswerForm, self).__init__(*args, **kwargs) - self.fields['text'].initial = revision.text - -class EditUserForm(forms.Form): - email = forms.EmailField(label=u'Email', help_text=_('this email does not have to be linked to gravatar'), required=True, 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})) - birthday = forms.DateField(label=_('Date of birth'), help_text=_('will not be shown, used to calculate age, format: YYYY-MM-DD'), required=False, widget=forms.TextInput(attrs={'size' : 35})) - about = forms.CharField(label=_('Profile'), required=False, widget=forms.Textarea(attrs={'cols' : 60})) - - def __init__(self, user, *args, **kwargs): - super(EditUserForm, self).__init__(*args, **kwargs) - self.fields['username'].initial = user.username - self.fields['username'].user_instance = user - self.fields['email'].initial = user.email - self.fields['realname'].initial = user.real_name - self.fields['website'].initial = user.website - self.fields['city'].initial = user.location - - if user.date_of_birth is not None: - self.fields['birthday'].initial = user.date_of_birth - else: - self.fields['birthday'].initial = '1990-01-01' - self.fields['about'].initial = user.about - self.user = user - - def clean_email(self): - """For security reason one unique email in database""" - if self.user.email != self.cleaned_data['email']: - #todo dry it, there is a similar thing in openidauth - if settings.EMAIL_UNIQUE == True: - if 'email' in self.cleaned_data: - try: - user = User.objects.get(email = self.cleaned_data['email']) - except User.DoesNotExist: - return self.cleaned_data['email'] - except User.MultipleObjectsReturned: - 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,save_unbound=False): - """ - with save_unbound==True will bypass form validation and save initial values - """ - 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) - if save_unbound: - #just save initial values instead - if form_field in self.initial: - new_value = self.initial[form_field] - else: - new_value = self.fields[form_field].initial - else: - new_value = self.cleaned_data[form_field] - if s.frequency != new_value: - s.frequency = new_value - 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 - - -class SimpleEmailSubscribeForm(forms.Form): - SIMPLE_SUBSCRIBE_CHOICES = ( - ('y',_('okay, let\'s try!')), - ('n',_('no OSQA community email please, thanks')) - ) - subscribe = forms.ChoiceField(widget=forms.widgets.RadioSelect(), \ - error_messages={'required':_('please choose one of the options above')}, - choices=SIMPLE_SUBSCRIBE_CHOICES) - - def save(self,user=None): - EFF = EditUserEmailFeedsForm - if self.cleaned_data['subscribe'] == 'y': - email_settings_form = EFF() - logging.debug('%s wants to subscribe' % user.username) - else: - email_settings_form = EFF(initial=EFF.NO_EMAIL_INITIAL) - email_settings_form.save(user,save_unbound=True) diff --git a/forum/models.py b/forum/models.py index acde1033..2da8ef7b 100644 --- a/forum/models.py +++ b/forum/models.py @@ -22,7 +22,7 @@ if settings.USE_SPHINX_SEARCH == True: from djangosphinx.models import SphinxSearch from forum.managers import * -from const import * +from forum.const import * def get_object_comments(self): comments = self.comments.all().order_by('id') @@ -619,58 +619,6 @@ class Activity(models.Model): class Meta: db_table = u'activity' -class Book(models.Model): - """ - Model for book info - """ - user = models.ForeignKey(User) - title = models.CharField(max_length=255) - short_name = models.CharField(max_length=255) - author = models.CharField(max_length=255) - price = models.DecimalField(max_digits=6, decimal_places=2) - pages = models.SmallIntegerField() - published_at = models.DateTimeField() - publication = models.CharField(max_length=255) - cover_img = models.CharField(max_length=255) - tagnames = models.CharField(max_length=125) - added_at = models.DateTimeField() - last_edited_at = models.DateTimeField() - questions = models.ManyToManyField(Question, related_name='book', db_table='book_question') - - def get_absolute_url(self): - return reverse('book', args=[django_urlquote(slugify(self.short_name))]) - - def __unicode__(self): - return self.title - class Meta: - db_table = u'book' - -class BookAuthorInfo(models.Model): - """ - Model for book author info - """ - user = models.ForeignKey(User) - book = models.ForeignKey(Book) - blog_url = models.CharField(max_length=255) - added_at = models.DateTimeField() - last_edited_at = models.DateTimeField() - - class Meta: - db_table = u'book_author_info' - -class BookAuthorRss(models.Model): - """ - Model for book author blog rss - """ - user = models.ForeignKey(User) - book = models.ForeignKey(Book) - title = models.CharField(max_length=255) - url = models.CharField(max_length=255) - rss_created_at = models.DateTimeField() - added_at = models.DateTimeField() - - class Meta: - db_table = u'book_author_rss' class AnonymousEmail(models.Model): #validation key, if used @@ -947,3 +895,61 @@ tags_updated.connect(record_update_tags, sender=Question) post_save.connect(record_favorite_question, sender=FavoriteQuestion) user_updated.connect(record_user_full_updated, sender=User) user_logged_in.connect(post_stored_anonymous_content) + +#todo later split this out to the books extension models +#from django.db import models +#from django.contrib.auth.models import User +#from forum.models import Question + +class Book(models.Model): + """ + Model for book info + """ + user = models.ForeignKey(User) + title = models.CharField(max_length=255) + short_name = models.CharField(max_length=255) + author = models.CharField(max_length=255) + price = models.DecimalField(max_digits=6, decimal_places=2) + pages = models.SmallIntegerField() + published_at = models.DateTimeField() + publication = models.CharField(max_length=255) + cover_img = models.CharField(max_length=255) + tagnames = models.CharField(max_length=125) + added_at = models.DateTimeField() + last_edited_at = models.DateTimeField() + questions = models.ManyToManyField(Question, related_name='book', db_table='book_question') + + def get_absolute_url(self): + return reverse('book', args=[django_urlquote(slugify(self.short_name))]) + + def __unicode__(self): + return self.title + class Meta: + db_table = u'book' + +class BookAuthorInfo(models.Model): + """ + Model for book author info + """ + user = models.ForeignKey(User) + book = models.ForeignKey(Book) + blog_url = models.CharField(max_length=255) + added_at = models.DateTimeField() + last_edited_at = models.DateTimeField() + + class Meta: + db_table = u'book_author_info' + +class BookAuthorRss(models.Model): + """ + Model for book author blog rss + """ + user = models.ForeignKey(User) + book = models.ForeignKey(Book) + title = models.CharField(max_length=255) + url = models.CharField(max_length=255) + rss_created_at = models.DateTimeField() + added_at = models.DateTimeField() + + class Meta: + db_table = u'book_author_rss' diff --git a/forum/models/models.py b/forum/models/models.py new file mode 100644 index 00000000..acde1033 --- /dev/null +++ b/forum/models/models.py @@ -0,0 +1,949 @@ +# encoding:utf-8 +import datetime +import hashlib +from urllib import quote_plus, urlencode +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 +from django.contrib.auth.models import User +from django.contrib.contenttypes import generic +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 +from django.conf import settings +import logging + +if settings.USE_SPHINX_SEARCH == True: + from djangosphinx.models import SphinxSearch + +from forum.managers import * +from const import * + +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) + created_by = models.ForeignKey(User, related_name='created_tags') + 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') + # Denormalised data + used_count = models.PositiveIntegerField(default=0) + + objects = TagManager() + + class Meta: + db_table = u'tag' + ordering = ('-used_count', 'name') + + def __unicode__(self): + return self.name + +class Comment(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + user = models.ForeignKey(User, related_name='comments') + comment = models.CharField(max_length=300) + added_at = models.DateTimeField(default=datetime.datetime.now) + + 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 + +class Vote(models.Model): + VOTE_UP = +1 + VOTE_DOWN = -1 + VOTE_CHOICES = ( + (VOTE_UP, u'Up'), + (VOTE_DOWN, u'Down'), + ) + + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + user = models.ForeignKey(User, related_name='votes') + vote = models.SmallIntegerField(choices=VOTE_CHOICES) + voted_at = models.DateTimeField(default=datetime.datetime.now) + + objects = VoteManager() + + class Meta: + unique_together = ('content_type', 'object_id', 'user') + db_table = u'vote' + def __unicode__(self): + return '[%s] voted at %s: %s' %(self.user, self.voted_at, self.vote) + + def is_upvote(self): + return self.vote == self.VOTE_UP + + def is_downvote(self): + return self.vote == self.VOTE_DOWN + +class FlaggedItem(models.Model): + """A flag on a Question or Answer indicating offensive content.""" + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + user = models.ForeignKey(User, related_name='flagged_items') + flagged_at = models.DateTimeField(default=datetime.datetime.now) + + objects = FlaggedItemManager() + + class Meta: + unique_together = ('content_type', 'object_id', 'user') + db_table = u'flagged_item' + def __unicode__(self): + return '[%s] flagged at %s' %(self.user, self.flagged_at) + +class Question(models.Model): + title = models.CharField(max_length=300) + author = models.ForeignKey(User, related_name='questions') + added_at = models.DateTimeField(default=datetime.datetime.now) + tags = models.ManyToManyField(Tag, related_name='questions') + # Status + wiki = models.BooleanField(default=False) + wikified_at = models.DateTimeField(null=True, blank=True) + answer_accepted = models.BooleanField(default=False) + closed = models.BooleanField(default=False) + closed_by = models.ForeignKey(User, null=True, blank=True, related_name='closed_questions') + closed_at = models.DateTimeField(null=True, blank=True) + close_reason = models.SmallIntegerField(choices=CLOSE_REASONS, null=True, blank=True) + 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_questions') + 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) + vote_down_count = models.IntegerField(default=0) + answer_count = models.PositiveIntegerField(default=0) + comment_count = models.PositiveIntegerField(default=0) + view_count = models.PositiveIntegerField(default=0) + offensive_flag_count = models.SmallIntegerField(default=0) + favourite_count = models.PositiveIntegerField(default=0) + last_edited_at = models.DateTimeField(null=True, blank=True) + last_edited_by = models.ForeignKey(User, null=True, blank=True, related_name='last_edited_questions') + last_activity_at = models.DateTimeField(default=datetime.datetime.now) + last_activity_by = models.ForeignKey(User, related_name='last_active_in_questions') + tagnames = models.CharField(max_length=125) + summary = models.CharField(max_length=180) + html = models.TextField() + comments = generic.GenericRelation(Comment) + votes = generic.GenericRelation(Vote) + flagged_items = generic.GenericRelation(FlaggedItem) + + 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 + is first saved. + + This is required as we're using ``tagnames`` as the sole means of + adding and editing tags. + """ + 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) + self.tags.add(*tags) + Tag.objects.update_use_counts(tags) + + def tagname_list(self): + """Creates a list of Tag names from the ``tagnames`` attribute.""" + return [name for name in self.tagnames.split(u' ')] + + def tagname_meta_generator(self): + 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(slugify(self.title))) + + def has_favorite_by_user(self, user): + if not user.is_authenticated(): + return False + return FavoriteQuestion.objects.filter(question=self, user=user).count() > 0 + + def get_answer_count_by_user(self, user_id): + query_set = Answer.objects.filter(author__id=user_id) + return query_set.filter(question=self).count() + + def get_question_title(self): + if self.closed: + attr = CONST['closed'] + elif self.deleted: + attr = CONST['deleted'] + else: + attr = None + if attr is not None: + return u'%s %s' % (self.title, attr) + else: + return self.title + + def get_revision_url(self): + return reverse('question_revisions', args=[self.id]) + + def get_latest_revision(self): + return self.revisions.all()[0] + + get_comments = get_object_comments + + def get_last_update_info(self): + + when, who = post_get_last_update_info(self) + + answers = self.answers.all() + if len(answers) > 0: + for a in answers: + a_when, a_who = a.get_last_update_info() + if a_when > when: + when = a_when + who = a_who + + return when, who + + def get_update_summary(self,last_reported_at=None,recipient_email=''): + edited = False + if self.last_edited_at and self.last_edited_at > last_reported_at: + if self.last_edited_by.email != recipient_email: + edited = True + comments = [] + for comment in self.comments.all(): + if comment.added_at > last_reported_at and comment.user.email != recipient_email: + comments.append(comment) + new_answers = [] + answer_comments = [] + modified_answers = [] + commented_answers = [] + import sets + commented_answers = sets.Set([]) + for answer in self.answers.all(): + if (answer.added_at > last_reported_at and answer.author.email != recipient_email): + new_answers.append(answer) + if (answer.last_edited_at + and answer.last_edited_at > last_reported_at + and answer.last_edited_by.email != recipient_email): + modified_answers.append(answer) + for comment in answer.comments.all(): + if comment.added_at > last_reported_at and comment.user.email != recipient_email: + commented_answers.add(answer) + answer_comments.append(comment) + + #create the report + 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}) + if new_answers: + names = sets.Set(map(lambda x: x.author.username,new_answers)) + people = ', '.join(names) + out.append(_('%(people)s posted %(new_answer_count)s new answers') \ + % {'new_answer_count':len(new_answers),'people':people}) + if comments: + names = sets.Set(map(lambda x: x.user.username,comments)) + people = ', '.join(names) + out.append(_('%(people)s commented the question') % {'people':people}) + if answer_comments: + names = sets.Set(map(lambda x: x.user.username,answer_comments)) + people = ', '.join(names) + if len(commented_answers) > 1: + out.append(_('%(people)s commented answers') % {'people':people}) + else: + out.append(_('%(people)s commented an answer') % {'people':people}) + url = settings.APP_URL + self.get_absolute_url() + retval = '<a href="%s">%s</a>:<br>\n' % (url,self.title) + out = map(lambda x: '<li>' + x + '</li>',out) + retval += '<ul>' + '\n'.join(out) + '</ul><br>\n' + return retval + else: + return None + + def __unicode__(self): + return self.title + + 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) + user = models.ForeignKey(User, related_name='user_favorite_questions') + added_at = models.DateTimeField(default=datetime.datetime.now) + class Meta: + db_table = u'favorite_question' + 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') + revision = models.PositiveIntegerField(blank=True) + title = models.CharField(max_length=300) + author = models.ForeignKey(User, related_name='question_revisions') + revised_at = models.DateTimeField() + tagnames = models.CharField(max_length=125) + summary = models.CharField(max_length=300, blank=True) + text = models.TextField() + + class Meta: + db_table = u'question_revision' + ordering = ('-revision',) + + def get_question_title(self): + return self.question.title + + def get_absolute_url(self): + #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.""" + if not self.revision: + self.revision = QuestionRevision.objects.filter( + question=self.question).values_list('revision', + flat=True)[0] + 1 + super(QuestionRevision, self).save(**kwargs) + + def __unicode__(self): + return u'revision %s of %s' % (self.revision, self.title) + +class AnonymousAnswer(models.Model): + question = models.ForeignKey(Question, related_name='anonymous_answers') + session_key = models.CharField(max_length=40) #session id for anonymous questions + wiki = models.BooleanField(default=False) + added_at = models.DateTimeField(default=datetime.datetime.now) + ip_addr = models.IPAddressField(max_length=21) #allow high port numbers + author = models.ForeignKey(User,null=True) + text = models.TextField() + summary = models.CharField(max_length=180) + + def publish(self,user): + from forum.views import create_new_answer + added_at = datetime.datetime.now() + #print user.id + create_new_answer(question=self.question,wiki=self.wiki, + added_at=added_at,text=self.text, + author=user) + self.delete() + +class AnonymousQuestion(models.Model): + title = models.CharField(max_length=300) + session_key = models.CharField(max_length=40) #session id for anonymous questions + text = models.TextField() + summary = models.CharField(max_length=180) + tagnames = models.CharField(max_length=125) + wiki = models.BooleanField(default=False) + added_at = models.DateTimeField(default=datetime.datetime.now) + ip_addr = models.IPAddressField(max_length=21) #allow high port numbers + author = models.ForeignKey(User,null=True) + + def publish(self,user): + from forum.views import create_new_question + added_at = datetime.datetime.now() + create_new_question(title=self.title, author=user, added_at=added_at, + wiki=self.wiki, tagnames=self.tagnames, + summary=self.summary, text=self.text) + self.delete() + +class Answer(models.Model): + question = models.ForeignKey(Question, related_name='answers') + author = models.ForeignKey(User, related_name='answers') + added_at = models.DateTimeField(default=datetime.datetime.now) + # Status + wiki = models.BooleanField(default=False) + wikified_at = models.DateTimeField(null=True, blank=True) + accepted = models.BooleanField(default=False) + accepted_at = models.DateTimeField(null=True, blank=True) + deleted = models.BooleanField(default=False) + deleted_by = models.ForeignKey(User, null=True, blank=True, related_name='deleted_answers') + locked = models.BooleanField(default=False) + locked_by = models.ForeignKey(User, null=True, blank=True, related_name='locked_answers') + locked_at = models.DateTimeField(null=True, blank=True) + # Denormalised data + score = models.IntegerField(default=0) + vote_up_count = models.IntegerField(default=0) + vote_down_count = models.IntegerField(default=0) + comment_count = models.PositiveIntegerField(default=0) + offensive_flag_count = models.SmallIntegerField(default=0) + last_edited_at = models.DateTimeField(null=True, blank=True) + last_edited_by = models.ForeignKey(User, null=True, blank=True, related_name='last_edited_answers') + html = models.TextField() + comments = generic.GenericRelation(Comment) + votes = generic.GenericRelation(Vote) + flagged_items = generic.GenericRelation(FlaggedItem) + + 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): + if user.__class__.__name__ == "AnonymousUser": + return None + + votes = self.votes.filter(user=user) + if votes and votes.count() > 0: + return votes[0] + else: + return None + + def get_latest_revision(self): + return self.revisions.all()[0] + + def get_question_title(self): + return self.question.title + + def get_absolute_url(self): + return '%s%s#%s' % (reverse('question', args=[self.question.id]), django_urlquote(slugify(self.question.title)), self.id) + + class Meta: + db_table = u'answer' + + def __unicode__(self): + return self.html + +class AnswerRevision(models.Model): + """A revision of an Answer.""" + answer = models.ForeignKey(Answer, related_name='revisions') + revision = models.PositiveIntegerField() + author = models.ForeignKey(User, related_name='answer_revisions') + revised_at = models.DateTimeField() + summary = models.CharField(max_length=300, blank=True) + text = models.TextField() + + def get_absolute_url(self): + return reverse('answer_revisions', kwargs={'id':self.answer.id}) + + def get_question_title(self): + return self.answer.question.title + + class Meta: + db_table = u'answer_revision' + ordering = ('-revision',) + + def save(self, **kwargs): + """Looks up the next available revision number if not set.""" + if not self.revision: + self.revision = AnswerRevision.objects.filter( + answer=self.answer).values_list('revision', + flat=True)[0] + 1 + super(AnswerRevision, self).save(**kwargs) + +class Badge(models.Model): + """Awarded for notable actions performed on the site by Users.""" + GOLD = 1 + SILVER = 2 + BRONZE = 3 + TYPE_CHOICES = ( + (GOLD, _('gold')), + (SILVER, _('silver')), + (BRONZE, _('bronze')), + ) + + name = models.CharField(max_length=50) + type = models.SmallIntegerField(choices=TYPE_CHOICES) + slug = models.SlugField(max_length=50, blank=True) + description = models.CharField(max_length=300) + multiple = models.BooleanField(default=False) + # Denormalised data + awarded_count = models.PositiveIntegerField(default=0) + + class Meta: + db_table = u'badge' + ordering = ('name',) + unique_together = ('name', 'type') + + def __unicode__(self): + return u'%s: %s' % (self.get_type_display(), self.name) + + def save(self, **kwargs): + if not self.slug: + self.slug = self.name#slugify(self.name) + super(Badge, self).save(**kwargs) + + def get_absolute_url(self): + return '%s%s/' % (reverse('badge', args=[self.id]), self.slug) + +class Award(models.Model): + """The awarding of a Badge to a User.""" + user = models.ForeignKey(User, related_name='award_user') + badge = models.ForeignKey(Badge, related_name='award_badge') + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + awarded_at = models.DateTimeField(default=datetime.datetime.now) + notified = models.BooleanField(default=False) + objects = AwardManager() + + def __unicode__(self): + return u'[%s] is awarded a badge [%s] at %s' % (self.user.username, self.badge.name, self.awarded_at) + + class Meta: + db_table = u'award' + +class Repute(models.Model): + """The reputation histories for user""" + user = models.ForeignKey(User) + positive = models.SmallIntegerField(default=0) + negative = models.SmallIntegerField(default=0) + question = models.ForeignKey(Question) + reputed_at = models.DateTimeField(default=datetime.datetime.now) + reputation_type = models.SmallIntegerField(choices=TYPE_REPUTATION) + reputation = models.IntegerField(default=1) + objects = ReputeManager() + + def __unicode__(self): + return u'[%s]\' reputation changed at %s' % (self.user.username, self.reputed_at) + + class Meta: + db_table = u'repute' + +class Activity(models.Model): + """ + We keep some history data for user activities + """ + user = models.ForeignKey(User) + activity_type = models.SmallIntegerField(choices=TYPE_ACTIVITY) + active_at = models.DateTimeField(default=datetime.datetime.now) + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + is_auditted = models.BooleanField(default=False) + + def __unicode__(self): + return u'[%s] was active at %s' % (self.user.username, self.active_at) + + class Meta: + db_table = u'activity' + +class Book(models.Model): + """ + Model for book info + """ + user = models.ForeignKey(User) + title = models.CharField(max_length=255) + short_name = models.CharField(max_length=255) + author = models.CharField(max_length=255) + price = models.DecimalField(max_digits=6, decimal_places=2) + pages = models.SmallIntegerField() + published_at = models.DateTimeField() + publication = models.CharField(max_length=255) + cover_img = models.CharField(max_length=255) + tagnames = models.CharField(max_length=125) + added_at = models.DateTimeField() + last_edited_at = models.DateTimeField() + questions = models.ManyToManyField(Question, related_name='book', db_table='book_question') + + def get_absolute_url(self): + return reverse('book', args=[django_urlquote(slugify(self.short_name))]) + + def __unicode__(self): + return self.title + class Meta: + db_table = u'book' + +class BookAuthorInfo(models.Model): + """ + Model for book author info + """ + user = models.ForeignKey(User) + book = models.ForeignKey(Book) + blog_url = models.CharField(max_length=255) + added_at = models.DateTimeField() + last_edited_at = models.DateTimeField() + + class Meta: + db_table = u'book_author_info' + +class BookAuthorRss(models.Model): + """ + Model for book author blog rss + """ + user = models.ForeignKey(User) + book = models.ForeignKey(Book) + title = models.CharField(max_length=255) + url = models.CharField(max_length=255) + rss_created_at = models.DateTimeField() + added_at = models.DateTimeField() + + class Meta: + db_table = u'book_author_rss' + +class AnonymousEmail(models.Model): + #validation key, if used + key = models.CharField(max_length=32) + email = models.EmailField(null=False,unique=True) + isvalid = models.BooleanField(default=False) + +# User extend properties +QUESTIONS_PER_PAGE_CHOICES = ( + (10, u'10'), + (30, u'30'), + (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('favorite_questions', + models.ManyToManyField(Question, through=FavoriteQuestion, + related_name='favorited_by')) +User.add_to_class('badges', models.ManyToManyField(Badge, through=Award, + related_name='awarded_to')) +User.add_to_class('gold', models.SmallIntegerField(default=0)) +User.add_to_class('silver', models.SmallIntegerField(default=0)) +User.add_to_class('bronze', models.SmallIntegerField(default=0)) +User.add_to_class('questions_per_page', + models.SmallIntegerField(choices=QUESTIONS_PER_PAGE_CHOICES, default=10)) +User.add_to_class('last_seen', + models.DateTimeField(default=datetime.datetime.now)) +User.add_to_class('real_name', models.CharField(max_length=100, blank=True)) +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"]) +edit_question_or_answer = django.dispatch.Signal(providing_args=["instance", "modified_by"]) +delete_post_or_answer = django.dispatch.Signal(providing_args=["instance", "deleted_by"]) +mark_offensive = django.dispatch.Signal(providing_args=["instance", "mark_by"]) +user_updated = django.dispatch.Signal(providing_args=["instance", "updated_by"]) +user_logged_in = django.dispatch.Signal(providing_args=["session"]) + + +def get_messages(self): + messages = [] + for m in self.message_set.all(): + messages.append(m.message) + return messages + +def delete_messages(self): + self.message_set.all().delete() + +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) + +def calculate_gravatar_hash(instance, **kwargs): + """Calculates a User's gravatar hash from their email address.""" + if kwargs.get('raw', False): + return + instance.gravatar = hashlib.md5(instance.email).hexdigest() + +def record_ask_event(instance, created, **kwargs): + if created: + activity = Activity(user=instance.author, active_at=instance.added_at, content_object=instance, activity_type=TYPE_ACTIVITY_ASK_QUESTION) + activity.save() + +def record_answer_event(instance, created, **kwargs): + if created: + activity = Activity(user=instance.author, active_at=instance.added_at, content_object=instance, activity_type=TYPE_ACTIVITY_ANSWER) + activity.save() + +def record_comment_event(instance, created, **kwargs): + if created: + from django.contrib.contenttypes.models import ContentType + question_type = ContentType.objects.get_for_model(Question) + question_type_id = question_type.id + if (instance.content_type_id == question_type_id): + type = TYPE_ACTIVITY_COMMENT_QUESTION + else: + type = TYPE_ACTIVITY_COMMENT_ANSWER + activity = Activity(user=instance.user, active_at=instance.added_at, content_object=instance, activity_type=type) + activity.save() + +def record_revision_question_event(instance, created, **kwargs): + if created and instance.revision <> 1: + activity = Activity(user=instance.author, active_at=instance.revised_at, content_object=instance, activity_type=TYPE_ACTIVITY_UPDATE_QUESTION) + activity.save() + +def record_revision_answer_event(instance, created, **kwargs): + if created and instance.revision <> 1: + activity = Activity(user=instance.author, active_at=instance.revised_at, content_object=instance, activity_type=TYPE_ACTIVITY_UPDATE_ANSWER) + activity.save() + +def record_award_event(instance, created, **kwargs): + """ + After we awarded a badge to user, we need to record this activity and notify user. + We also recaculate awarded_count of this badge and user information. + """ + if created: + activity = Activity(user=instance.user, active_at=instance.awarded_at, content_object=instance, + activity_type=TYPE_ACTIVITY_PRIZE) + activity.save() + + instance.badge.awarded_count += 1 + instance.badge.save() + + if instance.badge.type == Badge.GOLD: + instance.user.gold += 1 + if instance.badge.type == Badge.SILVER: + instance.user.silver += 1 + if instance.badge.type == Badge.BRONZE: + instance.user.bronze += 1 + instance.user.save() + +def notify_award_message(instance, created, **kwargs): + """ + Notify users when they have been awarded badges by using Django message. + """ + if created: + user = instance.user + user.message_set.create(message=u"Congratulations, you have received a badge '%s'" % instance.badge.name) + +def record_answer_accepted(instance, created, **kwargs): + """ + when answer is accepted, we record this for question author - who accepted it. + """ + if not created and instance.accepted: + activity = Activity(user=instance.question.author, active_at=datetime.datetime.now(), \ + content_object=instance, activity_type=TYPE_ACTIVITY_MARK_ANSWER) + activity.save() + +def update_last_seen(instance, created, **kwargs): + """ + when user has activities, we update 'last_seen' time stamp for him + """ + user = instance.user + user.last_seen = datetime.datetime.now() + user.save() + +def record_vote(instance, created, **kwargs): + """ + when user have voted + """ + if created: + if instance.vote == 1: + vote_type = TYPE_ACTIVITY_VOTE_UP + else: + vote_type = TYPE_ACTIVITY_VOTE_DOWN + + activity = Activity(user=instance.user, active_at=instance.voted_at, content_object=instance, activity_type=vote_type) + activity.save() + +def record_cancel_vote(instance, **kwargs): + """ + when user canceled vote, the vote will be deleted. + """ + activity = Activity(user=instance.user, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_CANCEL_VOTE) + activity.save() + +def record_delete_question(instance, delete_by, **kwargs): + """ + when user deleted the question + """ + if instance.__class__ == "Question": + activity_type = TYPE_ACTIVITY_DELETE_QUESTION + else: + activity_type = TYPE_ACTIVITY_DELETE_ANSWER + + activity = Activity(user=delete_by, active_at=datetime.datetime.now(), content_object=instance, activity_type=activity_type) + activity.save() + +def record_mark_offensive(instance, mark_by, **kwargs): + activity = Activity(user=mark_by, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_MARK_OFFENSIVE) + activity.save() + +def record_update_tags(question, **kwargs): + """ + when user updated tags of the question + """ + activity = Activity(user=question.author, active_at=datetime.datetime.now(), content_object=question, activity_type=TYPE_ACTIVITY_UPDATE_TAGS) + activity.save() + +def record_favorite_question(instance, created, **kwargs): + """ + when user add the question in him favorite questions list. + """ + if created: + activity = Activity(user=instance.user, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_FAVORITE) + activity.save() + +def record_user_full_updated(instance, **kwargs): + activity = Activity(user=instance, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_USER_FULL_UPDATED) + activity.save() + +def post_stored_anonymous_content(sender,user,session_key,signal,*args,**kwargs): + aq_list = AnonymousQuestion.objects.filter(session_key = session_key) + aa_list = AnonymousAnswer.objects.filter(session_key = session_key) + import settings + if settings.EMAIL_VALIDATION == 'on':#add user to the record + for aq in aq_list: + aq.author = user + aq.save() + for aa in aa_list: + aa.author = user + aa.save() + #maybe add pending posts message? + else: #just publish the questions + for aq in aq_list: + aq.publish(user) + for aa in aa_list: + aa.publish(user) + +#signal for User modle save changes +pre_save.connect(calculate_gravatar_hash, sender=User) +post_save.connect(record_ask_event, sender=Question) +post_save.connect(record_answer_event, sender=Answer) +post_save.connect(record_comment_event, sender=Comment) +post_save.connect(record_revision_question_event, sender=QuestionRevision) +post_save.connect(record_revision_answer_event, sender=AnswerRevision) +post_save.connect(record_award_event, sender=Award) +post_save.connect(notify_award_message, sender=Award) +post_save.connect(record_answer_accepted, sender=Answer) +post_save.connect(update_last_seen, sender=Activity) +post_save.connect(record_vote, sender=Vote) +post_delete.connect(record_cancel_vote, sender=Vote) +delete_post_or_answer.connect(record_delete_question, sender=Question) +delete_post_or_answer.connect(record_delete_question, sender=Answer) +mark_offensive.connect(record_mark_offensive, sender=Question) +mark_offensive.connect(record_mark_offensive, sender=Answer) +tags_updated.connect(record_update_tags, sender=Question) +post_save.connect(record_favorite_question, sender=FavoriteQuestion) +user_updated.connect(record_user_full_updated, sender=User) +user_logged_in.connect(post_stored_anonymous_content) diff --git a/forum/skins.py b/forum/skins/__init__.py index 2f0405d7..be6bd4f3 100644 --- a/forum/skins.py +++ b/forum/skins/__init__.py @@ -4,7 +4,6 @@ from django.template.loaders import filesystem from django.http import HttpResponse import os.path import logging -APP_NAME = os.path.basename(os.path.dirname(__file__)) #module for skinning osqa #at this point skin can be changed only in settings file @@ -36,7 +35,7 @@ def find_media_source(url): n = os.path.normpath j = os.path.join f = os.path.isfile - skins = n(j(d(__file__),'skins')) + skins = n(j(d(d(__file__)),'skins')) try: media = os.path.join(skins, settings.OSQA_DEFAULT_SKIN, url) assert(f(media)) diff --git a/forum/skins/default/media/js/com.cnprog.post.js b/forum/skins/default/media/js/com.cnprog.post.js index 5469a374..4325e665 100644 --- a/forum/skins/default/media/js/com.cnprog.post.js +++ b/forum/skins/default/media/js/com.cnprog.post.js @@ -153,17 +153,17 @@ var Vote = function(){ var setVoteImage = function(voteType, undo, object){ var flag = undo ? "" : "-on"; var arrow = (voteType == VoteType.questionUpVote || voteType == VoteType.answerUpVote) ? "up" : "down"; - object.attr("src", scriptUrl + "media/images/vote-arrow-"+ arrow + flag +".png"); + object.attr("src", mediaUrl("media/images/vote-arrow-"+ arrow + flag +".png")); // if undo voting, then undo the pair of arrows. if(undo){ if(voteType == VoteType.questionUpVote || voteType == VoteType.questionDownVote){ - $(getQuestionVoteUpButton()).attr("src", scriptUrl + "media/images/vote-arrow-up.png"); - $(getQuestionVoteDownButton()).attr("src", scriptUrl + "media/images/vote-arrow-down.png"); + $(getQuestionVoteUpButton()).attr("src", mediaUrl("media/images/vote-arrow-up.png")); + $(getQuestionVoteDownButton()).attr("src", mediaUrl("media/images/vote-arrow-down.png")); } else{ - $(getAnswerVoteUpButton(postId)).attr("src", scriptUrl + "media/images/vote-arrow-up.png"); - $(getAnswerVoteDownButton(postId)).attr("src", scriptUrl + "media/images/vote-arrow-down.png"); + $(getAnswerVoteUpButton(postId)).attr("src", mediaUrl("media/images/vote-arrow-up.png")); + $(getAnswerVoteDownButton(postId)).attr("src", mediaUrl("media/images/vote-arrow-down.png")); } } }; @@ -259,19 +259,19 @@ var Vote = function(){ showMessage(object, acceptOwnAnswerMessage); } else if(data.status == "1"){ - object.attr("src", scriptUrl + "media/images/vote-accepted.png"); + object.attr("src", mediaUrl("media/images/vote-accepted.png")); $("#"+answerContainerIdPrefix+postId).removeClass("accepted-answer"); $("#"+commentLinkIdPrefix+postId).removeClass("comment-link-accepted"); } else if(data.success == "1"){ var acceptedButtons = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixAccept +']'; - $(acceptedButtons).attr("src", scriptUrl + "media/images/vote-accepted.png"); + $(acceptedButtons).attr("src", mediaUrl("media/images/vote-accepted.png")); var answers = ("div[id^="+answerContainerIdPrefix +"]"); $(answers).removeClass("accepted-answer"); var commentLinks = ("div[id^="+answerContainerIdPrefix +"] div[id^="+ commentLinkIdPrefix +"]"); $(commentLinks).removeClass("comment-link-accepted"); - object.attr("src", scriptUrl + "media/images/vote-accepted-on.png"); + object.attr("src", mediaUrl("media/images/vote-accepted-on.png")); $("#"+answerContainerIdPrefix+postId).addClass("accepted-answer"); $("#"+commentLinkIdPrefix+postId).addClass("comment-link-accepted"); } @@ -285,7 +285,7 @@ var Vote = function(){ showMessage(object, favoriteAnonymousMessage.replace("{{QuestionID}}", questionId)); } else if(data.status == "1"){ - object.attr("src", scriptUrl + "media/images/vote-favorite-off.png"); + object.attr("src", mediaUrl("media/images/vote-favorite-off.png")); var fav = getFavoriteNumber(); fav.removeClass("my-favorite-number"); if(data.count === 0){ @@ -294,7 +294,7 @@ var Vote = function(){ fav.text(data.count); } else if(data.success == "1"){ - object.attr("src", scriptUrl + "media/images/vote-favorite-on.png"); + object.attr("src", mediaUrl("media/images/vote-favorite-on.png")); var fav = getFavoriteNumber(); fav.text(data.count); fav.addClass("my-favorite-number"); @@ -538,8 +538,8 @@ function createComments(type) { var renderDeleteCommentIcon = function(post_id, delete_url){ if (canPostComments(post_id)){ var html = ''; - var img = scriptUrl + "media/images/close-small.png"; - var imgHover = scriptUrl + "media/images/close-small-hover.png"; + var img = mediaUrl("media/images/close-small.png"); + var imgHover = mediaUrl("media/images/close-small-hover.png"); html += '<img class="delete-icon" onclick="' + objectType + 'Comments.deleteComment($(this), ' + post_id + ', \'' + delete_url + '\')" src="' + img; html += '" onmouseover="$(this).attr(\'src\', \'' + imgHover + '\')" onmouseout="$(this).attr(\'src\', \'' + img; html += '\')" title="' + $.i18n._('delete this comment') + '" />'; @@ -624,12 +624,12 @@ function createComments(type) { delete_icon.click(function(){CommentsClass.deleteComment($(this),comment_id,delete_url);}); delete_icon.unbind('mouseover').bind('mouseover', function(){ - $(this).attr('src',scriptUrl + 'media/images/close-small-hover.png'); + $(this).attr('src',mediaUrl('media/images/close-small-hover.png')); } ); delete_icon.unbind('mouseout').bind('mouseout', function(){ - $(this).attr('src',scriptUrl + 'media/images/close-small.png'); + $(this).attr('src',mediaUrl('media/images/close-small.png')); } ); } diff --git a/forum/skins/default/media/js/com.cnprog.tag_selector.js b/forum/skins/default/media/js/com.cnprog.tag_selector.js index 5cfc3561..e3279e65 100644 --- a/forum/skins/default/media/js/com.cnprog.tag_selector.js +++ b/forum/skins/default/media/js/com.cnprog.tag_selector.js @@ -45,10 +45,10 @@ function pickedTags(){ var setupTagDeleteEvents = function(obj,tag_store,tagname,reason,send_ajax){ obj.unbind('mouseover').bind('mouseover', function(){ - $(this).attr('src', scriptUrl + 'media/images/close-small-hover.png'); + $(this).attr('src', mediaUrl('media/images/close-small-hover.png')); }); obj.unbind('mouseout').bind('mouseout', function(){ - $(this).attr('src', scriptUrl + 'media/images/close-small-dark.png'); + $(this).attr('src', mediaUrl('media/images/close-small-dark.png')); }); obj.click( function(){ unpickTag(tag_store,tagname,reason,send_ajax); @@ -88,7 +88,7 @@ function pickedTags(){ tag_link.html(tagname); var del_link = $('<img></img>'); del_link.addClass('delete-icon'); - del_link.attr('src', scriptUrl + 'media/images/close-small-dark.png'); + del_link.attr('src', mediaUrl('/media/images/close-small-dark.png')); setupTagDeleteEvents(del_link, to_target, tagname, reason, true); diff --git a/forum/skins/default/media/js/com.cnprog.utils.js b/forum/skins/default/media/js/com.cnprog.utils.js index 7d8ea1f8..93083288 100644 --- a/forum/skins/default/media/js/com.cnprog.utils.js +++ b/forum/skins/default/media/js/com.cnprog.utils.js @@ -1,7 +1,11 @@ -//var $, scriptUrl; +//var $, scriptUrl, osqaSkin +var mediaUrl = function(resource){ + return scriptUrl + 'm/' + osqaSkin + '/' + resource; +}; + var showMessage = function(object, msg) { var div = $('<div class="vote-notification"><h3>' + msg + '</h3>(' + - $.i18n._('click to close') + ')</div>'); + $.i18n._('click to close') + ')</div>'); div.click(function(event) { $(".vote-notification").fadeOut("fast", function() { $(this).remove(); }); @@ -37,11 +41,11 @@ var notify = function() { function appendLoader(containerSelector) { $(containerSelector).append('<img class="ajax-loader" ' + - 'src="' + scriptUrl + 'media/images/indicator.gif" title="' + - $.i18n._('loading...') + - '" alt="' + - $.i18n._('loading...') + - '" />'); + 'src="mediaUrl("media/images/indicator.gif")" title="' + + $.i18n._('loading...') + + '" alt="' + + $.i18n._('loading...') + + '" />'); } function removeLoader() { diff --git a/forum/skins/default/media/js/wmd/wmd.js b/forum/skins/default/media/js/wmd/wmd.js index 6277de90..9888cda0 100644 --- a/forum/skins/default/media/js/wmd/wmd.js +++ b/forum/skins/default/media/js/wmd/wmd.js @@ -52,9 +52,9 @@ Attacklab.wmdBase = function(){ var imageDialogText = "<p style='margin-top: 0px'>" + $.i18n._('enter image url') + '</p>'; var linkDialogText = "<p style='margin-top: 0px'>" + $.i18n._('enter url') + '</p>'; var uploadImageHTML ="<div>" + $.i18n._('upload image') + "</div>" + - "<input type=\"file\" name=\"file-upload\" id=\"file-upload\" size=\"26\" "+ - "onchange=\"return ajaxFileUpload($('#image-url'));\"/><br>" + - "<img id=\"loading\" src=\"" + scriptUrl + "media/images/indicator.gif\" style=\"display:none;\"/>"; + '<input type="file" name="file-upload" id="file-upload" size="26" '+ + 'onchange="return ajaxFileUpload($("#image-url"));"/><br>' + + '<img id="loading" src="mediaUrl("media/images/indicator.gif")" style="display:none;"/>'; // The default text that appears in the dialog input box when entering // links. diff --git a/forum/skins/default/templates/base.html b/forum/skins/default/templates/base.html index 9fabe7a1..9033b0c9 100755 --- a/forum/skins/default/templates/base.html +++ b/forum/skins/default/templates/base.html @@ -17,30 +17,14 @@ <link href="{% media "/media/style/style.css" %}" rel="stylesheet" type="text/css" /> <script src="http://www.google.com/jsapi" type="text/javascript"></script> <script type="text/javascript">google.load("jquery", "1.2.6");</script> - <script type="text/javascript"> + <script type="text/javascript"> var i18nLang = '{{settings.LANGUAGE_CODE}}'; var scriptUrl = '/{{settings.FORUM_SCRIPT_ALIAS}}' - </script> + var osqaSkin = '{{settings.OSQA_SKIN}}'; + </script> <script type='text/javascript' src='{% media "/media/js/com.cnprog.i18n.js" %}'></script> - <script type='text/javascript' src='{% media "/media/js/jquery.i18n.js" %}'></script> + <script type='text/javascript' src='{% media "/media/js/jquery.i18n.js" %}'></script> <script type='text/javascript' src='{% media "/media/js/com.cnprog.utils.js" %}'></script> - <!--<script type="text/javascript"> - var uservoiceJsHost = ("https:" == document.location.protocol) ? "https://uservoice.com" : "http://cdn.uservoice.com"; - document.write(unescape("%3Cscript src='" + uservoiceJsHost + "/javascripts/widgets/tab.js' type='text/javascript'%3E%3C/script%3E")) - </script> - <script type="text/javascript"> - UserVoice.Tab.show({ - key: 'cnprog', - host: 'cnprog.uservoice.com', - forum: 'general', - alignment: 'left', /* 'left', 'right' */ - background_color:'#777', - text_color: 'white', /* 'white', 'black' */ - hover_color: '#06C', - lang: 'en' /* 'en', 'de', 'nl', 'es', 'fr' */ - }) - </script>--> - <!-- todo move this to settings --> {% if user_messages %} <style type="text/css"> body { margin-top:2.4em; } diff --git a/forum/skins/default/templates/base_content.html b/forum/skins/default/templates/base_content.html index 7f1ddf22..d1cf673b 100644 --- a/forum/skins/default/templates/base_content.html +++ b/forum/skins/default/templates/base_content.html @@ -11,38 +11,19 @@ {% endif %} <link rel="shortcut icon" href="{% media "/media/images/favicon.ico" %}" /> <link href="{% media "/media/style/style.css" %}" rel="stylesheet" type="text/css" /> - {% spaceless %} + {% spaceless %} {% block forestyle %}{% endblock %} {% endspaceless %} <script src="http://www.google.com/jsapi" type="text/javascript"></script> <script type="text/javascript">google.load("jquery", "1.2.6");</script> - <script type="text/javascript"> + <script type="text/javascript"> var i18nLang = '{{ settings.LANGUAGE_CODE }}'; var scriptUrl = '/{{settings.FORUM_SCRIPT_ALIAS}}' - </script> + var osqaSkin = '{{settings.OSQA_SKIN}}'; + </script> <script type='text/javascript' src='{% media "/media/js/com.cnprog.i18n.js" %}'></script> - <script type='text/javascript' src='{% media "/media/js/jquery.i18n.js" %}'></script> + <script type='text/javascript' src='{% media "/media/js/jquery.i18n.js" %}'></script> <script type='text/javascript' src='{% media "/media/js/com.cnprog.utils.js" %}'></script> - - <!-- <script type="text/javascript"> - var uservoiceJsHost = ("https:" == document.location.protocol) ? "https://uservoice.com" : "http://cdn.uservoice.com"; - document.write(unescape("%3Cscript src='" + uservoiceJsHost + "/javascripts/widgets/tab.js' type='text/javascript'%3E%3C/script%3E")) - </script> - <script type="text/javascript"> - UserVoice.Tab.show({ - //EDIT!!! - key: 'uservoicekey', - host: 'where.uservoice.com', - forum: 'general', - alignment: 'left', /* 'left', 'right' */ - background_color:'#777', - text_color: 'white', /* 'white', 'black' */ - hover_color: '#06C', - lang: 'en' /* 'en', 'de', 'nl', 'es', 'fr' */ - }) - </script>--> - <!-- todo move this to settings--> - {% if user_messages %} <style type="text/css"> body { margin-top:2.4em; } diff --git a/forum/skins/default/templates/question.html b/forum/skins/default/templates/question.html index 048c2d64..34f8fd1d 100644 --- a/forum/skins/default/templates/question.html +++ b/forum/skins/default/templates/question.html @@ -70,55 +70,53 @@ {% if question_vote %}
<img id="question-img-upvote-{{ question.id }}" class="question-img-upvote"
{% if question_vote.is_upvote %}
- src="{% media "/media/images/vote-arrow-up-on.png" %}"
+ src="{% media "/media/images/vote-arrow-up-on.png" %}"
{% else %}
- src="{% media "/media/images/vote-arrow-up.png" %}"
+ src="{% media "/media/images/vote-arrow-up.png" %}"
{% endif %}
alt="{% trans "i like this post (click again to cancel)" %}"
- title="{% trans "i like this post (click again to cancel)" %}" />
+ title="{% trans "i like this post (click again to cancel)" %}" />
<div id="question-vote-number-{{ question.id }}" class="vote-number"
- title="{% trans "current number of votes" %}">
+ title="{% trans "current number of votes" %}">
{{ question.score }}
</div>
<img id="question-img-downvote-{{ question.id }}" class="question-img-downvote"
{% if question_vote.is_downvote %}
- src="{% media "/media/images/vote-arrow-down-on.png" %}"
+ src="{% media "/media/images/vote-arrow-down-on.png" %}"
{% else %}
- src="{% media "/media/images/vote-arrow-down.png" %}"
+ src="{% media "/media/images/vote-arrow-down.png" %}"
{% endif %}
alt="{% trans "i dont like this post (click again to cancel)" %}"
- title="{% trans "i dont like this post (click again to cancel)" %}" />
+ title="{% trans "i dont like this post (click again to cancel)" %}" />
{% else %}
<img id="question-img-upvote-{{ question.id }}" class="question-img-upvote"
alt="{% trans "i like this post (click again to cancel)" %}"
- src="{% media "/media/images/vote-arrow-up.png" %}"
- title="{% trans "i like this post (click again to cancel)" %}" />
+ src="{% media "/media/images/vote-arrow-up.png" %}"
+ title="{% trans "i like this post (click again to cancel)" %}" />
<div id="question-vote-number-{{ question.id }}" class="vote-number"
- title="{% trans "current number of votes" %}">
+ title="{% trans "current number of votes" %}">
{{ question.score }}
</div>
<img id="question-img-downvote-{{ question.id }}" class="question-img-downvote"
- src="{% media "/media/images/vote-arrow-down.png" %}"
+ src="{% media "/media/images/vote-arrow-down.png" %}"
alt="{% trans "i dont like this post (click again to cancel)" %}"
title="{% trans "i dont like this post (click again to cancel)" %}" />
-
{% endif %}
{% if favorited %}
<img class="question-img-favorite" src="{% media "/media/images/vote-favorite-on.png" %}"
- alt="{% trans "mark this question as favorite (click again to cancel)" %}"
- title="{% trans "mark this question as favorite (click again to cancel)" %}" />
+ alt="{% trans "mark this question as favorite (click again to cancel)" %}"
+ title="{% trans "mark this question as favorite (click again to cancel)" %}" />
<div id="favorite-number" class="favorite-number my-favorite-number">
{{ question.favourite_count }}
</div>
{% else %}
<img class="question-img-favorite" src="{% media "/media/images/vote-favorite-off.png" %}"
- alt="{% trans "remove favorite mark from this question (click again to restore mark)" %}"
- title="{% trans "remove favorite mark from this question (click again to restore mark)" %}" />
+ alt="{% trans "remove favorite mark from this question (click again to restore mark)" %}"
+ title="{% trans "remove favorite mark from this question (click again to restore mark)" %}" />
<div id="favorite-number" class="favorite-number">
{% ifnotequal question.favourite_count 0 %}{{ question.favourite_count }}{% endifnotequal %}
</div>
-
{% endif %}
</div>
@@ -217,8 +215,8 @@ {% if question.closed %}
<div class="question-status" style="margin-bottom:15px">
<h3>{% blocktrans with question.get_close_reason_display as close_reason %}The question has been closed for the following reason "{{ close_reason }}" by{% endblocktrans %}
- <a href="{{ question.closed_by.get_profile_url }}">{{ question.closed_by.username }}</a>
- {% blocktrans with question.closed_at as closed_at %}close date {{closed_at}}{% endblocktrans %}</h3>
+ <a href="{{ question.closed_by.get_profile_url }}">{{ question.closed_by.username }}</a>
+ {% blocktrans with question.closed_at as closed_at %}close date {{closed_at}}{% endblocktrans %}</h3>
</div>
{% endif %}
{% if answers %}
@@ -258,21 +256,21 @@ {{ answer.score }}
</div>
<img id="answer-img-downvote-{{ answer.id }}" class="answer-img-downvote"
- src="{% blockmedia %}/media/images/vote-arrow-down{% get_user_vote_image user_answer_votes answer.id -1 %}.png{% endblockmedia %}"
- alt="{% trans "i dont like this answer (click again to cancel)" %}"
- title="{% trans "i dont like this answer (click again to cancel)" %}" />
+ src="{% blockmedia %}/media/images/vote-arrow-down{% get_user_vote_image user_answer_votes answer.id -1 %}.png{% endblockmedia %}"
+ alt="{% trans "i dont like this answer (click again to cancel)" %}"
+ title="{% trans "i dont like this answer (click again to cancel)" %}" />
{% ifequal request.user question.author %}
<img id="answer-img-accept-{{ answer.id }}" class="answer-img-accept"
- src="{% blockmedia %}/media/images/vote-accepted{% if answer.accepted %}-on{% endif %}.png{% endblockmedia %}"
- alt="{% trans "mark this answer as favorite (click again to undo)" %}"
- title="{% trans "mark this answer as favorite (click again to undo)" %}" />
+ src="{% blockmedia %}/media/images/vote-accepted{% if answer.accepted %}-on{% endif %}.png{% endblockmedia %}"
+ alt="{% trans "mark this answer as favorite (click again to undo)" %}"
+ title="{% trans "mark this answer as favorite (click again to undo)" %}" />
{% else %}
{% if answer.accepted %}
<img id="answer-img-accept-{{ answer.id }}" class="answer-img-accept"
- src="{% blockmedia %}/media/images/vote-accepted{% if answer.accepted %}-on{% endif %}.png{% endblockmedia %}"
- alt="{% trans "the author of the question has selected this answer as correct" %}"
- title="{% trans "the author of the question has selected this answer as correct" %}" />
+ src="{% blockmedia %}/media/images/vote-accepted{% if answer.accepted %}-on{% endif %}.png{% endblockmedia %}"
+ alt="{% trans "the author of the question has selected this answer as correct" %}"
+ title="{% trans "the author of the question has selected this answer as correct" %}" />
{% endif %}
{% endifequal %}
</div>
diff --git a/forum/templatetags/extra_tags.py b/forum/templatetags/extra_tags.py index 3084c8c2..26c52b8d 100644 --- a/forum/templatetags/extra_tags.py +++ b/forum/templatetags/extra_tags.py @@ -330,7 +330,7 @@ class BlockMediaUrlNode(template.Node): def __init__(self,nodelist): self.items = nodelist def render(self,context): - prefix = '///' + settings.FORUM_SCRIPT_ALIAS + '/m/' + prefix = '///' + settings.FORUM_SCRIPT_ALIAS + 'm/' url = '' if self.items: url += '/' @@ -338,7 +338,7 @@ class BlockMediaUrlNode(template.Node): url += item.render(context) url = skins.find_media_source(url) - url += prefix + url + url = prefix + url out = posixpath.normpath(url) + '?v=%d' % settings.RESOURCE_REVISION return out.replace(' ','') diff --git a/forum/user.py b/forum/user.py deleted file mode 100644 index 40bf6a89..00000000 --- a/forum/user.py +++ /dev/null @@ -1,74 +0,0 @@ -from django.utils.translation import ugettext as _ -class UserView: - def __init__(self, id, tab_title, tab_description, page_title, view_name, template_file, data_size=0): - self.id = id - self.tab_title = tab_title - self.tab_description = tab_description - self.page_title = page_title - self.view_name = view_name - self.template_file = template_file - self.data_size = data_size - - -USER_TEMPLATE_VIEWS = ( - UserView( - id = 'stats', - tab_title = _('overview'), - tab_description = _('user profile'), - page_title = _('user profile overview'), - view_name = 'user_stats', - template_file = 'user_stats.html' - ), - UserView( - id = 'recent', - tab_title = _('recent activity'), - tab_description = _('recent user activity'), - page_title = _('profile - recent activity'), - view_name = 'user_recent', - template_file = 'user_recent.html', - data_size = 50 - ), - UserView( - id = 'responses', - tab_title = _('responses'), - tab_description = _('comments and answers to others questions'), - page_title = _('profile - responses'), - view_name = 'user_responses', - template_file = 'user_responses.html', - data_size = 50 - ), - UserView( - id = 'reputation', - tab_title = _('reputation'), - tab_description = _('user reputation in the community'), - page_title = _('profile - user reputation'), - view_name = 'user_reputation', - template_file = 'user_reputation.html' - ), - UserView( - id = 'favorites', - tab_title = _('favorite questions'), - tab_description = _('users favorite questions'), - page_title = _('profile - favorite questions'), - view_name = 'user_favorites', - template_file = 'user_favorites.html', - data_size = 50 - ), - UserView( - id = 'votes', - tab_title = _('casted votes'), - tab_description = _('user vote record'), - page_title = _('profile - votes'), - view_name = 'user_votes', - template_file = 'user_votes.html', - data_size = 50 - ), - UserView( - 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/__init__.py b/forum/views/__init__.py new file mode 100644 index 00000000..0bd91fe6 --- /dev/null +++ b/forum/views/__init__.py @@ -0,0 +1,4 @@ +from forum.views.content import * +from forum.views.users import * +from forum.views.meta import * +from forum.views.books import * diff --git a/forum/views/books.py b/forum/views/books.py new file mode 100644 index 00000000..bb1bfe85 --- /dev/null +++ b/forum/views/books.py @@ -0,0 +1,139 @@ +from django.contrib.auth.decorators import login_required +from django.core.urlresolvers import reverse +from django.shortcuts import render_to_response +from forum.models import BookAuthorInfo, BookAuthorRss, Book +from forum.models import Question, QuestionRevision +from django.http import HttpResponseRedirect + +def books(request): + """this view seems to redirect to a default book + maybe it should instead show some popular titles? + """ + return HttpResponseRedirect(reverse('books') + 'mysql-zhaoyang') + +def book(request, short_name, unanswered=False): + """ + 1. questions list + 2. book info + 3. author info and blog rss items + """ + """ + List of Questions, Tagged questions, and Unanswered questions. + """ + books = Book.objects.extra(where=['short_name = %s'], params=[short_name]) + match_count = len(books) + if match_count == 0: + raise Http404 + else: + # the book info + book = books[0] + # get author info + author_info = BookAuthorInfo.objects.get(book=book) + # get author rss info + author_rss = BookAuthorRss.objects.filter(book=book) + + # get pagesize from session, if failed then get default value + user_page_size = request.session.get("pagesize", QUESTIONS_PAGE_SIZE) + # set pagesize equal to logon user specified value in database + if request.user.is_authenticated() and request.user.questions_per_page > 0: + user_page_size = request.user.questions_per_page + + try: + page = int(request.GET.get('page', '1')) + 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" + + # check if request is from tagged questions + if unanswered: + # check if request is from unanswered questions + # Article.objects.filter(publications__id__exact=1) + objects = Question.objects.filter(book__id__exact=book.id, deleted=False, answer_count=0).order_by(orderby) + else: + objects = Question.objects.filter(book__id__exact=book.id, deleted=False).order_by(orderby) + + # RISK - inner join queries + objects = objects.select_related(); + objects_list = Paginator(objects, user_page_size) + questions = objects_list.page(page) + + return render_to_response('book.html', { + "book" : book, + "author_info" : author_info, + "author_rss" : author_rss, + "questions" : questions, + "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' : user_page_size + } + }, context_instance=RequestContext(request)) + +@login_required +def ask_book(request, short_name): + if request.method == "POST": + form = AskForm(request.POST) + if form.is_valid(): + added_at = datetime.datetime.now() + html = sanitize_html(markdowner.convert(form.cleaned_data['text'])) + question = Question( + title = strip_tags(form.cleaned_data['title']), + author = request.user, + added_at = added_at, + last_activity_at = added_at, + last_activity_by = request.user, + wiki = form.cleaned_data['wiki'], + tagnames = form.cleaned_data['tags'].strip(), + html = html, + summary = strip_tags(html)[:120] + ) + if question.wiki: + question.last_edited_by = question.author + question.last_edited_at = added_at + question.wikified_at = added_at + + question.save() + + # create the first revision + QuestionRevision.objects.create( + question = question, + revision = 1, + title = question.title, + author = request.user, + revised_at = added_at, + tagnames = question.tagnames, + summary = CONST['default_version'], + text = form.cleaned_data['text'] + ) + + books = Book.objects.extra(where=['short_name = %s'], params=[short_name]) + match_count = len(books) + if match_count == 1: + # the book info + book = books[0] + book.questions.add(question) + + return HttpResponseRedirect(question.get_absolute_url()) + else: + form = AskForm() + + tags = _get_tags_cache_json() + return render_to_response('ask.html', { + 'form' : form, + 'tags' : tags, + 'email_validation_faq_url': reverse('faq') + '#validate', + }, context_instance=RequestContext(request)) + diff --git a/forum/views/content.py b/forum/views/content.py new file mode 100644 index 00000000..2833edfc --- /dev/null +++ b/forum/views/content.py @@ -0,0 +1,1393 @@ +# encoding:utf-8 +import os.path +import time, datetime, calendar, random +import logging +from urllib import quote, unquote +from django.conf import settings +from django.core.files.storage import default_storage +from django.shortcuts import render_to_response, get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.http import HttpResponseRedirect, HttpResponse, 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.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 +#from lxml.html.diff import htmldiff +from forum.diff import textDiff as htmldiff +from forum.forms import * +from forum.models import * +from forum.auth import * +from forum.const import * +from forum import auth +from utils.forms import get_next_url + +# used in index page +INDEX_PAGE_SIZE = 20 +INDEX_AWARD_SIZE = 15 +INDEX_TAGS_SIZE = 100 +# used in tags list +DEFAULT_PAGE_SIZE = 60 +# used in questions +QUESTIONS_PAGE_SIZE = 10 +# used in answers +ANSWERS_PAGE_SIZE = 10 + +markdowner = Markdown(html4tags=True) + +#system to display main content +def _get_tags_cache_json():#service routine + """returns list of all tags in json format + no caching yet, actually + """ + tags = Tag.objects.filter(deleted=False).all() + tags_list = [] + for tag in tags: + dic = {'n': tag.name, 'c': tag.used_count} + tags_list.append(dic) + tags = simplejson.dumps(tags_list) + return tags + +def _get_and_remember_questions_sort_method(request, view_dic, default):#service routine + """manages persistence of post sort order + it is assumed that when user wants newest question - + then he/she wants newest answers as well, etc. + how far should this assumption actually go - may be a good question + """ + 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] + +#refactor? - we have these +#views that generate a listing of questions in one way or another: +#index, unanswered, questions, search, tag +#should we dry them up? +#related topics - information drill-down, search refinement + +def index(request):#generates front page - shows listing of questions sorted in various ways + """index view mapped to the root url of the Q&A site + """ + view_dic = { + "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', { + '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 unanswered(request):#generates listing of unanswered questions + return questions(request, unanswered=True) + +def questions(request, tagname=None, unanswered=False):#a view generating listing of questions, used by 'unanswered' too + """ + List of Questions, Tagged questions, and Unanswered questions. + """ + # template file + # "questions.html" or maybe index.html in the future + template_file = "questions.html" + # Set flag to False by default. If it is equal to True, then need to be saved. + pagesize_changed = False + # get pagesize from session, if failed then get default value + pagesize = request.session.get("pagesize",10) + try: + page = int(request.GET.get('page', '1')) + except ValueError: + page = 1 + + 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: + qs = qs.filter(tags__name = unquote(tagname)) + + 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 + if questions.object_list.count() > 0: + related_tags = Tag.objects.get_tags_by_questions(questions.object_list) + else: + related_tags = None + 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) + + 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 search(request): #generates listing of questions matching a search query - including tags and just words + """generates listing of questions matching a search query + supports full text search in mysql db using sphinx and internally in postgresql + falls back on simple partial string matching approach if + full text search function is not available + """ + if request.method == "GET": + keywords = request.GET.get("q") + search_type = request.GET.get("t") + try: + page = int(request.GET.get('page', '1')) + except ValueError: + page = 1 + if keywords is None: + return HttpResponseRedirect(reverse(index)) + if search_type == 'tag': + return HttpResponseRedirect(reverse('tags') + '?q=%s&page=%s' % (keywords.strip(), page)) + elif search_type == "user": + return HttpResponseRedirect(reverse('users') + '?q=%s&page=%s' % (keywords.strip(), page)) + elif search_type == "question": + + template_file = "questions.html" + # Set flag to False by default. If it is equal to True, then need to be saved. + pagesize_changed = False + # get pagesize from session, if failed then get default value + user_page_size = request.session.get("pagesize", QUESTIONS_PAGE_SIZE) + # set pagesize equal to logon user specified value in database + if request.user.is_authenticated() and request.user.questions_per_page > 0: + user_page_size = request.user.questions_per_page + + try: + page = int(request.GET.get('page', '1')) + # get new pagesize from UI selection + pagesize = int(request.GET.get('pagesize', user_page_size)) + if pagesize <> user_page_size: + pagesize_changed = True + + except ValueError: + page = 1 + pagesize = user_page_size + + # save this pagesize to user database + if pagesize_changed: + request.session["pagesize"] = pagesize + if request.user.is_authenticated(): + user = request.user + user.questions_per_page = pagesize + user.save() + + 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" + + if settings.USE_PG_FTS: + objects = Question.objects.filter(deleted=False).extra( + select={ + 'ranking': "ts_rank_cd(tsv, plainto_tsquery(%s), 32)", + }, + where=["tsv @@ plainto_tsquery(%s)"], + params=[keywords], + select_params=[keywords] + ).order_by('-ranking') + + elif 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(); + + objects_list = Paginator(objects, pagesize) + questions = objects_list.page(page) + + # Get related tags from this page objects + related_tags = [] + for question in questions.object_list: + tags = list(question.tags.all()) + for tag in tags: + if tag not in related_tags: + related_tags.append(tag) + + #if is_search is true in the context, prepend this string to soting tabs urls + search_uri = "?q=%s&page=%d&t=question" % ("+".join(keywords.split()), page) + + return render_to_response(template_file, { + "questions" : questions, + "tab_id" : view_id, + "questions_count" : objects_list.count, + "tags" : related_tags, + "searchtag" : None, + "searchtitle" : keywords, + "keywords" : keywords, + "is_unanswered" : False, + "is_search": True, + "search_uri": search_uri, + "context" : { + 'is_paginated' : True, + 'pages': objects_list.num_pages, + 'page': page, + 'has_previous': questions.has_previous(), + 'has_next': questions.has_next(), + 'previous': questions.previous_page_number(), + 'next': questions.next_page_number(), + 'base_url' : request.path + '?t=question&q=%s&sort=%s&' % (keywords, view_id), + 'pagesize' : pagesize + }}, context_instance=RequestContext(request)) + + else: + raise Http404 + +def tag(request, tag):#generates listing of questions tagged with a single tag + return questions(request, tagname=tag) + +def tags(request):#view showing a listing of available tags - plain list + stag = "" + is_paginated = True + sortby = request.GET.get('sort', 'used') + try: + page = int(request.GET.get('page', '1')) + except ValueError: + page = 1 + + if request.method == "GET": + stag = request.GET.get("q", "").strip() + if stag != '': + objects_list = Paginator(Tag.objects.filter(deleted=False).exclude(used_count=0).extra(where=['name like %s'], params=['%' + stag + '%']), DEFAULT_PAGE_SIZE) + else: + if sortby == "name": + objects_list = Paginator(Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("name"), DEFAULT_PAGE_SIZE) + else: + objects_list = Paginator(Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("-used_count"), DEFAULT_PAGE_SIZE) + + try: + tags = objects_list.page(page) + except (EmptyPage, InvalidPage): + 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' : reverse('tags') + '?sort=%s&' % sortby + } + }, context_instance=RequestContext(request)) + +def question(request, id):#refactor - long subroutine. display question body, answers and comments + """view that displays body of the question and + all answers to it + """ + try: + page = int(request.GET.get('page', '1')) + except ValueError: + page = 1 + + 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: + 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) + try: + pattern = r'/%s%s%d/([\w-]+)' % (settings.FORUM_SCRIPT_ALIAS,_('question/'), question.id) + path_re = re.compile(pattern) + logging.debug(pattern) + logging.debug(request.path) + m = path_re.match(request.path) + if m: + slug = m.group(1) + logging.debug('have slug %s' % slug) + assert(slug == slugify(question.title)) + else: + logging.debug('no match!') + except: + return HttpResponseRedirect(question.get_absolute_url()) + + if question.deleted and not auth.can_view_deleted_post(request.user, question): + raise Http404 + answer_form = AnswerForm(question,request.user) + answers = Answer.objects.get_answers_from_question(question, request.user) + answers = answers.select_related(depth=1) + + favorited = question.has_favorite_by_user(request.user) + if request.user.is_authenticated(): + question_vote = question.votes.select_related().filter(user=request.user) + else: + question_vote = None #is this correct? + if question_vote is not None and question_vote.count() > 0: + question_vote = question_vote[0] + + user_answer_votes = {} + for answer in answers: + vote = answer.get_user_vote(request.user) + if vote is not None and not user_answer_votes.has_key(answer.id): + vote_value = -1 + if vote.is_upvote(): + vote_value = 1 + 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: + if answer.author_id == request.user.id: + filtered_answers.append(answer) + else: + filtered_answers.append(answer) + + objects_list = Paginator(filtered_answers, ANSWERS_PAGE_SIZE) + page_objects = objects_list.page(page) + + #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, + "question_comment_count":question.comments.count(), + "answer" : answer_form, + "answers" : page_objects.object_list, + "user_answer_votes": user_answer_votes, + "tags" : question.tags.all(), + "tab_id" : view_id, + "favorited" : favorited, + "similar_questions" : Question.objects.get_similar_questions(question), + "context" : { + 'is_paginated' : True, + 'pages': objects_list.num_pages, + 'page': page, + 'has_previous': page_objects.has_previous(), + 'has_next': page_objects.has_next(), + 'previous': page_objects.previous_page_number(), + 'next': page_objects.next_page_number(), + 'base_url' : request.path + '?sort=%s&' % view_id, + 'extend_url' : "#sort-top" + } + }, context_instance=RequestContext(request)) + +QUESTION_REVISION_TEMPLATE = ('<h1>%(title)s</h1>\n' + '<div class="text">%(html)s</div>\n' + '<div class="tags">%(tags)s</div>') +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, + 'html': sanitize_html(markdowner.convert(revision.text)), + 'tags': ' '.join(['<a class="post-tag">%s</a>' % tag + for tag in revision.tagnames.split(' ')]), + } + if i > 0: + revisions[i].diff = htmldiff(revisions[i-1].html, revision.html) + else: + 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].summary = _('initial version') + return render_to_response('revisions_question.html', { + 'post': post, + 'revisions': revisions, + }, context_instance=RequestContext(request)) + +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].diff = htmldiff(revisions[i-1].html, revision.html) + else: + revisions[i].diff = revisions[i].text + revisions[i].summary = _('initial version') + return render_to_response('revisions_answer.html', { + 'post': post, + 'revisions': revisions, + }, context_instance=RequestContext(request)) +#system to collect user actions and change content and store in the database +def create_new_answer( question=None, author=None, #service subroutine - refactor + added_at=None, wiki=False,\ + text='', email_notify=False): + """refactor + non-view subroutine + initializes the answer and revision + and updates stuff in the corresponding question + probably there is more Django-ish way to do it + """ + + html = sanitize_html(markdowner.convert(text)) + + #create answer + answer = Answer( + question = question, + author = author, + added_at = added_at, + wiki = wiki, + html = html + ) + if answer.wiki: + answer.last_edited_by = answer.author + answer.last_edited_at = added_at + answer.wikified_at = added_at + + answer.save() + + #update question data + question.last_activity_at = added_at + question.last_activity_by = author + question.save() + Question.objects.update_answer_count(question) + + #update revision + AnswerRevision.objects.create( + answer = answer, + revision = 1, + author = author, + revised_at = added_at, + summary = CONST['default_version'], + text = text + ) + + #set notification/delete + if email_notify: + 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: + question.followed_by.remove(author) + except: + pass + +def create_new_question(title=None,author=None,added_at=None, #service subroutine - refactor + wiki=False,tagnames=None,summary=None, + text=None): + """refactor + this is not a view saves new question and revision + and maybe should become one of the methods on Question object? + """ + html = sanitize_html(markdowner.convert(text)) + question = Question( + title = title, + author = author, + added_at = added_at, + last_activity_at = added_at, + last_activity_by = author, + wiki = wiki, + tagnames = tagnames, + html = html, + summary = summary + ) + if question.wiki: + question.last_edited_by = question.author + question.last_edited_at = added_at + question.wikified_at = added_at + + question.save() + + # create the first revision + QuestionRevision.objects.create( + question = question, + revision = 1, + title = question.title, + author = author, + revised_at = added_at, + tagnames = question.tagnames, + summary = CONST['default_version'], + text = text + ) + return question + +def upload(request):#ajax upload file to a question or answer + class FileTypeNotAllow(Exception): + pass + class FileSizeNotAllow(Exception): + pass + class UploadPermissionNotAuthorized(Exception): + pass + + #<result><msg><![CDATA[%s]]></msg><error><![CDATA[%s]]></error><file_url>%s</file_url></result> + xml_template = "<result><msg><![CDATA[%s]]></msg><error><![CDATA[%s]]></error><file_url>%s</file_url></result>" + + try: + f = request.FILES['file-upload'] + # check upload permission + if not auth.can_upload_files(request.user): + raise UploadPermissionNotAuthorized + + # check file type + file_name_suffix = os.path.splitext(f.name)[1].lower() + if not file_name_suffix in settings.ALLOW_FILE_TYPES: + raise FileTypeNotAllow + + # 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 + # byte + size = default_storage.size(new_file_name) + if size > settings.ALLOW_MAX_FILE_SIZE: + default_storage.delete(new_file_name) + raise FileSizeNotAllow + + result = xml_template % ('Good', '', default_storage.url(new_file_name)) + except UploadPermissionNotAuthorized: + result = xml_template % ('', _('uploading images is limited to users with >60 reputation points'), '') + except FileTypeNotAllow: + result = xml_template % ('', _("allowed file types are 'jpg', 'jpeg', 'gif', 'bmp', 'png', 'tiff'"), '') + except FileSizeNotAllow: + result = xml_template % ('', _("maximum upload file size is %sK") % settings.ALLOW_MAX_FILE_SIZE / 1024, '') + except Exception: + result = xml_template % ('', _('Error uploading file. Please contact the site administrator. Thank you. %s' % Exception), '') + + return HttpResponse(result, mimetype="application/xml") + +#@login_required #actually you can post anonymously, but then must register +def ask(request):#view used to ask a new question + """a view to ask a new question + gives space for q title, body, tags and checkbox for to post as wiki + + user can start posting a question anonymously but then + must login/register in order for the question go be shown + """ + if request.method == "POST": + form = AskForm(request.POST) + if form.is_valid(): + + added_at = datetime.datetime.now() + title = strip_tags(form.cleaned_data['title'].strip()) + wiki = form.cleaned_data['wiki'] + tagnames = form.cleaned_data['tags'].strip() + text = form.cleaned_data['text'] + html = sanitize_html(markdowner.convert(text)) + summary = strip_tags(html)[:120] + + if request.user.is_authenticated(): + author = request.user + + question = create_new_question( + title = title, + author = author, + added_at = added_at, + wiki = wiki, + tagnames = tagnames, + summary = summary, + text = text + ) + + return HttpResponseRedirect(question.get_absolute_url()) + else: + request.session.flush() + session_key = request.session.session_key + question = AnonymousQuestion( + session_key = session_key, + title = title, + tagnames = tagnames, + wiki = wiki, + text = text, + summary = summary, + added_at = added_at, + ip_addr = request.META['REMOTE_ADDR'], + ) + question.save() + 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, + 'email_validation_faq_url':reverse('faq') + '#validate', + }, context_instance=RequestContext(request)) + +@login_required +def close(request, id):#close question + """view to initiate and process + question close + """ + question = get_object_or_404(Question, id=id) + if not auth.can_close_question(request.user, question): + return HttpResponse('Permission denied.') + if request.method == 'POST': + form = CloseForm(request.POST) + if form.is_valid(): + reason = form.cleaned_data['reason'] + question.closed = True + question.closed_by = request.user + question.closed_at = datetime.datetime.now() + question.close_reason = reason + question.save() + return HttpResponseRedirect(question.get_absolute_url()) + else: + form = CloseForm() + return render_to_response('close.html', { + 'form' : form, + 'question' : question, + }, context_instance=RequestContext(request)) + +@login_required +def reopen(request, id):#re-open question + """view to initiate and process + question close + """ + question = get_object_or_404(Question, id=id) + # open question + if not auth.can_reopen_question(request.user, question): + return HttpResponse('Permission denied.') + if request.method == 'POST' : + Question.objects.filter(id=question.id).update(closed=False, + closed_by=None, closed_at=None, close_reason=None) + return HttpResponseRedirect(question.get_absolute_url()) + else: + return render_to_response('reopen.html', { + 'question' : question, + }, context_instance=RequestContext(request)) + +@login_required +def edit_question(request, id):#edit or retag a question + """view to edit question + """ + question = get_object_or_404(Question, id=id) + if question.deleted and not auth.can_view_deleted_post(request.user, question): + raise Http404 + if auth.can_edit_post(request.user, question): + return _edit_question(request, question) + elif auth.can_retag_questions(request.user): + return _retag_question(request, question) + else: + raise Http404 + +def _retag_question(request, question):#non-url subview of edit question - just retag + """retag question sub-view used by + view "edit_question" + """ + if request.method == 'POST': + form = RetagQuestionForm(question, request.POST) + if form.is_valid(): + if form.has_changed(): + latest_revision = question.get_latest_revision() + retagged_at = datetime.datetime.now() + # Update the Question itself + Question.objects.filter(id=question.id).update( + tagnames = form.cleaned_data['tags'], + last_edited_at = retagged_at, + last_edited_by = request.user, + last_activity_at = retagged_at, + last_activity_by = request.user + ) + # Update the Question's tag associations + tags_updated = Question.objects.update_tags(question, + form.cleaned_data['tags'], request.user) + # Create a new revision + QuestionRevision.objects.create( + question = question, + title = latest_revision.title, + author = request.user, + revised_at = retagged_at, + tagnames = form.cleaned_data['tags'], + summary = CONST['retagged'], + text = latest_revision.text + ) + # send tags updated singal + tags_updated.send(sender=question.__class__, question=question) + + return HttpResponseRedirect(question.get_absolute_url()) + else: + form = RetagQuestionForm(question) + return render_to_response('question_retag.html', { + 'question': question, + 'form' : form, + 'tags' : _get_tags_cache_json(), + }, context_instance=RequestContext(request)) + +def _edit_question(request, question):#non-url subview of edit_question - just edit the body/title + latest_revision = question.get_latest_revision() + revision_form = None + if request.method == 'POST': + if 'select_revision' in request.POST: + # user has changed revistion number + revision_form = RevisionForm(question, latest_revision, request.POST) + if revision_form.is_valid(): + # Replace with those from the selected revision + form = EditQuestionForm(question, + QuestionRevision.objects.get(question=question, + revision=revision_form.cleaned_data['revision'])) + else: + form = EditQuestionForm(question, latest_revision, request.POST) + else: + # Always check modifications against the latest revision + form = EditQuestionForm(question, latest_revision, request.POST) + if form.is_valid(): + html = sanitize_html(markdowner.convert(form.cleaned_data['text'])) + if form.has_changed(): + edited_at = datetime.datetime.now() + tags_changed = (latest_revision.tagnames != + form.cleaned_data['tags']) + tags_updated = False + # Update the Question itself + updated_fields = { + 'title': form.cleaned_data['title'], + 'last_edited_at': edited_at, + 'last_edited_by': request.user, + 'last_activity_at': edited_at, + 'last_activity_by': request.user, + 'tagnames': form.cleaned_data['tags'], + 'summary': strip_tags(html)[:120], + 'html': html, + } + + # only save when it's checked + # because wiki doesn't allow to be edited if last version has been enabled already + # and we make sure this in forms. + if ('wiki' in form.cleaned_data and + form.cleaned_data['wiki']): + updated_fields['wiki'] = True + updated_fields['wikified_at'] = edited_at + + Question.objects.filter( + id=question.id).update(**updated_fields) + # Update the Question's tag associations + if tags_changed: + tags_updated = Question.objects.update_tags( + question, form.cleaned_data['tags'], request.user) + # Create a new revision + revision = QuestionRevision( + question = question, + title = form.cleaned_data['title'], + author = request.user, + revised_at = edited_at, + tagnames = form.cleaned_data['tags'], + text = form.cleaned_data['text'], + ) + if form.cleaned_data['summary']: + revision.summary = form.cleaned_data['summary'] + else: + revision.summary = 'No.%s Revision' % latest_revision.revision + revision.save() + + return HttpResponseRedirect(question.get_absolute_url()) + else: + + revision_form = RevisionForm(question, latest_revision) + form = EditQuestionForm(question, latest_revision) + return render_to_response('question_edit.html', { + 'question': question, + 'revision_form': revision_form, + 'form' : form, + 'tags' : _get_tags_cache_json() + }, context_instance=RequestContext(request)) + +@login_required +def edit_answer(request, id): + answer = get_object_or_404(Answer, id=id) + if answer.deleted and not auth.can_view_deleted_post(request.user, answer): + raise Http404 + elif not auth.can_edit_post(request.user, answer): + raise Http404 + else: + latest_revision = answer.get_latest_revision() + if request.method == "POST": + if 'select_revision' in request.POST: + # user has changed revistion number + revision_form = RevisionForm(answer, latest_revision, request.POST) + if revision_form.is_valid(): + # Replace with those from the selected revision + form = EditAnswerForm(answer, + AnswerRevision.objects.get(answer=answer, + revision=revision_form.cleaned_data['revision'])) + else: + form = EditAnswerForm(answer, latest_revision, request.POST) + else: + form = EditAnswerForm(answer, latest_revision, request.POST) + if form.is_valid(): + html = sanitize_html(markdowner.convert(form.cleaned_data['text'])) + if form.has_changed(): + edited_at = datetime.datetime.now() + updated_fields = { + 'last_edited_at': edited_at, + 'last_edited_by': request.user, + 'html': html, + } + Answer.objects.filter(id=answer.id).update(**updated_fields) + + revision = AnswerRevision( + answer=answer, + author=request.user, + revised_at=edited_at, + text=form.cleaned_data['text'] + ) + + if form.cleaned_data['summary']: + revision.summary = form.cleaned_data['summary'] + else: + revision.summary = 'No.%s Revision' % latest_revision.revision + revision.save() + + answer.question.last_activity_at = edited_at + answer.question.last_activity_by = request.user + answer.question.save() + + return HttpResponseRedirect(answer.get_absolute_url()) + else: + revision_form = RevisionForm(answer, latest_revision) + form = EditAnswerForm(answer, latest_revision) + return render_to_response('answer_edit.html', { + 'answer': answer, + 'revision_form': revision_form, + 'form': form, + }, context_instance=RequestContext(request)) + + +def answer(request, id):#process a new answer + question = get_object_or_404(Question, id=id) + if request.method == "POST": + form = AnswerForm(question, request.user, request.POST) + if form.is_valid(): + wiki = form.cleaned_data['wiki'] + text = form.cleaned_data['text'] + update_time = datetime.datetime.now() + + if request.user.is_authenticated(): + create_new_answer( + question=question, + author=request.user, + added_at=update_time, + wiki=wiki, + text=text, + email_notify=form.cleaned_data['email_notify'] + ) + else: + request.session.flush() + html = sanitize_html(markdowner.convert(text)) + summary = strip_tags(html)[:120] + anon = AnonymousAnswer( + question=question, + wiki=wiki, + text=text, + summary=summary, + session_key=request.session.session_key, + ip_addr=request.META['REMOTE_ADDR'], + ) + anon.save() + return HttpResponseRedirect(reverse('user_signin_new_answer')) + + return HttpResponseRedirect(question.get_absolute_url()) + +def vote(request, id):#refactor - pretty incomprehensible view used by various ajax calls +#issues: this subroutine is too long, contains many magic numbers and other issues +#it's called "vote" but many actions processed here have nothing to do with voting + """ + vote_type: + acceptAnswer : 0, + questionUpVote : 1, + questionDownVote : 2, + favorite : 4, + answerUpVote: 5, + answerDownVote:6, + offensiveQuestion : 7, + offensiveAnswer:8, + removeQuestion: 9, + removeAnswer:10 + questionSubscribeUpdates:11 + + accept answer code: + response_data['allowed'] = -1, Accept his own answer 0, no allowed - Anonymous 1, Allowed - by default + response_data['success'] = 0, failed 1, Success - by default + response_data['status'] = 0, By default 1, Answer has been accepted already(Cancel) + + vote code: + allowed = -3, Don't have enough votes left + -2, Don't have enough reputation score + -1, Vote his own post + 0, no allowed - Anonymous + 1, Allowed - by default + status = 0, By default + 1, Cancel + 2, Vote is too old to be canceled + + offensive code: + allowed = -3, Don't have enough flags left + -2, Don't have enough reputation score to do this + 0, not allowed + 1, allowed + status = 0, by default + 1, can't do it again + """ + response_data = { + "allowed": 1, + "success": 1, + "status" : 0, + "count" : 0, + "message" : '' + } + + def __can_vote(vote_score, user):#refactor - belongs to auth.py + if vote_score == 1:#refactor magic number + return auth.can_vote_up(request.user) + else: + return auth.can_vote_down(request.user) + + try: + if not request.user.is_authenticated(): + response_data['allowed'] = 0 + response_data['success'] = 0 + + elif request.is_ajax(): + question = get_object_or_404(Question, id=id) + vote_type = request.POST.get('type') + + #accept answer + if vote_type == '0': + answer_id = request.POST.get('postId') + answer = get_object_or_404(Answer, id=answer_id) + # make sure question author is current user + if question.author == request.user: + # answer user who is also question author is not allow to accept answer + if answer.author == question.author: + response_data['success'] = 0 + response_data['allowed'] = -1 + # check if answer has been accepted already + elif answer.accepted: + onAnswerAcceptCanceled(answer, request.user) + response_data['status'] = 1 + else: + # set other answers in this question not accepted first + for answer_of_question in Answer.objects.get_answers_from_question(question, request.user): + if answer_of_question != answer and answer_of_question.accepted: + onAnswerAcceptCanceled(answer_of_question, request.user) + + #make sure retrieve data again after above author changes, they may have related data + answer = get_object_or_404(Answer, id=answer_id) + onAnswerAccept(answer, request.user) + else: + response_data['allowed'] = 0 + response_data['success'] = 0 + # favorite + elif vote_type == '4': + has_favorited = False + fav_questions = FavoriteQuestion.objects.filter(question=question) + # if the same question has been favorited before, then delete it + if fav_questions is not None: + for item in fav_questions: + if item.user == request.user: + item.delete() + response_data['status'] = 1 + response_data['count'] = len(fav_questions) - 1 + if response_data['count'] < 0: + response_data['count'] = 0 + has_favorited = True + # if above deletion has not been executed, just insert a new favorite question + if not has_favorited: + new_item = FavoriteQuestion(question=question, user=request.user) + new_item.save() + response_data['count'] = FavoriteQuestion.objects.filter(question=question).count() + Question.objects.update_favorite_count(question) + + elif vote_type in ['1', '2', '5', '6']: + post_id = id + post = question + vote_score = 1 + if vote_type in ['5', '6']: + answer_id = request.POST.get('postId') + answer = get_object_or_404(Answer, id=answer_id) + post_id = answer_id + post = answer + if vote_type in ['2', '6']: + vote_score = -1 + + if post.author == request.user: + response_data['allowed'] = -1 + elif not __can_vote(vote_score, request.user): + response_data['allowed'] = -2 + elif post.votes.filter(user=request.user).count() > 0: + vote = post.votes.filter(user=request.user)[0] + # unvote should be less than certain time + if (datetime.datetime.now().day - vote.voted_at.day) >= VOTE_RULES['scope_deny_unvote_days']: + response_data['status'] = 2 + else: + voted = vote.vote + if voted > 0: + # cancel upvote + onUpVotedCanceled(vote, post, request.user) + + else: + # cancel downvote + onDownVotedCanceled(vote, post, request.user) + + response_data['status'] = 1 + response_data['count'] = post.score + elif Vote.objects.get_votes_count_today_from_user(request.user) >= VOTE_RULES['scope_votes_per_user_per_day']: + response_data['allowed'] = -3 + else: + vote = Vote(user=request.user, content_object=post, vote=vote_score, voted_at=datetime.datetime.now()) + if vote_score > 0: + # upvote + onUpVoted(vote, post, request.user) + else: + # downvote + onDownVoted(vote, post, request.user) + + votes_left = VOTE_RULES['scope_votes_per_user_per_day'] - Vote.objects.get_votes_count_today_from_user(request.user) + if votes_left <= VOTE_RULES['scope_warn_votes_left']: + response_data['message'] = u'%s votes left' % votes_left + response_data['count'] = post.score + elif vote_type in ['7', '8']: + post = question + post_id = id + if vote_type == '8': + post_id = request.POST.get('postId') + post = get_object_or_404(Answer, id=post_id) + + if FlaggedItem.objects.get_flagged_items_count_today(request.user) >= VOTE_RULES['scope_flags_per_user_per_day']: + response_data['allowed'] = -3 + elif not auth.can_flag_offensive(request.user): + response_data['allowed'] = -2 + elif post.flagged_items.filter(user=request.user).count() > 0: + response_data['status'] = 1 + else: + item = FlaggedItem(user=request.user, content_object=post, flagged_at=datetime.datetime.now()) + onFlaggedItem(item, post, request.user) + response_data['count'] = post.offensive_flag_count + # send signal when question or answer be marked offensive + mark_offensive.send(sender=post.__class__, instance=post, mark_by=request.user) + elif vote_type in ['9', '10']: + post = question + post_id = id + if vote_type == '10': + post_id = request.POST.get('postId') + post = get_object_or_404(Answer, id=post_id) + + if not auth.can_delete_post(request.user, post): + response_data['allowed'] = -2 + elif post.deleted == True: + logging.debug('debug restoring post in view') + onDeleteCanceled(post, request.user) + response_data['status'] = 1 + else: + onDeleted(post, request.user) + delete_post_or_answer.send(sender=post.__class__, instance=post, delete_by=request.user) + elif vote_type == '11':#subscribe q updates + user = request.user + if user.is_authenticated(): + 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, 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: + pass + #response_data['status'] = 0 + #response_data['allowed'] = 0 + elif vote_type == '12':#unsubscribe q updates + user = request.user + if user.is_authenticated(): + 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.' + + data = simplejson.dumps(response_data) + + except Exception, e: + response_data['message'] = str(e) + data = simplejson.dumps(response_data) + return HttpResponse(data, mimetype="application/json") + +#internally grouped views - used by the tagging system +@ajax_login_required +def mark_tag(request, tag=None, **kwargs):#tagging system + 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):#ajax tagging and tag-filtering system + 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):#refactor? view processing ajax commands - note "vote" and view others do it too + 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 question_comments(request, id):#ajax handler for loading comments to question + question = get_object_or_404(Question, id=id) + user = request.user + return __comments(request, question, 'question') + +def answer_comments(request, id):#ajax handler for loading comments on answer + answer = get_object_or_404(Answer, id=id) + user = request.user + return __comments(request, answer, 'answer') + +def __comments(request, obj, type):#non-view generic ajax handler to load comments to an object + # only support get post comments by ajax now + user = request.user + if request.is_ajax(): + if request.method == "GET": + response = __generate_comments_json(obj, type, user) + elif request.method == "POST": + 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):#non-view generates json data for the post comments + 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 + #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_comment(request, object_id='', comment_id='', commented_object_type=None):#ajax handler to delete comment + response = None + commented_object = None + if commented_object_type == 'question': + commented_object = Question + elif commented_object_type == 'answer': + commented_object = Answer + + if request.is_ajax(): + comment = get_object_or_404(Comment, id=comment_id) + 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() + diff --git a/forum/views/meta.py b/forum/views/meta.py new file mode 100644 index 00000000..b7a1183c --- /dev/null +++ b/forum/views/meta.py @@ -0,0 +1,98 @@ +from django.shortcuts import render_to_response, get_object_or_404 +from django.core.urlresolvers import reverse +from django.template import RequestContext +from django.http import HttpResponseRedirect, HttpResponse +from forum.forms import FeedbackForm +from django.core.mail import mail_admins +from django.utils.translation import ugettext as _ +from utils.forms import get_next_url +from forum.models import Badge, Award + +def about(request): + return render_to_response('about.html', context_instance=RequestContext(request)) + +def faq(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)) + +def logout(request):#refactor/change behavior? +#currently you click logout and you get +#to this view which actually asks you again - do you really want to log out? +#I guess rationale was to tell the user that s/he may be still logged in +#through their external login sytem and we'd want to remind them about it +#however it might be a little annoying +#why not just show a message: you are logged out of osqa, but +#if you really want to log out -> go to your openid provider + return render_to_response('logout.html', { + 'next' : get_next_url(request), + }, context_instance=RequestContext(request)) + +def badges(request):#user status/reputation system + badges = Badge.objects.all().order_by('type') + my_badges = [] + if request.user.is_authenticated(): + my_badges = Award.objects.filter(user=request.user) + my_badges.query.group_by = ['badge_id'] + + return render_to_response('badges.html', { + '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) + awards = Award.objects.extra( + select={'id': 'auth_user.id', + 'name': 'auth_user.username', + 'rep':'auth_user.reputation', + 'gold': 'auth_user.gold', + 'silver': 'auth_user.silver', + 'bronze': 'auth_user.bronze'}, + tables=['award', 'auth_user'], + where=['badge_id=%s AND user_id=auth_user.id'], + params=[id] + ).distinct('id') + + return render_to_response('badge.html', { + 'awards' : awards, + 'badge' : badge, + }, context_instance=RequestContext(request)) + +#osqa-user communication system +def read_message(request):#marks message a read + if request.method == "POST": + if request.POST['formdata'] == 'required': + request.session['message_silent'] = 1 + if request.user.is_authenticated(): + request.user.delete_messages() + return HttpResponse('') diff --git a/forum/views/users.py b/forum/views/users.py new file mode 100644 index 00000000..03d38834 --- /dev/null +++ b/forum/views/users.py @@ -0,0 +1,943 @@ +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.core.paginator import Paginator, EmptyPage, InvalidPage +from django.core.urlresolvers import reverse +from django.shortcuts import render_to_response, get_object_or_404 +from django.template import RequestContext +from django.http import HttpResponse, HttpResponseForbidden +from django.utils.translation import ugettext as _ +from forum.forms import *#incomplete list is EditUserForm, ModerateUserForm, TagFilterSelectionForm, +from forum import auth + +question_type = ContentType.objects.get_for_model(Question) +answer_type = ContentType.objects.get_for_model(Answer) +comment_type = ContentType.objects.get_for_model(Comment) +question_revision_type = ContentType.objects.get_for_model(QuestionRevision) +answer_revision_type = ContentType.objects.get_for_model(AnswerRevision) +repute_type = ContentType.objects.get_for_model(Repute) +question_type_id = question_type.id +answer_type_id = answer_type.id +comment_type_id = comment_type.id +question_revision_type_id = question_revision_type.id +answer_revision_type_id = answer_revision_type.id +repute_type_id = repute_type.id + +# used in users +class UserView: + def __init__(self, id, tab_title, tab_description, page_title, view_name, template_file, data_size=0): + self.id = id + self.tab_title = tab_title + self.tab_description = tab_description + self.page_title = page_title + self.view_name = view_name + self.template_file = template_file + self.data_size = data_size + +USER_TEMPLATE_VIEWS = ( + UserView( + id = 'stats', + tab_title = _('overview'), + tab_description = _('user profile'), + page_title = _('user profile overview'), + view_name = 'user_stats', + template_file = 'user_stats.html' + ), + UserView( + id = 'recent', + tab_title = _('recent activity'), + tab_description = _('recent user activity'), + page_title = _('profile - recent activity'), + view_name = 'user_recent', + template_file = 'user_recent.html', + data_size = 50 + ), + UserView( + id = 'responses', + tab_title = _('responses'), + tab_description = _('comments and answers to others questions'), + page_title = _('profile - responses'), + view_name = 'user_responses', + template_file = 'user_responses.html', + data_size = 50 + ), + UserView( + id = 'reputation', + tab_title = _('reputation'), + tab_description = _('user reputation in the community'), + page_title = _('profile - user reputation'), + view_name = 'user_reputation', + template_file = 'user_reputation.html' + ), + UserView( + id = 'favorites', + tab_title = _('favorite questions'), + tab_description = _('users favorite questions'), + page_title = _('profile - favorite questions'), + view_name = 'user_favorites', + template_file = 'user_favorites.html', + data_size = 50 + ), + UserView( + id = 'votes', + tab_title = _('casted votes'), + tab_description = _('user vote record'), + page_title = _('profile - votes'), + view_name = 'user_votes', + template_file = 'user_votes.html', + data_size = 50 + ), + UserView( + 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' + ) +) + +def user(request, id): + sort = request.GET.get('sort', 'stats') + user_view = dict((v.id, v) for v in USER_TEMPLATE_VIEWS).get(sort, USER_TEMPLATE_VIEWS[0]) + from forum import views + func = getattr(views, user_view.view_name) + return func(request, id, user_view) + + +USERS_PAGE_SIZE = 35# refactor - move to some constants file + +def users(request): + is_paginated = True + sortby = request.GET.get('sort', 'reputation') + suser = request.REQUEST.get('q', "") + try: + page = int(request.GET.get('page', '1')) + except ValueError: + page = 1 + + if suser == "": + if sortby == "newest": + objects_list = Paginator(User.objects.all().order_by('-date_joined'), USERS_PAGE_SIZE) + elif sortby == "last": + objects_list = Paginator(User.objects.all().order_by('date_joined'), USERS_PAGE_SIZE) + elif sortby == "user": + objects_list = Paginator(User.objects.all().order_by('username'), USERS_PAGE_SIZE) + # default + else: + objects_list = Paginator(User.objects.all().order_by('-reputation'), USERS_PAGE_SIZE) + 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 = reverse('users') + '?name=%s&sort=%s&' % (suser, sortby) + + try: + users = objects_list.page(page) + except (EmptyPage, InvalidPage): + 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)) + +@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: + raise Http404 + if request.method == "POST": + form = EditUserForm(user, request.POST) + if form.is_valid(): + new_email = sanitize_html(form.cleaned_data['email']) + + 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']) + user.date_of_birth = sanitize_html(form.cleaned_data['birthday']) + if len(user.date_of_birth) == 0: + user.date_of_birth = '1900-01-01' + user.about = sanitize_html(form.cleaned_data['about']) + + user.save() + # send user updated singal if full fields have been updated + if user.email and user.real_name and user.website and user.location and \ + user.date_of_birth and user.about: + user_updated.send(sender=user.__class__, instance=user, updated_by=user) + return HttpResponseRedirect(user.get_profile_url()) + else: + form = EditUserForm(user) + return render_to_response('user_edit.html', { + 'form' : form, + 'gravatar_faq_url' : reverse('faq') + '#gravatar', + }, context_instance=RequestContext(request)) + +def user_stats(request, user_id, user_view): + user = get_object_or_404(User, id=user_id) + questions = Question.objects.extra( + select={ + 'vote_count' : 'question.score', + 'favorited_myself' : 'SELECT count(*) FROM favorite_question f WHERE f.user_id = %s AND f.question_id = question.id', + 'la_user_id' : 'auth_user.id', + 'la_username' : 'auth_user.username', + 'la_user_gold' : 'auth_user.gold', + 'la_user_silver' : 'auth_user.silver', + 'la_user_bronze' : 'auth_user.bronze', + 'la_user_reputation' : 'auth_user.reputation' + }, + select_params=[user_id], + tables=['question', 'auth_user'], + where=['question.deleted=False AND question.author_id=%s AND question.last_activity_by_id = auth_user.id'], + params=[user_id], + order_by=['-vote_count', '-last_activity_at'] + ).values('vote_count', + 'favorited_myself', + 'id', + 'title', + 'author_id', + 'added_at', + 'answer_accepted', + 'answer_count', + 'comment_count', + 'view_count', + 'favourite_count', + 'summary', + 'tagnames', + 'vote_up_count', + 'vote_down_count', + 'last_activity_at', + 'la_user_id', + 'la_username', + 'la_user_gold', + 'la_user_silver', + 'la_user_bronze', + '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=False AND question.deleted=False 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 = auth.VOTE_RULES['scope_votes_per_user_per_day'] + + 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') + total_awards = awards.count() + 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') + 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'] + + 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], + "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) + def get_type_name(type_id): + for item in TYPE_ACTIVITY: + if type_id in item: + return item[1] + + class Event: + def __init__(self, time, type, title, summary, answer_id, question_id): + self.time = time + self.type = get_type_name(type) + self.type_id = type + self.title = title + self.summary = summary + slug_title = slugify(title) + self.title_link = reverse('question', kwargs={'id':question_id}) + u'%s' % slug_title + if int(answer_id) > 0: + self.title_link += '#%s' % answer_id + + class AwardEvent: + def __init__(self, time, type, id): + self.time = time + self.type = get_type_name(type) + self.type_id = type + self.badge = get_object_or_404(Badge, id=id) + + 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 question.deleted=False 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] + activities.extend(questions) + + # 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 answer.deleted=False AND activity.user_id=%s AND '+ + 'activity.activity_type=%s AND question.deleted=False'], + 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] + activities.extend(answers) + + # 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 AND ' + + 'question.deleted=False'], + 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', \ + q['question_id'])) for q in comments] + activities.extend(comments) + + # 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 AND '+ + 'answer.deleted=False AND question.deleted=False'], + 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'], \ + q['question_id'])) for q in comments] + activities.extend(comments) + + # 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', 'question'], + where=['activity.content_type_id = %s AND activity.object_id = question_revision.id AND '+ + 'question_revision.id=question.id AND question.deleted=False 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', \ + q['question_id'])) for q in revisions] + activities.extend(revisions) + + # 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 '+ + 'question.deleted=False AND answer.deleted=False 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'], \ + q['answer_id'], q['question_id'])) for q in revisions] + activities.extend(revisions) + + # 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.deleted=False AND question.deleted=False 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] + activities.extend(accept_answers) + #award history + awards = Activity.objects.extra( + select={ + 'badge_id' : 'badge.id', + 'awarded_at': 'award.awarded_at', + 'activity_type' : 'activity.activity_type' + }, + tables=['activity', 'award', 'badge'], + where=['activity.user_id = award.user_id AND activity.user_id = %s AND '+ + 'award.badge_id=badge.id AND activity.object_id=award.id AND activity.activity_type=%s'], + params=[user_id, TYPE_ACTIVITY_PRIZE], + order_by=['-activity.active_at'] + ).values( + 'badge_id', + 'awarded_at', + 'activity_type' + ) + if len(awards) > 0: + awards = [(AwardEvent(q['awarded_at'], q['activity_type'], q['badge_id'])) for q in awards] + activities.extend(awards) + + 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)) + +def user_responses(request, user_id, user_view): + """ + We list answers for question, comments, and answer accepted by others for this user. + """ + class Response: + def __init__(self, type, title, question_id, answer_id, time, username, user_id, content): + self.type = type + self.title = title + self.titlelink = reverse('question', args=[question_id]) + u'%s#%s' % (slugify(title), answer_id) + self.time = time + self.userlink = reverse('users') + u'%s/%s/' % (user_id, username) + self.username = username + self.content = u'%s ...' % strip_tags(content)[:300] + + def __unicode__(self): + return u'%s %s' % (self.type, self.titlelink) + + 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=False AND question.deleted=False 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] + responses.extend(answers) + + + # 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=False 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'], + '', c['added_at'], c['username'], c['user_id'], c['comment'])) for c in comments] + responses.extend(comments) + + # answer comments + comments = Comment.objects.extra( + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'answer_id' : 'answer.id', + 'added_at' : 'comment.added_at', + 'comment' : 'comment.comment', + 'username' : 'auth_user.username', + 'user_id' : 'auth_user.id' + }, + tables=['answer', 'auth_user', 'comment', 'question'], + where=['answer.deleted=False AND answer.author_id = %s AND comment.object_id=answer.id AND '+ + 'comment.content_type_id=%s AND comment.user_id <> %s AND comment.user_id = auth_user.id '+ + 'AND question.id = answer.question_id'], + params=[user_id, answer_type_id, user_id], + order_by=['-comment.added_at'] + ).values( + 'title', + 'question_id', + 'answer_id', + 'added_at', + 'comment', + 'username', + 'user_id' + ) + + if len(comments) > 0: + comments = [(Response(TYPE_RESPONSE['ANSWER_COMMENTED'], c['title'], c['question_id'], + c['answer_id'], c['added_at'], c['username'], c['user_id'], c['comment'])) for c in comments] + responses.extend(comments) + + # answer has been accepted + answers = Answer.objects.extra( + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'answer_id' : 'answer.id', + 'added_at' : 'answer.accepted_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=False AND question.deleted=False AND '+ + 'answer.author_id = %s AND answer.accepted=True AND question.author_id=auth_user.id'], + params=[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['ANSWER_ACCEPTED'], a['title'], a['question_id'], + a['answer_id'], a['added_at'], a['username'], a['user_id'], a['html'])) for a in answers] + responses.extend(answers) + + # sort posts by time + responses.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, + "responses" : responses[:user_view.data_size], + + }, context_instance=RequestContext(request)) + +def user_votes(request, user_id, user_view): + user = get_object_or_404(User, id=user_id) + if not auth.can_view_user_votes(request.user, user): + raise Http404 + votes = [] + question_votes = Vote.objects.extra( + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'answer_id' : 0, + 'voted_at' : 'vote.voted_at', + 'vote' : 'vote', + }, + select_params=[user_id], + tables=['vote', 'question', 'auth_user'], + where=['vote.content_type_id = %s AND vote.user_id = %s AND vote.object_id = question.id '+ + 'AND vote.user_id=auth_user.id'], + params=[question_type_id, user_id], + order_by=['-vote.id'] + ).values( + 'title', + 'question_id', + 'answer_id', + 'voted_at', + 'vote', + ) + if(len(question_votes) > 0): + votes.extend(question_votes) + + answer_votes = Vote.objects.extra( + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'answer_id' : 'answer.id', + 'voted_at' : 'vote.voted_at', + 'vote' : 'vote', + }, + select_params=[user_id], + tables=['vote', 'answer', 'question', 'auth_user'], + where=['vote.content_type_id = %s AND vote.user_id = %s AND vote.object_id = answer.id '+ + 'AND answer.question_id = question.id AND vote.user_id=auth_user.id'], + params=[answer_type_id, user_id], + order_by=['-vote.id'] + ).values( + 'title', + 'question_id', + 'answer_id', + 'voted_at', + 'vote', + ) + if(len(answer_votes) > 0): + votes.extend(answer_votes) + votes.sort(lambda x,y: cmp(y['voted_at'], x['voted_at'])) + 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, + "votes" : votes[:user_view.data_size] + + }, context_instance=RequestContext(request)) + +def user_reputation(request, user_id, user_view): + user = get_object_or_404(User, id=user_id) + try: + from django.db.models import Sum + reputation = Repute.objects.extra( + select={'question_id':'question_id', + 'title': 'question.title'}, + tables=['repute', 'question'], + order_by=['-reputed_at'], + where=['user_id=%s AND question_id=question.id'], + params=[user.id] + ).values('question_id', 'title', 'reputed_at', 'reputation') + reputation = reputation.annotate(positive=Sum("positive"), negative=Sum("negative")) + except ImportError: + reputation = Repute.objects.extra( + select={'positive':'sum(positive)', 'negative':'sum(negative)', 'question_id':'question_id', + 'title': 'question.title'}, + tables=['repute', 'question'], + order_by=['-reputed_at'], + where=['user_id=%s AND question_id=question.id'], + params=[user.id] + ).values('positive', 'negative', 'question_id', 'title', 'reputed_at', 'reputation') + reputation.query.group_by = ['question_id'] + + rep_list = [] + for rep in Repute.objects.filter(user=user).order_by('reputed_at'): + dic = '[%s,%s]' % (calendar.timegm(rep.reputed_at.timetuple()) * 1000, rep.reputation) + rep_list.append(dic) + reps = ','.join(rep_list) + reps = '[%s]' % reps + + 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, + "reputation": reputation, + "reps": reps + }, context_instance=RequestContext(request)) + +def user_favorites(request, user_id, user_view): + user = get_object_or_404(User, id=user_id) + questions = Question.objects.extra( + select={ + 'vote_count' : 'question.vote_up_count + question.vote_down_count', + 'favorited_myself' : 'SELECT count(*) FROM favorite_question f WHERE f.user_id = %s '+ + 'AND f.question_id = question.id', + 'la_user_id' : 'auth_user.id', + 'la_username' : 'auth_user.username', + 'la_user_gold' : 'auth_user.gold', + 'la_user_silver' : 'auth_user.silver', + 'la_user_bronze' : 'auth_user.bronze', + 'la_user_reputation' : 'auth_user.reputation' + }, + select_params=[user_id], + tables=['question', 'auth_user', 'favorite_question'], + where=['question.deleted=True AND question.last_activity_by_id = auth_user.id '+ + 'AND favorite_question.question_id = question.id AND favorite_question.user_id = %s'], + params=[user_id], + order_by=['-vote_count', '-question.id'] + ).values('vote_count', + 'favorited_myself', + 'id', + 'title', + 'author_id', + 'added_at', + 'answer_accepted', + 'answer_count', + 'comment_count', + 'view_count', + 'favourite_count', + 'summary', + 'tagnames', + 'vote_up_count', + 'vote_down_count', + 'last_activity_at', + 'la_user_id', + 'la_username', + 'la_user_gold', + 'la_user_silver', + 'la_user_bronze', + 'la_user_reputation') + 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, + "questions" : questions[:user_view.data_size], + "view_user" : user + }, context_instance=RequestContext(request)) + +def user_email_subscriptions(request, user_id, user_view): + user = get_object_or_404(User, id=user_id) + 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)) + diff --git a/forum/views.py b/forum/views/views.py index 4d214bad..4d214bad 100644 --- a/forum/views.py +++ b/forum/views/views.py |