Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / templatetags / astakos_tags.py @ 8998f09a

History | View | Annotate | Download (9.9 kB)

1 dd5f8f4d Kostas Papadimitriou
# Copyright 2011 GRNET S.A. All rights reserved.
2 dd5f8f4d Kostas Papadimitriou
#
3 dd5f8f4d Kostas Papadimitriou
# Redistribution and use in source and binary forms, with or
4 dd5f8f4d Kostas Papadimitriou
# without modification, are permitted provided that the following
5 dd5f8f4d Kostas Papadimitriou
# conditions are met:
6 dd5f8f4d Kostas Papadimitriou
#
7 dd5f8f4d Kostas Papadimitriou
#   1. Redistributions of source code must retain the above
8 dd5f8f4d Kostas Papadimitriou
#      copyright notice, this list of conditions and the following
9 dd5f8f4d Kostas Papadimitriou
#      disclaimer.
10 dd5f8f4d Kostas Papadimitriou
#
11 dd5f8f4d Kostas Papadimitriou
#   2. Redistributions in binary form must reproduce the above
12 dd5f8f4d Kostas Papadimitriou
#      copyright notice, this list of conditions and the following
13 dd5f8f4d Kostas Papadimitriou
#      disclaimer in the documentation and/or other materials
14 dd5f8f4d Kostas Papadimitriou
#      provided with the distribution.
15 dd5f8f4d Kostas Papadimitriou
#
16 dd5f8f4d Kostas Papadimitriou
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 dd5f8f4d Kostas Papadimitriou
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 dd5f8f4d Kostas Papadimitriou
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 dd5f8f4d Kostas Papadimitriou
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 dd5f8f4d Kostas Papadimitriou
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 dd5f8f4d Kostas Papadimitriou
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 dd5f8f4d Kostas Papadimitriou
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 dd5f8f4d Kostas Papadimitriou
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 dd5f8f4d Kostas Papadimitriou
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 dd5f8f4d Kostas Papadimitriou
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 dd5f8f4d Kostas Papadimitriou
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 dd5f8f4d Kostas Papadimitriou
# POSSIBILITY OF SUCH DAMAGE.
28 dd5f8f4d Kostas Papadimitriou
#
29 dd5f8f4d Kostas Papadimitriou
# The views and conclusions contained in the software and
30 dd5f8f4d Kostas Papadimitriou
# documentation are those of the authors and should not be
31 dd5f8f4d Kostas Papadimitriou
# interpreted as representing official policies, either expressed
32 dd5f8f4d Kostas Papadimitriou
# or implied, of GRNET S.A.
33 dd5f8f4d Kostas Papadimitriou
34 dd5f8f4d Kostas Papadimitriou
import urllib
35 dd5f8f4d Kostas Papadimitriou
36 dd5f8f4d Kostas Papadimitriou
from inspect import getargspec
37 dd5f8f4d Kostas Papadimitriou
38 4f78c22c Sofia Papagiannaki
from django import template
39 c0b26605 Sofia Papagiannaki
from django.core.urlresolvers import resolve
40 4f78c22c Sofia Papagiannaki
from django.conf import settings
41 dd5f8f4d Kostas Papadimitriou
from django.template import TemplateSyntaxError, Variable
42 058b6ec7 Kostas Papadimitriou
from django.utils.translation import ugettext as _
43 734107ef Kostas Papadimitriou
from synnefo_branding.utils import render_to_string
44 058b6ec7 Kostas Papadimitriou
from django.template import RequestContext
45 058b6ec7 Kostas Papadimitriou
from django.core.urlresolvers import reverse
46 058b6ec7 Kostas Papadimitriou
from django.utils.safestring import mark_safe
47 4f78c22c Sofia Papagiannaki
48 4f78c22c Sofia Papagiannaki
register = template.Library()
49 4f78c22c Sofia Papagiannaki
50 4f78c22c Sofia Papagiannaki
MESSAGES_VIEWS_MAP = getattr(settings, 'ASTAKOS_MESSAGES_VIEWS_MAP', {
51 4f78c22c Sofia Papagiannaki
    'astakos.im.views.index': 'LOGIN_MESSAGES',
52 4f78c22c Sofia Papagiannaki
    'astakos.im.views.logout': 'LOGIN_MESSAGES',
53 4f78c22c Sofia Papagiannaki
    'astakos.im.views.login': 'LOGIN_MESSAGES',
54 4f78c22c Sofia Papagiannaki
    'astakos.im.views.signup': 'SIGNUP_MESSAGES',
55 4f78c22c Sofia Papagiannaki
    'astakos.im.views.edit_profile': 'PROFILE_MESSAGES',
56 4f78c22c Sofia Papagiannaki
    'astakos.im.views.change_password': 'PROFILE_MESSAGES',
57 4f78c22c Sofia Papagiannaki
    'astakos.im.views.invite': 'PROFILE_MESSAGES',
58 4f78c22c Sofia Papagiannaki
    'astakos.im.views.feedback': 'PROFILE_MESSAGES',
59 4f78c22c Sofia Papagiannaki
})
60 4f78c22c Sofia Papagiannaki
61 9a06d96f Olga Brani
62 dd5f8f4d Kostas Papadimitriou
# helper tag decorator
63 dd5f8f4d Kostas Papadimitriou
# https://github.com/djblets/djblets/blob/master/djblets/util/decorators.py#L96
64 dd5f8f4d Kostas Papadimitriou
def basictag(takes_context=False):
65 dd5f8f4d Kostas Papadimitriou
    """
66 dd5f8f4d Kostas Papadimitriou
    A decorator similar to Django's @register.simple_tag that optionally
67 dd5f8f4d Kostas Papadimitriou
    takes a context parameter. This condenses many tag implementations down
68 dd5f8f4d Kostas Papadimitriou
    to a few lines of code.
69 dd5f8f4d Kostas Papadimitriou

70 dd5f8f4d Kostas Papadimitriou
    Example:
71 dd5f8f4d Kostas Papadimitriou
        @register.tag
72 dd5f8f4d Kostas Papadimitriou
        @basictag(takes_context=True)
73 dd5f8f4d Kostas Papadimitriou
        def printuser(context):
74 dd5f8f4d Kostas Papadimitriou
            return context['user']
75 dd5f8f4d Kostas Papadimitriou
    """
