reactpy_django 3.8.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. js/node_modules/flatted/python/flatted.py +149 -0
  2. js/node_modules/flatted/python/test.py +63 -0
  3. reactpy_django/__init__.py +28 -0
  4. reactpy_django/apps.py +11 -0
  5. reactpy_django/checks.py +542 -0
  6. reactpy_django/clean.py +141 -0
  7. reactpy_django/components.py +286 -0
  8. reactpy_django/config.py +135 -0
  9. reactpy_django/database.py +31 -0
  10. reactpy_django/decorators.py +101 -0
  11. reactpy_django/exceptions.py +34 -0
  12. reactpy_django/hooks.py +496 -0
  13. reactpy_django/http/__init__.py +0 -0
  14. reactpy_django/http/urls.py +18 -0
  15. reactpy_django/http/views.py +62 -0
  16. reactpy_django/management/__init__.py +0 -0
  17. reactpy_django/management/commands/__init__.py +0 -0
  18. reactpy_django/management/commands/clean_reactpy.py +37 -0
  19. reactpy_django/migrations/0001_initial.py +25 -0
  20. reactpy_django/migrations/0002_rename_created_at_componentparams_last_accessed.py +17 -0
  21. reactpy_django/migrations/0003_componentsession_delete_componentparams.py +28 -0
  22. reactpy_django/migrations/0004_config.py +27 -0
  23. reactpy_django/migrations/0005_alter_componentsession_last_accessed.py +17 -0
  24. reactpy_django/migrations/0006_userdatamodel.py +28 -0
  25. reactpy_django/migrations/__init__.py +0 -0
  26. reactpy_django/models.py +48 -0
  27. reactpy_django/py.typed +1 -0
  28. reactpy_django/router/__init__.py +5 -0
  29. reactpy_django/router/converters.py +7 -0
  30. reactpy_django/router/resolvers.py +58 -0
  31. reactpy_django/static/reactpy_django/client.js +1630 -0
  32. reactpy_django/templates/reactpy/component.html +27 -0
  33. reactpy_django/templatetags/__init__.py +0 -0
  34. reactpy_django/templatetags/reactpy.py +221 -0
  35. reactpy_django/types.py +121 -0
  36. reactpy_django/utils.py +384 -0
  37. reactpy_django/websocket/__init__.py +0 -0
  38. reactpy_django/websocket/consumer.py +218 -0
  39. reactpy_django/websocket/paths.py +17 -0
  40. reactpy_django-3.8.1.dist-info/LICENSE.md +9 -0
  41. reactpy_django-3.8.1.dist-info/METADATA +152 -0
  42. reactpy_django-3.8.1.dist-info/RECORD +44 -0
  43. reactpy_django-3.8.1.dist-info/WHEEL +6 -0
  44. reactpy_django-3.8.1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,496 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Awaitable,
