Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
thalia
concrexit
Commits
bd41b5e0
Commit
bd41b5e0
authored
Sep 21, 2017
by
Tom van Bussel
Browse files
Merge branch 'feature/events-refactor' into 'master'
Events refactor See merge request
!549
parents
c9bcbe55
b6af0bdb
Changes
25
Hide whitespace changes
Inline
Side-by-side
website/events/admin.py
View file @
bd41b5e0
# -*- coding: utf-8 -*-
from
django.contrib
import
admin
from
django.db.models
import
Q
from
django.http
import
HttpResponseRedirect
from
django.template.defaultfilters
import
date
as
_date
from
django.urls
import
reverse
from
django.utils
import
timezone
from
django.utils.html
import
format_html
from
django.utils.http
import
is_safe_url
from
django.utils.translation
import
ugettext_lazy
as
_
from
django.template.defaultfilters
import
date
as
_date
from
events
import
services
from
members.models
import
Member
from
utils.translation
import
TranslatedModelAdmin
from
.
import
forms
,
models
...
...
@@ -73,12 +73,9 @@ class EventAdmin(DoNextModelAdmin):
def
has_change_permission
(
self
,
request
,
event
=
None
):
try
:
if
(
not
request
.
user
.
is_superuser
and
event
is
not
None
and
not
request
.
user
.
has_perm
(
'events.override_organiser'
)):
committees
=
request
.
user
.
member
.
get_committees
().
filter
(
Q
(
pk
=
event
.
organiser
.
pk
)).
count
()
if
committees
==
0
:
return
False
if
(
event
is
not
None
and
not
services
.
is_organiser
(
request
.
user
,
event
)):
return
False
except
Member
.
DoesNotExist
:
pass
return
super
().
has_change_permission
(
request
,
event
)
...
...
@@ -150,7 +147,8 @@ class EventAdmin(DoNextModelAdmin):
# Use custom queryset for organiser field
# Only get the current active committees the user is a member of
try
:
if
not
request
.
user
.
is_superuser
:
if
not
(
request
.
user
.
is_superuser
or
request
.
user
.
has_perm
(
'events.override_organiser'
)):
kwargs
[
'queryset'
]
=
request
.
user
.
member
.
get_committees
()
except
Member
.
DoesNotExist
:
...
...
website/events/admin_views.py
0 → 100644
View file @
bd41b5e0
import
csv
import
json
from
django.contrib.admin.views.decorators
import
staff_member_required
from
django.contrib.auth.decorators
import
permission_required
from
django.http
import
HttpResponse
,
HttpResponseRedirect
,
JsonResponse
from
django.shortcuts
import
get_object_or_404
,
render
from
django.utils
import
timezone
from
django.utils.text
import
slugify
from
django.utils.translation
import
pgettext_lazy
from
django.utils.translation
import
ugettext_lazy
as
_
from
django.views.decorators.http
import
require_http_methods
from
events.decorators
import
organiser_only
from
.models
import
Event
,
Registration
@
staff_member_required
@
permission_required
(
'events.change_event'
)
@
organiser_only
def
details
(
request
,
event_id
):
event
=
get_object_or_404
(
Event
,
pk
=
event_id
)
return
render
(
request
,
'events/admin/details.html'
,
{
'event'
:
event
,
})
@
staff_member_required
@
permission_required
(
'events.change_event'
)
@
organiser_only
@
require_http_methods
([
"POST"
])
def
change_registration
(
request
,
event_id
,
action
=
None
):
data
=
{
'success'
:
True
}
try
:
id
=
request
.
POST
.
get
(
"id"
,
-
1
)
checked
=
json
.
loads
(
request
.
POST
.
get
(
"checked"
))
obj
=
Registration
.
objects
.
get
(
event
=
event_id
,
pk
=
id
)
if
checked
is
not
None
:
if
action
==
'present'
:
obj
.
present
=
checked
elif
action
==
'paid'
:
obj
.
paid
=
checked
obj
.
save
()
except
Registration
.
DoesNotExist
:
data
[
'success'
]
=
False
return
JsonResponse
(
data
)
@
staff_member_required
@
permission_required
(
'events.change_event'
)
@
organiser_only
def
export
(
request
,
event_id
):
event
=
get_object_or_404
(
Event
,
pk
=
event_id
)
extra_fields
=
event
.
registrationinformationfield_set
.
all
()
registrations
=
event
.
registration_set
.
all
()
header_fields
=
(
[
'name'
,
'email'
,
'paid'
,
'present'
,
'status'
,
'phone number'
]
+
[
field
.
name
for
field
in
extra_fields
]
+
[
'date'
,
'date cancelled'
])
rows
=
[]
if
event
.
price
==
0
:
header_fields
.
remove
(
'paid'
)
for
i
,
registration
in
enumerate
(
registrations
):
if
registration
.
member
:
name
=
registration
.
member
.
get_full_name
()
else
:
name
=
registration
.
name
status
=
pgettext_lazy
(
'registration status'
,
'registered'
)
cancelled
=
None
if
registration
.
date_cancelled
:
if
registration
.
is_late_cancellation
():
status
=
pgettext_lazy
(
'registration status'
,
'late cancellation'
)
else
:
status
=
pgettext_lazy
(
'registration status'
,
'cancelled'
)
cancelled
=
timezone
.
localtime
(
registration
.
date_cancelled
)
elif
registration
.
queue_position
:
status
=
pgettext_lazy
(
'registration status'
,
'waiting'
)
data
=
{
'name'
:
name
,
'date'
:
timezone
.
localtime
(
registration
.
date
).
strftime
(
"%Y-%m-%d %H:%m"
),
'present'
:
_
(
'Yes'
)
if
registration
.
present
else
''
,
'phone number'
:
(
registration
.
member
.
phone_number
if
registration
.
member
else
''
),
'email'
:
(
registration
.
member
.
user
.
email
if
registration
.
member
else
''
),
'status'
:
status
,
'date cancelled'
:
cancelled
,
}
if
event
.
price
>
0
:
if
registration
.
payment
==
'cash_payment'
:
data
[
'paid'
]
=
_
(
'Cash'
)
elif
registration
.
payment
==
'pin_payment'
:
data
[
'paid'
]
=
_
(
'Pin'
)
else
:
data
[
'paid'
]
=
_
(
'No'
)
data
.
update
({
field
[
'field'
].
name
:
field
[
'value'
]
for
field
in
registration
.
information_fields
})
rows
.
append
(
data
)
response
=
HttpResponse
(
content_type
=
'text/csv'
)
writer
=
csv
.
DictWriter
(
response
,
header_fields
)
writer
.
writeheader
()
rows
=
sorted
(
rows
,
key
=
lambda
row
:
(
row
[
'status'
]
==
pgettext_lazy
(
'registration status'
,
'late cancellation'
),
row
[
'date'
]),
reverse
=
True
,
)
for
row
in
rows
:
writer
.
writerow
(
row
)
response
[
'Content-Disposition'
]
=
(
'attachment; filename="{}.csv"'
.
format
(
slugify
(
event
.
title
)))
return
response
@
staff_member_required
@
permission_required
(
'events.change_event'
)
def
export_email
(
request
,
event_id
):
event
=
get_object_or_404
(
Event
,
pk
=
event_id
)
registrations
=
event
.
registration_set
.
filter
(
date_cancelled
=
None
).
prefetch_related
(
'member__user'
)
registrations
=
registrations
[:
event
.
max_participants
]
addresses
=
[
r
.
member
.
user
.
email
for
r
in
registrations
if
r
.
member
]
no_addresses
=
[
r
.
name
for
r
in
registrations
if
not
r
.
member
]
return
render
(
request
,
'events/admin/email_export.html'
,
{
'event'
:
event
,
'addresses'
:
addresses
,
'no_addresses'
:
no_addresses
})
@
staff_member_required
@
permission_required
(
'events.change_event'
)
@
organiser_only
def
all_present
(
request
,
event_id
):
event
=
get_object_or_404
(
Event
,
pk
=
event_id
)
if
event
.
max_participants
is
None
:
registrations_query
=
event
.
registration_set
.
filter
(
date_cancelled
=
None
)
else
:
registrations_query
=
(
event
.
registration_set
.
filter
(
date_cancelled
=
None
)
.
order_by
(
'date'
)[:
event
.
max_participants
])
event
.
registration_set
.
filter
(
pk__in
=
registrations_query
).
update
(
present
=
True
,
payment
=
'cash_payment'
)
return
HttpResponseRedirect
(
'/events/admin/{}'
.
format
(
event_id
))
website/events/api/serializers.py
View file @
bd41b5e0
from
html
import
unescape
from
django.templatetags.static
import
static
from
django.urls
import
reverse
from
django.utils
import
timezone
from
django.utils.html
import
strip_tags
from
html
import
unescape
from
rest_framework
import
serializers
from
rest_framework.fields
import
empty
from
events.models
import
Event
,
Registration
from
events
import
services
from
events.exceptions
import
RegistrationError
from
events.models
import
Event
,
Registration
,
RegistrationInformationField
from
pizzas.models
import
PizzaEvent
from
thaliawebsite.settings
import
settings
...
...
@@ -71,11 +73,11 @@ class EventCalenderJSSerializer(CalenderJSSerializer):
model
=
Event
def
_url
(
self
,
instance
):
return
reverse
(
'events:event'
,
kwargs
=
{
'
event_id
'
:
instance
.
id
})
return
reverse
(
'events:event'
,
kwargs
=
{
'
pk
'
:
instance
.
id
})
def
_registered
(
self
,
instance
):
try
:
return
instan
ce
.
is_
memb
er_registered
(
self
.
context
[
'user'
]
.
member
)
return
servi
ce
s
.
is_
us
er_registered
(
instance
,
self
.
context
[
'user'
])
except
AttributeError
:
return
None
...
...
@@ -111,21 +113,50 @@ class EventRetrieveSerializer(serializers.ModelSerializer):
registration_allowed
=
serializers
.
SerializerMethodField
(
'_registration_allowed'
)
has_fields
=
serializers
.
SerializerMethodField
(
'_has_fields'
)
status
=
serializers
.
SerializerMethodField
(
'_status'
)
# DEPRECATED
REGISTRATION_NOT_NEEDED
=
-
1
REGISTRATION_NOT_YET_OPEN
=
0
REGISTRATION_OPEN
=
1
REGISTRATION_OPEN_NO_CANCEL
=
2
REGISTRATION_CLOSED
=
3
REGISTRATION_CLOSED_CANCEL_ONLY
=
4
""" DEPRECATED """
def
_status
(
self
,
instance
):
now
=
timezone
.
now
()
if
instance
.
registration_start
or
instance
.
registration_end
:
if
now
<=
instance
.
registration_start
:
return
self
.
REGISTRATION_NOT_YET_OPEN
elif
(
instance
.
registration_end
<=
now
<
instance
.
cancel_deadline
):
return
self
.
REGISTRATION_CLOSED_CANCEL_ONLY
elif
(
instance
.
cancel_deadline
<=
now
<
instance
.
registration_end
):
return
self
.
REGISTRATION_OPEN_NO_CANCEL
elif
(
now
>=
instance
.
registration_end
and
now
>=
instance
.
cancel_deadline
):
return
self
.
REGISTRATION_CLOSED
else
:
return
self
.
REGISTRATION_OPEN
else
:
return
self
.
REGISTRATION_NOT_NEEDED
def
_description
(
self
,
instance
):
return
unescape
(
strip_tags
(
instance
.
description
))
def
_num_participants
(
self
,
instance
):
if
(
instance
.
max_participants
and
instance
.
num_
participants
()
>
instance
.
max_participants
):
instance
.
participants
.
count
()
>
instance
.
max_participants
):
return
instance
.
max_participants
return
instance
.
num_
participants
()
return
instance
.
participants
.
count
()
def
_user_registration
(
self
,
instance
):
try
:
reg
=
instance
.
registration_set
.
get
(
member
=
self
.
context
[
'request'
].
user
.
member
)
return
RegistrationSerializer
(
reg
,
context
=
self
.
context
).
data
return
Registration
List
Serializer
(
reg
,
context
=
self
.
context
).
data
except
Registration
.
DoesNotExist
:
return
None
...
...
@@ -154,8 +185,8 @@ class EventListSerializer(serializers.ModelSerializer):
def
_registered
(
self
,
instance
):
try
:
return
instan
ce
.
is_
memb
er_registered
(
self
.
context
[
'request'
].
user
.
member
)
return
servi
ce
s
.
is_
us
er_registered
(
instance
,
self
.
context
[
'request'
].
user
)
except
AttributeError
:
return
None
...
...
@@ -164,7 +195,7 @@ class EventListSerializer(serializers.ModelSerializer):
return
pizza_events
.
exists
()
class
RegistrationSerializer
(
serializers
.
ModelSerializer
):
class
Registration
List
Serializer
(
serializers
.
ModelSerializer
):
class
Meta
:
model
=
Registration
fields
=
(
'pk'
,
'member'
,
'name'
,
'photo'
,
'registered_on'
,
...
...
@@ -173,43 +204,75 @@ class RegistrationSerializer(serializers.ModelSerializer):
name
=
serializers
.
SerializerMethodField
(
'_name'
)
photo
=
serializers
.
SerializerMethodField
(
'_photo'
)
member
=
serializers
.
SerializerMethodField
(
'_member'
)
registered_on
=
serializers
.
SerializerMethodField
(
'_registered_on
'
)
registered_on
=
serializers
.
DateTimeField
(
source
=
'date
'
)
is_cancelled
=
serializers
.
SerializerMethodField
(
'_is_cancelled'
)
is_late_cancellation
=
serializers
.
SerializerMethodField
(
'_is_late_cancellation'
)
queue_position
=
serializers
.
SerializerMethodField
(
'_queue_position'
)
def
_has_view_permission
(
self
,
instance
):
# We dont have an explicit viewing permission model, so we rely on the
# 'change' permission (Django provides add/change/delete by default)
return
(
self
.
context
[
'request'
].
user
.
has_perm
(
'events.change_event'
)
or
instance
.
member
.
user
==
self
.
context
[
'request'
].
user
)
'_queue_position'
,
read_only
=
False
)
def
_is_late_cancellation
(
self
,
instance
):
if
self
.
_has_view_permission
(
instance
):
return
instance
.
is_late_cancellation
()
return
None
return
instance
.
is_late_cancellation
()
def
_queue_position
(
self
,
instance
):
if
self
.
_has_view_permission
(
instance
):
pos
=
instance
.
queue_position
()
return
pos
if
pos
>
0
else
None
return
None
pos
=
instance
.
queue_position
return
pos
if
pos
>
0
else
None
def
_is_cancelled
(
self
,
instance
):
return
instance
.
date_cancelled
is
not
None
def
_
registered_on
(
self
,
instance
):
if
self
.
_has_view_permission
(
instance
)
:
return
serializers
.
DateTimeField
().
to_representation
(
instance
.
date
)
def
_
member
(
self
,
instance
):
if
instance
.
member
:
return
instance
.
member
.
pk
return
None
def
_name
(
self
,
instance
):
if
instance
.
member
:
return
instance
.
member
.
display_name
()
return
instance
.
name
def
_photo
(
self
,
instance
):
if
instance
.
member
and
instance
.
member
.
photo
:
return
self
.
context
[
'request'
].
build_absolute_uri
(
'%s%s'
%
(
settings
.
MEDIA_URL
,
instance
.
member
.
photo
))
else
:
return
self
.
context
[
'request'
].
build_absolute_uri
(
static
(
'members/images/default-avatar.jpg'
))
class
RegistrationSerializer
(
serializers
.
ModelSerializer
):
information_fields
=
None
class
Meta
:
model
=
Registration
fields
=
(
'pk'
,
'member'
,
'name'
,
'photo'
,
'registered_on'
,
'is_late_cancellation'
,
'is_cancelled'
,
'queue_position'
)
name
=
serializers
.
SerializerMethodField
(
'_name'
)
photo
=
serializers
.
SerializerMethodField
(
'_photo'
)
member
=
serializers
.
SerializerMethodField
(
'_member'
)
registered_on
=
serializers
.
DateTimeField
(
source
=
'date'
,
read_only
=
True
)
is_cancelled
=
serializers
.
SerializerMethodField
(
'_is_cancelled'
)
is_late_cancellation
=
serializers
.
SerializerMethodField
(
'_is_late_cancellation'
)
queue_position
=
serializers
.
SerializerMethodField
(
'_queue_position'
,
read_only
=
False
)
def
_is_late_cancellation
(
self
,
instance
):
val
=
instance
.
is_late_cancellation
()
return
False
if
val
is
None
else
val
def
_is_cancelled
(
self
,
instance
):
if
self
.
_has_view_permission
(
instance
):
return
instance
.
date_cancelled
is
not
None
return
None
return
instance
.
date_cancelled
is
not
None
def
_queue_position
(
self
,
instance
):
pos
=
instance
.
queue_position
return
pos
if
pos
>
0
else
None
def
_member
(
self
,
instance
):
if
instance
.
member
:
return
instance
.
member
.
user
.
pk
return
instance
.
member
.
pk
return
None
def
_name
(
self
,
instance
):
...
...
@@ -224,3 +287,59 @@ class RegistrationSerializer(serializers.ModelSerializer):
else
:
return
self
.
context
[
'request'
].
build_absolute_uri
(
static
(
'members/images/default-avatar.jpg'
))
def
__init__
(
self
,
instance
=
None
,
data
=
empty
,
**
kwargs
):
super
().
__init__
(
instance
,
data
,
**
kwargs
)
try
:
if
instance
:
self
.
information_fields
=
services
.
registration_fields
(
instance
.
member
.
user
,
instance
.
event
)
except
RegistrationError
:
pass
def
get_fields
(
self
):
fields
=
super
().
get_fields
()
if
self
.
information_fields
:
for
key
,
field
in
self
.
information_fields
.
items
():
key
=
'fields[{}]'
.
format
(
key
)
field_type
=
field
[
'type'
]
if
field_type
==
RegistrationInformationField
.
BOOLEAN_FIELD
:
fields
[
key
]
=
serializers
.
BooleanField
(
required
=
False
,
write_only
=
True
)
elif
field_type
==
RegistrationInformationField
.
INTEGER_FIELD
:
fields
[
key
]
=
serializers
.
IntegerField
(
required
=
field
[
'required'
],
write_only
=
True
)
elif
field_type
==
RegistrationInformationField
.
TEXT_FIELD
:
fields
[
key
]
=
serializers
.
CharField
(
required
=
field
[
'required'
],
write_only
=
True
)
fields
[
key
].
label
=
field
[
'label'
]
fields
[
key
].
help_text
=
field
[
'description'
]
fields
[
key
].
initial
=
field
[
'value'
]
fields
[
key
].
default
=
field
[
'value'
]
try
:
if
key
in
self
.
information_fields
:
fields
[
key
].
initial
=
self
.
validated_data
[
key
]
except
AssertionError
:
pass
return
fields
def
to_representation
(
self
,
instance
):
data
=
super
().
to_representation
(
instance
)
data
[
'fields'
]
=
self
.
information_fields
return
data
def
field_values
(
self
):
return
((
name
[
7
:
len
(
name
)
-
1
],
value
)
for
name
,
value
in
self
.
validated_data
.
items
()
if
"info_field"
in
name
)
website/events/api/urls.py
View file @
bd41b5e0
...
...
@@ -4,4 +4,5 @@ from events.api import viewsets
router
=
routers
.
SimpleRouter
()
router
.
register
(
r
'events'
,
viewsets
.
EventViewset
)
router
.
register
(
r
'registrations'
,
viewsets
.
RegistrationViewSet
)
urlpatterns
=
router
.
urls
website/events/api/viewsets.py
View file @
bd41b5e0
...
...
@@ -3,22 +3,26 @@ from datetime import datetime
from
django.utils
import
timezone
from
rest_framework
import
viewsets
,
filters
from
rest_framework.decorators
import
list_route
,
detail_route
from
rest_framework.exceptions
import
ParseError
from
rest_framework.exceptions
import
ParseError
,
PermissionDenied
,
NotFound
from
rest_framework.generics
import
get_object_or_404
from
rest_framework.mixins
import
RetrieveModelMixin
,
UpdateModelMixin
from
rest_framework.permissions
import
(
IsAuthenticated
,
IsAdminUser
,
IsAuthenticatedOrReadOnly
)
from
rest_framework.response
import
Response
from
rest_framework.viewsets
import
GenericViewSet
from
events
import
services
from
events.api.permissions
import
UnpublishedEventPermissions
from
events.api.serializers
import
(
EventCalenderJSSerializer
,
UnpublishedEventSerializer
,
EventRetrieveSerializer
,
EventListSerializer
,
RegistrationSerializer
)
RegistrationListSerializer
,
RegistrationSerializer
)
from
events.exceptions
import
RegistrationError
from
events.models
import
Event
,
Registration
...
...
@@ -52,21 +56,28 @@ class EventViewset(viewsets.ReadOnlyModelViewSet):
def
get_serializer_context
(
self
):
return
super
().
get_serializer_context
()
@
detail_route
()
@
detail_route
(
methods
=
[
'get'
,
'post'
]
)
def
registrations
(
self
,
request
,
pk
):
event
=
get_object_or_404
(
Event
,
pk
=
pk
)
if
request
.
method
.
lower
()
==
'post'
:
try
:
registration
=
services
.
create_registration
(
request
.
user
,
event
)
serializer
=
RegistrationSerializer
(
instance
=
registration
,
context
=
{
'request'
:
request
}
)
return
Response
(
status
=
201
,
data
=
serializer
.
data
)
except
RegistrationError
as
e
:
raise
PermissionDenied
(
detail
=
e
)
status
=
request
.
query_params
.
get
(
'status'
,
None
)
# Make sure you can only access other registrations when you have
# the permissions to do so
if
not
request
.
user
.
has_perm
(
'events.change_
event
'
):
if
not
services
.
is_organiser
(
request
.
user
,
event
):
status
=
'registered'
elif
(
not
request
.
user
.
is_superuser
and
not
request
.
user
.
has_perm
(
'events.override_organiser'
)):
committees
=
request
.
user
.
member
.
get_committees
().
filter
(
pk
=
event
.
organiser
.
pk
).
count
()
if
committees
==
0
:
status
=
'registered'
queryset
=
Registration
.
objects
.
filter
(
event
=
pk
)
if
status
is
not
None
:
...
...
@@ -80,8 +91,8 @@ class EventViewset(viewsets.ReadOnlyModelViewSet):
queryset
=
Registration
.
objects
.
filter
(
event
=
pk
,
date_cancelled
=
None
)[:
event
.
max_participants
]
serializer
=
RegistrationSerializer
(
queryset
,
many
=
True
,
context
=
{
'request'
:
request
})
serializer
=
Registration
List
Serializer
(
queryset
,
many
=
True
,
context
=
{
'request'
:
request
})
return
Response
(
serializer
.
data
)
...
...
@@ -112,3 +123,49 @@ class EventViewset(viewsets.ReadOnlyModelViewSet):
serializer
=
UnpublishedEventSerializer
(
queryset
,
many
=
True
,
context
=
{
'user'
:
request
.
user
})
return
Response
(
serializer
.
data
)
class
RegistrationViewSet
(
GenericViewSet
,
RetrieveModelMixin
,
UpdateModelMixin
):
queryset
=
Registration
.
objects
.
all
()
serializer_class
=
RegistrationSerializer
permission_classes
=
[
IsAuthenticated
]
def
get_serializer_context
(
self
):
context
=
super
().
get_serializer_context
()
context
[
'request'
]
=
self
.
request
return
context
def
get_object
(
self
):
instance
=
super
().
get_object
()
if
(
instance
.
member
.
user
.
pk
!=
self
.
request
.
user
.
pk
and
not
services
.
is_organiser
(
self
.
request
.
user
,
instance
.
event
)):
raise
NotFound
()
return
instance
# Always set instance so that OPTIONS call will show the info fields too
def
get_serializer
(
self
,
*
args
,
**
kwargs
):
if
len
(
args
)
==
0
and
"instance"
not
in
kwargs
:
kwargs
[
"instance"
]
=
self
.
get_object
()
return
super
().
get_serializer
(
*
args
,
**
kwargs
)
def
perform_update
(
self
,
serializer
):
super
().
perform_update
(
serializer
)
registration
=
serializer
.
instance
services
.
update_registration
(
registration
.
member
.
user
,
registration
.
event
,
serializer
.
field_values
())
serializer
.
information_fields
=
services
.
registration_fields
(
registration
.
member
.
user
,
registration
.
event
)
def
destroy
(
self
,
request
,
pk
=
None
,
**
kwargs
):
registration
=
self
.
get_object
()
try
:
services
.
cancel_registration
(
request
,
registration
.
member
.
user
,
registration
.
event
)