From: hernani Date: Mon, 24 May 2010 11:10:22 +0000 (+0000) Subject: Some improvements in cache. X-Git-Tag: live~816 X-Git-Url: https://git.openstreetmap.org./osqa.git/commitdiff_plain/0ba16baba0615dd405486c7d87f943d71518375c Some improvements in cache. Convert answer to comment. Fixed error user profile when editing date. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@315 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- diff --git a/forum/actions/meta.py b/forum/actions/meta.py index 1862cb3..7c95110 100644 --- a/forum/actions/meta.py +++ b/forum/actions/meta.py @@ -27,6 +27,14 @@ class VoteAction(ActionProxy): except: return None + @classmethod + def get_action_for(cls, user, node): + try: + vote = Vote.objects.get(user=user, node=node) + return vote.action + except: + return None + def describe_vote(self, vote_desc, viewer=None): return _("%(user)s %(vote_desc)s %(post_desc)s") % { 'user': self.hyperlink(self.user.get_profile_url(), self.friendly_username(viewer, self.user)), @@ -183,9 +191,9 @@ class DeleteAction(ActionProxy): self.node.question.reset_answer_count_cache() def describe(self, viewer=None): - return _("%(user)s deleted %(post_desc)s: %(reason)s") % { + return _("%(user)s deleted %(post_desc)s") % { 'user': self.hyperlink(self.user.get_profile_url(), self.friendly_username(viewer, self.user)), - 'post_desc': self.describe_node(viewer, self.node), 'reason': self.reason(), + 'post_desc': self.describe_node(viewer, self.node) } def reason(self): diff --git a/forum/actions/node.py b/forum/actions/node.py index 931b6e1..5283a8e 100644 --- a/forum/actions/node.py +++ b/forum/actions/node.py @@ -148,4 +148,28 @@ class CloseAction(ActionProxy): 'user': self.hyperlink(self.user.get_profile_url(), self.friendly_username(viewer, self.user)), 'post_desc': self.describe_node(viewer, self.node), 'reason': self.extra - } \ No newline at end of file + } + +class AnswerToCommentAction(ActionProxy): + verb = _("converted") + + def process_data(self, new_parent=None): + self.node.parent = new_parent + self.node.node_type = "comment" + + for comment in self.node.comments.all(): + comment.parent = new_parent + comment.save() + + self.node.save() + try: + self.node.abs_parent.reset_answer_count_cache() + except AttributeError: + pass + + def describe(self, viewer=None): + return _("%(user)s converted an answer to %(question)s into a comment") % { + 'user': self.hyperlink(self.user.get_profile_url(), self.friendly_username(viewer, self.user)), + 'question': self.describe_node(viewer, self.node.abs_parent), + } + diff --git a/forum/models/action.py b/forum/models/action.py index 4eed471..d892ac9 100644 --- a/forum/models/action.py +++ b/forum/models/action.py @@ -4,18 +4,24 @@ from threading import Thread from base import * import re -class ActionQuerySet(models.query.QuerySet): +class ActionQuerySet(CachedQuerySet): + def obj_from_datadict(self, datadict): + cls = ActionProxyMetaClass.types.get(datadict['action_type'], None) + if cls: + obj = cls() + obj.__dict__.update(datadict) + return obj + else: + return super(ActionQuerySet, self).obj_from_datadict(datadict) + def get(self, *args, **kwargs): - action = super(ActionQuerySet, self).get(*args, **kwargs) - if self.model == Action: - return action.leaf() - return action + return super(ActionQuerySet, self).get(*args, **kwargs).leaf() -class ActionManager(models.Manager): +class ActionManager(CachedManager): use_for_related_fields = True def get_query_set(self): - qs = ActionQuerySet(self.model).filter(canceled=False) + qs = ActionQuerySet(self.model) if self.model is not Action: return qs.filter(action_type=self.model.get_type()) @@ -26,7 +32,8 @@ class ActionManager(models.Manager): kwargs['action_type__in'] = [t.get_type() for t in types] return self.get(*args, **kwargs) -class Action(models.Model): + +class Action(BaseModel): user = models.ForeignKey('User', related_name="actions") ip = models.CharField(max_length=16) node = models.ForeignKey('Node', null=True, related_name="actions") @@ -94,7 +101,9 @@ class Action(models.Model): return self leaf = leaf_cls() - leaf.__dict__ = self.__dict__ + d = self._as_dict() + leaf.__dict__.update(self._as_dict()) + l = leaf._as_dict() return leaf @classmethod @@ -121,9 +130,9 @@ class Action(models.Model): return self - def delete(self): + def delete(self, *args, **kwargs): self.cancel_action() - super(Action, self).delete() + super(Action, self).delete(*args, **kwargs) def cancel(self, user=None, ip=None): if not self.canceled: @@ -174,7 +183,7 @@ def trigger_hooks_threaded(action, hooks, new): except Exception, e: logging.error("Error in %s hook: %s" % (cls.__name__, str(e))) -class ActionProxyMetaClass(models.Model.__metaclass__): +class ActionProxyMetaClass(BaseMetaClass): types = {} def __new__(cls, *args, **kwargs): diff --git a/forum/models/base.py b/forum/models/base.py index 827dfb2..dfdf989 100644 --- a/forum/models/base.py +++ b/forum/models/base.py @@ -40,6 +40,11 @@ class CachedQuerySet(models.query.QuerySet): else: return self + def obj_from_datadict(self, datadict): + obj = self.model() + obj.__dict__.update(datadict) + return obj + def get(self, *args, **kwargs): try: pk = [v for (k,v) in kwargs.items() if k in ('pk', 'pk__exact', 'id', 'id__exact' @@ -53,30 +58,21 @@ class CachedQuerySet(models.query.QuerySet): if obj is None: obj = super(CachedQuerySet, self).get(*args, **kwargs) - obj.__class__.objects.cache_obj(obj) + obj.cache() + else: + obj = self.obj_from_datadict(obj) + obj.reset_original_state() return obj return super(CachedQuerySet, self).get(*args, **kwargs) -from action import Action - class CachedManager(models.Manager): use_for_related_fields = True - int_cache_re = re.compile('^_[\w_]+cache$') def get_query_set(self): return CachedQuerySet(self.model) - def cache_obj(self, obj): - int_cache_keys = [k for k in obj.__dict__.keys() if self.int_cache_re.match(k)] - d = obj.__dict__ - for k in int_cache_keys: - if not isinstance(obj.__dict__[k], Action): - del obj.__dict__[k] - - cache.set(self.model.cache_key(obj.id), obj, 60 * 60) - def get_or_create(self, *args, **kwargs): try: return self.get(*args, **kwargs) @@ -90,7 +86,7 @@ class DenormalizedField(object): self.filter = kwargs def setup_class(self, cls, name): - dict_name = '_%s_cache_' % name + dict_name = '_%s_dencache_' % name def getter(inst): val = inst.__dict__.get(dict_name, None) @@ -98,13 +94,13 @@ class DenormalizedField(object): if val is None: val = getattr(inst, self.manager).filter(**self.filter).count() inst.__dict__[dict_name] = val - inst.__class__.objects.cache_obj(inst) + inst.cache() return val def reset_cache(inst): inst.__dict__.pop(dict_name, None) - inst.__class__.objects.cache_obj(inst) + inst.uncache() cls.add_to_class(name, property(getter)) cls.add_to_class("reset_%s_cache" % name, reset_cache) @@ -139,20 +135,35 @@ class BaseModel(models.Model): def __init__(self, *args, **kwargs): super(BaseModel, self).__init__(*args, **kwargs) - self._original_state = dict([(k, v) for k,v in self.__dict__.items() if not k in kwargs]) + self.reset_original_state() @classmethod def cache_key(cls, pk): - return '%s.%s:%s' % (settings.APP_URL, cls.__name__, pk) + return '%s:%s:%s' % (settings.APP_URL, cls.__name__, pk) + + def reset_original_state(self): + self._original_state = self._as_dict() def get_dirty_fields(self): - missing = object() - return dict([(k, self._original_state.get(k, None)) for k,v in self.__dict__.items() - if self._original_state.get(k, missing) == missing or self._original_state[k] != v]) + return [f.name for f in self._meta.fields if self._original_state[f.attname] != self.__dict__[f.attname]] + + def _as_dict(self): + return dict([(name, getattr(self, name)) for name in + ([f.attname for f in self._meta.fields] + [k for k in self.__dict__.keys() if k.endswith('_dencache_')]) + ]) + + def _get_update_kwargs(self): + return dict([ + (f.name, getattr(self, f.name)) for f in self._meta.fields if self._original_state[f.attname] != self.__dict__[f.attname] + ]) def save(self, *args, **kwargs): put_back = [k for k, v in self.__dict__.items() if isinstance(v, models.expressions.ExpressionNode)] - super(BaseModel, self).save() + + if self.id: + self.__class__.objects.filter(id=self.id).update(**self._get_update_kwargs()) + else: + super(BaseModel, self).save() if put_back: try: @@ -163,115 +174,23 @@ class BaseModel(models.Model): logging.error("Unable to read %s from %s" % (", ".join(put_back), self.__class__.__name__)) self.uncache() - self._original_state = dict(self.__dict__) + self.reset_original_state() self.cache() def cache(self): - self.__class__.objects.cache_obj(self) + cache.set(self.cache_key(self.id), self._as_dict(), 60 * 60) def uncache(self): - cache.delete(self.cache_key(self.pk)) + cache.delete(self.cache_key(self.id)) def delete(self): self.uncache() super(BaseModel, self).delete() -class ActiveObjectManager(models.Manager): - use_for_related_fields = True - def get_query_set(self): - return super(ActiveObjectManager, self).get_query_set().filter(canceled=False) - -class UndeletedObjectManager(models.Manager): - def get_query_set(self): - return super(UndeletedObjectManager, self).get_query_set().filter(deleted=False) - -class GenericContent(models.Model): - content_type = models.ForeignKey(ContentType) - object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey('content_type', 'object_id') - - class Meta: - abstract = True - app_label = 'forum' - -class MetaContent(BaseModel): - node = models.ForeignKey('Node', null=True, related_name='%(class)ss') - - def __init__(self, *args, **kwargs): - if 'content_object' in kwargs: - kwargs['node'] = kwargs['content_object'] - del kwargs['content_object'] - - super (MetaContent, self).__init__(*args, **kwargs) - - @property - def content_object(self): - return self.node.leaf - - class Meta: - abstract = True - app_label = 'forum' - from user import User - -class UserContent(models.Model): - user = models.ForeignKey(User, related_name='%(class)ss') - - class Meta: - abstract = True - app_label = 'forum' - - -class DeletableContent(models.Model): - deleted = models.BooleanField(default=False) - deleted_at = models.DateTimeField(null=True, blank=True) - deleted_by = models.ForeignKey(User, null=True, blank=True, related_name='deleted_%(class)ss') - - active = UndeletedObjectManager() - - class Meta: - abstract = True - app_label = 'forum' - - def mark_deleted(self, user): - if not self.deleted: - self.deleted = True - self.deleted_at = datetime.datetime.now() - self.deleted_by = user - self.save() - return True - else: - return False - - def unmark_deleted(self): - if self.deleted: - self.deleted = False - self.save() - return True - else: - return False - -mark_canceled = django.dispatch.Signal(providing_args=['instance']) - -class CancelableContent(models.Model): - canceled = models.BooleanField(default=False) - - def cancel(self): - if not self.canceled: - self.canceled = True - self.save() - mark_canceled.send(sender=self.__class__, instance=self) - return True - - return False - - class Meta: - abstract = True - app_label = 'forum' - - from node import Node, NodeRevision, NodeManager +from action import Action diff --git a/forum/models/node.py b/forum/models/node.py index 6f4e12b..4be2a00 100644 --- a/forum/models/node.py +++ b/forum/models/node.py @@ -79,13 +79,17 @@ class NodeMetaClass(BaseMetaClass): class NodeQuerySet(CachedQuerySet): - def get(self, *args, **kwargs): - node = super(NodeQuerySet, self).get(*args, **kwargs) - cls = NodeMetaClass.types.get(node.node_type, None) + def obj_from_datadict(self, datadict): + cls = NodeMetaClass.types.get(datadict.get("node_type", ""), None) + if cls: + obj = cls() + obj.__dict__.update(datadict) + return obj + else: + return super(NodeQuerySet, self).obj_from_datadict(datadict) - if cls and (node.__class__ is not cls): - return node.leaf - return node + def get(self, *args, **kwargs): + return super(NodeQuerySet, self).get(*args, **kwargs).leaf class NodeManager(CachedManager): @@ -140,7 +144,7 @@ class Node(BaseModel, NodeContent): @classmethod def cache_key(cls, pk): - return '%s.node:%s' % (settings.APP_URL, pk) + return '%s:node:%s' % (settings.APP_URL, pk) @classmethod def get_type(cls): @@ -209,8 +213,8 @@ class Node(BaseModel, NodeContent): if not 'tagnames' in dirty: return None else: - if dirty['tagnames']: - old_tags = set(name for name in dirty['tagnames'].split(u' ')) + if self._original_state['tagnames']: + old_tags = set(name for name in self._original_state['tagnames'].split(u' ')) else: old_tags = set() new_tags = set(name for name in self.tagnames.split(u' ') if name) @@ -269,6 +273,8 @@ class Node(BaseModel, NodeContent): tag.save() def save(self, *args, **kwargs): + tags_changed = self._process_changes_in_tags() + if not self.id: self.node_type = self.get_type() super(BaseModel, self).save(*args, **kwargs) @@ -278,8 +284,6 @@ class Node(BaseModel, NodeContent): if self.parent_id and not self.abs_parent_id: self.abs_parent = self.parent.absolute_parent - tags_changed = self._process_changes_in_tags() - super(Node, self).save(*args, **kwargs) if tags_changed: self.tags = list(Tag.objects.filter(name__in=self.tagname_list())) diff --git a/forum/models/tag.py b/forum/models/tag.py index 16f1c6c..1344597 100644 --- a/forum/models/tag.py +++ b/forum/models/tag.py @@ -3,22 +3,45 @@ from base import * from django.utils.translation import ugettext as _ import django.dispatch -class ActiveTagManager(UndeletedObjectManager): +class ActiveTagManager(models.Manager): def get_query_set(self): - return super(UndeletedObjectManager, self).get_query_set().exclude(used_count=0) + return super(ActiveTagManager, self).get_query_set().exclude(deleted=False, used_count=0) -class Tag(BaseModel, DeletableContent): +class Tag(BaseModel): name = models.CharField(max_length=255, unique=True) created_by = models.ForeignKey(User, related_name='created_tags') marked_by = models.ManyToManyField(User, related_name="marked_tags", through="MarkedTag") # Denormalised data used_count = models.PositiveIntegerField(default=0) + 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_%(class)ss') + active = ActiveTagManager() - class Meta(DeletableContent.Meta): + def mark_deleted(self, user): + if not self.deleted: + self.deleted = True + self.deleted_at = datetime.datetime.now() + self.deleted_by = user + self.save() + return True + else: + return False + + def unmark_deleted(self): + if self.deleted: + self.deleted = False + self.save() + return True + else: + return False + + class Meta: ordering = ('-used_count', 'name') + app_label = 'forum' def __unicode__(self): return self.name diff --git a/forum/models/user.py b/forum/models/user.py index 3e92e16..c196bd4 100644 --- a/forum/models/user.py +++ b/forum/models/user.py @@ -55,12 +55,18 @@ class AnonymousUser(DjangoAnonymousUser): def can_delete_comment(self, comment): return False + def can_convert_to_comment(self, answer): + return False + def can_accept_answer(self, answer): return False def can_edit_post(self, post): return False + def can_wikify(self, post): + return False + def can_retag_questions(self): return False @@ -201,6 +207,9 @@ class User(BaseModel, DjangoUser): def can_delete_comment(self, comment): return self == comment.author or self.reputation >= int(settings.REP_TO_DELETE_COMMENTS) + def can_convert_to_comment(self, answer): + return (not answer.marked) and (self.is_superuser or self.is_staff or answer.author == self or self.reputation >= int(settings.REP_TO_CONVERT_TO_COMMENT)) + @true_if_is_super_or_staff def can_accept_answer(self, answer): return self == answer.question.author @@ -210,6 +219,10 @@ class User(BaseModel, DjangoUser): return self == post.author or self.reputation >= int(settings.REP_TO_EDIT_OTHERS ) or (post.wiki and self.reputation >= int(settings.REP_TO_EDIT_WIKI)) + @true_if_is_super_or_staff + def can_wikify(self, post): + return self == post.author or self.reputation >= int(settings.REP_TO_WIKIFY) + @true_if_is_super_or_staff def can_retag_questions(self): return self.reputation >= int(settings.REP_TO_RETAG) diff --git a/forum/settings/minrep.py b/forum/settings/minrep.py index 7170a03..579215d 100644 --- a/forum/settings/minrep.py +++ b/forum/settings/minrep.py @@ -43,6 +43,10 @@ REP_TO_EDIT_WIKI = Setting('REP_TO_EDIT_WIKI', 750, MIN_REP_SET, dict( label = _("Minimum reputation to edit wiki posts"), help_text = _("The minimum reputation an user must have to be allowed to edit community wiki posts."))) +REP_TO_WIKIFY = Setting('REP_TO_WIKIFY', 2000, MIN_REP_SET, dict( +label = _("Minimum reputation to mark post as community wiki"), +help_text = _("The minimum reputation an user must have to be allowed to mark a post as community wiki."))) + REP_TO_EDIT_OTHERS = Setting('REP_TO_EDIT_OTHERS', 2000, MIN_REP_SET, dict( label = _("Minimum reputation to edit others posts"), help_text = _("The minimum reputation an user must have to be allowed to edit others posts."))) @@ -55,6 +59,10 @@ REP_TO_DELETE_COMMENTS = Setting('REP_TO_DELETE_COMMENTS', 2000, MIN_REP_SET, di label = _("Minimum reputation to delete comments"), help_text = _("The minimum reputation an user must have to be allowed to delete comments."))) +REP_TO_CONVERT_TO_COMMENT = Setting('REP_TO_CONVERT_TO_COMMENT', 2000, MIN_REP_SET, dict( +label = _("Minimum reputation to convert answers to comment"), +help_text = _("The minimum reputation an user must have to be allowed to convert an answer into a comment."))) + REP_TO_VIEW_FLAGS = Setting('REP_TO_VIEW_FLAGS', 2000, MIN_REP_SET, dict( label = _("Minimum reputation to view offensive flags"), help_text = _("The minimum reputation an user must have to view offensive flags."))) diff --git a/forum/skins/default/media/js/osqa.main.js b/forum/skins/default/media/js/osqa.main.js index b5aee40..af80421 100644 --- a/forum/skins/default/media/js/osqa.main.js +++ b/forum/skins/default/media/js/osqa.main.js @@ -229,10 +229,14 @@ function load_prompt(evt, url) { $.get(url, function(data) { var $dialog = show_dialog({ html: data, - //extra_class: 'warning', + extra_class: 'prompt', event: evt, yes_callback: function() { - $.post(url, {prompt: $dialog.find('.prompt-return').val()}, function(data) { + var postvars = {}; + $dialog.find('input, textarea, select').each(function() { + postvars[$(this).attr('name')] = $(this).val(); + }); + $.post(url, postvars, function(data) { $dialog.fadeOut('fast', function() { $dialog.remove(); }); @@ -300,6 +304,22 @@ $(function() { return false }); + $('.context-menu').each(function() { + var $menu = $(this); + var $trigger = $menu.find('.context-menu-trigger'); + var $dropdown = $menu.find('.context-menu-dropdown'); + + $trigger.click(function() { + $dropdown.slideToggle('fast', function() { + if ($dropdown.is(':visible')) { + $dropdown.one('clickoutside', function() { + $dropdown.slideUp('fast') + }); + } + }); + }); + }); + $('div.comment-form-container').each(function() { var $container = $(this); var $form = $container.find('form'); diff --git a/forum/skins/default/media/style/style.css b/forum/skins/default/media/style/style.css index 3084b55..a9d2072 100644 --- a/forum/skins/default/media/style/style.css +++ b/forum/skins/default/media/style/style.css @@ -1330,20 +1330,6 @@ div.comment-tools a:hover { background: url('/m/default/media/images/vote-accepted-on.png') } -.user-prompt { - width: 300px; -} - -.user-prompt select, .user-prompt textarea { - width: 100%; - padding: 0; - border: 0; -} - -.user-prompt .prompt-buttons { - text-align: right; -} - .comment-form-buttons { width: 18%; height: 100%; @@ -1373,7 +1359,11 @@ div.comment-tools a:hover { overflow-y: auto; } -div.dialog { +.context-menu { + position: relative; +} + +div.dialog, .context-menu-dropdown { position: absolute; background-color: #EEEEEE; -moz-border-radius: 5px; @@ -1382,6 +1372,49 @@ div.dialog { -webkit-box-shadow: 2px 2px 5px #3060A8; } +.context-menu-dropdown { + display: none; + right: 0px; + top: 1.5em; + text-align: left; + list-style-type: none; +} + +.context-menu-dropdown li.item { + padding: 4px 8px 4px 8px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} + +.context-menu-dropdown li.item a { + color: inherit; + white-space: nowrap; + text-decoration: none; +} + +.context-menu-dropdown li.separator { + text-align: center; + padding: 10px 0 4px 0; + font-size: 120%; + font-weight: bold; +} + +.context-menu-dropdown li.item:hover { + background-color: #3060A8; + color: white; +} + +.context-menu-dropdown span { + margin-right: 4px; + float: left; + width: 16px; + height: 16px; +} + +.context-menu-trigger { + cursor: pointer; +} + div.dialog .dialog-content { padding: 12px 12px 37px 12px; } @@ -1415,4 +1448,18 @@ div.dialog.confirm, div.dialog.warning { div.dialog.confirm { font-size: 140%; font-weight: bold; +} + +div.dialog.prompt { + width: 300px; +} + +div.dialog.prompt .dialog-content select, div.dialog.prompt .dialog-content textarea, div.dialog.prompt .dialog-content input[type=text] { + width: 100%; + padding: 0; + border: 0; +} + +.user-prompt .prompt-buttons { + text-align: right; } \ No newline at end of file diff --git a/forum/skins/default/templates/_question_list.html b/forum/skins/default/templates/_question_list.html deleted file mode 100644 index 02ef16d..0000000 --- a/forum/skins/default/templates/_question_list.html +++ /dev/null @@ -1,39 +0,0 @@ -{% load i18n %} -{% load humanize %} -{% load extra_filters %} -{% load extra_tags %} - -
- {% for question in questions.object_list %} -
-
-
-
{{question.score|intcomma}}
-
{% trans "votes" %}
-
-
-
{{question.answer_count|intcomma}}
-
{% trans "answers" %}
-
-
-
{{question.view_count|cnprog_intword|safe}}
-
{% trans "views" %}
-
-
- -