9
+ Callable,
10
+ DefaultDict,
11
+ Sequence,
12
+ Union,
13
+ cast,
14
+ overload,
15
+ )
16
+ from uuid import uuid4
17
+
18
+ import orjson
19
+ from channels import DEFAULT_CHANNEL_LAYER
20
+ from channels.db import database_sync_to_async
21
+ from channels.layers import InMemoryChannelLayer, get_channel_layer
22
+ from reactpy import use_callback, use_effect, use_memo, use_ref, use_state
23
+ from reactpy import use_connection as _use_connection
24
+ from reactpy import use_location as _use_location
25
+ from reactpy import use_scope as _use_scope
26
+ from reactpy.backend.types import Location
27
+
28
+ from reactpy_django.exceptions import UserNotFoundError
29
+ from reactpy_django.types import (
30
+ AsyncMessageReceiver,
31
+ AsyncMessageSender,
32
+ ConnectionType,
33
+ FuncParams,
34
+ Inferred,
35
+ Mutation,
36
+ MutationOptions,
37
+ Query,
38
+ QueryOptions,
39
+ UserData,
40
+ )
41
+ from reactpy_django.utils import generate_obj_name, get_pk
42
+
43
+ if TYPE_CHECKING:
44
+ from channels_redis.core import RedisChannelLayer
45
+ from django.contrib.auth.models import AbstractUser
46
+
47
+
48
+ _logger = logging.getLogger(__name__)
49
+ _REFETCH_CALLBACKS: DefaultDict[Callable[..., Any], set[Callable[[], None]]] = (
50
+ DefaultDict(set)
51
+ )
52
+
53
+
54
+ # TODO: Deprecate this once the equivalent hook gets moved to reactpy.hooks.*
55
+ def use_location() -> Location:
56
+ """Get the current route as a `Location` object"""
57
+ return _use_location()
58
+
59
+
60
+ def use_origin() -> str | None:
61
+ """Get the current origin as a string. If the browser did not send an origin header,
62
+ this will be None."""
63
+ scope = _use_scope()
64
+ try:
65
+ if scope["type"] == "websocket":
66
+ return next(
67
+ (
68
+ header[1].decode("utf-8")
69
+ for header in scope["headers"]
70
+ if header[0] == b"origin"
71
+ ),
72
+ None,
73
+ )
74
+ if scope["type"] == "http":
75
+ host = next(
76
+ (
77
+ header[1].decode("utf-8")
78
+ for header in scope["headers"]
79
+ if header[0] == b"host"
80
+ )
81
+ )
82
+ return f"{scope['scheme']}://{host}" if host else None
83
+ except Exception:
84
+ _logger.info("Failed to get origin")
85
+ return None
86
+
87
+
88
+ # TODO: Deprecate this once the equivalent hook gets moved to reactpy.hooks.*
89
+ def use_scope() -> dict[str, Any]:
90
+ """Get the current ASGI scope dictionary"""
91
+ scope = _use_scope()
92
+
93
+ if isinstance(scope, dict):
94
+ return scope
95
+
96
+ raise TypeError(f"Expected scope to be a dict, got {type(scope)}")
97
+
98
+
99
+ # TODO: Deprecate this once the equivalent hook gets moved to reactpy.hooks.*
100
+ def use_connection() -> ConnectionType:
101
+ """Get the current `Connection` object"""
102
+ return _use_connection()
103
+
104
+
105
+ @overload
106
+ def use_query(
107
+ options: QueryOptions,
108
+ /,
109
+ query: Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred],
110
+ *args: FuncParams.args,
111
+ **kwargs: FuncParams.kwargs,
112
+ ) -> Query[Inferred]: ...
113
+
114
+
115
+ @overload
116
+ def use_query(
117
+ query: Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred],
118
+ *args: FuncParams.args,
119
+ **kwargs: FuncParams.kwargs,
120
+ ) -> Query[Inferred]: ...
121
+
122
+
123
+ def use_query(*args, **kwargs) -> Query[Inferred]:
124
+ """This hook is used to execute functions in the background and return the result, \
125
+ typically to read data the Django ORM.
126
+
127
+ Args:
128
+ options: An optional `QueryOptions` object that can modify how the query is executed.
129
+ query: A callable that returns a Django `Model` or `QuerySet`.
130
+ *args: Positional arguments to pass into `query`.
131
+
132
+ Keyword Args:
133
+ **kwargs: Keyword arguments to pass into `query`."""
134
+
135
+ should_execute, set_should_execute = use_state(True)
136
+ data, set_data = use_state(cast(Inferred, None))
137
+ loading, set_loading = use_state(True)
138
+ error, set_error = use_state(cast(Union[Exception, None], None))
139
+ if isinstance(args[0], QueryOptions):
140
+ query_options, query, query_args, query_kwargs = _use_query_args_1(
141
+ *args, **kwargs
142
+ )
143
+ else:
144
+ query_options, query, query_args, query_kwargs = _use_query_args_2(
145
+ *args, **kwargs
146
+ )
147
+ query_ref = use_ref(query)
148
+ if query_ref.current is not query:
149
+ raise ValueError(f"Query function changed from {query_ref.current} to {query}.")
150
+
151
+ async def execute_query() -> None:
152
+ """The main running function for `use_query`"""
153
+ try:
154
+ # Run the query
155
+ if asyncio.iscoroutinefunction(query):
156
+ new_data = await query(*query_args, **query_kwargs)
157
+ else:
158
+ new_data = await database_sync_to_async(
159
+ query,
160
+ thread_sensitive=query_options.thread_sensitive,
161
+ )(*query_args, **query_kwargs)
162
+
163
+ # Run the postprocessor
164
+ if query_options.postprocessor:
165
+ if asyncio.iscoroutinefunction(query_options.postprocessor):
166
+ new_data = await query_options.postprocessor(
167
+ new_data, **query_options.postprocessor_kwargs
168
+ )
169
+ else:
170
+ new_data = await database_sync_to_async(
171
+ query_options.postprocessor,
172
+ thread_sensitive=query_options.thread_sensitive,
173
+ )(new_data, **query_options.postprocessor_kwargs)
174
+
175
+ # Log any errors and set the error state
176
+ except Exception as e:
177
+ set_data(cast(Inferred, None))
178
+ set_loading(False)
179
+ set_error(e)
180
+ _logger.exception(f"Failed to execute query: {generate_obj_name(query)}")
181
+ return
182
+
183
+ # Query was successful
184
+ else:
185
+ set_data(new_data)
186
+ set_loading(False)
187
+ set_error(None)
188
+
189
+ @use_effect(dependencies=None)
190
+ def schedule_query() -> None:
191
+ """Schedule the query to be run when needed"""
192
+ # Make sure we don't re-execute the query unless we're told to
193
+ if not should_execute:
194
+ return
195
+ set_should_execute(False)
196
+
197
+ # Execute the query in the background
198
+ asyncio.create_task(execute_query())
199
+
200
+ @use_callback
201
+ def refetch() -> None:
202
+ """Callable provided to the user, used to re-execute the query"""
203
+ set_should_execute(True)
204
+ set_loading(True)
205
+ set_error(None)
206
+
207
+ @use_effect(dependencies=[])
208
+ def register_refetch_callback() -> Callable[[], None]:
209
+ """Track the refetch callback so we can re-execute the query"""
210
+ # By tracking callbacks globally, any usage of the query function will be re-run
211
+ # if the user has told a mutation to refetch it.
212
+ _REFETCH_CALLBACKS[query].add(refetch)
213
+ return lambda: _REFETCH_CALLBACKS[query].remove(refetch)
214
+
215
+ # The query's user API
216
+ return Query(data, loading, error, refetch)
217
+
218
+
219
+ @overload
220
+ def use_mutation(
221
+ options: MutationOptions,
222
+ mutation: (
223
+ Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]]
224
+ ),
225
+ refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None,
226
+ ) -> Mutation[FuncParams]: ...
227
+
228
+
229
+ @overload
230
+ def use_mutation(
231
+ mutation: (
232
+ Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]]
233
+ ),
234
+ refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None,
235
+ ) -> Mutation[FuncParams]: ...
236
+
237
+
238
+ def use_mutation(*args: Any, **kwargs: Any) -> Mutation[FuncParams]:
239
+ """This hook is used to modify data in the background, typically to create/update/delete \
240
+ data from the Django ORM.
241
+
242
+ Mutation functions can `return False` to prevent executing your `refetch` function. All \
243
+ other returns are ignored. Mutation functions can be sync or async.
244
+
245
+ Args:
246
+ mutation: A callable that performs Django ORM create, update, or delete \
247
+ functionality. If this function returns `False`, then your `refetch` \
248
+ function will not be used.
249
+ refetch: A query function (the function you provide to your `use_query` \
250
+ hook) or a sequence of query functions that need a `refetch` if the \
251
+ mutation succeeds. This is useful for refreshing data after a mutation \
252
+ has been performed.
253
+ """
254
+
255
+ loading, set_loading = use_state(False)
256
+ error, set_error = use_state(cast(Union[Exception, None], None))
257
+ if isinstance(args[0], MutationOptions):
258
+ mutation_options, mutation, refetch = _use_mutation_args_1(*args, **kwargs)
259
+ else:
260
+ mutation_options, mutation, refetch = _use_mutation_args_2(*args, **kwargs)
261
+
262
+ # The main "running" function for `use_mutation`
263
+ async def execute_mutation(exec_args, exec_kwargs) -> None:
264
+ # Run the mutation
265
+ try:
266
+ if asyncio.iscoroutinefunction(mutation):
267
+ should_refetch = await mutation(*exec_args, **exec_kwargs)
268
+ else:
269
+ should_refetch = await database_sync_to_async(
270
+ mutation, thread_sensitive=mutation_options.thread_sensitive
271
+ )(*exec_args, **exec_kwargs)
272
+
273
+ # Log any errors and set the error state
274
+ except Exception as e:
275
+ set_loading(False)
276
+ set_error(e)
277
+ _logger.exception(
278
+ f"Failed to execute mutation: {generate_obj_name(mutation)}"
279
+ )
280
+
281
+ # Mutation was successful
282
+ else:
283
+ set_loading(False)
284
+ set_error(None)
285
+
286
+ # `refetch` will execute unless explicitly told not to
287
+ # or if `refetch` was not defined.
288
+ if should_refetch is not False and refetch:
289
+ for query in (refetch,) if callable(refetch) else refetch:
290
+ for callback in _REFETCH_CALLBACKS.get(query) or ():
291
+ callback()
292
+
293
+ # Schedule the mutation to be run when needed
294
+ @use_callback
295
+ def schedule_mutation(
296
+ *exec_args: FuncParams.args, **exec_kwargs: FuncParams.kwargs
297
+ ) -> None:
298
+ # Set the loading state.
299
+ # It's okay to re-execute the mutation if we're told to. The user
300
+ # can use the `loading` state to prevent this.
301
+ set_loading(True)
302
+
303
+ # Execute the mutation in the background
304
+ asyncio.ensure_future(
305
+ execute_mutation(exec_args=exec_args, exec_kwargs=exec_kwargs)
306
+ )
307
+
308
+ # Used when the user has told us to reset this mutation
309
+ @use_callback
310
+ def reset() -> None:
311
+ set_loading(False)
312
+ set_error(None)
313
+
314
+ # The mutation's user API
315
+ return Mutation(schedule_mutation, loading, error, reset)
316
+
317
+
318
+ def use_user() -> AbstractUser:
319
+ """Get the current `User` object from either the WebSocket or HTTP request."""
320
+ connection = use_connection()
321
+ user = connection.scope.get("user") or getattr(connection.carrier, "user", None)
322
+ if user is None:
323
+ raise UserNotFoundError("No user is available in the current environment.")
324
+ return user
325
+
326
+
327
+ def use_user_data(
328
+ default_data: (
329
+ None | dict[str, Callable[[], Any] | Callable[[], Awaitable[Any]] | Any]
330
+ ) = None,
331
+ save_default_data: bool = False,
332
+ ) -> UserData:
333
+ """Get or set user data stored within the REACTPY_DATABASE.
334
+
335
+ Kwargs:
336
+ default_data: A dictionary containing `{key: default_value}` pairs. \
337
+ For computationally intensive defaults, your `default_value` \
338
+ can be sync or async functions that return the value to set.
339
+ save_default_data: If True, `default_data` values will automatically be stored \
340
+ within the database if they do not exist.
341
+ """
342
+ from reactpy_django.models import UserDataModel
343
+
344
+ user = use_user()
345
+
346
+ async def _set_user_data(data: dict):
347
+ if not isinstance(data, dict):
348
+ raise TypeError(f"Expected dict while setting user data, got {type(data)}")
349
+ if user.is_anonymous:
350
+ raise ValueError("AnonymousUser cannot have user data.")
351
+
352
+ pk = get_pk(user)
353
+ model, _ = await UserDataModel.objects.aget_or_create(user_pk=pk)
354
+ model.data = orjson.dumps(data)
355
+ await model.asave()
356
+
357
+ query: Query[dict | None] = use_query(
358
+ QueryOptions(postprocessor=None),
359
+ _get_user_data,
360
+ user=user,
361
+ default_data=default_data,
362
+ save_default_data=save_default_data,
363
+ )
364
+ mutation = use_mutation(_set_user_data, refetch=_get_user_data)
365
+
366
+ return UserData(query, mutation)
367
+
368
+
369
+ def use_channel_layer(
370
+ name: str | None = None,
371
+ *,
372
+ group_name: str | None = None,
373
+ group_add: bool = True,
374
+ group_discard: bool = True,
375
+ receiver: AsyncMessageReceiver | None = None,
376
+ layer: str = DEFAULT_CHANNEL_LAYER,
377
+ ) -> AsyncMessageSender:
378
+ """
379
+ Subscribe to a Django Channels layer to send/receive messages.
380
+
381
+ Args:
382
+ name: The name of the channel to subscribe to. If you define a `group_name`, you \
383
+ can keep `name` undefined to auto-generate a unique name.
384
+ group_name: If configured, any messages sent within this hook will be broadcasted \
385
+ to all channels in this group.
386
+ group_add: If `True`, the channel will automatically be added to the group \
387
+ when the component mounts.
388
+ group_discard: If `True`, the channel will automatically be removed from the \
389
+ group when the component dismounts.
390
+ receiver: An async function that receives a `message: dict` from a channel. \
391
+ If more than one receiver waits on the same channel name, a random receiver \
392
+ will get the result.
393
+ layer: The channel layer to use. This layer must be defined in \
394
+ `settings.py:CHANNEL_LAYERS`.
395
+ """
396
+ channel_layer: InMemoryChannelLayer | RedisChannelLayer = get_channel_layer(layer)
397
+ channel_name = use_memo(lambda: str(name or uuid4()))
398
+
399
+ if not name and not group_name:
400
+ raise ValueError("You must define a `name` or `group_name` for the channel.")
401
+
402
+ if not channel_layer:
403
+ raise ValueError(
404
+ f"Channel layer '{layer}' is not available. Are you sure you"
405
+ " configured settings.py:CHANNEL_LAYERS properly?"
406
+ )
407
+
408
+ # Add/remove a group's channel during component mount/dismount respectively.
409
+ @use_effect(dependencies=[])
410
+ async def group_manager():
411
+ if group_name and group_add:
412
+ await channel_layer.group_add(group_name, channel_name)
413
+
414
+ if group_name and group_discard:
415
+ return lambda: asyncio.run(
416
+ channel_layer.group_discard(group_name, channel_name)
417
+ )
418
+
419
+ # Listen for messages on the channel using the provided `receiver` function.
420
+ @use_effect
421
+ async def message_receiver():
422
+ if not receiver:
423
+ return
424
+
425
+ while True:
426
+ message = await channel_layer.receive(channel_name)
427
+ await receiver(message)
428
+
429
+ # User interface for sending messages to the channel
430
+ async def message_sender(message: dict):
431
+ if group_name:
432
+ await channel_layer.group_send(group_name, message)
433
+ else:
434
+ await channel_layer.send(channel_name, message)
435
+
436
+ return message_sender
437
+
438
+
439
+ def use_root_id() -> str:
440
+ """Get the root element's ID. This value is guaranteed to be unique. Current versions of \
441
+ ReactPy-Django return a `uuid4` string."""
442
+ scope = use_scope()
443
+
444
+ return scope["reactpy"]["id"]
445
+
446
+
447
+ def _use_query_args_1(options: QueryOptions, /, query: Query, *args, **kwargs):
448
+ return options, query, args, kwargs
449
+
450
+
451
+ def _use_query_args_2(query: Query, *args, **kwargs):
452
+ return QueryOptions(), query, args, kwargs
453
+
454
+
455
+ def _use_mutation_args_1(options: MutationOptions, mutation: Mutation, refetch=None):
456
+ return options, mutation, refetch
457
+
458
+
459
+ def _use_mutation_args_2(mutation, refetch=None):
460
+ return MutationOptions(), mutation, refetch
461
+
462
+
463
+ async def _get_user_data(
464
+ user: AbstractUser, default_data: None | dict, save_default_data: bool
465
+ ) -> dict | None:
466
+ """The mutation function for `use_user_data`"""
467
+ from reactpy_django.models import UserDataModel
468
+
469
+ if not user or user.is_anonymous:
470
+ return None
471
+
472
+ pk = get_pk(user)
473
+ model, _ = await UserDataModel.objects.aget_or_create(user_pk=pk)
474
+ data = orjson.loads(model.data) if model.data else {}
475
+
476
+ if not isinstance(data, dict):
477
+ raise TypeError(f"Expected dict while loading user data, got {type(data)}")
478
+
479
+ # Set default values, if needed
480
+ if default_data:
481
+ changed = False
482
+ for key, value in default_data.items():
483
+ if key not in data:
484
+ new_value: Any = value
485
+ if asyncio.iscoroutinefunction(value):
486
+ new_value = await value()
487
+ elif callable(value):
488
+ new_value = value()
489
+ data[key] = new_value
490
+ changed = True
491
+ if changed:
492
+ model.data = orjson.dumps(data)
493
+ if save_default_data:
494
+ await model.asave()
495
+
496
+ return data
File without changes
@@ -0,0 +1,18 @@
1
+ from django.urls import path
2
+
3
+ from . import views
4
+
5
+ app_name = "reactpy"
6
+
7
+ urlpatterns = [
8
+ path(
9
+ "web_module/<path:file>",
10
+ views.web_modules_file,
11
+ name="web_modules",
12
+ ),
13
+ path(
14
+ "iframe/<str:dotted_path>",
15
+ views.view_to_iframe,
16
+ name="view_to_iframe",
17
+ ),
18
+ ]
@@ -0,0 +1,62 @@
1
+ import os
2
+ from urllib.parse import parse_qs
3
+
4
+ from aiofile import async_open
5
+ from django.core.cache import caches
6
+ from django.core.exceptions import SuspiciousOperation
7
+ from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
8
+ from reactpy.config import REACTPY_WEB_MODULES_DIR
9
+
10
+ from reactpy_django.utils import create_cache_key, render_view
11
+
12
+
13
+ async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:
14
+ """Gets JavaScript required for ReactPy modules at runtime. These modules are
15
+ returned from cache if available."""
16
+ from reactpy_django.config import REACTPY_CACHE
17
+
18
+ web_modules_dir = REACTPY_WEB_MODULES_DIR.current
19
+ path = os.path.abspath(web_modules_dir.joinpath(file))
20
+
21
+ # Prevent attempts to walk outside of the web modules dir
22
+ if str(web_modules_dir) != os.path.commonpath((path, web_modules_dir)):
23
+ raise SuspiciousOperation(
24
+ "Attempt to access a directory outside of REACTPY_WEB_MODULES_DIR."
25
+ )
26
+
27
+ # Fetch the file from cache, if available
28
+ last_modified_time = os.stat(path).st_mtime
29
+ cache_key = create_cache_key("web_modules", path)
30
+ file_contents = await caches[REACTPY_CACHE].aget(
31
+ cache_key, version=int(last_modified_time)
32
+ )
33
+ if file_contents is None:
34
+ async with async_open(path, "r") as fp:
35
+ file_contents = await fp.read()
36
+ await caches[REACTPY_CACHE].adelete(cache_key)
37
+ await caches[REACTPY_CACHE].aset(
38
+ cache_key, file_contents, timeout=604800, version=int(last_modified_time)
39
+ )
40
+ return HttpResponse(file_contents, content_type="text/javascript")
41
+
42
+
43
+ async def view_to_iframe(request: HttpRequest, dotted_path: str) -> HttpResponse:
44
+ """Returns a view that was registered by reactpy_django.components.view_to_iframe."""
45
+ from reactpy_django.config import REACTPY_REGISTERED_IFRAME_VIEWS
46
+
47
+ # Get the view
48
+ registered_view = REACTPY_REGISTERED_IFRAME_VIEWS.get(dotted_path)
49
+ if not registered_view:
50
+ return HttpResponseNotFound()
51
+
52
+ # Get args and kwargs from the request
53
+ query = request.META.get("QUERY_STRING", "")
54
+ kwargs = {k: v if len(v) > 1 else v[0] for k, v in parse_qs(query).items()}
55
+ args = kwargs.pop("_args", [])
56
+
57
+ # Render the view
58
+ response = await render_view(registered_view, request, args, kwargs)
59
+
60
+ # Ensure page can be rendered as an iframe
61
+ response["X-Frame-Options"] = "SAMEORIGIN"
62
+ return response
File without changes
File without changes
@@ -0,0 +1,37 @@
1
+ from typing import Literal
2
+
3
+ from django.core.management.base import BaseCommand
4
+
5
+
6
+ class Command(BaseCommand):
7
+ help = "Manually clean ReactPy data. When using this command without args, it will perform all cleaning operations."
8
+
9
+ def handle(self, **options):
10
+ from reactpy_django.clean import clean
11
+
12
+ verbosity = options.get("verbosity", 1)
13
+
14
+ cleaning_args: set[Literal["all", "sessions", "user_data"]] = set()
15
+ if options.get("sessions"):
16
+ cleaning_args.add("sessions")
17
+ if options.get("user_data"):
18
+ cleaning_args.add("user_data")
19
+ if not cleaning_args:
20
+ cleaning_args = {"all"}
21
+
22
+ clean(*cleaning_args, immediate=True, verbosity=verbosity)
23
+
24
+ if verbosity >= 1:
25
+ print("ReactPy data has been cleaned!")
26
+
27
+ def add_arguments(self, parser):
28
+ parser.add_argument(
29
+ "--sessions",
30
+ action="store_true",
31
+ help="Configure this clean to only clean session data (and other configured cleaning options).",
32
+ )
33
+ parser.add_argument(
34
+ "--user-data",
35
+ action="store_true",
36
+ help="Configure this clean to only clean user data (and other configured cleaning options).",
37
+ )
@@ -0,0 +1,25 @@
1
+ # Generated by Django 4.1.3 on 2023-01-10 00:54
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ initial = True
8
+
9
+ dependencies = []
10
+
11
+ operations = [
12
+ migrations.CreateModel(
13
+ name="ComponentParams",
14
+ fields=[
15
+ (
16
+ "uuid",
17
+ models.UUIDField(
18
+ editable=False, primary_key=True, serialize=False, unique=True
19
+ ),
20
+ ),
21
+ ("data", models.BinaryField()),
22
+ ("created_at", models.DateTimeField(auto_now_add=True)),
23
+ ],
24
+ ),
25
+ ]
@@ -0,0 +1,17 @@
1
+ # Generated by Django 4.1.5 on 2023-01-11 01:55
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("reactpy_django", "0001_initial"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.RenameField(
13
+ model_name="componentparams",
14
+ old_name="created_at",
15
+ new_name="last_accessed",
16
+ ),
17
+ ]
@@ -0,0 +1,28 @@
1
+ # Generated by Django 4.1.6 on 2023-02-21 10:59
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("reactpy_django", "0002_rename_created_at_componentparams_last_accessed"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.CreateModel(
13
+ name="ComponentSession",
14
+ fields=[
15
+ (
16
+ "uuid",
17
+ models.UUIDField(
18
+ editable=False, primary_key=True, serialize=False, unique=True
19
+ ),
20
+ ),
21
+ ("params", models.BinaryField()),
22
+ ("last_accessed", models.DateTimeField(auto_now_add=True)),
23
+ ],
24
+ ),
25
+ migrations.DeleteModel(
26
+ name="ComponentParams",
27
+ ),
28
+ ]