]> git.openstreetmap.org Git - osqa.git/blob - forum/models/action.py
Fixes a bug happening when processing an user reputation more than once in the same...
[osqa.git] / forum / models / action.py
1 from django.utils.translation import ugettext as _
2 from utils import PickledObjectField
3 from threading import Thread
4 from forum.utils import html
5 from base import *
6 import re
7
8 class ActionQuerySet(CachedQuerySet):
9     def obj_from_datadict(self, datadict):
10         cls = ActionProxyMetaClass.types.get(datadict['action_type'], None)
11         if cls:
12             obj = cls()
13             obj.__dict__.update(datadict)
14             return obj
15         else:
16             return super(ActionQuerySet, self).obj_from_datadict(datadict)
17
18     def get(self, *args, **kwargs):
19         return super(ActionQuerySet, self).get(*args, **kwargs).leaf()
20
21 class ActionManager(CachedManager):
22     use_for_related_fields = True
23
24     def get_query_set(self):
25         qs = ActionQuerySet(self.model)
26
27         if self.model is not Action:
28             return qs.filter(action_type=self.model.get_type())
29         else:
30             return qs
31
32     def get_for_types(self, types, *args, **kwargs):
33         kwargs['action_type__in'] = [t.get_type() for t in types]
34         return self.get(*args, **kwargs)
35
36
37 class Action(BaseModel):
38     user = models.ForeignKey('User', related_name="actions")
39     ip   = models.CharField(max_length=16)
40     node = models.ForeignKey('Node', null=True, related_name="actions")
41     action_type = models.CharField(max_length=16)
42     action_date = models.DateTimeField(default=datetime.datetime.now)
43
44     extra = PickledObjectField()
45
46     canceled = models.BooleanField(default=False)
47     canceled_by = models.ForeignKey('User', null=True, related_name="canceled_actions")
48     canceled_at = models.DateTimeField(null=True)
49     canceled_ip = models.CharField(max_length=16)
50
51     hooks = {}
52
53     objects = ActionManager()
54
55     @property
56     def at(self):
57         return self.action_date
58
59     @property
60     def by(self):
61         return self.user
62
63     def repute_users(self):
64         pass
65
66     def process_data(self, **data):
67         pass
68
69     def process_action(self):
70         pass
71
72     def cancel_action(self):
73         pass
74
75     @property
76     def verb(self):
77         return ""
78
79     def describe(self, viewer=None):
80         return self.__class__.__name__
81
82     def get_absolute_url(self):
83         if self.node:
84             return self.node.get_absolute_url()
85         else:
86             return self.user.get_profile_url()
87
88     def repute(self, user, value):
89         repute = ActionRepute(action=self, user=user, value=value)
90         repute.save()
91         return repute
92
93     def cancel_reputes(self):
94         for repute in self.reputes.all():
95             cancel = ActionRepute(action=self, user=repute.user, value=(-repute.value), by_canceled=True)
96             cancel.save()
97
98     def leaf(self):
99         leaf_cls = ActionProxyMetaClass.types.get(self.action_type, None)
100
101         if leaf_cls is None:
102             return self
103
104         leaf = leaf_cls()
105         d = self._as_dict()
106         leaf.__dict__.update(self._as_dict())
107         l = leaf._as_dict()
108         return leaf
109
110     @classmethod
111     def get_type(cls):
112         return re.sub(r'action$', '', cls.__name__.lower())
113
114     def save(self, data=None, *args, **kwargs):
115         isnew = False
116
117         if not self.id:
118             self.action_type = self.__class__.get_type()
119             isnew = True
120
121         if data:
122             self.process_data(**data)
123
124         super(Action, self).save(*args, **kwargs)
125
126         if isnew:
127             if (self.node is None) or (not self.node.nis.wiki):
128                 self.repute_users()
129             self.process_action()
130             self.trigger_hooks(True)
131
132         return self
133
134     def delete(self, *args, **kwargs):
135         self.cancel_action()
136         super(Action, self).delete(*args, **kwargs)
137
138     def cancel(self, user=None, ip=None):
139         if not self.canceled:
140             self.canceled = True
141             self.canceled_at = datetime.datetime.now()
142             self.canceled_by = (user is None) and self.user or user
143             if ip:
144                 self.canceled_ip = ip
145             self.save()
146             self.cancel_reputes()
147             self.cancel_action()
148             #self.trigger_hooks(False)
149
150     @classmethod
151     def get_current(cls, **kwargs):
152         kwargs['canceled'] = False
153
154         try:
155             return cls.objects.get(**kwargs)
156         except cls.MultipleObjectsReturned:
157             logging.error("Got multiple values for action %s with args %s", cls.__name__,
158                           ", ".join(["%s='%s'" % i for i in kwargs.items()]))
159             raise
160         except cls.DoesNotExist:
161             return None
162
163     @classmethod
164     def hook(cls, fn):
165         if not Action.hooks.get(cls, None):
166             Action.hooks[cls] = []
167
168         Action.hooks[cls].append(fn)
169
170     def trigger_hooks(self, new=True):
171         thread = Thread(target=trigger_hooks_threaded,  args=[self, Action.hooks, new])
172         thread.setDaemon(True)
173         thread.start()
174
175     class Meta:
176         app_label = 'forum'
177
178 def trigger_hooks_threaded(action, hooks, new):
179     for cls, hooklist in hooks.items():
180         if isinstance(action, cls):
181             for hook in hooklist:
182                 try:
183                     hook(action=action, new=new)
184                 except Exception, e:
185                     import traceback
186                     logging.error("Error in %s hook: %s" % (cls.__name__, str(e)))
187                     logging.error(traceback.format_exc())
188
189 class ActionProxyMetaClass(BaseMetaClass):
190     types = {}
191
192     def __new__(cls, *args, **kwargs):
193         new_cls = super(ActionProxyMetaClass, cls).__new__(cls, *args, **kwargs)
194         cls.types[new_cls.get_type()] = new_cls
195
196         class Meta:
197             proxy = True
198
199         new_cls.Meta = Meta
200         return new_cls
201
202 class ActionProxy(Action):
203     __metaclass__ = ActionProxyMetaClass
204
205     def friendly_username(self, viewer, user):
206         return (viewer == user) and _('You') or user.username
207
208     def friendly_ownername(self, owner, user):
209         return (owner == user) and _('your') or user.username
210
211     def viewer_or_user_verb(self, viewer, user, viewer_verb, user_verb):
212         return (viewer == user) and viewer_verb or user_verb    
213
214     def hyperlink(self, url, title, **attrs):
215         return html.hyperlink(url, title, **attrs)
216
217     def describe_node(self, viewer, node):
218         node_link = self.hyperlink(node.get_absolute_url(), node.headline)
219
220         if node.parent:
221             node_desc = _("on %(link)s") % {'link': node_link}
222         else:
223             node_desc = node_link
224
225         return _("%(user)s %(node_name)s %(node_desc)s") % {
226             'user': self.hyperlink(node.author.get_profile_url(), self.friendly_ownername(viewer, node.author)),
227             'node_name': node.friendly_name,
228             'node_desc': node_desc,
229         }
230     
231     class Meta:
232         proxy = True
233
234 class DummyActionProxyMetaClass(type):
235     def __new__(cls, *args, **kwargs):
236         new_cls = super(DummyActionProxyMetaClass, cls).__new__(cls, *args, **kwargs)
237         ActionProxyMetaClass.types[new_cls.get_type()] = new_cls
238         return new_cls
239
240 class DummyActionProxy(object):
241     __metaclass__ = DummyActionProxyMetaClass
242
243     hooks = []
244
245     def __init__(self, ip=None):
246         self.ip = ip
247
248     def process_data(self, **data):
249         pass
250
251     def process_action(self):
252         pass
253
254     def save(self, data=None):
255         self.process_action()
256
257         if data:
258             self.process_data(**data)
259
260         for hook in self.__class__.hooks:
261             hook(self, True)
262
263     @classmethod
264     def get_type(cls):
265         return re.sub(r'action$', '', cls.__name__.lower())
266
267     @classmethod
268     def hook(cls, fn):
269         cls.hooks.append(fn)
270
271
272
273 class ActionRepute(models.Model):
274     action = models.ForeignKey(Action, related_name='reputes')
275     date = models.DateTimeField(default=datetime.datetime.now)
276     user = models.ForeignKey('User', related_name='reputes')
277     value = models.IntegerField(default=0)
278     by_canceled = models.BooleanField(default=False)
279
280     @property
281     def positive(self):
282         if self.value > 0: return self.value
283         return 0
284
285     @property
286     def negative(self):
287         if self.value < 0: return self.value
288         return 0
289
290     def save(self, *args, **kwargs):
291         super(ActionRepute, self).save(*args, **kwargs)
292         self.user.reputation = models.F('reputation') + self.value
293         self.user.save()
294
295     def delete(self):
296         self.user.reputation = models.F('reputation') - self.value
297         self.user.save()
298         super(ActionRepute, self).delete()
299
300     class Meta:
301         app_label = 'forum'
302