diff options
-rw-r--r-- | forum/const/__init__.py | 8 | ||||
-rw-r--r-- | forum/middleware/pagesize.py | 3 | ||||
-rw-r--r-- | forum/models/__init__.py | 205 | ||||
-rw-r--r-- | forum/models/answer.py | 17 | ||||
-rw-r--r-- | forum/models/base.py | 107 | ||||
-rw-r--r-- | forum/models/meta.py | 53 | ||||
-rw-r--r-- | forum/models/question.py | 46 | ||||
-rw-r--r-- | forum/models/repute.py | 8 | ||||
-rw-r--r-- | forum/models/signals.py | 7 | ||||
-rw-r--r-- | forum/models/tag.py | 5 | ||||
-rw-r--r-- | forum/models/user.py | 51 | ||||
-rw-r--r-- | forum/utils/markup.py (renamed from forum/models/utils.py) | 82 | ||||
-rwxr-xr-x | log/README.TXT | 1 |
13 files changed, 342 insertions, 251 deletions
diff --git a/forum/const/__init__.py b/forum/const/__init__.py index e7328c54..fcba80d1 100644 --- a/forum/const/__init__.py +++ b/forum/const/__init__.py @@ -56,6 +56,12 @@ POST_SCOPE_LIST = ( ) DEFAULT_POST_SCOPE = 'all' PAGE_SIZE_CHOICES = (('10','10',),('30','30',),('50','50',),) +#todo: remove this duplication +QUESTIONS_PER_PAGE_USER_CHOICES = ( + (10, u'10'), + (30, u'30'), + (50, u'50'), +) UNANSWERED_QUESTION_MEANING_CHOICES = ( ('NO_ANSWERS', _('Question has no answers')), @@ -144,7 +150,7 @@ TYPE_RESPONSE = { 'ANSWER_ACCEPTED' : _('answer_accepted'), } -CONST = { +POST_STATUS = { 'closed' : _('[closed]'), 'deleted' : _('[deleted]'), 'default_version' : _('initial version'), diff --git a/forum/middleware/pagesize.py b/forum/middleware/pagesize.py index 486193dc..7090a04c 100644 --- a/forum/middleware/pagesize.py +++ b/forum/middleware/pagesize.py @@ -30,4 +30,5 @@ class QuestionsPageSizeMiddleware(object): def process_exception(self,request,exception): import logging - logging.debug('have exception %s' % str(exception)) + import traceback + logging.debug(repr(traceback.extract_tb(exc_traceback))) diff --git a/forum/models/__init__.py b/forum/models/__init__.py index 6777339a..6085ce5a 100644 --- a/forum/models/__init__.py +++ b/forum/models/__init__.py @@ -1,3 +1,4 @@ +import signals from question import Question ,QuestionRevision, QuestionView, AnonymousQuestion, FavoriteQuestion from answer import Answer, AnonymousAnswer, AnswerRevision from tag import Tag, MarkedTag @@ -5,53 +6,27 @@ from meta import Vote, Comment, FlaggedItem from user import Activity, ValidationHash, EmailFeedSetting from user import AuthKeyUserAssociation from repute import Badge, Award, Repute -import signals from django.core.urlresolvers import reverse from django.core.mail import EmailMessage from forum.search.indexer import create_fulltext_indexes -from django.db.models.signals import post_syncdb +from django.db.models import signals as django_signals from django.template import loader, Context from django.utils.translation import ugettext as _ +from django.contrib.auth.models import User +from django.template.defaultfilters import slugify +from django.utils.safestring import mark_safe +from django.db import models from forum import const import logging import re +import hashlib -from base import * import datetime from django.contrib.contenttypes.models import ContentType #todo: must go after signals from forum import auth -# 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 - -def user_get_absolute_url(self): - return "/users/%d/%s/" % (self.id, (self.username)) - 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)) @@ -70,7 +45,10 @@ 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)) + models.SmallIntegerField( + choices=const.QUESTIONS_PER_PAGE_USER_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)) @@ -78,17 +56,37 @@ 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, + choices=const.TAG_EMAIL_FILTER_CHOICES, default='ignored' ) ) -User.add_to_class('get_absolute_url', user_get_absolute_url) + +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 + +def user_get_absolute_url(self): + return "/users/%d/%s/" % (self.id, (self.username)) + def get_messages(self): messages = [] @@ -224,14 +222,21 @@ def flag_post(self, post, timestamp=None, cancel=False): ) auth.onFlaggedItem(flag, post, user, timestamp=timestamp) -def user_should_receive_instant_notification_about_post(user, post): +def user_should_receive_instant_notification_about_post( + user, + post, + newly_mentioned_users = [] + ): return EmailFeedSetting.objects.exists_match_to_post_and_subscriber( - subscriber = user, - post = post, - frequency = 'i', - ) - + subscriber = user, + post = post, + frequency = 'i', + newly_mentioned_users = newly_mentioned_users + ) +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('get_absolute_url', user_get_absolute_url) User.add_to_class('upvote', upvote) User.add_to_class('downvote', downvote) User.add_to_class('accept_answer', accept_answer) @@ -246,11 +251,11 @@ User.add_to_class( user_should_receive_instant_notification_about_post ) - def send_instant_notifications_about_activity_in_post( - activity, - post, - receiving_users + activity = None, + post = None, + receiving_users = [], + newly_mentioned_users = [] ): """ function called when posts are updated @@ -265,18 +270,12 @@ def send_instant_notifications_about_activity_in_post( } template = loader.get_template('instant_notification.html') - for u in receiving_users: - if u.should_receive_instant_notification_about_post(post): + for u in set(receiving_users) + set(newly_mentioned_users): + if u.should_receive_instant_notification_about_post( + post, + newly_mentioned_users = newly_mentioned_users + ): - mentions = Activity.objects.get_mentions( - mentioned_whom = u, - mentioned_in = post, - reported = False - ) - if mentions: - #todo: find a more semantic way to do this - mentions.update(is_auditted = True) - #get details about update #todo: is there a way to solve this import issue? from forum.conf import settings as forum_settings @@ -358,50 +357,35 @@ def record_answer_event(instance, created, **kwargs): activity.receiving_users.add(*receiving_users) -def record_comment_event(instance, **kwargs): - if isinstance(instance.content_object, Question): - activity_type = const.TYPE_ACTIVITY_COMMENT_QUESTION - elif isinstance(instance.content_object, Answer): - activity_type = const.TYPE_ACTIVITY_COMMENT_ANSWER - else: - logging.critical( - 'recording comment for %s is not implemented'\ - % type(instance.content_object) - ) +#todo: change to more general post_update_activity +def record_post_update_activity(post, newly_mentioned_users, **kwargs): + #todo: take into account created == True case + activity_type = post.get_updated_activity_type() + #fields will depend on post type and maybe activity type + #post has to be saved already, b/c Activity is in generic relation to post activity = Activity( - user = instance.user, - active_at = instance.added_at, - content_object = instance, + user = post.get_last_author(), + active_at = post.added_at, + content_object = post, activity_type = activity_type ) activity.save() - receiving_users = set() - receiving_users.update( - #get authors of parent object and all associated comments - instance.content_object.get_author_list( - include_comments = True, - ) - ) - - receiving_users.update( - instance.get_newly_mentioned_users() - ) - - receiving_users -= set([instance.user])#remove activity user - receiving_users = list(receiving_users) + #what users are included depends on the post type + #for example for question - all Q&A contributors + #are included, for comments only authors of comments and parent + #post are included + receiving_users = post.get_potentially_interested_users() activity.receiving_users.add(*receiving_users) - print 'in post_save handler on comment' - print receiving_users - send_instant_notifications_about_activity_in_post( - activity, - instance, - receiving_users - ) + activity = activity, + post = post, + receiving_users = receiving_users, + newly_mentioned_users = newly_mentioned_users + ) def record_revision_question_event(instance, created, **kwargs): @@ -423,10 +407,6 @@ def record_revision_question_event(instance, created, **kwargs): for a in instance.question.answers.all(): receiving_users.update(a.get_author_list()) - receiving_users.update( - instance.question.get_newly_mentioned_users() - ) - receiving_users -= set([instance.author])#remove activity user receiving_users = list(receiving_users) @@ -456,10 +436,6 @@ def record_revision_answer_event(instance, created, **kwargs): ) receiving_users.update(instance.answer.question.get_author_list()) - receiving_users.update( - instance.answer.get_newly_mentioned_users() - ) - receiving_users -= set([instance.author]) receiving_users = list(receiving_users) @@ -656,18 +632,18 @@ def post_stored_anonymous_content(sender,user,session_key,signal,*args,**kwargs) aa.publish(user) #signal for User model 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_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_save.connect(record_favorite_question, sender=FavoriteQuestion) -post_delete.connect(record_cancel_vote, sender=Vote) +django_signals.pre_save.connect(calculate_gravatar_hash, sender=User) +django_signals.post_save.connect(record_ask_event, sender=Question) +django_signals.post_save.connect(record_answer_event, sender=Answer) +django_signals.post_save.connect(record_revision_question_event, sender=QuestionRevision) +django_signals.post_save.connect(record_revision_answer_event, sender=AnswerRevision) +django_signals.post_save.connect(record_award_event, sender=Award) +django_signals.post_save.connect(notify_award_message, sender=Award) +django_signals.post_save.connect(record_answer_accepted, sender=Answer) +django_signals.post_save.connect(update_last_seen, sender=Activity) +django_signals.post_save.connect(record_vote, sender=Vote) +django_signals.post_save.connect(record_favorite_question, sender=FavoriteQuestion) +django_signals.post_delete.connect(record_cancel_vote, sender=Vote) #change this to real m2m_changed with Django1.2 signals.delete_post_or_answer.connect(record_delete_question, sender=Question) @@ -677,7 +653,10 @@ signals.mark_offensive.connect(record_mark_offensive, sender=Answer) signals.tags_updated.connect(record_update_tags, sender=Question) signals.user_updated.connect(record_user_full_updated, sender=User) signals.user_logged_in.connect(post_stored_anonymous_content) -signals.comment_post_save.connect(record_comment_event, sender=Comment) +signals.post_updated.connect( + record_post_update_activity, + sender=Comment + ) #post_syncdb.connect(create_fulltext_indexes) #todo: wtf??? what is x=x about? diff --git a/forum/models/answer.py b/forum/models/answer.py index 1c33cad8..9179226f 100644 --- a/forum/models/answer.py +++ b/forum/models/answer.py @@ -1,9 +1,12 @@ -from base import * +from base import Content, AnonymousContent, ContentRevision, DeletableContent #todo: take care of copy-paste markdowner stuff maybe make html automatic field? -from forum.const import CONST +from forum import const from markdown2 import Markdown from django.utils.html import strip_tags from forum.utils.html import sanitize_html +from django.db import models +from django.utils.http import urlquote as django_urlquote +from django.template.defaultfilters import slugify import datetime markdowner = Markdown(html4tags=True) @@ -27,10 +30,10 @@ class AnswerManager(models.Manager): answer.save() answer.add_revision( - revised_by=author, - revised_at=added_at, - text=text, - comment=CONST['default_version'], + revised_by = author, + revised_at = added_at, + text = text, + comment = const.POST_STATUS['default_version'], ) #update question data @@ -122,7 +125,7 @@ class Answer(Content, DeletableContent): rev_no = self.revisions.all().count() + 1 if comment in (None, ''): if rev_no == 1: - comment = CONST['default_version'] + comment = const.POST_STATUS['default_version'] else: comment = 'No.%s Revision' % rev_no return AnswerRevision.objects.create( diff --git a/forum/models/base.py b/forum/models/base.py index c153bb5d..e276c6ee 100644 --- a/forum/models/base.py +++ b/forum/models/base.py @@ -1,39 +1,104 @@ import datetime import hashlib from urllib import quote_plus, urlencode -from django.db import models, IntegrityError, connection, transaction +from django.db import models 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 +from forum.utils import markup +from django.utils import html import logging -#todo: sphinx search import used to be here +#todo: following methods belong to a future common post class +def render_post_text_and_get_newly_mentioned_users(post, + urlize_content = False): -from forum.const import * + text = post.get_text() -#todo: this method belongs to a common post class -def get_newly_mentioned_users_in_post(post): - from forum.models import Activity - mentions = Activity.objects.get_mentions( - mentioned_in = post, - reported = False - ) - users = set() - for m in mentions: - users.update(m.receiving_users.all()) - return list(users) + if urlize_content: + text = html.urlize(text) + + if '@' not in text: + return list() + + from forum.models.user import Activity + + mentioned_by = post.get_last_author() + + op = post.get_origin_post() + anticipated_authors = op.get_author_list( include_comments = True, recursive = True ) + + extra_name_seeds = markup.extract_mentioned_name_seeds(text) + extra_authors = set() + for name_seed in extra_name_seeds: + extra_authors.update(User.objects.filter(username__startswith = name_seed)) + + #it is important to preserve order here so that authors of post get mentioned first + anticipated_authors += list(extra_authors) + + mentioned_authors, post.html = markup.mentionize_text(text, anticipated_authors) + + #maybe delete some previous mentions + if self.id != None: + #only look for previous mentions if post was already saved before + prev_mention_qs = Activity.objects.get_mentions( + mentioned_in = post + ) + new_set = set(mentioned_authors) + for mention in prev_mention_qs: + delta_set = set(mention.receiving_users.all()) - new_set + if not delta_set: + mention.delete() + new_set -= delta_set + + mentioned_authors = list(new_set) + + return mentioned_authors + +def save_content(self, urlize_content = False, **kwargs): + """generic save method to use with posts + """ + + new_mentions = self._render_text_and_get_newly_mentioned_users( + urlize_content + ) + + from forum.models.user import Activity + + #this save must precede saving the mention activity + super(self.__class__, self).save(**kwargs) + + post_author = self.get_last_author() + + for u in new_mentions: + Activity.objects.create_new_mention( + mentioned_whom = u, + mentioned_in = self, + mentioned_by = post_author + ) + #todo: this is handled in signal because models for posts + #are too spread out + from forum.models import signals + signals.post_updated.send( + post = self, + newly_mentioned_users = new_mentions, + sender = self.__class__ + ) + + try: + ping_google() + except Exception: + logging.debug('problem pinging google did you register you sitemap with google?') + class UserContent(models.Model): user = models.ForeignKey(User, related_name='%(class)ss') @@ -145,6 +210,10 @@ class Content(models.Model): comments = self.comments.all().order_by('id') return comments + #todo: maybe remove this wnen post models are unified + def get_text(self): + return self.text + def add_comment(self, comment=None, user=None, added_at=None): if added_at is None: added_at = datetime.datetime.now() @@ -184,8 +253,6 @@ class Content(models.Model): authors -= set(exclude_list) return list(authors) - get_newly_mentioned_users = get_newly_mentioned_users_in_post - def passes_tag_filter_for_user(self, user): tags = self.get_origin_post().tags.all() @@ -202,8 +269,6 @@ class Content(models.Model): else: raise Exception('unexpected User.tag_filter_setting %' % self.tag_filter_setting) - - def post_get_last_update_info(self):#todo: rename this subroutine when = self.added_at who = self.author diff --git a/forum/models/meta.py b/forum/models/meta.py index 0e49064e..c72bd948 100644 --- a/forum/models/meta.py +++ b/forum/models/meta.py @@ -1,7 +1,9 @@ -from base import * +from django.db import models +from base import MetaContent, UserContent +from base import render_post_text_and_get_newly_mentioned_users +from base import save_content from forum import const -from django.utils.html import urlize -from forum.models import signals +import datetime class VoteManager(models.Manager): def get_up_vote_count_from_user(self, user): @@ -88,22 +90,35 @@ class Comment(MetaContent, UserContent): def get_origin_post(self): return self.content_object.get_origin_post() + #todo: maybe remove this wnen post models are unified + def get_text(self): + return self.comment + + _render_text_and_get_newly_mentioned_users = \ + render_post_text_and_get_newly_mentioned_users + + _save = save_content + def save(self,**kwargs): - print 'before first save' - super(Comment,self).save(**kwargs) - print 'after first save' - from forum.models.utils import mentionize - self.html = mentionize(urlize(self.comment, nofollow=True), context_object = self) - print 'mentionized' - #todo - try post_save to install mentions - super(Comment,self).save(**kwargs)#have to save twice!!, b/c need id for generic relation - - signals.comment_post_save.send(instance = self, sender = Comment) - - try: - ping_google() - except Exception: - logging.debug('problem pinging google did you register you sitemap with google?') + self._save(urlize_content = True) + + def get_updated_activity_type(self): + if self.content_object.__class__.__name__ == 'Question': + return const.TYPE_ACTIVITY_COMMENT_QUESTION + elif self.content_object.__class__.__name__ == 'Answer': + return const.TYPE_ACTIVITY_COMMENT_ANSWER + + def get_potentially_interested_users(self): + users = set() + users.update( + #get authors of parent object and all associated comments + comment.content_object.get_author_list( + include_comments = True, + ) + ) + + users -= set([comment.user])#remove activity user + return list(users) def delete(self, **kwargs): #todo: not very good import in models of other models @@ -121,7 +136,5 @@ class Comment(MetaContent, UserContent): def get_latest_revision_number(self): return 1 - get_newly_mentioned_users = get_newly_mentioned_users_in_post - def __unicode__(self): return self.comment diff --git a/forum/models/question.py b/forum/models/question.py index 0793e601..0f46e55a 100644 --- a/forum/models/question.py +++ b/forum/models/question.py @@ -1,8 +1,6 @@ -from base import * #todo maybe remove * +from base import Content, DeletableContent, AnonymousContent, ContentRevision from forum.models import signals from tag import Tag -#todo: make uniform import for consts -from forum.const import CONST from forum import const from forum.utils.html import sanitize_html from markdown2 import Markdown @@ -11,7 +9,11 @@ import datetime from django.conf import settings from django.utils.datastructures import SortedDict from forum.models.tag import MarkedTag -from django.db.models import Q +from django.db import models +from django.contrib.auth.models import User +from django.utils.http import urlquote as django_urlquote +from django.template.defaultfilters import slugify +from django.core.urlresolvers import reverse markdowner = Markdown(html4tags=True) @@ -56,7 +58,7 @@ class QuestionManager(models.Manager): question.add_revision( author=author, text=text, - comment=CONST['default_version'], + comment=const.POST_STATUS['default_version'], revised_at=added_at, ) return question @@ -85,11 +87,12 @@ class QuestionManager(models.Manager): if search_query: try: - qs = qs.filter( Q(title__search = search_query) \ - | Q(text__search = search_query) \ - | Q(tagnames__search = search_query) \ - | Q(answers__text__search = search_query) - ) + qs = qs.filter( + models.Q(title__search = search_query) \ + | models.Q(text__search = search_query) \ + | models.Q(tagnames__search = search_query) \ + | models.Q(answers__text__search = search_query) + ) except: #fallback to dumb title match search qs = qs.extra( @@ -117,7 +120,10 @@ class QuestionManager(models.Manager): if author_selector: try: u = User.objects.get(id=int(author_selector)) - qs = qs.filter(Q(author=u, deleted=False) | Q(answers__author=u, answers__deleted=False)) + qs = qs.filter( + models.Q(author=u, deleted=False) \ + | models.Q(answers__author=u, answers__deleted=False) + ) meta_data['author_name'] = u.username except User.DoesNotExist: meta_data['author_name'] = None @@ -180,8 +186,8 @@ class QuestionManager(models.Manager): for q in question_list: answer_list.extend(list(q.answers.all())) return User.objects.filter( - Q(questions__in=question_list) \ - | Q(answers__in=answer_list) + models.Q(questions__in=question_list) \ + | models.Q(answers__in=answer_list) ).distinct().order_by('?') def get_author_list(self, **kwargs): @@ -285,7 +291,11 @@ class Question(Content, DeletableContent): 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) + close_reason = models.SmallIntegerField( + choices=const.CLOSE_REASONS, + null=True, + blank=True + ) followed_by = models.ManyToManyField(User, related_name='followed_questions') # Denormalised data @@ -336,7 +346,7 @@ class Question(Content, DeletableContent): author = retagged_by, revised_at = retagged_at, tagnames = tagnames, - summary = CONST['retagged'], + summary = const.POST_STATUS['retagged'], text = latest_revision.text ) # send tags updated singal @@ -402,7 +412,7 @@ class Question(Content, DeletableContent): rev_no = self.revisions.all().count() + 1 if comment in (None, ''): if rev_no == 1: - comment = CONST['default_version'] + comment = const.POST_STATUS['default_version'] else: comment = 'No.%s Revision' % rev_no @@ -458,9 +468,9 @@ class Question(Content, DeletableContent): def get_question_title(self): if self.closed: - attr = CONST['closed'] + attr = const.POST_STATUS['closed'] elif self.deleted: - attr = CONST['deleted'] + attr = const.POST_STATUS['deleted'] else: attr = None if attr is not None: diff --git a/forum/models/repute.py b/forum/models/repute.py index f71be4db..ad640f59 100644 --- a/forum/models/repute.py +++ b/forum/models/repute.py @@ -1,8 +1,10 @@ -from base import * from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic from django.contrib.auth.models import User - +from django.db import models from django.utils.translation import ugettext as _ +import datetime +from forum import const class Badge(models.Model): """Awarded for notable actions performed on the site by Users.""" @@ -109,7 +111,7 @@ class Repute(models.Model): 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_type = models.SmallIntegerField(choices=const.TYPE_REPUTATION) reputation = models.IntegerField(default=1) objects = ReputeManager() diff --git a/forum/models/signals.py b/forum/models/signals.py index b3850979..f711260b 100644 --- a/forum/models/signals.py +++ b/forum/models/signals.py @@ -14,4 +14,9 @@ user_updated = django.dispatch.Signal(providing_args=['instance', 'updated_by']) #todo: move this to authentication app user_logged_in = django.dispatch.Signal(providing_args=['session']) -comment_post_save = django.dispatch.Signal(providing_args=['instance']) +post_updated = django.dispatch.Signal( + providing_args=[ + 'post', + 'newly_mentioned_users' + ] + ) diff --git a/forum/models/tag.py b/forum/models/tag.py index e13baf9b..0e585547 100644 --- a/forum/models/tag.py +++ b/forum/models/tag.py @@ -1,4 +1,7 @@ -from base import * +from base import DeletableContent +from django.db import models +from django.db import connection, transaction +from django.contrib.auth.models import User from django.utils.translation import ugettext as _ diff --git a/forum/models/user.py b/forum/models/user.py index d7678801..85e22f1d 100644 --- a/forum/models/user.py +++ b/forum/models/user.py @@ -1,12 +1,9 @@ -from base import * #todo: remove this with Django 1.2 +from django.db import models from django.contrib.contenttypes.models import ContentType from forum.models import signals from django.contrib.contenttypes import generic from django.contrib.auth.models import User -from forum.models.question import Question, QuestionRevision -from forum.models.answer import Answer, AnswerRevision -from forum.models.meta import Comment from hashlib import md5 import string from random import Random @@ -66,7 +63,6 @@ class ActivityManager(models.Manager): else: kwargs['is_auditted'] = False - mention_activity = Activity(**kwargs) mention_activity.save() @@ -78,7 +74,6 @@ class ActivityManager(models.Manager): return mention_activity - def get_mentions( self, mentioned_by = None, @@ -146,26 +141,28 @@ class Activity(models.Model): class EmailFeedSettingManager(models.Manager): - def exists_match_to_post_and_subscriber(self, post = None, subscriber = None, **kwargs): + def exists_match_to_post_and_subscriber( + self, + post = None, + subscriber = None, + newly_mentioned_users = [], + **kwargs + ): """returns list of feeds matching the post and subscriber + newly_mentioned_user parameter is there to save + on a database hit looking for mentions of subscriber + in the current post """ feeds = self.filter(subscriber = subscriber, **kwargs) for feed in feeds: if feed.feed_type == 'm_and_c': - if isinstance(post, Comment): + if post.__clas__.__name__ == 'Comment':#isinstance(post, Comment): return True else: - post_content_type = ContentType.objects.get_for_model(post) - subscriber_mentions = Mention.objects.filter( - content_type = post_content_type, - object_id = post.id, - mentioned_whom = subscriber, - is_auditted = False - ) - if subscriber_mentions: + if subscriber in newly_mentioned_users: return True else: if feed.feed_type == 'q_all': @@ -256,7 +253,10 @@ class EmailFeedSetting(models.Model): 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) + 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) @@ -284,7 +284,14 @@ from forum.utils.time import one_day_from_now class ValidationHashManager(models.Manager): def _generate_md5_hash(self, user, type, hash_data, seed): - return md5("%s%s%s%s" % (seed, "".join(map(str, hash_data)), user.id, type)).hexdigest() + return md5( + "%s%s%s%s" % ( + seed, + "".join(map(str, hash_data)), + user.id, + type + ) + ).hexdigest() def create_new(self, user, type, hash_data=[], expiration=None): seed = ''.join(Random().sample(string.letters+string.digits, 12)) @@ -314,7 +321,13 @@ class ValidationHashManager(models.Manager): if obj.user != user: return False - valid = (obj.hash_code == self._generate_md5_hash(obj.user, type, hash_data, obj.seed)) + valid = (obj.hash_code == self._generate_md5_hash( + obj.user, + type, + hash_data, + obj.seed + ) + ) if valid: if obj.expiration < datetime.datetime.now(): diff --git a/forum/models/utils.py b/forum/utils/markup.py index bfe7b1c9..2b2b8ab1 100644 --- a/forum/models/utils.py +++ b/forum/utils/markup.py @@ -1,30 +1,16 @@ from forum import const -#from forum.models import Comment, Question, Answer -#from forum.models import QuestionRevision, AnswerRevision -from forum.models import Activity, User -#todo: don't like that this file deals with models directly -def _make_mention(mentioned_whom, context_object = None): - mentioned_by = context_object.get_last_author() - if mentioned_whom: - if mentioned_whom != mentioned_by: - m = Activity.objects.create_new_mention( - mentioned_by = mentioned_by, - mentioned_whom = mentioned_whom, - mentioned_in = context_object - ) - url = mentioned_whom.get_profile_url() - username = mentioned_whom.username - return '<a href="%s">@%s</a>' % (url, username) - else: - return '@' +def format_mention_in_html(mentioned_user): + url = mentioned_user.get_profile_url() + username = mentioned_user.username + return '<a href="%s">@%s</a>' % (url, username) -def _extract_matching_mentioned_author(text, authors): +def extract_first_matching_mentioned_author(text, anticipated_authors): if len(text) == 0: return None, '' - for a in authors: + for a in anticipated_authors: if text.startswith(a.username): ulen = len(a.username) if len(text) == ulen: @@ -38,40 +24,36 @@ def _extract_matching_mentioned_author(text, authors): return a, text return None, text -def mentionize(text, context_object = None): - - if '@' not in text: - return text - - op = context_object.get_origin_post() - authors = op.get_author_list( include_comments = True, recursive = True ) - - text_copy = text +def extract_mentioned_name_seeds(text): extra_name_seeds = set() - while '@' in text_copy: - pos = text_copy.index('@') - text_copy = text_copy[pos+1:]#chop off prefix + while '@' in text: + pos = text.index('@') + text = text[pos+1:]#chop off prefix name_seed = '' - for c in text_copy: + for c in text: if c in const.TWITTER_STYLE_MENTION_TERMINATION_CHARS: extra_name_seeds.add(name_seed) + name_seed = '' break if len(name_seed) > 10: extra_name_seeds.add(name_seed) + name_seed = '' break if c == '@': - extra_name_seeds.add(name_seed) + if len(name_seed) > 0: + extra_name_seeds.add(name_seed) + name_seed = '' break name_seed += c - - extra_authors = set() - for name_seed in extra_name_seeds: if len(name_seed) > 0: - extra_authors.update(User.objects.filter(username__startswith = name_seed)) + #in case we run off the end of text + extra_name_seeds.add(name_seed) - authors += list(extra_authors) + return extra_name_seeds +def mentionize_text(text, anticipated_authors): output = '' + mentioned_authors = list() while '@' in text: #the purpose of this loop is to convert any occurance of '@mention ' syntax #to user account links leading space is required unless @ is the first @@ -94,8 +76,15 @@ def mentionize(text, context_object = None): #if there is a termination character before @mention #indeed try to find a matching person text = text[pos+1:] - matching_author, text = _extract_matching_mentioned_author(text, authors) - output += _make_mention(matching_author, context_object = context_object) + mentioned_author, text = extract_first_matching_mentioned_author( + text, + authors + ) + if mentioned_author: + mentioned_authors.append(mentioned_author) + output += format_mention_in_html(mentioned_author) + else: + output += '@' else: #if there isn't, i.e. text goes like something@mention, do not look up people @@ -104,10 +93,13 @@ def mentionize(text, context_object = None): else: #do this if @ is the first character text = text[1:] - matching_author, text = _extract_matching_mentioned_author(text, authors) - output += _make_mention(matching_author, context_object = context_object) + mentioned_author, text = extract_first_matching_mentioned_name(text, authors) + if mentioned_author: + mentioned_authors.append(mentioned_author) + output += format_mention_in_html(mentioned_author) + else: + output += '@' #append the rest of text that did not have @ symbols output += text - return output - + return mentioned_authors, output diff --git a/log/README.TXT b/log/README.TXT deleted file mode 100755 index 9c51276d..00000000 --- a/log/README.TXT +++ /dev/null @@ -1 +0,0 @@ -this file is just a placeholder so the empty directory is not ignored by version control |