{{question.title}}

-
- {% diff_date question.last_activity_at %} - {% if question.last_activity_by %} - {{ question.last_activity_by }} {% get_score_badge question.last_activity_by %} - {% endif %} -
- -
- {% for tag in question.tagname_list %} - - {% endfor %} -
-
- {% endfor %} -
\ No newline at end of file diff --git a/forum/skins/default/templates/node/convert_to_comment.html b/forum/skins/default/templates/node/convert_to_comment.html new file mode 100644 index 0000000..cbaa4cd --- /dev/null +++ b/forum/skins/default/templates/node/convert_to_comment.html @@ -0,0 +1,10 @@ +{% load i18n %} + +
+

{% trans "Place the comment under:" %}

+ +
\ No newline at end of file diff --git a/forum/skins/default/templates/node/post_controls.html b/forum/skins/default/templates/node/post_controls.html index c24660b..9311e4b 100644 --- a/forum/skins/default/templates/node/post_controls.html +++ b/forum/skins/default/templates/node/post_controls.html @@ -1,3 +1,4 @@ +{% load i18n %} {% spaceless %} {% for control in controls %} @@ -8,4 +9,15 @@ | {% endifnotequal %} {% endfor %} +{% if menu|length %} + | + {% trans "more" %} ▼ + + +{% endif %} {% endspaceless %} \ No newline at end of file diff --git a/forum/skins/default/templates/node/report.html b/forum/skins/default/templates/node/report.html index c8607bb..68b66d9 100644 --- a/forum/skins/default/templates/node/report.html +++ b/forum/skins/default/templates/node/report.html @@ -1,14 +1,12 @@ {% load i18n %} -
- {% trans "Please select a reason bellow or use the text box to input your own reason." %} - - -
+{% trans "Please select a reason bellow or use the text box to input your own reason." %} + + @@ -34,7 +35,7 @@
{{ rep.negative }}
- {{ rep.action.describe|safe }}
({{ rep.date }}) + {% activity_item rep.action request.user %}

