-
Notifications
You must be signed in to change notification settings - Fork 35
/
decorators.py
187 lines (157 loc) · 6.44 KB
/
decorators.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
"""
This module includes functions that are used as decorators in the API endpoints.
"""
from __future__ import annotations
import json
import logging
import random
import re
import threading
from functools import wraps
from typing import TYPE_CHECKING
from urllib import error, parse, request
from django.conf import settings
from django.http import Http404, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from ..cms.constants import feedback_ratings
from ..cms.models import Language, Region
if TYPE_CHECKING:
from collections.abc import Callable
from typing import Any
from django.http import HttpRequest, HttpResponseRedirect
logger = logging.getLogger(__name__)
def feedback_handler(func: Callable) -> Callable:
"""
Decorator definition for feedback API functions and methods
:param func: decorated function
:return: The decorated feedback view function
"""
@csrf_exempt
@wraps(func)
def handle_feedback(
request: HttpRequest, region_slug: str, language_slug: str
) -> JsonResponse:
"""
Parse feedback API request parameters
:param request: Django request
:param region_slug: slug of a region
:param language_slug: slug of a language
:return: The decorated feedback view function
"""
if request.method != "POST":
return JsonResponse({"error": "Invalid request."}, status=405)
try:
region = Region.objects.get(slug=region_slug)
language = Language.objects.get(slug=language_slug)
except Region.DoesNotExist:
return JsonResponse(
{"error": f'No region found with slug "{region_slug}"'}, status=404
)
except Language.DoesNotExist:
return JsonResponse(
{"error": f'No language found with slug "{language_slug}"'}, status=404
)
data = (
json.loads(request.body.decode())
if request.content_type == "application/json"
else request.POST.dict()
)
comment = data.pop("comment", "")
rating = data.pop("rating", None)
category = data.pop("category", None)
if rating not in [None, "up", "down"]:
return JsonResponse({"error": "Invalid rating."}, status=400)
if not comment and not rating:
return JsonResponse(
{"error": "Either comment or rating is required."}, status=400
)
rating_normalized: bool | None = feedback_ratings.NOT_STATED
if rating == "up":
rating_normalized = feedback_ratings.POSITIVE
elif rating == "down":
rating_normalized = feedback_ratings.NEGATIVE
is_technical = category == "Technisches Feedback"
return func(data, region, language, comment, rating_normalized, is_technical)
return handle_feedback
def json_response(function: Callable) -> Callable:
"""
This decorator can be used to catch :class:`~django.http.Http404` exceptions and convert them to a :class:`~django.http.JsonResponse`.
Without this decorator, the exceptions would be converted to :class:`~django.http.HttpResponse`.
:param function: The view function which should always return JSON
:return: The decorated function
"""
@wraps(function)
def wrap(
request: dict[str, str] | HttpRequest, *args: Any, **kwargs: Any
) -> HttpResponseRedirect | JsonResponse:
r"""
The inner function for this decorator.
It tries to execute the decorated view function and returns the unaltered result with the exception of a
:class:`~django.http.Http404` error, which is converted into JSON format.
:param request: Django request
:param \*args: The supplied arguments
:param \**kwargs: The supplied kwargs
:return: The response of the given function or an 404 :class:`~django.http.JsonResponse`
"""
try:
return function(request, *args, **kwargs)
except Http404 as e:
return JsonResponse({"error": str(e) or "Not found."}, status=404)
return wrap
def matomo_tracking(func: Callable) -> Callable:
"""
This decorator is supposed to be applied to API content endpoints. It will track
the request in Matomo. The request to the Matomo API is executed asynchronously in its
own thread to not block the Integreat CMS API request.
Only the URL and the User Agent will be sent to Matomo.
:param func: decorated function
:return: The decorated feedback view function
"""
def matomo_request(host: str, data: dict) -> None:
"""
Wrap HTTP request to Matomo in threaded function.
:param host: Hostname + protocol of Matomo installation
:param data: Data to send to Matomo API
"""
data_str = parse.urlencode(data)
url = f"{host}/matomo.php?{data_str}"
req = request.Request(url)
try:
with request.urlopen(req):
pass
except error.HTTPError as e:
logger.error(
"Matomo Tracking API request to %r failed with: %s",
# Mask auth token in log
re.sub(r"&token_auth=[^&]+", "&token_auth=********", url),
e,
)
@wraps(func)
def wrap(request: HttpRequest, *args: Any, **kwargs: Any) -> JsonResponse:
r"""
The inner function for this decorator.
:param request: Django request
:param \*args: The supplied arguments
:param \**kwargs: The supplied kwargs
:return: The response of the given function or an 404 :class:`~django.http.JsonResponse`
"""
if (
not settings.MATOMO_TRACKING
or not request.region.matomo_id
or not request.region.matomo_token
):
return func(request, *args, **kwargs)
data = {
"idsite": request.region.matomo_id,
"token_auth": request.region.matomo_token,
"rec": 1,
"url": request.build_absolute_uri(),
"urlref": settings.BASE_URL,
"ua": request.META.get("HTTP_USER_AGENT", "unknown user agent"),
"cip": f"{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(0, 255)}",
}
t = threading.Thread(target=matomo_request, args=(settings.MATOMO_URL, data))
t.daemon = True
t.start()
return func(request, *args, **kwargs)
return wrap