maquinaweb-shared-auth 0.1.0__tar.gz
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.
Potentially problematic release.
This version of maquinaweb-shared-auth might be problematic. Click here for more details.
- maquinaweb_shared_auth-0.1.0/PKG-INFO +1016 -0
- maquinaweb_shared_auth-0.1.0/README.md +995 -0
- maquinaweb_shared_auth-0.1.0/maquinaweb_shared_auth.egg-info/PKG-INFO +1016 -0
- maquinaweb_shared_auth-0.1.0/maquinaweb_shared_auth.egg-info/SOURCES.txt +16 -0
- maquinaweb_shared_auth-0.1.0/maquinaweb_shared_auth.egg-info/dependency_links.txt +1 -0
- maquinaweb_shared_auth-0.1.0/maquinaweb_shared_auth.egg-info/requires.txt +3 -0
- maquinaweb_shared_auth-0.1.0/maquinaweb_shared_auth.egg-info/top_level.txt +1 -0
- maquinaweb_shared_auth-0.1.0/pyproject.toml +32 -0
- maquinaweb_shared_auth-0.1.0/setup.cfg +4 -0
- maquinaweb_shared_auth-0.1.0/setup.py +16 -0
- maquinaweb_shared_auth-0.1.0/shared_auth/__init__.py +21 -0
- maquinaweb_shared_auth-0.1.0/shared_auth/conf.py +10 -0
- maquinaweb_shared_auth-0.1.0/shared_auth/exceptions.py +21 -0
- maquinaweb_shared_auth-0.1.0/shared_auth/fields.py +51 -0
- maquinaweb_shared_auth-0.1.0/shared_auth/managers.py +232 -0
- maquinaweb_shared_auth-0.1.0/shared_auth/mixins.py +201 -0
- maquinaweb_shared_auth-0.1.0/shared_auth/models.py +216 -0
- maquinaweb_shared_auth-0.1.0/shared_auth/serializers.py +162 -0
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maquinaweb-shared-auth
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Models read-only para autenticaΓ§Γ£o compartilhada entre projetos Django.
|
|
5
|
+
Author-email: Seu Nome <seuemail@dominio.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/maquinaweb/maquinaweb-shared-auth
|
|
8
|
+
Project-URL: Repository, https://github.com/maquinaweb/maquinaweb-shared-auth
|
|
9
|
+
Project-URL: Issues, https://github.com/maquinaweb/maquinaweb-shared-auth/issues
|
|
10
|
+
Keywords: django,auth,models,shared
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: django>=5
|
|
18
|
+
Requires-Dist: djangorestframework>=3
|
|
19
|
+
Requires-Dist: setuptools>=80
|
|
20
|
+
Dynamic: requires-python
|
|
21
|
+
|
|
22
|
+
# π Guia Completo - Biblioteca Compartilhada de AutenticaΓ§Γ£o
|
|
23
|
+
|
|
24
|
+
## π― O que Esta SoluΓ§Γ£o Faz?
|
|
25
|
+
|
|
26
|
+
Permite que **mΓΊltiplos sistemas Django** acessem os dados de **autenticaΓ§Γ£o e organizaΓ§Γ΅es** diretamente do banco de dados, sem fazer requisiΓ§Γ΅es HTTP, mantendo a mesma interface do Django ORM que vocΓͺ conhece.
|
|
27
|
+
|
|
28
|
+
### Vantagens
|
|
29
|
+
|
|
30
|
+
β
**Acesso direto ao banco** - Sem latΓͺncia de API
|
|
31
|
+
β
**Interface Django nativa** - `rascunho.user` funciona igual ao Django normal
|
|
32
|
+
β
**Read-only** - ImpossΓvel modificar dados por engano
|
|
33
|
+
β
**Sem duplicaΓ§Γ£o** - Um ΓΊnico banco de autenticaΓ§Γ£o para todos os sistemas
|
|
34
|
+
β
**Type-safe** - ValidaΓ§Γ΅es e exceΓ§Γ΅es customizadas
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## ποΈ Arquitetura
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
βββββββββββββββββββββββββββββββββββββββββββ
|
|
42
|
+
β Sistema de AutenticaΓ§Γ£o (Principal) β
|
|
43
|
+
β β
|
|
44
|
+
β ββββββββββββββββ ββββββββββββββββ β
|
|
45
|
+
β β Organization β β User β β
|
|
46
|
+
β ββββββββββββββββ ββββββββββββββββ β
|
|
47
|
+
β β β β
|
|
48
|
+
β βββββββββββ¬ββββββββββ β
|
|
49
|
+
β β β
|
|
50
|
+
β ββββββββΌβββββββ β
|
|
51
|
+
β β Member β β
|
|
52
|
+
β βββββββββββββββ β
|
|
53
|
+
ββββββββββββββββββββ¬βββββββββββββββββββββββ
|
|
54
|
+
β
|
|
55
|
+
ββββββββββββββ΄βββββββββββββ
|
|
56
|
+
β Banco de Dados Auth β
|
|
57
|
+
β (PostgreSQL/MySQL) β
|
|
58
|
+
ββββββββββββββ¬βββββββββββββ
|
|
59
|
+
β
|
|
60
|
+
ββββββββββββββ΄βββββββββββββ
|
|
61
|
+
β β
|
|
62
|
+
βββββββΌββββββ βββββββΌββββββ
|
|
63
|
+
β Sistema A β β Sistema B β
|
|
64
|
+
β β β β
|
|
65
|
+
β Rascunho β β Pedido β
|
|
66
|
+
β ββ org β β ββ org β
|
|
67
|
+
β ββ user β β ββ user β
|
|
68
|
+
βββββββββββββ βββββββββββββ
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## π¦ Passo 1: Criar a Biblioteca
|
|
74
|
+
|
|
75
|
+
### 1.1. Estrutura de DiretΓ³rios
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
mkdir shared-auth-lib
|
|
79
|
+
cd shared-auth-lib
|
|
80
|
+
|
|
81
|
+
# Criar estrutura
|
|
82
|
+
mkdir shared_auth
|
|
83
|
+
touch setup.py README.md
|
|
84
|
+
touch shared_auth/__init__.py
|
|
85
|
+
touch shared_auth/models.py
|
|
86
|
+
touch shared_auth/managers.py
|
|
87
|
+
touch shared_auth/exceptions.py
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 1.2. Copiar o CΓ³digo
|
|
91
|
+
|
|
92
|
+
Copie os arquivos do artifact anterior para a estrutura criada.
|
|
93
|
+
|
|
94
|
+
### 1.3. Instalar Localmente
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Modo desenvolvimento (changes refletem automaticamente)
|
|
98
|
+
pip install -e /path/to/shared-auth-lib
|
|
99
|
+
|
|
100
|
+
# Ou adicionar ao requirements.txt
|
|
101
|
+
echo "-e /path/to/shared-auth-lib" >> requirements.txt
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## π§ Passo 2: Configurar Banco de Dados
|
|
107
|
+
|
|
108
|
+
### 2.1. Criar UsuΓ‘rio Read-Only no PostgreSQL
|
|
109
|
+
|
|
110
|
+
```sql
|
|
111
|
+
-- No banco do sistema de autenticaΓ§Γ£o
|
|
112
|
+
CREATE USER readonly_user WITH PASSWORD 'senha_segura';
|
|
113
|
+
|
|
114
|
+
-- Conceder permissΓ΅es de leitura
|
|
115
|
+
GRANT CONNECT ON DATABASE sistema_auth_db TO readonly_user;
|
|
116
|
+
GRANT USAGE ON SCHEMA public TO readonly_user;
|
|
117
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;
|
|
118
|
+
|
|
119
|
+
-- Para tabelas futuras
|
|
120
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
121
|
+
GRANT SELECT ON TABLES TO readonly_user;
|
|
122
|
+
|
|
123
|
+
-- Garantir read-only
|
|
124
|
+
ALTER USER readonly_user SET default_transaction_read_only = on;
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 2.2. Para MySQL
|
|
128
|
+
|
|
129
|
+
```sql
|
|
130
|
+
CREATE USER 'readonly_user'@'%' IDENTIFIED BY 'senha_segura';
|
|
131
|
+
GRANT SELECT ON sistema_auth_db.* TO 'readonly_user'@'%';
|
|
132
|
+
FLUSH PRIVILEGES;
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## βοΈ Passo 3: Configurar Sistema Cliente
|
|
138
|
+
|
|
139
|
+
### 3.1. Settings.py
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
# settings.py do seu outro sistema
|
|
143
|
+
|
|
144
|
+
DATABASES = {
|
|
145
|
+
'default': {
|
|
146
|
+
# Banco do sistema atual
|
|
147
|
+
'ENGINE': 'django.db.backends.postgresql',
|
|
148
|
+
'NAME': 'meu_sistema_db',
|
|
149
|
+
'USER': 'meu_user',
|
|
150
|
+
'PASSWORD': 'senha',
|
|
151
|
+
'HOST': 'localhost',
|
|
152
|
+
'PORT': '5432',
|
|
153
|
+
},
|
|
154
|
+
'auth_db': {
|
|
155
|
+
# Banco do sistema de autenticaΓ§Γ£o (READ-ONLY)
|
|
156
|
+
'ENGINE': 'django.db.backends.postgresql',
|
|
157
|
+
'NAME': 'sistema_auth_db',
|
|
158
|
+
'USER': 'readonly_user',
|
|
159
|
+
'PASSWORD': 'senha_readonly',
|
|
160
|
+
'HOST': 'localhost', # ou IP do servidor de auth
|
|
161
|
+
'PORT': '5432',
|
|
162
|
+
'OPTIONS': {
|
|
163
|
+
'options': '-c default_transaction_read_only=on'
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Router para direcionar queries
|
|
169
|
+
DATABASE_ROUTERS = ['myapp.routers.SharedAuthRouter']
|
|
170
|
+
|
|
171
|
+
INSTALLED_APPS = [
|
|
172
|
+
# ... suas apps
|
|
173
|
+
'shared_auth', # Adicionar biblioteca
|
|
174
|
+
]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 3.2. Database Router
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
# myapp/routers.py
|
|
181
|
+
|
|
182
|
+
class SharedAuthRouter:
|
|
183
|
+
"""
|
|
184
|
+
Direciona queries dos models compartilhados para o banco correto
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
route_app_labels = {'shared_auth'}
|
|
188
|
+
|
|
189
|
+
def db_for_read(self, model, **hints):
|
|
190
|
+
"""Direciona leituras para auth_db"""
|
|
191
|
+
if model._meta.app_label in self.route_app_labels:
|
|
192
|
+
return 'auth_db'
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
def db_for_write(self, model, **hints):
|
|
196
|
+
"""Bloqueia escritas"""
|
|
197
|
+
if model._meta.app_label in self.route_app_labels:
|
|
198
|
+
return None # Impede qualquer escrita
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
def allow_migrate(self, db, app_label, model_name=None, **hints):
|
|
202
|
+
"""Bloqueia migrations"""
|
|
203
|
+
if app_label in self.route_app_labels:
|
|
204
|
+
return False
|
|
205
|
+
return None
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## π» Passo 4: Usar nos Seus Models
|
|
211
|
+
|
|
212
|
+
### 4.1. Model BΓ‘sico
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
# myapp/models.py
|
|
216
|
+
from django.db import models
|
|
217
|
+
|
|
218
|
+
class Rascunho(models.Model):
|
|
219
|
+
titulo = models.CharField(max_length=200)
|
|
220
|
+
conteudo = models.TextField()
|
|
221
|
+
|
|
222
|
+
# IDs de referΓͺncia
|
|
223
|
+
organization_id = models.IntegerField()
|
|
224
|
+
user_id = models.IntegerField()
|
|
225
|
+
|
|
226
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def organization(self):
|
|
230
|
+
"""Acessa organizaΓ§Γ£o do banco de auth"""
|
|
231
|
+
from shared_auth.models import SharedOrganization
|
|
232
|
+
return SharedOrganization.objects.using('auth_db').get_or_fail(
|
|
233
|
+
self.organization_id
|
|
234
|
+
```
|
|
235
|
+
# π Guia PrΓ‘tico - Mixins e Serializers Compartilhados
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
# models.py - UMA LINHA!
|
|
239
|
+
from shared_auth.mixins import OrganizationUserMixin, TimestampedMixin
|
|
240
|
+
from shared_auth.managers import BaseAuthManager
|
|
241
|
+
|
|
242
|
+
class Rascunho(OrganizationUserMixin, TimestampedMixin):
|
|
243
|
+
titulo = models.CharField(max_length=200)
|
|
244
|
+
conteudo = models.TextField()
|
|
245
|
+
|
|
246
|
+
objects = BaseAuthManager()
|
|
247
|
+
|
|
248
|
+
# Pronto! JΓ‘ tem tudo:
|
|
249
|
+
# - organization_id, user_id
|
|
250
|
+
# - properties: organization, user
|
|
251
|
+
# - mΓ©todos: validate_user_belongs_to_organization()
|
|
252
|
+
# - created_at, updated_at
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# serializers.py - UMA LINHA!
|
|
256
|
+
from shared_auth.serializers import OrganizationUserSerializerMixin
|
|
257
|
+
|
|
258
|
+
class RascunhoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
259
|
+
class Meta:
|
|
260
|
+
model = Rascunho
|
|
261
|
+
fields = [
|
|
262
|
+
'id', 'titulo', 'conteudo',
|
|
263
|
+
'organization_name', 'organization_email', # JΓ‘ disponΓveis!
|
|
264
|
+
'user_email', 'user_full_name', # JΓ‘ disponΓveis!
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
# Pronto! Todos os campos jΓ‘ funcionam!
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## π Exemplos de Uso por CenΓ‘rio
|
|
273
|
+
|
|
274
|
+
### 1. Model que Pertence APENAS a OrganizaΓ§Γ£o
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
# Exemplo: ConfiguraΓ§Γ£o da empresa
|
|
278
|
+
from shared_auth.mixins import OrganizationMixin
|
|
279
|
+
|
|
280
|
+
class EmpresaConfig(OrganizationMixin):
|
|
281
|
+
"""ConfiguraΓ§Γ΅es especΓficas da organizaΓ§Γ£o"""
|
|
282
|
+
|
|
283
|
+
tema_cor = models.CharField(max_length=7, default='#3490dc')
|
|
284
|
+
logo = models.ImageField(upload_to='logos/')
|
|
285
|
+
timezone = models.CharField(max_length=50, default='America/Sao_Paulo')
|
|
286
|
+
|
|
287
|
+
objects = BaseAuthManager()
|
|
288
|
+
|
|
289
|
+
def __str__(self):
|
|
290
|
+
return f"Config de {self.organization.name}"
|
|
291
|
+
|
|
292
|
+
# Uso
|
|
293
|
+
config = EmpresaConfig.objects.create(
|
|
294
|
+
organization_id=123,
|
|
295
|
+
tema_cor='#ff0000'
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
print(config.organization.name) # Funciona!
|
|
299
|
+
print(config.organization_is_active()) # MΓ©todo do mixin
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### 2. Model que Pertence APENAS a UsuΓ‘rio
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
# Exemplo: PreferΓͺncias do usuΓ‘rio
|
|
306
|
+
from shared_auth.mixins import UserMixin
|
|
307
|
+
|
|
308
|
+
class UserPreferences(UserMixin):
|
|
309
|
+
"""PreferΓͺncias pessoais do usuΓ‘rio"""
|
|
310
|
+
|
|
311
|
+
theme = models.CharField(max_length=20, default='light')
|
|
312
|
+
notifications_enabled = models.BooleanField(default=True)
|
|
313
|
+
language = models.CharField(max_length=5, default='pt-BR')
|
|
314
|
+
|
|
315
|
+
objects = BaseAuthManager()
|
|
316
|
+
|
|
317
|
+
# Uso
|
|
318
|
+
prefs = UserPreferences.objects.create(
|
|
319
|
+
user_id=456,
|
|
320
|
+
theme='dark'
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
print(prefs.user.email) # Funciona!
|
|
324
|
+
print(prefs.user_full_name) # Property do mixin
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### 3. Model com OrganizaΓ§Γ£o E UsuΓ‘rio (mais comum)
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
# Exemplo: Pedido, Rascunho, Tarefa, etc
|
|
331
|
+
from shared_auth.mixins import OrganizationUserMixin, TimestampedMixin
|
|
332
|
+
|
|
333
|
+
class Pedido(OrganizationUserMixin, TimestampedMixin):
|
|
334
|
+
"""Pedido pertence a organizaΓ§Γ£o e foi criado por usuΓ‘rio"""
|
|
335
|
+
|
|
336
|
+
numero = models.CharField(max_length=20, unique=True)
|
|
337
|
+
valor_total = models.DecimalField(max_digits=10, decimal_places=2)
|
|
338
|
+
status = models.CharField(max_length=20, default='pending')
|
|
339
|
+
|
|
340
|
+
objects = BaseAuthManager()
|
|
341
|
+
|
|
342
|
+
def __str__(self):
|
|
343
|
+
return f"Pedido {self.numero}"
|
|
344
|
+
|
|
345
|
+
# Uso
|
|
346
|
+
pedido = Pedido.objects.create(
|
|
347
|
+
organization_id=123,
|
|
348
|
+
user_id=456,
|
|
349
|
+
numero='PED-001',
|
|
350
|
+
valor_total=100.00
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Acessos automΓ‘ticos
|
|
354
|
+
print(pedido.organization.name) # OrganizaΓ§Γ£o
|
|
355
|
+
print(pedido.user.email) # UsuΓ‘rio que criou
|
|
356
|
+
print(pedido.user_full_name) # Nome completo
|
|
357
|
+
|
|
358
|
+
# ValidaΓ§Γ£o automΓ‘tica
|
|
359
|
+
if pedido.validate_user_belongs_to_organization():
|
|
360
|
+
print("OK - UsuΓ‘rio pertence Γ organizaΓ§Γ£o")
|
|
361
|
+
|
|
362
|
+
# Verificar se outro usuΓ‘rio pode acessar
|
|
363
|
+
if pedido.user_can_access(outro_user_id):
|
|
364
|
+
print("Pode acessar")
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## π¨ Serializers - Casos de Uso
|
|
370
|
+
|
|
371
|
+
### Caso 1: Serializer BΓ‘sico
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
from shared_auth.serializers import OrganizationUserSerializerMixin
|
|
375
|
+
|
|
376
|
+
class PedidoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
377
|
+
class Meta:
|
|
378
|
+
model = Pedido
|
|
379
|
+
fields = [
|
|
380
|
+
'id', 'numero', 'valor_total', 'status',
|
|
381
|
+
# Campos automΓ‘ticos do mixin:
|
|
382
|
+
'organization_name',
|
|
383
|
+
'organization_cnpj',
|
|
384
|
+
'organization_email',
|
|
385
|
+
'user_email',
|
|
386
|
+
'user_full_name',
|
|
387
|
+
'created_at', 'updated_at',
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
# Response JSON automΓ‘tica:
|
|
391
|
+
{
|
|
392
|
+
"id": 1,
|
|
393
|
+
"numero": "PED-001",
|
|
394
|
+
"valor_total": "100.00",
|
|
395
|
+
"status": "pending",
|
|
396
|
+
"organization_name": "Empresa XYZ",
|
|
397
|
+
"organization_cnpj": "12.345.678/0001-90",
|
|
398
|
+
"organization_email": "contato@xyz.com",
|
|
399
|
+
"user_email": "joao@xyz.com",
|
|
400
|
+
"user_full_name": "JoΓ£o Silva",
|
|
401
|
+
"created_at": "2025-10-01T10:00:00Z",
|
|
402
|
+
"updated_at": "2025-10-01T10:00:00Z"
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Caso 2: Serializer com Fields Customizados
|
|
407
|
+
|
|
408
|
+
```python
|
|
409
|
+
from shared_auth.serializers import OrganizationUserSerializerMixin
|
|
410
|
+
from shared_auth.fields import OrganizationField, UserField
|
|
411
|
+
|
|
412
|
+
class PedidoDetailSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
413
|
+
# Fields customizados que retornam objetos completos
|
|
414
|
+
organization_full = OrganizationField(source='*')
|
|
415
|
+
user_full = UserField(source='*')
|
|
416
|
+
|
|
417
|
+
class Meta:
|
|
418
|
+
model = Pedido
|
|
419
|
+
fields = [
|
|
420
|
+
'id', 'numero', 'valor_total',
|
|
421
|
+
'organization_full', # Objeto completo
|
|
422
|
+
'user_full', # Objeto completo
|
|
423
|
+
]
|
|
424
|
+
|
|
425
|
+
# Response JSON:
|
|
426
|
+
{
|
|
427
|
+
"id": 1,
|
|
428
|
+
"numero": "PED-001",
|
|
429
|
+
"valor_total": "100.00",
|
|
430
|
+
"organization_full": {
|
|
431
|
+
"id": 123,
|
|
432
|
+
"name": "Empresa XYZ",
|
|
433
|
+
"fantasy_name": "XYZ Ltda",
|
|
434
|
+
"cnpj": "12.345.678/0001-90",
|
|
435
|
+
"email": "contato@xyz.com",
|
|
436
|
+
"is_active": true
|
|
437
|
+
},
|
|
438
|
+
"user_full": {
|
|
439
|
+
"id": 456,
|
|
440
|
+
"username": "joao",
|
|
441
|
+
"email": "joao@xyz.com",
|
|
442
|
+
"full_name": "JoΓ£o Silva",
|
|
443
|
+
"is_active": true
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Caso 3: Serializer Apenas com Organization
|
|
449
|
+
|
|
450
|
+
```python
|
|
451
|
+
from shared_auth.serializers import OrganizationSerializerMixin
|
|
452
|
+
|
|
453
|
+
class EmpresaConfigSerializer(OrganizationSerializerMixin, serializers.ModelSerializer):
|
|
454
|
+
class Meta:
|
|
455
|
+
model = EmpresaConfig
|
|
456
|
+
fields = [
|
|
457
|
+
'id', 'tema_cor', 'logo', 'timezone',
|
|
458
|
+
'organization_name', # Do
|
|
459
|
+
# π¦ Guia Completo - Serializers com Objetos Aninhados
|
|
460
|
+
|
|
461
|
+
## Estrutura da Resposta JSON
|
|
462
|
+
|
|
463
|
+
### β
Antes (campos separados)
|
|
464
|
+
```json
|
|
465
|
+
{
|
|
466
|
+
"id": 1,
|
|
467
|
+
"titulo": "Meu Rascunho",
|
|
468
|
+
"organization_id": 123,
|
|
469
|
+
"organization_name": "Empresa XYZ",
|
|
470
|
+
"organization_cnpj": "12.345.678/0001-90",
|
|
471
|
+
"user_id": 456,
|
|
472
|
+
"user_email": "joao@xyz.com",
|
|
473
|
+
"user_full_name": "JoΓ£o Silva"
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### β¨ Depois (objetos aninhados)
|
|
478
|
+
```json
|
|
479
|
+
{
|
|
480
|
+
"id": 1,
|
|
481
|
+
"titulo": "Meu Rascunho",
|
|
482
|
+
"organization": {
|
|
483
|
+
"id": 123,
|
|
484
|
+
"name": "Empresa XYZ",
|
|
485
|
+
"cnpj": "12.345.678/0001-90",
|
|
486
|
+
"email": "contato@xyz.com"
|
|
487
|
+
},
|
|
488
|
+
"user": {
|
|
489
|
+
"id": 456,
|
|
490
|
+
"email": "joao@xyz.com",
|
|
491
|
+
"full_name": "JoΓ£o Silva"
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## π― CenΓ‘rios de Uso
|
|
499
|
+
|
|
500
|
+
### 1. Serializer Completo (Detail)
|
|
501
|
+
|
|
502
|
+
```python
|
|
503
|
+
# serializers.py
|
|
504
|
+
from rest_framework import serializers
|
|
505
|
+
from shared_auth.serializers import OrganizationUserSerializerMixin
|
|
506
|
+
from .models import Rascunho
|
|
507
|
+
|
|
508
|
+
class RascunhoDetailSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
509
|
+
"""Serializer completo para detalhes do rascunho"""
|
|
510
|
+
|
|
511
|
+
class Meta:
|
|
512
|
+
model = Rascunho
|
|
513
|
+
fields = [
|
|
514
|
+
'id',
|
|
515
|
+
'titulo',
|
|
516
|
+
'conteudo',
|
|
517
|
+
'organization', # Objeto completo
|
|
518
|
+
'user', # Objeto completo
|
|
519
|
+
'created_at',
|
|
520
|
+
'updated_at',
|
|
521
|
+
]
|
|
522
|
+
read_only_fields = ['organization', 'user', 'created_at', 'updated_at']
|
|
523
|
+
|
|
524
|
+
# views.py
|
|
525
|
+
class RascunhoViewSet(viewsets.ModelViewSet):
|
|
526
|
+
queryset = Rascunho.objects.all()
|
|
527
|
+
|
|
528
|
+
def get_serializer_class(self):
|
|
529
|
+
if self.action == 'retrieve':
|
|
530
|
+
return RascunhoDetailSerializer
|
|
531
|
+
return RascunhoListSerializer
|
|
532
|
+
|
|
533
|
+
# Response GET /api/rascunhos/1/
|
|
534
|
+
{
|
|
535
|
+
"id": 1,
|
|
536
|
+
"titulo": "Meu Rascunho",
|
|
537
|
+
"conteudo": "ConteΓΊdo completo aqui...",
|
|
538
|
+
"organization": {
|
|
539
|
+
"id": 123,
|
|
540
|
+
"name": "Empresa XYZ Ltda",
|
|
541
|
+
"fantasy_name": "XYZ",
|
|
542
|
+
"cnpj": "12.345.678/0001-90",
|
|
543
|
+
"email": "contato@xyz.com",
|
|
544
|
+
"telephone": "11-3333-4444",
|
|
545
|
+
"cellphone": "11-99999-8888",
|
|
546
|
+
"is_branch": false,
|
|
547
|
+
"is_active": true
|
|
548
|
+
},
|
|
549
|
+
"user": {
|
|
550
|
+
"id": 456,
|
|
551
|
+
"username": "joao.silva",
|
|
552
|
+
"email": "joao@xyz.com",
|
|
553
|
+
"first_name": "JoΓ£o",
|
|
554
|
+
"last_name": "Silva",
|
|
555
|
+
"full_name": "JoΓ£o Silva",
|
|
556
|
+
"is_active": true
|
|
557
|
+
},
|
|
558
|
+
"created_at": "2025-10-01T10:00:00Z",
|
|
559
|
+
"updated_at": "2025-10-01T15:30:00Z"
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### 2. Serializer Simplificado (List)
|
|
564
|
+
|
|
565
|
+
```python
|
|
566
|
+
from shared_auth.serializers import OrganizationSimpleSerializerMixin, UserSimpleSerializerMixin
|
|
567
|
+
|
|
568
|
+
class RascunhoListSerializer(OrganizationSimpleSerializerMixin, UserSimpleSerializerMixin, serializers.ModelSerializer):
|
|
569
|
+
"""Serializer simplificado para listagens"""
|
|
570
|
+
|
|
571
|
+
class Meta:
|
|
572
|
+
model = Rascunho
|
|
573
|
+
fields = [
|
|
574
|
+
'id',
|
|
575
|
+
'titulo',
|
|
576
|
+
'organization', # Simplificado
|
|
577
|
+
'user', # Simplificado
|
|
578
|
+
'created_at',
|
|
579
|
+
]
|
|
580
|
+
|
|
581
|
+
# Response GET /api/rascunhos/
|
|
582
|
+
{
|
|
583
|
+
"count": 10,
|
|
584
|
+
"results": [
|
|
585
|
+
{
|
|
586
|
+
"id": 1,
|
|
587
|
+
"titulo": "Rascunho 1",
|
|
588
|
+
"organization": {
|
|
589
|
+
"id": 123,
|
|
590
|
+
"name": "Empresa XYZ",
|
|
591
|
+
"cnpj": "12.345.678/0001-90"
|
|
592
|
+
},
|
|
593
|
+
"user": {
|
|
594
|
+
"id": 456,
|
|
595
|
+
"email": "joao@xyz.com",
|
|
596
|
+
"full_name": "JoΓ£o Silva"
|
|
597
|
+
},
|
|
598
|
+
"created_at": "2025-10-01T10:00:00Z"
|
|
599
|
+
},
|
|
600
|
+
// ... mais resultados
|
|
601
|
+
]
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### 3. Apenas Organization
|
|
606
|
+
|
|
607
|
+
```python
|
|
608
|
+
from shared_auth.serializers import OrganizationSerializerMixin
|
|
609
|
+
|
|
610
|
+
class EmpresaConfigSerializer(OrganizationSerializerMixin, serializers.ModelSerializer):
|
|
611
|
+
"""ConfiguraΓ§Γ΅es da empresa"""
|
|
612
|
+
|
|
613
|
+
class Meta:
|
|
614
|
+
model = EmpresaConfig
|
|
615
|
+
fields = [
|
|
616
|
+
'id',
|
|
617
|
+
'tema_cor',
|
|
618
|
+
'logo',
|
|
619
|
+
'timezone',
|
|
620
|
+
'organization', # Objeto completo
|
|
621
|
+
]
|
|
622
|
+
|
|
623
|
+
# Response
|
|
624
|
+
{
|
|
625
|
+
"id": 1,
|
|
626
|
+
"tema_cor": "#3490dc",
|
|
627
|
+
"logo": "https://example.com/logos/xyz.png",
|
|
628
|
+
"timezone": "America/Sao_Paulo",
|
|
629
|
+
"organization": {
|
|
630
|
+
"id": 123,
|
|
631
|
+
"name": "Empresa XYZ Ltda",
|
|
632
|
+
"fantasy_name": "XYZ",
|
|
633
|
+
"cnpj": "12.345.678/0001-90",
|
|
634
|
+
"email": "contato@xyz.com",
|
|
635
|
+
"telephone": "11-3333-4444",
|
|
636
|
+
"cellphone": "11-99999-8888",
|
|
637
|
+
"is_branch": false,
|
|
638
|
+
"is_active": true
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### 4. Apenas User
|
|
644
|
+
|
|
645
|
+
```python
|
|
646
|
+
from shared_auth.serializers import UserSerializerMixin
|
|
647
|
+
|
|
648
|
+
class UserPreferencesSerializer(UserSerializerMixin, serializers.ModelSerializer):
|
|
649
|
+
"""PreferΓͺncias do usuΓ‘rio"""
|
|
650
|
+
|
|
651
|
+
class Meta:
|
|
652
|
+
model = UserPreferences
|
|
653
|
+
fields = [
|
|
654
|
+
'id',
|
|
655
|
+
'theme',
|
|
656
|
+
'notifications_enabled',
|
|
657
|
+
'language',
|
|
658
|
+
'user', # Objeto completo
|
|
659
|
+
]
|
|
660
|
+
|
|
661
|
+
# Response
|
|
662
|
+
{
|
|
663
|
+
"id": 1,
|
|
664
|
+
"theme": "dark",
|
|
665
|
+
"notifications_enabled": true,
|
|
666
|
+
"language": "pt-BR",
|
|
667
|
+
"user": {
|
|
668
|
+
"id": 456,
|
|
669
|
+
"username": "joao.silva",
|
|
670
|
+
"email": "joao@xyz.com",
|
|
671
|
+
"first_name": "JoΓ£o",
|
|
672
|
+
"last_name": "Silva",
|
|
673
|
+
"full_name": "JoΓ£o Silva",
|
|
674
|
+
"is_active": true
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
## π§ CustomizaΓ§Γ£o AvanΓ§ada
|
|
682
|
+
|
|
683
|
+
### Adicionar Campos Extras ao Organization
|
|
684
|
+
|
|
685
|
+
```python
|
|
686
|
+
from shared_auth.serializers import OrganizationUserSerializerMixin
|
|
687
|
+
|
|
688
|
+
class RascunhoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
689
|
+
# Sobrescrever o mΓ©todo para adicionar campos extras
|
|
690
|
+
def get_organization(self, obj):
|
|
691
|
+
org_data = super().get_organization(obj)
|
|
692
|
+
if org_data:
|
|
693
|
+
# Adicionar campos customizados
|
|
694
|
+
org_data['logo_url'] = f"/logos/{obj.organization_id}.png"
|
|
695
|
+
org_data['member_count'] = obj.organization.members.count()
|
|
696
|
+
return org_data
|
|
697
|
+
|
|
698
|
+
class Meta:
|
|
699
|
+
model = Rascunho
|
|
700
|
+
fields = ['id', 'titulo', 'organization', 'user']
|
|
701
|
+
|
|
702
|
+
# Response
|
|
703
|
+
{
|
|
704
|
+
"id": 1,
|
|
705
|
+
"titulo": "Teste",
|
|
706
|
+
"organization": {
|
|
707
|
+
"id": 123,
|
|
708
|
+
"name": "Empresa XYZ",
|
|
709
|
+
// ... campos padrΓ£o
|
|
710
|
+
"logo_url": "/logos/123.png",
|
|
711
|
+
"member_count": 15
|
|
712
|
+
},
|
|
713
|
+
"user": { ... }
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
### Condicional - Mostrar Mais ou Menos Dados
|
|
718
|
+
|
|
719
|
+
```python
|
|
720
|
+
class RascunhoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
721
|
+
|
|
722
|
+
def get_organization(self, obj):
|
|
723
|
+
# UsuΓ‘rio logado Γ© da mesma organizaΓ§Γ£o?
|
|
724
|
+
request = self.context.get('request')
|
|
725
|
+
same_org = (request and
|
|
726
|
+
hasattr(request.user, 'logged_organization_id') and
|
|
727
|
+
request.user.logged_organization_id == obj.organization_id)
|
|
728
|
+
|
|
729
|
+
try:
|
|
730
|
+
org = obj.organization
|
|
731
|
+
data = {
|
|
732
|
+
'id': org.pk,
|
|
733
|
+
'name': org.name,
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
# Mostrar mais dados se for da mesma organizaΓ§Γ£o
|
|
737
|
+
if same_org:
|
|
738
|
+
data.update({
|
|
739
|
+
'cnpj': org.cnpj,
|
|
740
|
+
'email': org.email,
|
|
741
|
+
'telephone': org.telephone,
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
return data
|
|
745
|
+
except:
|
|
746
|
+
return None
|
|
747
|
+
|
|
748
|
+
class Meta:
|
|
749
|
+
model = Rascunho
|
|
750
|
+
fields = ['id', 'titulo', 'organization']
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
## π Performance - OtimizaΓ§Γ£o
|
|
756
|
+
|
|
757
|
+
### Problema N+1
|
|
758
|
+
|
|
759
|
+
```python
|
|
760
|
+
# RUIM: Causa N+1 queries
|
|
761
|
+
class RascunhoViewSet(viewsets.ModelViewSet):
|
|
762
|
+
queryset = Rascunho.objects.all() # 1 query
|
|
763
|
+
serializer_class = RascunhoSerializer
|
|
764
|
+
|
|
765
|
+
# Cada serializaΓ§Γ£o faz 2 queries (org + user)
|
|
766
|
+
# Total: 1 + (N * 2) queries
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### SoluΓ§Γ£o: Prefetch
|
|
770
|
+
|
|
771
|
+
```python
|
|
772
|
+
# BOM: Usa prefetch do manager
|
|
773
|
+
class RascunhoViewSet(viewsets.ModelViewSet):
|
|
774
|
+
serializer_class = RascunhoSerializer
|
|
775
|
+
|
|
776
|
+
def get_queryset(self):
|
|
777
|
+
# Usa o manager customizado
|
|
778
|
+
return Rascunho.objects.with_auth_data()
|
|
779
|
+
# Total: 3 queries (rascunhos + orgs + users)
|
|
780
|
+
|
|
781
|
+
# Ainda MELHOR: Cache no serializer
|
|
782
|
+
class RascunhoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
783
|
+
|
|
784
|
+
def get_organization(self, obj):
|
|
785
|
+
# Verifica se jΓ‘ estΓ‘ cacheado
|
|
786
|
+
if hasattr(obj, '_cached_organization'):
|
|
787
|
+
org = obj._cached_organization
|
|
788
|
+
else:
|
|
789
|
+
org = obj.organization
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
'id': org.pk,
|
|
793
|
+
'name': org.name,
|
|
794
|
+
// ...
|
|
795
|
+
}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
## π‘ Casos de Uso Reais
|
|
801
|
+
|
|
802
|
+
### Sistema de Pedidos
|
|
803
|
+
|
|
804
|
+
```python
|
|
805
|
+
class ItemPedidoSerializer(serializers.ModelSerializer):
|
|
806
|
+
class Meta:
|
|
807
|
+
model = ItemPedido
|
|
808
|
+
fields = ['id', 'produto', 'quantidade', 'valor_unitario']
|
|
809
|
+
|
|
810
|
+
class PedidoSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
811
|
+
itens = ItemPedidoSerializer(many=True, read_only=True)
|
|
812
|
+
|
|
813
|
+
class Meta:
|
|
814
|
+
model = Pedido
|
|
815
|
+
fields = [
|
|
816
|
+
'id', 'numero', 'valor_total', 'status',
|
|
817
|
+
'organization', # Cliente
|
|
818
|
+
'user', # Vendedor
|
|
819
|
+
'itens',
|
|
820
|
+
'created_at'
|
|
821
|
+
]
|
|
822
|
+
|
|
823
|
+
# Response
|
|
824
|
+
{
|
|
825
|
+
"id": 1,
|
|
826
|
+
"numero": "PED-001",
|
|
827
|
+
"valor_total": "1500.00",
|
|
828
|
+
"status": "pending",
|
|
829
|
+
"organization": {
|
|
830
|
+
"id": 123,
|
|
831
|
+
"name": "Cliente ABC",
|
|
832
|
+
"cnpj": "12.345.678/0001-90",
|
|
833
|
+
"email": "contato@abc.com"
|
|
834
|
+
},
|
|
835
|
+
"user": {
|
|
836
|
+
"id": 456,
|
|
837
|
+
"email": "vendedor@empresa.com",
|
|
838
|
+
"full_name": "JoΓ£o Vendedor"
|
|
839
|
+
},
|
|
840
|
+
"itens": [
|
|
841
|
+
{
|
|
842
|
+
"id": 1,
|
|
843
|
+
"produto": "Produto A",
|
|
844
|
+
"quantidade": 2,
|
|
845
|
+
"valor_unitario": "500.00"
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
"id": 2,
|
|
849
|
+
"produto": "Produto B",
|
|
850
|
+
"quantidade": 1,
|
|
851
|
+
"valor_unitario": "500.00"
|
|
852
|
+
}
|
|
853
|
+
],
|
|
854
|
+
"created_at": "2025-10-01T10:00:00Z"
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
### Sistema de Tarefas com MΓΊltiplos UsuΓ‘rios
|
|
859
|
+
|
|
860
|
+
```python
|
|
861
|
+
class TarefaSerializer(OrganizationUserSerializerMixin, serializers.ModelSerializer):
|
|
862
|
+
# user do mixin Γ© o criador
|
|
863
|
+
responsavel = serializers.SerializerMethodField()
|
|
864
|
+
|
|
865
|
+
def get_responsavel(self, obj):
|
|
866
|
+
"""Retorna usuΓ‘rio responsΓ‘vel"""
|
|
867
|
+
try:
|
|
868
|
+
resp = obj.responsavel
|
|
869
|
+
return {
|
|
870
|
+
'id': resp.pk,
|
|
871
|
+
'email': resp.email,
|
|
872
|
+
'full_name': resp.get_full_name(),
|
|
873
|
+
}
|
|
874
|
+
except:
|
|
875
|
+
return None
|
|
876
|
+
|
|
877
|
+
class Meta:
|
|
878
|
+
model = Tarefa
|
|
879
|
+
fields = [
|
|
880
|
+
'id', 'titulo', 'descricao', 'status', 'prioridade',
|
|
881
|
+
'organization', # OrganizaΓ§Γ£o dona da tarefa
|
|
882
|
+
'user', # Quem criou
|
|
883
|
+
'responsavel', # Quem vai fazer
|
|
884
|
+
'created_at'
|
|
885
|
+
]
|
|
886
|
+
|
|
887
|
+
# Response
|
|
888
|
+
{
|
|
889
|
+
"id": 1,
|
|
890
|
+
"titulo": "Implementar feature X",
|
|
891
|
+
"descricao": "DescriΓ§Γ£o detalhada...",
|
|
892
|
+
"status": "in_progress",
|
|
893
|
+
"prioridade": "high",
|
|
894
|
+
"organization": {
|
|
895
|
+
"id": 123,
|
|
896
|
+
"name": "Empresa XYZ"
|
|
897
|
+
},
|
|
898
|
+
"user": {
|
|
899
|
+
"id": 456,
|
|
900
|
+
"email": "gerente@xyz.com",
|
|
901
|
+
"full_name": "Gerente Silva"
|
|
902
|
+
},
|
|
903
|
+
"responsavel": {
|
|
904
|
+
"id": 789,
|
|
905
|
+
"email": "dev@xyz.com",
|
|
906
|
+
"full_name": "Dev Junior"
|
|
907
|
+
},
|
|
908
|
+
"created_at": "2025-10-01T10:00:00Z"
|
|
909
|
+
}
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
---
|
|
913
|
+
|
|
914
|
+
## π¨ Frontend - Consumindo a API
|
|
915
|
+
|
|
916
|
+
### React Example
|
|
917
|
+
|
|
918
|
+
```javascript
|
|
919
|
+
// components/RascunhoCard.jsx
|
|
920
|
+
function RascunhoCard({ rascunho }) {
|
|
921
|
+
return (
|
|
922
|
+
<div className="card">
|
|
923
|
+
<h3>{rascunho.titulo}</h3>
|
|
924
|
+
<p>{rascunho.conteudo}</p>
|
|
925
|
+
|
|
926
|
+
{/* Acessar dados aninhados */}
|
|
927
|
+
<div className="meta">
|
|
928
|
+
<span className="organization">
|
|
929
|
+
{rascunho.organization.name}
|
|
930
|
+
</span>
|
|
931
|
+
<span className="user">
|
|
932
|
+
Por: {rascunho.user.full_name}
|
|
933
|
+
</span>
|
|
934
|
+
</div>
|
|
935
|
+
|
|
936
|
+
{/* Verificar status */}
|
|
937
|
+
{!rascunho.organization.is_active && (
|
|
938
|
+
<div className="warning">
|
|
939
|
+
OrganizaΓ§Γ£o inativa
|
|
940
|
+
</div>
|
|
941
|
+
)}
|
|
942
|
+
</div>
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Uso
|
|
947
|
+
fetch('/api/rascunhos/1/')
|
|
948
|
+
.then(res => res.json())
|
|
949
|
+
.then(data => {
|
|
950
|
+
console.log(data.organization.name); // FΓ‘cil!
|
|
951
|
+
console.log(data.user.email); // Direto!
|
|
952
|
+
});
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
### Vue Example
|
|
956
|
+
|
|
957
|
+
```vue
|
|
958
|
+
<template>
|
|
959
|
+
<div class="rascunho">
|
|
960
|
+
<h3>{{ rascunho.titulo }}</h3>
|
|
961
|
+
|
|
962
|
+
<!-- Dados da organizaΓ§Γ£o -->
|
|
963
|
+
<div class="organization-info">
|
|
964
|
+
<h4>{{ rascunho.organization.name }}</h4>
|
|
965
|
+
<p>{{ rascunho.organization.cnpj }}</p>
|
|
966
|
+
<p>{{ rascunho.organization.email }}</p>
|
|
967
|
+
</div>
|
|
968
|
+
|
|
969
|
+
<!-- Dados do usuΓ‘rio -->
|
|
970
|
+
<div class="user-info">
|
|
971
|
+
<span>Criado por: {{ rascunho.user.full_name }}</span>
|
|
972
|
+
<span>Email: {{ rascunho.user.email }}</span>
|
|
973
|
+
</div>
|
|
974
|
+
</div>
|
|
975
|
+
</template>
|
|
976
|
+
|
|
977
|
+
<script>
|
|
978
|
+
export default {
|
|
979
|
+
data() {
|
|
980
|
+
return {
|
|
981
|
+
rascunho: null
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
async mounted() {
|
|
985
|
+
const response = await fetch('/api/rascunhos/1/');
|
|
986
|
+
this.rascunho = await response.json();
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
</script>
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
---
|
|
993
|
+
|
|
994
|
+
## β
Vantagens desta Abordagem
|
|
995
|
+
|
|
996
|
+
1. **Mais SemΓ’ntico**: Dados relacionados agrupados
|
|
997
|
+
2. **FΓ‘cil de Consumir**: Frontend acessa `obj.organization.name` diretamente
|
|
998
|
+
3. **FlexΓvel**: Pode ter versΓ£o completa e simplificada
|
|
999
|
+
4. **Type-Safe**: TypeScript/Flow adoram objetos aninhados
|
|
1000
|
+
5. **Cacheable**: Pode cachear o objeto `organization` inteiro
|
|
1001
|
+
6. **ReutilizΓ‘vel**: Mesma estrutura em todos os endpoints
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
## π Checklist de ImplementaΓ§Γ£o
|
|
1006
|
+
|
|
1007
|
+
- [ ] Atualizar `shared_auth/serializers.py` com novos mixins
|
|
1008
|
+
- [ ] Reinstalar biblioteca: `pip install -e /path/to/shared-auth-lib`
|
|
1009
|
+
- [ ] Atualizar serializers existentes
|
|
1010
|
+
- [ ] Testar responses dos endpoints
|
|
1011
|
+
- [ ] Atualizar documentaΓ§Γ£o da API
|
|
1012
|
+
- [ ] Atualizar cΓ³digo do frontend
|
|
1013
|
+
- [ ] Verificar performance (N+1 queries)
|
|
1014
|
+
- [ ] Adicionar testes
|
|
1015
|
+
|
|
1016
|
+
Pronto! Agora seus serializers retornam dados organizados em objetos `organization` e `user`! π
|