76 dd5f8f4d Kostas Papadimitriou
    class BasicTagNode(template.Node):
77 dd5f8f4d Kostas Papadimitriou
        def __init__(self, take_context, tag_name, tag_func, args):
78 dd5f8f4d Kostas Papadimitriou
            self.takes_context = takes_context
79 dd5f8f4d Kostas Papadimitriou
            self.tag_name = tag_name
80 dd5f8f4d Kostas Papadimitriou
            self.tag_func = tag_func
81 dd5f8f4d Kostas Papadimitriou
            self.args = args
82 dd5f8f4d Kostas Papadimitriou
83 dd5f8f4d Kostas Papadimitriou
        def render(self, context):
84 dd5f8f4d Kostas Papadimitriou
            args = [Variable(var).resolve(context) for var in self.args]
85 dd5f8f4d Kostas Papadimitriou
86 dd5f8f4d Kostas Papadimitriou
            if self.takes_context:
87 dd5f8f4d Kostas Papadimitriou
                return self.tag_func(context, *args)
88 dd5f8f4d Kostas Papadimitriou
            else:
89 dd5f8f4d Kostas Papadimitriou
                return self.tag_func(*args)
90 dd5f8f4d Kostas Papadimitriou
91 dd5f8f4d Kostas Papadimitriou
    def basictag_func(tag_func):
92 dd5f8f4d Kostas Papadimitriou
        def _setup_tag(parser, token):
93 dd5f8f4d Kostas Papadimitriou
            bits = token.split_contents()
94 dd5f8f4d Kostas Papadimitriou
            tag_name = bits[0]
95 dd5f8f4d Kostas Papadimitriou
            del(bits[0])
96 dd5f8f4d Kostas Papadimitriou
97 dd5f8f4d Kostas Papadimitriou
            params, xx, xxx, defaults = getargspec(tag_func)
