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