{% endfor %} diff --git a/forum/templatetags/node_tags.py b/forum/templatetags/node_tags.py index 72bfaf2..4548194 100644 --- a/forum/templatetags/node_tags.py +++ b/forum/templatetags/node_tags.py @@ -29,7 +29,7 @@ def accept_button(answer, user): @register.inclusion_tag('node/favorite_mark.html') def favorite_mark(question, user): try: - FavoriteAction.objects.get(node=question, user=user) + FavoriteAction.objects.get(canceled=False, node=question, user=user) favorited = True except: favorited = False @@ -42,6 +42,7 @@ def post_control(text, url, command=False, withprompt=False, title=""): @register.inclusion_tag('node/post_controls.html') def post_controls(post, user): controls = [] + menu = [] if user.is_authenticated(): post_type = (post.__class__ is Question) and 'question' or 'answer' @@ -78,7 +79,15 @@ def post_controls(post, user): controls.append(post_control(_('delete'), reverse('delete_post', kwargs={'id': post.id}), command=True)) - return {'controls': controls} + if user.can_wikify(post): + menu.append(post_control(_('mark as community wiki'), reverse('wikify', kwargs={'id': post.id}), + command=True)) + + if post.node_type == "answer" and user.can_convert_to_comment(post): + menu.append(post_control(_('convert to comment'), reverse('convert_to_comment', kwargs={'id': post.id}), + command=True, withprompt=True)) + + return {'controls': controls, 'menu': menu, 'post': post, 'user': user} @register.inclusion_tag('node/comments.html') def comments(post, user): @@ -131,8 +140,8 @@ def contributors_info(node): 'node': node, } - @register.inclusion_tag("node/reviser_info.html") def reviser_info(revision): return {'revision': revision} + diff --git a/forum/templatetags/user_tags.py b/forum/templatetags/user_tags.py index d41cf47..b3f2bdf 100644 --- a/forum/templatetags/user_tags.py +++ b/forum/templatetags/user_tags.py @@ -42,6 +42,7 @@ class ActivityNode(template.Node): describe = mark_safe(action.describe(viewer)) return self.template.render(template.Context(dict(action=action, describe=describe))) except Exception, e: + #return action.action_type + ":" + str(e) logging.error("Error in %s action describe: %s" % (action.action_type, str(e))) @register.tag diff --git a/forum/urls.py b/forum/urls.py index 588c50d..00894ef 100644 --- a/forum/urls.py +++ b/forum/urls.py @@ -73,9 +73,11 @@ urlpatterns += patterns('', url(r'^%s(?P\d+)/$' % _('subscribe/'), app.commands.subscribe, name="subscribe"), url(r'^%s' % _('matching_tags/'), app.commands.matching_tags, name='matching_tags'), url(r'^%s(?P\d+)/' % _('node_markdown/'), app.commands.node_markdown, name='node_markdown'), + url(r'^%s(?P\d+)/' % _('convert/'), app.commands.convert_to_comment, name='convert_to_comment'), + url(r'^%s(?P\d+)/' % _('wikify/'), app.commands.wikify, name='wikify'), #place general question item in the end of other operations - url(r'^%s(?P\d+)/(?P[\w-]*)$' % _('question/'), app.readers.question, name='question'), + url(r'^%s(?P\d+)/(?P[\w-]*)$' % _('questions/'), app.readers.question, name='question'), url(r'^%s$' % _('tags/'), app.readers.tags, name='tags'), url(r'^%s(?P.*)/$' % _('tags/'), app.readers.tag, name='tag_questions'), diff --git a/forum/views/admin.py b/forum/views/admin.py index 543623d..bfe8919 100644 --- a/forum/views/admin.py +++ b/forum/views/admin.py @@ -143,7 +143,7 @@ def get_recent_activity(): return Action.objects.order_by('-action_date')[0:30] def get_flagged_posts(): - return Action.objects.filter(action_type="flag").order_by('-action_date')[0:30] + return Action.objects.filter(canceled=False, action_type="flag").order_by('-action_date')[0:30] def get_statistics(): return { diff --git a/forum/views/commands.py b/forum/views/commands.py index e09924b..1b3afc5 100644 --- a/forum/views/commands.py +++ b/forum/views/commands.py @@ -7,6 +7,7 @@ from django.shortcuts import get_object_or_404, render_to_response from django.utils.translation import ungettext, ugettext as _ from django.template import RequestContext from forum.models import * +from forum.models.node import NodeMetaClass from forum.actions import * from django.core.urlresolvers import reverse from django.contrib.auth.decorators import login_required @@ -69,9 +70,9 @@ def vote_post(request, id, vote_type): new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction score_inc = 0 - try: - old_vote = Action.objects.get_for_types((VoteUpAction, VoteDownAction), node=post, user=user) + old_vote = VoteAction.get_action_for(node=post, user=user) + if old_vote: if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)): raise CommandException( _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") % @@ -80,8 +81,6 @@ def vote_post(request, id, vote_type): old_vote.cancel(ip=request.META['REMOTE_ADDR']) score_inc += (old_vote.__class__ == VoteDownAction) and 1 or -1 - except ObjectDoesNotExist: - old_vote = None if old_vote.__class__ != new_vote_cls: new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save() @@ -127,7 +126,7 @@ def flag_post(request, id): raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY)) try: - current = FlagAction.objects.get(user=user, node=post) + current = FlagAction.objects.get(canceled=False, user=user, node=post) raise CommandException(_("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra}) except ObjectDoesNotExist: reason = request.POST.get('prompt', '').strip() @@ -153,11 +152,12 @@ def like_comment(request, id): if not user.can_like_comment(comment): raise NotEnoughRepPointsException( _('like comments')) - try: - like = VoteUpCommentAction.objects.get(node=comment, user=user) + like = VoteAction.get_action_for(node=comment, user=user) + + if like: like.cancel(ip=request.META['REMOTE_ADDR']) likes = False - except ObjectDoesNotExist: + else: VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save() likes = True @@ -196,7 +196,7 @@ def mark_favorite(request, id): raise AnonymousNotAllowedException(_('mark a question as favorite')) try: - favorite = FavoriteAction.objects.get(node=question, user=request.user) + favorite = FavoriteAction.objects.get(canceled=False, node=question, user=request.user) favorite.cancel(ip=request.META['REMOTE_ADDR']) added = False except ObjectDoesNotExist: @@ -358,6 +358,45 @@ def close(request, id, close): } } +@command +def wikify(request, id): + pass + +@command +def convert_to_comment(request, id): + user = request.user + answer = get_object_or_404(Answer, id=id) + question = answer.question + + if not request.POST: + description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': a.author.username, 'snippet': a.summary[:10]} + nodes = [(question.id, _("Question"))] + [nodes.append((a.id, description(a))) for a in question.answers.filter(deleted=None).exclude(id=answer.id)] + + return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes}) + + if not user.is_authenticated(): + raise AnonymousNotAllowedException(_("convert answers to comments")) + + if not user.can_convert_to_comment(answer): + raise NotEnoughRepPointsException(_("convert answers to comments")) + + try: + new_parent = Node.objects.get(id=request.POST.get('under', None)) + except: + raise CommandException(_("That is an invalid post to put the comment under")) + + if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)): + raise CommandException(_("That is an invalid post to put the comment under")) + + AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent)) + + return { + 'commands': { + 'refresh_page': [] + } + } + @command def subscribe(request, id): question = get_object_or_404(Question, id=id) diff --git a/forum/views/users.py b/forum/views/users.py index 55a1609..67f0a90 100644 --- a/forum/views/users.py +++ b/forum/views/users.py @@ -15,7 +15,7 @@ from django.utils import simplejson from django.core.urlresolvers import reverse from forum.forms import * from forum.utils.html import sanitize_html -from datetime import date +from datetime import datetime, date import decorators from forum.actions import EditProfileAction, FavoriteAction, BonusRepAction @@ -96,9 +96,9 @@ def edit_user(request, id): 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']) + user.date_of_birth = form.cleaned_data['birthday'] if user.date_of_birth == "None": - user.date_of_birth = '1900-01-01' + user.date_of_birth = datetime(1900, 1, 1, 0, 0) user.about = sanitize_html(form.cleaned_data['about']) user.save() @@ -233,7 +233,7 @@ def user_reputation(request, user): @user_view('users/questions.html', 'favorites', _('favorite questions'), _('favorite questions')) def user_favorites(request, user): - favorites = FavoriteAction.objects.filter(user=user) + favorites = FavoriteAction.objects.filter(canceled=False, user=user) return {"favorites" : favorites, "view_user" : user} diff --git a/forum_modules/pgfulltext/handlers.py b/forum_modules/pgfulltext/handlers.py index 6e2165b..ff29f14 100644 --- a/forum_modules/pgfulltext/handlers.py +++ b/forum_modules/pgfulltext/handlers.py @@ -4,19 +4,21 @@ from forum.modules.decorators import decorate @decorate(QuestionManager.search, needs_origin=False) def question_search(self, keywords): + tsquery = " | ".join(keywords.split(' ')) + return self.extra( tables = ['forum_rootnode_doc'], select={ 'ranking': """ - rank_exact_matches(ts_rank_cd('{0.1, 0.2, 0.8, 1.0}'::float4[], "forum_rootnode_doc"."document", plainto_tsquery('english', %s), 32)) + rank_exact_matches(ts_rank_cd('{0.1, 0.2, 0.8, 1.0}'::float4[], "forum_rootnode_doc"."document", to_tsquery('english', %s), 32)) """, }, where=[""" - "forum_rootnode_doc"."node_id" = "forum_node"."id" AND ("forum_rootnode_doc"."document" @@ plainto_tsquery('english', %s) OR + "forum_rootnode_doc"."node_id" = "forum_node"."id" AND ("forum_rootnode_doc"."document" @@ to_tsquery('english', %s) OR "forum_node"."title" ILIKE '""" + keywords + """%%') """], - params=[keywords], - select_params=[keywords], + params=[tsquery], + select_params=[tsquery], order_by=['-ranking'] )