98 dd5f8f4d Kostas Papadimitriou
            max_args = len(params)
99 dd5f8f4d Kostas Papadimitriou
100 dd5f8f4d Kostas Papadimitriou
            if takes_context:
101 dd5f8f4d Kostas Papadimitriou
                if params[0] == 'context':
102 dd5f8f4d Kostas Papadimitriou
                    max_args -= 1 # Ignore context
103 dd5f8f4d Kostas Papadimitriou
                else:
104 dd5f8f4d Kostas Papadimitriou
                    raise TemplateSyntaxError, \
105 dd5f8f4d Kostas Papadimitriou
                        "Any tag function decorated with takes_context=True " \
106 dd5f8f4d Kostas Papadimitriou
                        "must have a first argument of 'context'"
107 dd5f8f4d Kostas Papadimitriou
108 dd5f8f4d Kostas Papadimitriou
            min_args = max_args - len(defaults or [])
109 dd5f8f4d Kostas Papadimitriou
110 dd5f8f4d Kostas Papadimitriou
            if not min_args <= len(bits) <= max_args:
111 dd5f8f4d Kostas Papadimitriou
                if min_args == max_args:
112 dd5f8f4d Kostas Papadimitriou
                    raise TemplateSyntaxError, \
113 dd5f8f4d Kostas Papadimitriou
                        "%r tag takes %d arguments." % (tag_name, min_args)
114 dd5f8f4d Kostas Papadimitriou
                else:
115 dd5f8f4d Kostas Papadimitriou
                    raise TemplateSyntaxError, \
116 dd5f8f4d Kostas Papadimitriou
                        "%r tag takes %d to %d arguments, got %d." % \
117 dd5f8f4d Kostas Papadimitriou
                        (tag_name, min_args, max_args, len(bits))
118 dd5f8f4d Kostas Papadimitriou
119 dd5f8f4d Kostas Papadimitriou
            return BasicTagNode(takes_context, tag_name, tag_func, bits)
120 dd5f8f4d Kostas Papadimitriou
121 dd5f8f4d Kostas Papadimitriou
        _setup_tag.__name__ = tag_func.__name__
122 dd5f8f4d Kostas Papadimitriou
        _setup_tag.__doc__ = tag_func.__doc__
123 dd5f8f4d Kostas Papadimitriou
        _setup_tag.__dict__.update(tag_func.__dict__)
124 dd5f8f4d Kostas Papadimitriou
        return _setup_tag
125 dd5f8f4d Kostas Papadimitriou
126 dd5f8f4d Kostas Papadimitriou
    return basictag_func
127 dd5f8f4d Kostas Papadimitriou
128 dd5f8f4d Kostas Papadimitriou
129 4f78c22c Sofia Papagiannaki
@register.tag(name='display_messages')
130 4f78c22c Sofia Papagiannaki
def display_messages(parser, token):
131 4f78c22c Sofia Papagiannaki
    return MessagesNode()
132 4f78c22c Sofia Papagiannaki
133 9a06d96f Olga Brani
134 4f78c22c Sofia Papagiannaki
class DummyMessage(object):
135 4f78c22c Sofia Papagiannaki
    def __init__(self, type, msg):
136 4f78c22c Sofia Papagiannaki
        self.message = msg
137 4f78c22c Sofia Papagiannaki
        self.tags = type
138 4f78c22c Sofia Papagiannaki
139 4f78c22c Sofia Papagiannaki
    def __repr__(self):
140 4f78c22c Sofia Papagiannaki
        return "%s: %s" % (self.tags, self.message)
141 4f78c22c Sofia Papagiannaki
142 9a06d96f Olga Brani
143 4f78c22c Sofia Papagiannaki
class MessagesNode(template.Node):
144 4f78c22c Sofia Papagiannaki
145 4f78c22c Sofia Papagiannaki
    def get_view_messages(self, context):
146 4f78c22c Sofia Papagiannaki
        messages = list(context['GLOBAL_MESSAGES'])
147 4f78c22c Sofia Papagiannaki
        try:
