##// END OF EJS Templates
api-utils: added helpers flag to extrac boolean flags from Optional parameters.
marcink -
r1265:59c22420 default
parent child Browse files
Show More
@@ -1,407 +1,412 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2016 RhodeCode GmbH
3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 JSON RPC utils
22 JSON RPC utils
23 """
23 """
24
24
25 import collections
25 import collections
26 import logging
26 import logging
27
27
28 from rhodecode.api.exc import JSONRPCError
28 from rhodecode.api.exc import JSONRPCError
29 from rhodecode.lib.auth import HasPermissionAnyApi, HasRepoPermissionAnyApi, \
29 from rhodecode.lib.auth import (
30 HasRepoGroupPermissionAnyApi
30 HasPermissionAnyApi, HasRepoPermissionAnyApi, HasRepoGroupPermissionAnyApi)
31 from rhodecode.lib.utils import safe_unicode
31 from rhodecode.lib.utils import safe_unicode
32 from rhodecode.lib.vcs.exceptions import RepositoryError
32 from rhodecode.controllers.utils import get_commit_from_ref_name
33 from rhodecode.controllers.utils import get_commit_from_ref_name
33 from rhodecode.lib.vcs.exceptions import RepositoryError
34 from rhodecode.lib.utils2 import str2bool
34
35
35 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
36
37
37
38
38 class OAttr(object):
39 class OAttr(object):
39 """
40 """
40 Special Option that defines other attribute, and can default to them
41 Special Option that defines other attribute, and can default to them
41
42
42 Example::
43 Example::
43
44
44 def test(apiuser, userid=Optional(OAttr('apiuser')):
45 def test(apiuser, userid=Optional(OAttr('apiuser')):
45 user = Optional.extract(userid, evaluate_locals=local())
46 user = Optional.extract(userid, evaluate_locals=local())
46 #if we pass in userid, we get it, else it will default to apiuser
47 #if we pass in userid, we get it, else it will default to apiuser
47 #attribute
48 #attribute
48 """
49 """
49
50
50 def __init__(self, attr_name):
51 def __init__(self, attr_name):
51 self.attr_name = attr_name
52 self.attr_name = attr_name
52
53
53 def __repr__(self):
54 def __repr__(self):
54 return '<OptionalAttr:%s>' % self.attr_name
55 return '<OptionalAttr:%s>' % self.attr_name
55
56
56 def __call__(self):
57 def __call__(self):
57 return self
58 return self
58
59
59
60
60 class Optional(object):
61 class Optional(object):
61 """
62 """
62 Defines an optional parameter::
63 Defines an optional parameter::
63
64
64 param = param.getval() if isinstance(param, Optional) else param
65 param = param.getval() if isinstance(param, Optional) else param
65 param = param() if isinstance(param, Optional) else param
66 param = param() if isinstance(param, Optional) else param
66
67
67 is equivalent of::
68 is equivalent of::
68
69
69 param = Optional.extract(param)
70 param = Optional.extract(param)
70
71
71 """
72 """
72
73
73 def __init__(self, type_):
74 def __init__(self, type_):
74 self.type_ = type_
75 self.type_ = type_
75
76
76 def __repr__(self):
77 def __repr__(self):
77 return '<Optional:%s>' % self.type_.__repr__()
78 return '<Optional:%s>' % self.type_.__repr__()
78
79
79 def __call__(self):
80 def __call__(self):
80 return self.getval()
81 return self.getval()
81
82
82 def getval(self, evaluate_locals=None):
83 def getval(self, evaluate_locals=None):
83 """
84 """
84 returns value from this Optional instance
85 returns value from this Optional instance
85 """
86 """
86 if isinstance(self.type_, OAttr):
87 if isinstance(self.type_, OAttr):
87 param_name = self.type_.attr_name
88 param_name = self.type_.attr_name
88 if evaluate_locals:
89 if evaluate_locals:
89 return evaluate_locals[param_name]
90 return evaluate_locals[param_name]
90 # use params name
91 # use params name
91 return param_name
92 return param_name
92 return self.type_
93 return self.type_
93
94
94 @classmethod
95 @classmethod
95 def extract(cls, val, evaluate_locals=None):
96 def extract(cls, val, evaluate_locals=None, binary=None):
96 """
97 """
97 Extracts value from Optional() instance
98 Extracts value from Optional() instance
98
99
99 :param val:
100 :param val:
100 :return: original value if it's not Optional instance else
101 :return: original value if it's not Optional instance else
101 value of instance
102 value of instance
102 """
103 """
103 if isinstance(val, cls):
104 if isinstance(val, cls):
104 return val.getval(evaluate_locals)
105 val = val.getval(evaluate_locals)
106
107 if binary:
108 val = str2bool(val)
109
105 return val
110 return val
106
111
107
112
108 def parse_args(cli_args, key_prefix=''):
113 def parse_args(cli_args, key_prefix=''):
109 from rhodecode.lib.utils2 import (escape_split)
114 from rhodecode.lib.utils2 import (escape_split)
110 kwargs = collections.defaultdict(dict)
115 kwargs = collections.defaultdict(dict)
111 for el in escape_split(cli_args, ','):
116 for el in escape_split(cli_args, ','):
112 kv = escape_split(el, '=', 1)
117 kv = escape_split(el, '=', 1)
113 if len(kv) == 2:
118 if len(kv) == 2:
114 k, v = kv
119 k, v = kv
115 kwargs[key_prefix + k] = v
120 kwargs[key_prefix + k] = v
116 return kwargs
121 return kwargs
117
122
118
123
119 def get_origin(obj):
124 def get_origin(obj):
120 """
125 """
121 Get origin of permission from object.
126 Get origin of permission from object.
122
127
123 :param obj:
128 :param obj:
124 """
129 """
125 origin = 'permission'
130 origin = 'permission'
126
131
127 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
132 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
128 # admin and owner case, maybe we should use dual string ?
133 # admin and owner case, maybe we should use dual string ?
129 origin = 'owner'
134 origin = 'owner'
130 elif getattr(obj, 'owner_row', ''):
135 elif getattr(obj, 'owner_row', ''):
131 origin = 'owner'
136 origin = 'owner'
132 elif getattr(obj, 'admin_row', ''):
137 elif getattr(obj, 'admin_row', ''):
133 origin = 'super-admin'
138 origin = 'super-admin'
134 return origin
139 return origin
135
140
136
141
137 def store_update(updates, attr, name):
142 def store_update(updates, attr, name):
138 """
143 """
139 Stores param in updates dict if it's not instance of Optional
144 Stores param in updates dict if it's not instance of Optional
140 allows easy updates of passed in params
145 allows easy updates of passed in params
141 """
146 """
142 if not isinstance(attr, Optional):
147 if not isinstance(attr, Optional):
143 updates[name] = attr
148 updates[name] = attr
144
149
145
150
146 def has_superadmin_permission(apiuser):
151 def has_superadmin_permission(apiuser):
147 """
152 """
148 Return True if apiuser is admin or return False
153 Return True if apiuser is admin or return False
149
154
150 :param apiuser:
155 :param apiuser:
151 """
156 """
152 if HasPermissionAnyApi('hg.admin')(user=apiuser):
157 if HasPermissionAnyApi('hg.admin')(user=apiuser):
153 return True
158 return True
154 return False
159 return False
155
160
156
161
157 def validate_repo_permissions(apiuser, repoid, repo, perms):
162 def validate_repo_permissions(apiuser, repoid, repo, perms):
158 """
163 """
159 Raise JsonRPCError if apiuser is not authorized or return True
164 Raise JsonRPCError if apiuser is not authorized or return True
160
165
161 :param apiuser:
166 :param apiuser:
162 :param repoid:
167 :param repoid:
163 :param repo:
168 :param repo:
164 :param perms:
169 :param perms:
165 """
170 """
166 if not HasRepoPermissionAnyApi(*perms)(
171 if not HasRepoPermissionAnyApi(*perms)(
167 user=apiuser, repo_name=repo.repo_name):
172 user=apiuser, repo_name=repo.repo_name):
168 raise JSONRPCError(
173 raise JSONRPCError(
169 'repository `%s` does not exist' % repoid)
174 'repository `%s` does not exist' % repoid)
170
175
171 return True
176 return True
172
177
173
178
174 def validate_repo_group_permissions(apiuser, repogroupid, repo_group, perms):
179 def validate_repo_group_permissions(apiuser, repogroupid, repo_group, perms):
175 """
180 """
176 Raise JsonRPCError if apiuser is not authorized or return True
181 Raise JsonRPCError if apiuser is not authorized or return True
177
182
178 :param apiuser:
183 :param apiuser:
179 :param repogroupid: just the id of repository group
184 :param repogroupid: just the id of repository group
180 :param repo_group: instance of repo_group
185 :param repo_group: instance of repo_group
181 :param perms:
186 :param perms:
182 """
187 """
183 if not HasRepoGroupPermissionAnyApi(*perms)(
188 if not HasRepoGroupPermissionAnyApi(*perms)(
184 user=apiuser, group_name=repo_group.group_name):
189 user=apiuser, group_name=repo_group.group_name):
185 raise JSONRPCError(
190 raise JSONRPCError(
186 'repository group `%s` does not exist' % repogroupid)
191 'repository group `%s` does not exist' % repogroupid)
187
192
188 return True
193 return True
189
194
190
195
191 def validate_set_owner_permissions(apiuser, owner):
196 def validate_set_owner_permissions(apiuser, owner):
192 if isinstance(owner, Optional):
197 if isinstance(owner, Optional):
193 owner = get_user_or_error(apiuser.user_id)
198 owner = get_user_or_error(apiuser.user_id)
194 else:
199 else:
195 if has_superadmin_permission(apiuser):
200 if has_superadmin_permission(apiuser):
196 owner = get_user_or_error(owner)
201 owner = get_user_or_error(owner)
197 else:
202 else:
198 # forbid setting owner for non-admins
203 # forbid setting owner for non-admins
199 raise JSONRPCError(
204 raise JSONRPCError(
200 'Only RhodeCode super-admin can specify `owner` param')
205 'Only RhodeCode super-admin can specify `owner` param')
201 return owner
206 return owner
202
207
203
208
204 def get_user_or_error(userid):
209 def get_user_or_error(userid):
205 """
210 """
206 Get user by id or name or return JsonRPCError if not found
211 Get user by id or name or return JsonRPCError if not found
207
212
208 :param userid:
213 :param userid:
209 """
214 """
210 from rhodecode.model.user import UserModel
215 from rhodecode.model.user import UserModel
211
216
212 user_model = UserModel()
217 user_model = UserModel()
213 try:
218 try:
214 user = user_model.get_user(int(userid))
219 user = user_model.get_user(int(userid))
215 except ValueError:
220 except ValueError:
216 user = user_model.get_by_username(userid)
221 user = user_model.get_by_username(userid)
217
222
218 if user is None:
223 if user is None:
219 raise JSONRPCError("user `%s` does not exist" % (userid,))
224 raise JSONRPCError("user `%s` does not exist" % (userid,))
220 return user
225 return user
221
226
222
227
223 def get_repo_or_error(repoid):
228 def get_repo_or_error(repoid):
224 """
229 """
225 Get repo by id or name or return JsonRPCError if not found
230 Get repo by id or name or return JsonRPCError if not found
226
231
227 :param repoid:
232 :param repoid:
228 """
233 """
229 from rhodecode.model.repo import RepoModel
234 from rhodecode.model.repo import RepoModel
230
235
231 repo = RepoModel().get_repo(repoid)
236 repo = RepoModel().get_repo(repoid)
232 if repo is None:
237 if repo is None:
233 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
238 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
234 return repo
239 return repo
235
240
236
241
237 def get_repo_group_or_error(repogroupid):
242 def get_repo_group_or_error(repogroupid):
238 """
243 """
239 Get repo group by id or name or return JsonRPCError if not found
244 Get repo group by id or name or return JsonRPCError if not found
240
245
241 :param repogroupid:
246 :param repogroupid:
242 """
247 """
243 from rhodecode.model.repo_group import RepoGroupModel
248 from rhodecode.model.repo_group import RepoGroupModel
244
249
245 repo_group = RepoGroupModel()._get_repo_group(repogroupid)
250 repo_group = RepoGroupModel()._get_repo_group(repogroupid)
246 if repo_group is None:
251 if repo_group is None:
247 raise JSONRPCError(
252 raise JSONRPCError(
248 'repository group `%s` does not exist' % (repogroupid,))
253 'repository group `%s` does not exist' % (repogroupid,))
249 return repo_group
254 return repo_group
250
255
251
256
252 def get_user_group_or_error(usergroupid):
257 def get_user_group_or_error(usergroupid):
253 """
258 """
254 Get user group by id or name or return JsonRPCError if not found
259 Get user group by id or name or return JsonRPCError if not found
255
260
256 :param usergroupid:
261 :param usergroupid:
257 """
262 """
258 from rhodecode.model.user_group import UserGroupModel
263 from rhodecode.model.user_group import UserGroupModel
259
264
260 user_group = UserGroupModel().get_group(usergroupid)
265 user_group = UserGroupModel().get_group(usergroupid)
261 if user_group is None:
266 if user_group is None:
262 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
267 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
263 return user_group
268 return user_group
264
269
265
270
266 def get_perm_or_error(permid, prefix=None):
271 def get_perm_or_error(permid, prefix=None):
267 """
272 """
268 Get permission by id or name or return JsonRPCError if not found
273 Get permission by id or name or return JsonRPCError if not found
269
274
270 :param permid:
275 :param permid:
271 """
276 """
272 from rhodecode.model.permission import PermissionModel
277 from rhodecode.model.permission import PermissionModel
273
278
274 perm = PermissionModel.cls.get_by_key(permid)
279 perm = PermissionModel.cls.get_by_key(permid)
275 if perm is None:
280 if perm is None:
276 raise JSONRPCError('permission `%s` does not exist' % (permid,))
281 raise JSONRPCError('permission `%s` does not exist' % (permid,))
277 if prefix:
282 if prefix:
278 if not perm.permission_name.startswith(prefix):
283 if not perm.permission_name.startswith(prefix):
279 raise JSONRPCError('permission `%s` is invalid, '
284 raise JSONRPCError('permission `%s` is invalid, '
280 'should start with %s' % (permid, prefix))
285 'should start with %s' % (permid, prefix))
281 return perm
286 return perm
282
287
283
288
284 def get_gist_or_error(gistid):
289 def get_gist_or_error(gistid):
285 """
290 """
286 Get gist by id or gist_access_id or return JsonRPCError if not found
291 Get gist by id or gist_access_id or return JsonRPCError if not found
287
292
288 :param gistid:
293 :param gistid:
289 """
294 """
290 from rhodecode.model.gist import GistModel
295 from rhodecode.model.gist import GistModel
291
296
292 gist = GistModel.cls.get_by_access_id(gistid)
297 gist = GistModel.cls.get_by_access_id(gistid)
293 if gist is None:
298 if gist is None:
294 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
299 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
295 return gist
300 return gist
296
301
297
302
298 def get_pull_request_or_error(pullrequestid):
303 def get_pull_request_or_error(pullrequestid):
299 """
304 """
300 Get pull request by id or return JsonRPCError if not found
305 Get pull request by id or return JsonRPCError if not found
301
306
302 :param pullrequestid:
307 :param pullrequestid:
303 """
308 """
304 from rhodecode.model.pull_request import PullRequestModel
309 from rhodecode.model.pull_request import PullRequestModel
305
310
306 try:
311 try:
307 pull_request = PullRequestModel().get(int(pullrequestid))
312 pull_request = PullRequestModel().get(int(pullrequestid))
308 except ValueError:
313 except ValueError:
309 raise JSONRPCError('pullrequestid must be an integer')
314 raise JSONRPCError('pullrequestid must be an integer')
310 if not pull_request:
315 if not pull_request:
311 raise JSONRPCError('pull request `%s` does not exist' % (
316 raise JSONRPCError('pull request `%s` does not exist' % (
312 pullrequestid,))
317 pullrequestid,))
313 return pull_request
318 return pull_request
314
319
315
320
316 def build_commit_data(commit, detail_level):
321 def build_commit_data(commit, detail_level):
317 parsed_diff = []
322 parsed_diff = []
318 if detail_level == 'extended':
323 if detail_level == 'extended':
319 for f in commit.added:
324 for f in commit.added:
320 parsed_diff.append(_get_commit_dict(filename=f.path, op='A'))
325 parsed_diff.append(_get_commit_dict(filename=f.path, op='A'))
321 for f in commit.changed:
326 for f in commit.changed:
322 parsed_diff.append(_get_commit_dict(filename=f.path, op='M'))
327 parsed_diff.append(_get_commit_dict(filename=f.path, op='M'))
323 for f in commit.removed:
328 for f in commit.removed:
324 parsed_diff.append(_get_commit_dict(filename=f.path, op='D'))
329 parsed_diff.append(_get_commit_dict(filename=f.path, op='D'))
325
330
326 elif detail_level == 'full':
331 elif detail_level == 'full':
327 from rhodecode.lib.diffs import DiffProcessor
332 from rhodecode.lib.diffs import DiffProcessor
328 diff_processor = DiffProcessor(commit.diff())
333 diff_processor = DiffProcessor(commit.diff())
329 for dp in diff_processor.prepare():
334 for dp in diff_processor.prepare():
330 del dp['stats']['ops']
335 del dp['stats']['ops']
331 _stats = dp['stats']
336 _stats = dp['stats']
332 parsed_diff.append(_get_commit_dict(
337 parsed_diff.append(_get_commit_dict(
333 filename=dp['filename'], op=dp['operation'],
338 filename=dp['filename'], op=dp['operation'],
334 new_revision=dp['new_revision'],
339 new_revision=dp['new_revision'],
335 old_revision=dp['old_revision'],
340 old_revision=dp['old_revision'],
336 raw_diff=dp['raw_diff'], stats=_stats))
341 raw_diff=dp['raw_diff'], stats=_stats))
337
342
338 return parsed_diff
343 return parsed_diff
339
344
340
345
341 def get_commit_or_error(ref, repo):
346 def get_commit_or_error(ref, repo):
342 try:
347 try:
343 ref_type, _, ref_hash = ref.split(':')
348 ref_type, _, ref_hash = ref.split(':')
344 except ValueError:
349 except ValueError:
345 raise JSONRPCError(
350 raise JSONRPCError(
346 'Ref `{ref}` given in a wrong format. Please check the API'
351 'Ref `{ref}` given in a wrong format. Please check the API'
347 ' documentation for more details'.format(ref=ref))
352 ' documentation for more details'.format(ref=ref))
348 try:
353 try:
349 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
354 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
350 # once get_commit supports ref_types
355 # once get_commit supports ref_types
351 return get_commit_from_ref_name(repo, ref_hash)
356 return get_commit_from_ref_name(repo, ref_hash)
352 except RepositoryError:
357 except RepositoryError:
353 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
358 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
354
359
355
360
356 def resolve_ref_or_error(ref, repo):
361 def resolve_ref_or_error(ref, repo):
357 def _parse_ref(type_, name, hash_=None):
362 def _parse_ref(type_, name, hash_=None):
358 return type_, name, hash_
363 return type_, name, hash_
359
364
360 try:
365 try:
361 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
366 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
362 except TypeError:
367 except TypeError:
363 raise JSONRPCError(
368 raise JSONRPCError(
364 'Ref `{ref}` given in a wrong format. Please check the API'
369 'Ref `{ref}` given in a wrong format. Please check the API'
365 ' documentation for more details'.format(ref=ref))
370 ' documentation for more details'.format(ref=ref))
366
371
367 try:
372 try:
368 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
373 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
369 except (KeyError, ValueError):
374 except (KeyError, ValueError):
370 raise JSONRPCError(
375 raise JSONRPCError(
371 'The specified {type} `{name}` does not exist'.format(
376 'The specified {type} `{name}` does not exist'.format(
372 type=ref_type, name=ref_name))
377 type=ref_type, name=ref_name))
373
378
374 return ':'.join([ref_type, ref_name, ref_hash])
379 return ':'.join([ref_type, ref_name, ref_hash])
375
380
376
381
377 def _get_commit_dict(
382 def _get_commit_dict(
378 filename, op, new_revision=None, old_revision=None,
383 filename, op, new_revision=None, old_revision=None,
379 raw_diff=None, stats=None):
384 raw_diff=None, stats=None):
380 if stats is None:
385 if stats is None:
381 stats = {
386 stats = {
382 "added": None,
387 "added": None,
383 "binary": None,
388 "binary": None,
384 "deleted": None
389 "deleted": None
385 }
390 }
386 return {
391 return {
387 "filename": safe_unicode(filename),
392 "filename": safe_unicode(filename),
388 "op": op,
393 "op": op,
389
394
390 # extra details
395 # extra details
391 "new_revision": new_revision,
396 "new_revision": new_revision,
392 "old_revision": old_revision,
397 "old_revision": old_revision,
393
398
394 "raw_diff": raw_diff,
399 "raw_diff": raw_diff,
395 "stats": stats
400 "stats": stats
396 }
401 }
397
402
398
403
399 # TODO: mikhail: Think about moving this function to some library
404 # TODO: mikhail: Think about moving this function to some library
400 def _get_ref_hash(repo, type_, name):
405 def _get_ref_hash(repo, type_, name):
401 vcs_repo = repo.scm_instance()
406 vcs_repo = repo.scm_instance()
402 if type_ == 'branch' and vcs_repo.alias in ('hg', 'git'):
407 if type_ == 'branch' and vcs_repo.alias in ('hg', 'git'):
403 return vcs_repo.branches[name]
408 return vcs_repo.branches[name]
404 elif type_ == 'bookmark' and vcs_repo.alias == 'hg':
409 elif type_ == 'bookmark' and vcs_repo.alias == 'hg':
405 return vcs_repo.bookmarks[name]
410 return vcs_repo.bookmarks[name]
406 else:
411 else:
407 raise ValueError()
412 raise ValueError()
General Comments 0
You need to be logged in to leave comments. Login now