├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.md
├── django_nice
├── __init__.py
├── config.py
├── frontend.py
├── signals.py
├── sse.py
├── urls.py
└── views.py
├── pyproject.toml
└── setup.cfg
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish to PyPI
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*.*.*"
7 |
8 | jobs:
9 | publish:
10 | name: Publish to PyPI
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v2
16 |
17 | - name: Set up Python
18 | uses: actions/setup-python@v2
19 | with:
20 | python-version: 3.x
21 |
22 | - name: Install dependencies
23 | run: |
24 | python -m pip install --upgrade pip
25 | pip install build twine
26 |
27 | - name: Build the package
28 | run: python -m build
29 |
30 | - name: Publish to PyPI
31 | env:
32 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
33 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
34 | run: twine upload dist/*
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | venv/
2 | dist/
3 | build/
4 | django_nice.egg-info/
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 rexsum420
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
2 | include LICENSE
3 | include *.txt
4 | recursive-include django_nice/static *
5 | recursive-include django_nice/templates *
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # django-nice
2 |
3 | `django-nice` is a Python library designed to seamlessly integrate Django models with NiceGUI elements and Server-Sent Events (SSE) for real-time synchronization. This library allows you to bind NiceGUI frontend components (such as text areas, input fields, etc.) to Django model fields, ensuring that changes to either the backend or the frontend are synchronized in real-time.
4 |
5 | ## Why Use django-nice?
6 |
7 | When working with Django and NiceGUI, binding frontend elements directly to Django models in a dynamic, real-time manner can be challenging. Out-of-the-box integrations often rely on manual updates, polling, or heavy reliance on traditional forms, which can be slow or cumbersome for web applications that require seamless real-time interactions.
8 |
9 | `django-nice` solves these challenges by:
10 |
11 | 1. **Real-Time Sync with SSE**: The library leverages **Server-Sent Events (SSE)** to keep the frontend NiceGUI elements in sync with the backend Django models in real-time. When the backend data changes, the frontend is updated immediately without needing to refresh or manually poll.
12 |
13 | 2. **Bidirectional Data Binding**: The library allows changes in the frontend to automatically update the corresponding Django model, and vice versa. This ensures consistency between the client and the server.
14 |
15 | 3. **REST API-Based Updates**: The library expose model fields as API endpoints. This makes the process of updating the Django backend from the frontend smooth, without needing to implement complex form handling.
16 |
17 | ### Advantages Over Regular Django-NiceGUI Integration:
18 |
19 | - **Real-Time Updates**: Typical Django-NiceGUI integrations doesn’t provide automatic real-time syncing data between the frontend and backend in real-time. `django-nice` provides automatic updates through SSE, allowing the frontend to reflect changes as soon as they happen in the backend.
20 | - **Effortless Binding**: Instead of manually writing JavaScript, forms, or custom API calls to keep frontend elements in sync with Django models, `django-nice` handles this for you with minimal configuration.
21 | - **Improved User Experience**: By offering real-time data updates, the library enhances the responsiveness of your NiceGUI app, creating a smoother and more interactive user experience.
22 |
23 | ## Tutorial
24 |
25 | In your project's `urls.py` file, add the necessary API and SSE endpoints, this tutorial will use custom `.env` variables:
26 |
27 | ### 1.0 Installation and settings
28 |
29 | After installing the package `django-nice`, just edit your `settings.py` file:
30 |
31 | ```python
32 | INSTALLED_APPS += [
33 | 'corsheaders'
34 | ]
35 | MIDDLEWARE = [
36 | 'corsheaders.middleware.CorsMiddleware',
37 | ]
38 | CORS_ALLOW_ALL_ORIGINS = True
39 | ```
40 |
41 | Remember to install the `corsheaders` package as well.
42 |
43 | ### 1.1 Define Model Endpoints in Django
44 |
45 | ```python
46 | from django_nice.config import Config
47 | from dotenv import load_dotenv
48 | import os
49 |
50 | load_dotenv()
51 |
52 |
53 | config = Config.configure(
54 | host=os.getenv("DJANGO_DOMAIN") + ":" + os.getenv("DJANGO_PORT"),
55 | api_endpoint=os.getenv("NICEGUI_ENDPOINT"),
56 | require_auth=True, # By default this value is True
57 | )
58 | config.add_urls_to_project(urlpatterns, app_label="your-app", model_name="User")
59 | ```
60 |
61 | ### 1.2 Customize the User Model
62 |
63 | A custom User model is required to add a token field for authentication.
64 | Follow a tutorial like [this one](https://testdriven.io/blog/django-custom-user-model/) and add a `token` field:
65 |
66 | ```python
67 | token = models.CharField(max_length=65, unique=True)
68 | ```
69 |
70 | ### 1.3 Set Up Environment Variables
71 |
72 | ```
73 | DJANGO_SETTINGS_MODULE=your-app.settings
74 | DJANGO_PORT="8000"
75 | DJANGO_DOMAIN="http://localhost"
76 |
77 | NICEGUI_STORAGE_SECRETKEY=your-secret
78 | NICEGUI_ENDPOINT=api
79 | NICEGUI_HOST="http://localhost"
80 | NICEGUI_PORT="8080"
81 | ```
82 |
83 | ### 2. Create NiceGUI server
84 |
85 | Organize the NiceGUI server as needed; in this tutorial, we’ll use a single NiceGUI file with login and model-binding functionality.
86 | Copy that content and create a file inside the root folder where is your `urls.py` file, call it as example `frontend.py`.
87 |
88 | Refer to inline comments for explanations.
89 |
90 | ```python
91 | #!usr/bin/env python
92 | import binascii
93 | import os
94 | import django
95 | import jwt
96 |
97 | from django_nice.frontend import bind_element_to_model
98 | from django_nice.config import Config
99 | from django.contrib.auth import aauthenticate
100 | from django.utils.decorators import sync_and_async_middleware
101 | from django.conf import settings
102 | from asgiref.sync import sync_to_async
103 | from dotenv import load_dotenv
104 | from nicegui import ui
105 | from nicegui import app
106 |
107 | load_dotenv()
108 | django.setup()
109 |
110 | async def login_save(user):
111 | await ui.context.client.connected()
112 | payload = {"token": binascii.hexlify(os.urandom(30)).decode()}
113 | user.token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
114 | await sync_to_async(user.save)()
115 | app.storage.tab.update({"user_id": user.id, "token": user.token}) # We are using https://nicegui.io/documentation/storage
116 |
117 |
118 | async def logout_save():
119 | await ui.context.client.connected()
120 | user = await get_logged_user()
121 | user.token = ""
122 | await sync_to_async(user.save)()
123 | app.storage.tab.clear()
124 |
125 |
126 | async def is_logged():
127 | await ui.context.client.connected()
128 | if os.getenv("DEBUG"):
129 | from your_app.models.users import User
130 |
131 | user = await wrap_async(User.objects.get, id=1) # In this way you can calls Django methods inside NiceGUI
132 | login_save(user)
133 | return True
134 | return app.storage.tab.get("token", False)
135 |
136 |
137 | async def get_logged_user():
138 | from your_app.models.users import User
139 |
140 | user_id = app.storage.tab.get("user_id", False)
141 | return await wrap_async(User.objects.get, id=user_id) # In this way you can calls Django methods inside NiceGUI
142 |
143 |
144 | async def wrap_async(method, **parameters):
145 | return await sync_to_async(method)(**parameters)
146 |
147 |
148 | Config.configure(
149 | host=os.getenv("DJANGO_DOMAIN") + ":" + os.getenv("DJANGO_PORT"),
150 | api_endpoint="/" + os.getenv("NICEGUI_ENDPOINT"),
151 | require_auth=True, # By default `require_auth` is set to True
152 | )
153 |
154 |
155 | @ui.page("/logout")
156 | async def logout() -> None:
157 | await logout_save()
158 | ui.navigate.to("/login")
159 |
160 |
161 | @ui.page("/login")
162 | @sync_and_async_middleware
163 | async def login() -> RedirectResponse | None:
164 | async def try_login() -> None:
165 | user = await aauthenticate(username=username.value, password=password.value) # This is already async
166 | if user is not None:
167 | await login_save(user)
168 | ui.navigate.to(app.storage.user.get("referrer_path", "/"))
169 | else:
170 | ui.notify("Wrong username or password", color="negative")
171 |
172 | if await is_logged():
173 | ui.navigate.to(app.storage.user.get("referrer_path", "/"))
174 |
175 | with ui.card().classes("absolute-center"):
176 | username = ui.input("Username").on("keydown.enter", try_login)
177 | password = ui.input("Password", password=True, password_toggle_button=True).on("keydown.enter", try_login)
178 | ui.button("Log in", on_click=try_login)
179 | return None
180 |
181 | @ui.page("/")
182 | async def index():
183 | if await is_logged():
184 | user = await get_logged_user()
185 | ui.label("Welcome " + user.username)
186 | inputbox = ui.input("").style("width: 25%")
187 | bind_element_to_model(
188 | inputbox,
189 | app_label="your-app",
190 | model_name="User",
191 | object_id=user.id,
192 | fields=["email"],
193 | element_id="email",
194 | token=user.token, # the token already saved to the user is used to match iff it is valid
195 | )
196 | else:
197 | ui.navigate.to("/login")
198 |
199 | ui.run(
200 | host=os.getenv("NICEGUI_HOST").replace("http://", ""),
201 | port=int(os.getenv("NICEGUI_PORT")),
202 | storage_secret=os.getenv("NICEGUI_STORAGE_SECRETKEY"), # We use it to save some data on browser side for login stuff
203 | )
204 | ```
205 |
206 | This code includes a full login system in NiceGUI, if `DEBUG` is set automatically login as the first user in the DB and create a custom function `wrap_async` used to call Django method (that are sync, in a async way).
207 |
208 | ## Start servers for both apps
209 |
210 | ```bash
211 | (venv)$ ./manage.py runserver 0.0.0.0:8000
212 | Server started on port 8000
213 | ```
214 | in a different terminal
215 |
216 | ```bash
217 | (venv)$ python3 frontend/frontend.py
218 | Server started on port 8080
219 | ```
220 |
221 | ## Notes
222 |
223 | ### 1. **Dynamic Binding with `dynamic_query`**
224 |
225 | - The `bind_element_to_model` function supports **dynamic queries** (`dynamic_query` parameter), which allows model instances to be retrieved dynamically based on any criteria (e.g., a logged-in user's ID, the current high score, etc.).
226 |
227 | **Example:**
228 | ```python
229 | bind_element_to_model(
230 | element,
231 | app_label='people',
232 | model_name='Person',
233 | dynamic_query={'id': request.user.id}, # Bind to the logged-in user's instance
234 | field_name='first_name',
235 | element_id='userFirstName'
236 | )
237 | ```
238 |
239 | ### 2. **Binding Multiple Fields to a Single UI Element**
240 |
241 | - The function allows **binding multiple fields** of a model instance to a single UI element. This is achieved by passing a list of fields through the `fields` parameter.
242 |
243 | **Example:**
244 | ```python
245 | bind_element_to_model(
246 | element,
247 | app_label='people',
248 | model_name='Person',
249 | fields=['first_name', 'last_name', 'age'], # Bind multiple fields
250 | element_id='personInfo'
251 | )
252 | ```
253 |
--------------------------------------------------------------------------------
/django_nice/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rexsum420/django-nice/ed1d757b853897eb5d272655ccc54fa5e3cf1922/django_nice/__init__.py
--------------------------------------------------------------------------------
/django_nice/config.py:
--------------------------------------------------------------------------------
1 | import os
2 | import django
3 | from django.apps import apps
4 | from django.db.models.signals import post_save
5 | from .urls import register_endpoints
6 | from .signals import model_update_signal, setup_signals
7 |
8 | def register_signals_dynamically(app_label, model_name):
9 | for app in apps.get_app_configs():
10 | if apps.is_installed(app_label):
11 | model = apps.get_model(app_label, model_name)
12 | setup_signals(app_label, model, model_update_signal)
13 |
14 | class Config:
15 | _instance = None
16 |
17 | def __new__(cls):
18 | if cls._instance is None:
19 | cls._instance = super(Config, cls).__new__(cls)
20 | cls._instance.host = 'http://127.0.0.1:8000'
21 | cls._instance.api_endpoint = '/api'
22 | cls._instance.require_auth = True
23 | cls.setup_django_environment()
24 |
25 | django.setup()
26 |
27 | return cls._instance
28 |
29 | @classmethod
30 | def setup_django_environment(cls):
31 | """Dynamically configure Django environment variables."""
32 | if not os.getenv('DJANGO_SETTINGS_MODULE'):
33 | project_settings = cls._find_django_settings()
34 | if project_settings:
35 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', project_settings)
36 | else:
37 | raise RuntimeError(
38 | "DJANGO_SETTINGS_MODULE is not set, and the settings module could not be dynamically determined."
39 | )
40 |
41 | # Ensure apps are ready before proceeding
42 | if not apps.ready:
43 | django.setup()
44 |
45 | @classmethod
46 | def _find_django_settings(cls):
47 | """Attempt to dynamically find the Django settings module."""
48 | possible_settings = [
49 | os.getenv('DJANGO_SETTINGS_MODULE'),
50 | ]
51 | for settings_module in possible_settings:
52 | if settings_module:
53 | return settings_module
54 | return None
55 |
56 | @classmethod
57 | def configure(cls, host, api_endpoint='/api', require_auth=True):
58 | config = cls() or cls()._instance
59 | config.host = host.rstrip('/')
60 | config.api_endpoint = api_endpoint.rstrip('/')
61 | config.require_auth = require_auth
62 | return cls
63 |
64 | @classmethod
65 | def get_host(cls):
66 | return cls._instance.host
67 |
68 | @classmethod
69 | def get_api_endpoint(cls):
70 | return cls._instance.api_endpoint
71 |
72 | @classmethod
73 | def get_model(cls, app_label, model_name):
74 | return apps.get_model(app_label, model_name)
75 |
76 | @classmethod
77 | def get_auth(cls):
78 | return cls._instance.require_auth
79 |
80 | @classmethod
81 | def add_urls_to_project(cls, urlpatterns, app_label, model_name):
82 | try:
83 | api_endpoint = cls._instance.get_api_endpoint()
84 | except:
85 | api_endpoint = 'api'
86 |
87 | # Use the get_model method from the class
88 | model = cls.get_model(app_label, model_name)
89 |
90 | post_save.connect(model_update_signal, sender=model)
91 | register_signals_dynamically(app_label, model_name)
92 | urlpatterns += register_endpoints(app_label, model_name, api_endpoint, cls.get_auth())
93 |
--------------------------------------------------------------------------------
/django_nice/frontend.py:
--------------------------------------------------------------------------------
1 | from nicegui import ui
2 | import requests
3 | from .config import Config
4 | from django.utils.decorators import sync_and_async_middleware
5 |
6 | @sync_and_async_middleware
7 | def bind_element_to_model(element, app_label, model_name, object_id=None, fields=None, element_id=None,
8 | property_name='value', dynamic_query=None, token=None):
9 | if fields is None or not isinstance(fields, list):
10 | return
11 |
12 | host = Config.get_host()
13 | api_endpoint = Config.get_api_endpoint()
14 | model = Config.get_model(app_label, model_name)
15 |
16 | headers = {
17 | "Authorization": f"Bearer {token}"
18 | }
19 |
20 | # Use dynamic queries (e.g., find by user ID or high score) if provided
21 | if dynamic_query:
22 | instance = model.objects.filter(**dynamic_query).first()
23 | if instance:
24 | object_id = instance.pk
25 | else:
26 | return # No instance found for the dynamic query
27 |
28 | if not object_id:
29 | return # Fail gracefully if object_id is still None
30 |
31 | # Fetch initial data for all fields
32 | def fetch_initial_data():
33 | data = {}
34 | for field_name in fields:
35 | url = f'{host}{api_endpoint}/{app_label}/{model_name}/{object_id}/{field_name}'
36 | response = requests.get(url, headers=headers)
37 | if response.status_code == 200:
38 | data[field_name] = response.json().get(field_name, '')
39 | else:
40 | print("There was an error with the request " + url + " with status " + response.status_code)
41 | data[field_name] = ''
42 | return data
43 |
44 | # Update data for a specific field
45 | def update_data(field_name, value):
46 | if value is None or value == '':
47 | pass
48 | else:
49 | url = f'{host}{api_endpoint}/{app_label}/{model_name}/{object_id}/{field_name}/'
50 | requests.post(url, json={field_name: value}, headers=headers)
51 |
52 | # Initialize the element with combined data from all fields
53 | initial_data = fetch_initial_data()
54 | combined_data = ', '.join([initial_data[field] for field in fields])
55 | setattr(element, property_name, combined_data)
56 |
57 | # Listener events based on the element type
58 | if isinstance(element, ui.input):
59 | listener_event = 'update:model-value'
60 | element_tag = 'input'
61 | elif isinstance(element, ui.checkbox):
62 | listener_event = 'update:model-checked'
63 | element_tag = 'input[type="checkbox"]'
64 | elif isinstance(element, ui.slider):
65 | listener_event = 'update:model-value'
66 | element_tag = 'input[type="range"]'
67 | elif isinstance(element, ui.textarea):
68 | listener_event = 'update:model-value'
69 | element_tag = 'textarea'
70 | elif isinstance(element, ui.button):
71 | listener_event = 'click'
72 | element_tag = 'button'
73 | else:
74 | listener_event = f'update:model-{property_name}'
75 | element_tag = '*'
76 |
77 | # Handle frontend changes by updating the respective field in the model
78 | def on_frontend_change(e):
79 | new_value = ''.join(e.args).split(', ')
80 | field_values = {field: value for field, value in zip(fields, new_value)}
81 | for field_name, value in field_values.items():
82 | update_data(field_name, value)
83 |
84 | element.on(listener_event, on_frontend_change)
85 | element.props(f'class=model-element-class id={element_id}')
86 |
87 | # Set up Server-Sent Events (SSE) to update the element when any field changes
88 | def set_value_in_element(new_data):
89 | combined_data = ', '.join([f'{field}: {new_data[field]}' for field in fields])
90 | element.set_value(combined_data)
91 |
92 | for field_name in fields:
93 | sse_url = f'{host}{api_endpoint}/sse/{app_label}/{model_name}/{object_id}/{field_name}/'
94 | ui.add_body_html(f"""
95 |
123 | """)
124 |
125 |
--------------------------------------------------------------------------------
/django_nice/signals.py:
--------------------------------------------------------------------------------
1 | from django.db.models.signals import post_save
2 | from django.dispatch import receiver
3 | from django.apps import apps
4 | from .sse import SSEManager
5 | from django.db.models import Model
6 |
7 | # You can update the signals like so for different bindings
8 |
9 | # @receiver(post_save, sender=HighScore)
10 | # def high_score_update_signal(sender, instance, **kwargs):
11 | # if instance.is_highest:
12 | # # Notify all listeners about the new high score and the user
13 | # SSEManager.notify_listeners(sender.__name__, instance.pk, 'score', instance.score)
14 | # SSEManager.notify_listeners(sender.__name__, instance.pk, 'user', instance.user.username)
15 |
16 | @receiver(post_save)
17 | def model_update_signal(sender, instance, **kwargs):
18 |
19 | for field in instance._meta.fields:
20 | field_name = field.name
21 | new_value = getattr(instance, field_name, None)
22 |
23 | if new_value is not None:
24 | SSEManager.notify_listeners(sender.__name__, instance.pk, field_name, new_value)
25 |
26 |
27 | def setup_signals(app_label, model, signal_handler):
28 | post_save.connect(signal_handler, sender=model)
--------------------------------------------------------------------------------
/django_nice/sse.py:
--------------------------------------------------------------------------------
1 | from django.http import StreamingHttpResponse
2 | from collections import deque
3 | import time
4 |
5 | class SSEManager:
6 | _listeners = {}
7 |
8 | @classmethod
9 | def register_listener(cls, model_name, object_id, field_name):
10 | if model_name not in cls._listeners:
11 | cls._listeners[model_name] = {}
12 | if object_id not in cls._listeners[model_name]:
13 | cls._listeners[model_name][object_id] = {}
14 | if field_name not in cls._listeners[model_name][object_id]:
15 | cls._listeners[model_name][object_id][field_name] = deque()
16 | return cls._listeners[model_name][object_id][field_name]
17 |
18 | @classmethod
19 | def notify_listeners(cls, model_name, object_id, field_name, new_value):
20 | listeners = cls._listeners.get(model_name, {}).get(object_id, {}).get(field_name, deque())
21 | listeners.append(new_value)
22 |
23 | @classmethod
24 | def stream_updates(cls, request, app_label, model_name, object_id, field_name):
25 | def event_stream():
26 | listeners = cls.register_listener(model_name, object_id, field_name)
27 |
28 | from django.apps import apps
29 | model = apps.get_model(app_label, model_name)
30 | try:
31 | instance = model.objects.get(pk=object_id)
32 | last_value = getattr(instance, field_name)
33 | except model.DoesNotExist:
34 | last_value = None
35 |
36 | if last_value is not None:
37 | yield f"data: {last_value}\n\n"
38 |
39 | try:
40 | while True:
41 | if listeners:
42 | try:
43 | new_value = listeners.popleft()
44 | yield f"data: {new_value}\n\n"
45 | except IndexError:
46 | pass
47 | yield ":\n\n"
48 | time.sleep(1)
49 | except GeneratorExit:
50 | cls._listeners[model_name][object_id][field_name].clear()
51 | raise
52 |
53 | return StreamingHttpResponse(event_stream(), content_type='text/event-stream')
54 |
--------------------------------------------------------------------------------
/django_nice/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from .views import ModelAPI, AuthModelAPI
3 | from .sse import SSEManager
4 |
5 | def register_endpoints(app_label, model_name, api_endpoint, require_auth):
6 | view = ModelAPI.as_view()
7 | if require_auth:
8 | view = AuthModelAPI.as_view()
9 |
10 | return [
11 | path(
12 | f'{api_endpoint}/////',
13 | view,
14 | name=f'{model_name}_detail'
15 | ),
16 | path(
17 | f'{api_endpoint}/sse/////',
18 | lambda request, app_label, model_name, object_id, field_name:
19 | SSEManager.stream_updates(request, app_label, model_name, object_id, field_name),
20 | name=f'{model_name}_sse'
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/django_nice/views.py:
--------------------------------------------------------------------------------
1 | from django.views.decorators.csrf import csrf_exempt
2 | from django.utils.decorators import method_decorator
3 | from django.http import JsonResponse
4 | from django.views import View
5 | from django.apps import apps
6 | from django.contrib.auth import get_user_model
7 | from django.conf import settings
8 | import jwt
9 |
10 | import json
11 |
12 | @method_decorator(csrf_exempt, name='dispatch')
13 | class ModelAPI(View):
14 | def get(self, request, app_label, model_name, object_id, field_name):
15 | model = apps.get_model(app_label, model_name)
16 | try:
17 | instance = model.objects.get(pk=object_id)
18 | field_value = getattr(instance, field_name)
19 | data = {
20 | field_name: field_value
21 | }
22 | return JsonResponse(data)
23 | except model.DoesNotExist:
24 | return JsonResponse({"error": "Object not found"}, status=404)
25 |
26 | def post(self, request, app_label, model_name, object_id, field_name):
27 | model = apps.get_model(app_label, model_name)
28 | instance = model.objects.get(pk=object_id)
29 | try:
30 | data = json.loads(request.body)
31 | field_value = data.get(field_name)
32 | except ValueError:
33 | return JsonResponse({'error': 'Invalid JSON'}, status=400)
34 |
35 | if field_value is None or field_value == '':
36 | return JsonResponse({'error': 'Field value cannot be empty'}, status=400)
37 |
38 | if field_name and hasattr(instance, field_name):
39 | setattr(instance, field_name, field_value)
40 | instance.save(update_fields=[field_name])
41 | return JsonResponse({field_name: getattr(instance, field_name)})
42 |
43 | return JsonResponse({'error': 'Field not found or invalid data'}, status=400)
44 |
45 |
46 | class AuthModelAPI(ModelAPI):
47 |
48 | def get(self, request, app_label, model_name, object_id, field_name):
49 | auth_header = request.headers.get("Authorization")
50 | if auth_header != '':
51 | token = auth_header.split(" ")[1] if " " in auth_header else auth_header
52 | payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
53 | print(payload.token)
54 | user = get_user_model().objects.get(token=payload.token)
55 | if user:
56 | return super().get(request, app_label, model_name, object_id, field_name)
57 | return JsonResponse({field_name: 'No valid access'})
58 |
59 | def post(self, request, app_label, model_name, object_id, field_name):
60 | auth_header = request.headers.get("Authorization")
61 | if auth_header != '':
62 | token = auth_header.split(" ")[1] if " " in auth_header else auth_header
63 | payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
64 | user = get_user_model().objects.get(token=payload.token)
65 | if user:
66 | return super().post(request, app_label, model_name, object_id, field_name)
67 | return JsonResponse({field_name: 'No valid access'})
68 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = [
3 | "setuptools",
4 | "wheel",
5 | ]
6 | build-backend = "setuptools.build_meta"
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = django-nice
3 | version = 0.5.11
4 | description = Library to bind Django models with NiceGUI elements using API and SSE.
5 | author = Jeffery Springs
6 | author_email = rexsum420@gmail.com
7 | url = https://github.com/rexsum420/django-nice
8 | long_description = file: README.md
9 | long_description_content_type = text/markdown
10 |
11 | [options]
12 | packages=find:
13 | install_requires =
14 | Django>=3.2
15 | django-sse
16 | nicegui
17 | requests
18 | python_requires = >=3.6
19 | include_package_data = True
20 |
21 | [options.extras_require]
22 | dev =
23 | pytest
24 | black
25 | flake8
26 |
27 | [options.package_data]
28 | * = static/*, templates/*
--------------------------------------------------------------------------------