148 4f78c22c Sofia Papagiannaki
            view = resolve(context['request'].get_full_path())[0]
149 4f78c22c Sofia Papagiannaki
            view_name = "%s.%s" % (view.__module__, view.func_name)
150 4f78c22c Sofia Papagiannaki
            messages += context[MESSAGES_VIEWS_MAP.get(view_name)]
151 4f78c22c Sofia Papagiannaki
            return messages
152 4f78c22c Sofia Papagiannaki
        except Exception, e:
153 4f78c22c Sofia Papagiannaki
            return messages
154 4f78c22c Sofia Papagiannaki
155 4f78c22c Sofia Papagiannaki
    def render(self, context):
156 4f78c22c Sofia Papagiannaki
        if self not in context.render_context:
157 4f78c22c Sofia Papagiannaki
            messages = list(context['messages'])
158 4f78c22c Sofia Papagiannaki
            if context['EXTRA_MESSAGES_SET']:
159 4f78c22c Sofia Papagiannaki
                view_messages = self.get_view_messages(context)
160 4f78c22c Sofia Papagiannaki
                for msg_object in view_messages:
161 4f78c22c Sofia Papagiannaki
                    messages.append(DummyMessage(msg_object[0], msg_object[1]))
162 4f78c22c Sofia Papagiannaki
163 4f78c22c Sofia Papagiannaki
            if not messages:
164 4f78c22c Sofia Papagiannaki
                return ""
165 4f78c22c Sofia Papagiannaki
166 4f78c22c Sofia Papagiannaki
            cls = messages[-1].tags
167 4f78c22c Sofia Papagiannaki
            content = '<div class="top-msg active %s">' % cls
168 4f78c22c Sofia Papagiannaki
            for msg in messages:
169 9a06d96f Olga Brani
                content += '<div class="msg %s">%s</div>' % (
170 9a06d96f Olga Brani
                    msg.tags, msg.message)
171 4f78c22c Sofia Papagiannaki
172 4f78c22c Sofia Papagiannaki
            content += '<a href="#" title="close" class="close">X</a>'
173 4f78c22c Sofia Papagiannaki
            content += '</div>'
174 4f78c22c Sofia Papagiannaki
            context.render_context[self] = content
175 4f78c22c Sofia Papagiannaki
176 4f78c22c Sofia Papagiannaki
        return context.render_context[self]
177 279d6e51 Olga Brani
178 279d6e51 Olga Brani
179 279d6e51 Olga Brani
@register.simple_tag
180 669cfe19 Olga Brani
def get_grant_value(rname, form):
181 669cfe19 Olga Brani
    grants = form.instance.grants
182 669cfe19 Olga Brani
    try:
183 26551b92 Kostas Papadimitriou
        r = form.instance.projectresourcegrant_set.get(resource__name=rname).member_capacity
184 26551b92 Kostas Papadimitriou
    except Exception, e:
185 26551b92 Kostas Papadimitriou
        r = ''
186 26551b92 Kostas Papadimitriou
    return r
187 dd5f8f4d Kostas Papadimitriou
188 dd5f8f4d Kostas Papadimitriou
@register.tag(name="provider_login_url")
189 dd5f8f4d Kostas Papadimitriou
@basictag(takes_context=True)
190 0e79735c Kostas Papadimitriou
def provider_login_url(context, provider, from_login=False):
191 dd5f8f4d Kostas Papadimitriou
    request = context['request'].REQUEST
192 dd5f8f4d Kostas Papadimitriou
    next = request.get('next', None)
193 dd5f8f4d Kostas Papadimitriou
    code = request.get('code', None)
194 dd5f8f4d Kostas Papadimitriou
    key = request.get('key', None)
195 dd5f8f4d Kostas Papadimitriou
196 dd5f8f4d Kostas Papadimitriou
    attrs = {}
197 dd5f8f4d Kostas Papadimitriou
    if next:
198 dd5f8f4d Kostas Papadimitriou
        attrs['next'] = next
199 dd5f8f4d Kostas Papadimitriou
    if code:
