├── .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/* --------------------------------------------------------------------------------