improve / refactor feed-downloader
[mygpo.git] / mygpo / decorators.py
blobbe77d600d9c257a3c1a9acc03e690b9bf5ea832c
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2009 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 from functools import wraps
21 import inspect
23 from couchdbkit import ResourceConflict
25 from django.http import Http404
26 from django.shortcuts import render
27 from django.http import HttpResponseForbidden, HttpResponseNotAllowed
29 import logging
30 logger = logging.getLogger(__name__)
33 def requires_token(token_name, denied_template=None):
34 """
35 returns a decorator that checks if the security token in the 'token' GET
36 parameter matches the requires token for the resource. The protected
37 resource is indicated by
38 * the username parameter passed to the decorated function
39 * token_name passed to this method
41 The decorated method is returned, if
42 * no token is required for the resource
43 * the token in the 'token' GET parameter matches the required token
45 If the passed token does not match
46 * the denied_template is rendered and returned if given
47 * HttpResponseForbidden is returned, if denied_template is not given
48 """
49 def decorator(fn):
50 @wraps(fn)
51 def tmp(request, username, *args, **kwargs):
53 from mygpo.users.models import User
54 user = User.get_user(username)
55 if not user:
56 raise Http404
58 token = user.get_token(token_name)
59 u_token = request.GET.get('token', '')
61 if token == '' or token == u_token:
62 return fn(request, username, *args, **kwargs)
64 else:
65 if denied_template:
66 return render(request, denied_template, {
67 'other_user': user
70 else:
71 return HttpResponseForbidden()
73 return tmp
74 return decorator
77 def allowed_methods(methods):
78 def decorator(fn):
79 @wraps(fn)
80 def tmp(request, *args, **kwargs):
81 if request.method in methods:
82 return fn(request, *args, **kwargs)
83 else:
84 return HttpResponseNotAllowed(methods)
86 return tmp
88 return decorator
91 class repeat_on_conflict(object):
92 """ Repeats an update operation in case of a ResourceConflict
94 In case of a CouchDB ResourceConflict, reloads the parameter with the given
95 name and repeats the function call until it succeeds. When calling the
96 function, the parameter that should be reloaded must be given as a
97 keyword-argument """
99 ARGSPEC = '__repeat_argspec__'
101 def __init__(self, obj_names=[], reload_f=None):
102 self.obj_names = obj_names
103 self.reload_f = reload_f or self.default_reload
105 def default_reload(self, obj):
106 # if the object knows its DB, use this one
107 if obj._db:
108 doc = obj._db.get(obj._id)
109 return obj.__class__.wrap(doc)
110 # otherwise the class' default DB is used
111 return obj.__class__.get(obj._id)
113 def build_locals(self, f, args, kwargs):
114 argspec = getattr(f, self.ARGSPEC)
115 if len(args) > len(argspec.args):
116 varargs = args[len(args):]
117 args = args[:len(args)]
118 else:
119 varargs = []
120 locals = dict(zip(argspec.args, args))
121 if argspec.varargs is not None:
122 locals.update({argspec.varargs: varargs})
123 if argspec.keywords is not None:
124 locals.update({argspec.keywords: kwargs})
125 locals.update(kwargs)
126 return locals
128 def __call__(self, f):
130 if not hasattr(f, self.ARGSPEC):
131 argspec = inspect.getargspec(f)
132 setattr(f, self.ARGSPEC, argspec)
134 @wraps(f)
135 def wrapper(*args, **kwargs):
136 all_args = before = self.build_locals(f, args, kwargs)
138 # repeat until operation succeeds
139 # TODO: adding an upper bound might make sense
140 while True:
141 try:
142 return f(**all_args)
144 except ResourceConflict as e:
145 logger.info('retrying %s, reloading %s', f, self.obj_names)
146 for obj_name in self.obj_names:
147 obj = all_args[obj_name]
148 all_args[obj_name] = self.reload_f(obj)
150 return wrapper
153 def query_if_required():
154 """ If required, queries some resource before calling the function
156 The decorated method is expected to be bound and its class is
157 expected to have define the methods _needs_query() and _query().
160 def decorator(f):
161 @wraps(f)
162 def wrapper(self, *args, **kwargs):
164 if self._needs_query():
165 self._query()
167 return f(self, *args, **kwargs)
169 return wrapper
170 return decorator
173 def cors_origin(allowed_origin='*'):
174 """ Adds an Access-Control-Allow-Origin header to the response """
176 def decorator(f):
177 @wraps(f)
178 def wrapper(*args, **kwargs):
179 resp = f(*args, **kwargs)
180 resp['Access-Control-Allow-Origin'] = allowed_origin
181 return resp
183 return wrapper
184 return decorator