200 dd5f8f4d Kostas Papadimitriou
        attrs['code'] = code
201 dd5f8f4d Kostas Papadimitriou
    if key:
202 dd5f8f4d Kostas Papadimitriou
        attrs['key'] = key
203 0e79735c Kostas Papadimitriou
    if from_login:
204 0e79735c Kostas Papadimitriou
        attrs['from_login'] = 1
205 dd5f8f4d Kostas Papadimitriou
206 9d20fe23 Kostas Papadimitriou
    url = provider.urls.get('login')
207 dd5f8f4d Kostas Papadimitriou
208 dd5f8f4d Kostas Papadimitriou
    joinchar = "?"
209 dd5f8f4d Kostas Papadimitriou
    if "?" in url:
210 dd5f8f4d Kostas Papadimitriou
        joinchar = "&"
211 dd5f8f4d Kostas Papadimitriou
212 9d20fe23 Kostas Papadimitriou
    return "%s%s%s" % (url, joinchar, urllib.urlencode(attrs))
213 dd5f8f4d Kostas Papadimitriou
214 058b6ec7 Kostas Papadimitriou
215 058b6ec7 Kostas Papadimitriou
EXTRA_CONTENT_MAP = {
216 058b6ec7 Kostas Papadimitriou
    'confirm_text': '<textarea name="reason"></textarea>'
217 058b6ec7 Kostas Papadimitriou
}
218 058b6ec7 Kostas Papadimitriou
219 058b6ec7 Kostas Papadimitriou
CONFIRM_LINK_PROMPT_MAP = {
220 058b6ec7 Kostas Papadimitriou
    'project_modification_cancel': _('Are you sure you want to dismiss this '
221 058b6ec7 Kostas Papadimitriou
                                     'project ?'),
222 058b6ec7 Kostas Papadimitriou
    'project_app_cancel': _('Are you sure you want to cancel this project ?'),
223 058b6ec7 Kostas Papadimitriou
    'project_app_approve': _('Are you sure you want to approve this '
224 058b6ec7 Kostas Papadimitriou
                             'project ?'),
225 058b6ec7 Kostas Papadimitriou
    'project_app_deny': _('Are you sure you want to deny this project ? '
226 058b6ec7 Kostas Papadimitriou
                          '<br /><br />You '
227 058b6ec7 Kostas Papadimitriou
                          'may optionally provide denial reason in the '
228 44f2d10d Kostas Papadimitriou
                          'following field: <br /><br /><textarea '
229 44f2d10d Kostas Papadimitriou
                          'class="deny_reason" name="reason"></textarea>'),
230 058b6ec7 Kostas Papadimitriou
    'project_app_dismiss': _('Are you sure you want to dismiss this '
231 058b6ec7 Kostas Papadimitriou
                             'project ?'),
232 44f2d10d Kostas Papadimitriou
    'project_join': _('Are you sure you want to join this project ?'),
233 44f2d10d Kostas Papadimitriou
    'project_leave': _('Are you sure you want to leave from the project ?'),
234 058b6ec7 Kostas Papadimitriou
}
235 058b6ec7 Kostas Papadimitriou
236 058b6ec7 Kostas Papadimitriou
237 058b6ec7 Kostas Papadimitriou
@register.tag(name="confirm_link")
238 058b6ec7 Kostas Papadimitriou
@basictag(takes_context=True)
239 058b6ec7 Kostas Papadimitriou
def confirm_link(context, title, prompt='', url=None, urlarg=None,
240 058b6ec7 Kostas Papadimitriou
                 extracontent='',
241 058b6ec7 Kostas Papadimitriou
                 confirm_prompt=None,
242 058b6ec7 Kostas Papadimitriou
                 inline=True,
243 058b6ec7 Kostas Papadimitriou
                 template="im/table_rich_link_column.html"):
244 058b6ec7 Kostas Papadimitriou
245 058b6ec7 Kostas Papadimitriou
    urlargs = None
246 058b6ec7 Kostas Papadimitriou
    if urlarg:
247 058b6ec7 Kostas Papadimitriou
        urlargs = (urlarg,)
248 058b6ec7 Kostas Papadimitriou
249 058b6ec7 Kostas Papadimitriou
    if CONFIRM_LINK_PROMPT_MAP.get(prompt, None):
250 058b6ec7 Kostas Papadimitriou
        prompt = mark_safe(CONFIRM_LINK_PROMPT_MAP.get(prompt))
251 058b6ec7 Kostas Papadimitriou
252 058b6ec7 Kostas Papadimitriou
    url = reverse(url, args=urlargs)
253 058b6ec7 Kostas Papadimitriou
    title = _(title)
254 058b6ec7 Kostas Papadimitriou
    tpl_context = RequestContext(context.get('request'))
255 058b6ec7 Kostas Papadimitriou
    tpl_context.update({
256 058b6ec7 Kostas Papadimitriou
        'col': {
257 058b6ec7 Kostas Papadimitriou
            'method': 'POST',
258 058b6ec7 Kostas Papadimitriou
            'cancel_prompt': 'CANCEL',
259 058b6ec7 Kostas Papadimitriou
            'confirm_prompt': confirm_prompt or title
260 058b6ec7 Kostas Papadimitriou
        },
261 058b6ec7 Kostas Papadimitriou
        'inline': inline,
262 058b6ec7 Kostas Papadimitriou
        'url': url,
263 058b6ec7 Kostas Papadimitriou
        'action': title,
264 058b6ec7 Kostas Papadimitriou
        'prompt': prompt,
265 058b6ec7 Kostas Papadimitriou
        'extra_form_content': EXTRA_CONTENT_MAP.get(extracontent, ''),
266 058b6ec7 Kostas Papadimitriou
        'confirm': True
267 058b6ec7 Kostas Papadimitriou
    })
268 058b6ec7 Kostas Papadimitriou
269 058b6ec7 Kostas Papadimitriou
    content = render_to_string(template, tpl_context)
270 058b6ec7 Kostas Papadimitriou
    return content
271 4e03ba30 Kostas Papadimitriou
272 4e03ba30 Kostas Papadimitriou
273 4e03ba30 Kostas Papadimitriou
class VerbatimNode(template.Node):
274 4e03ba30 Kostas Papadimitriou
275 4e03ba30 Kostas Papadimitriou
    def __init__(self, text):
276 4e03ba30 Kostas Papadimitriou
        self.text = text
277 4e03ba30 Kostas Papadimitriou
278 4e03ba30 Kostas Papadimitriou
    def render(self, context):
279 4e03ba30 Kostas Papadimitriou
        return self.text
280 4e03ba30 Kostas Papadimitriou
281 4e03ba30 Kostas Papadimitriou
282 4e03ba30 Kostas Papadimitriou
@register.tag
283 4e03ba30 Kostas Papadimitriou
def verbatim(parser, token):
284 4e03ba30 Kostas Papadimitriou
    text = []
285 4e03ba30 Kostas Papadimitriou
    while 1:
286 4e03ba30 Kostas Papadimitriou
        token = parser.tokens.pop(0)
287 4e03ba30 Kostas Papadimitriou
        if token.contents == 'endverbatim':
288 4e03ba30 Kostas Papadimitriou
            break
289 4e03ba30 Kostas Papadimitriou
        if token.token_type == template.TOKEN_VAR:
290 4e03ba30 Kostas Papadimitriou
            text.append('{{')
291 4e03ba30 Kostas Papadimitriou
        elif token.token_type == template.TOKEN_BLOCK:
292 4e03ba30 Kostas Papadimitriou
            text.append('{%')
293 4e03ba30 Kostas Papadimitriou
        text.append(token.contents)
294 4e03ba30 Kostas Papadimitriou
        if token.token_type == template.TOKEN_VAR:
295 4e03ba30 Kostas Papadimitriou
            text.append('}}')
296 4e03ba30 Kostas Papadimitriou
        elif token.token_type == template.TOKEN_BLOCK:
297 4e03ba30 Kostas Papadimitriou
            text.append('%}')
298 4e03ba30 Kostas Papadimitriou
    return VerbatimNode(''.join(text))