wagtail-enap-designsystem 1.2.1.167__py3-none-any.whl → 1.2.1.168__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wagtail-enap-designsystem might be problematic. Click here for more details.

@@ -91,6 +91,7 @@ from enap_designsystem.blocks import EnapBannerBlock
91
91
  from enap_designsystem.blocks import FeatureImageTextBlock
92
92
  from enap_designsystem.blocks import EnapAccordionBlock
93
93
  from enap_designsystem.blocks.base_blocks import ButtonGroupBlock
94
+ from enap_designsystem.blocks.content_blocks import CardBlock
94
95
  from enap_designsystem.blocks.base_blocks import CarouselBlock
95
96
  from enap_designsystem.blocks import CourseIntroTopicsBlock
96
97
  from .blocks import WhyChooseEnaptBlock
@@ -6047,3 +6048,515 @@ class FooterGenericoSnippet(ClusterableModel):
6047
6048
 
6048
6049
 
6049
6050
 
6051
+
6052
+
6053
+ # enap_designsystem/models.py
6054
+ from django.db import models
6055
+ from django import forms
6056
+ from wagtail.models import Page
6057
+ from wagtail.fields import RichTextField
6058
+ from wagtail.admin.panels import FieldPanel, MultiFieldPanel
6059
+
6060
+
6061
+ # ============================================
6062
+ # PÁGINA MÃE - ÍNDICE DAS CÁPSULAS
6063
+ # ============================================
6064
+
6065
+ class CapsulaIndexPage(Page):
6066
+ """
6067
+ Página mãe que lista todas as cápsulas com sistema de filtros
6068
+ """
6069
+ template = "enap_designsystem/pages/capsula_index_page.html"
6070
+
6071
+ intro = RichTextField(
6072
+ blank=True,
6073
+ verbose_name="Texto introdutório",
6074
+ help_text="Texto que aparece no topo da página de listagem"
6075
+ )
6076
+
6077
+ content_panels = Page.content_panels + [
6078
+ FieldPanel('intro'),
6079
+ ]
6080
+
6081
+ # Não restringe onde pode ser criada
6082
+ subpage_types = ['enap_designsystem.CapsulaPage'] # Só aceita cápsulas como filhas
6083
+ max_count = 1 # Só pode ter uma página de índice no site
6084
+
6085
+ class Meta:
6086
+ verbose_name = "Página Índice de Cápsulas"
6087
+ verbose_name_plural = "Páginas Índice de Cápsulas"
6088
+
6089
+ def get_context(self, request):
6090
+ context = super().get_context(request)
6091
+
6092
+ # Todas as cápsulas publicadas (filhas desta página)
6093
+ capsulas = CapsulaPage.objects.child_of(self).live().public()
6094
+
6095
+ # SOLUÇÃO: Converter para lista e filtrar em Python para SQLite
6096
+ capsulas_list = list(capsulas)
6097
+
6098
+ # Aplicar filtros da URL
6099
+ tipos_def = request.GET.getlist('tipo_deficiencia')
6100
+ if tipos_def:
6101
+ capsulas_list = [
6102
+ c for c in capsulas_list
6103
+ if c.tipos_deficiencia and any(t in c.tipos_deficiencia for t in tipos_def)
6104
+ ]
6105
+
6106
+ formatos = request.GET.getlist('formato_acao')
6107
+ if formatos:
6108
+ capsulas_list = [
6109
+ c for c in capsulas_list
6110
+ if c.formatos_acao and any(f in c.formatos_acao for f in formatos)
6111
+ ]
6112
+
6113
+ recursos = request.GET.getlist('tipo_recurso')
6114
+ if recursos:
6115
+ capsulas_list = [
6116
+ c for c in capsulas_list
6117
+ if c.tipos_recurso and any(r in c.tipos_recurso for r in recursos)
6118
+ ]
6119
+
6120
+ perfis = request.GET.getlist('perfil_profissional')
6121
+ if perfis:
6122
+ capsulas_list = [
6123
+ c for c in capsulas_list
6124
+ if c.perfis_profissionais and any(p in c.perfis_profissionais for p in perfis)
6125
+ ]
6126
+
6127
+ prioridade = request.GET.get('prioridade')
6128
+ if prioridade:
6129
+ capsulas_list = [c for c in capsulas_list if c.prioridade == prioridade]
6130
+
6131
+ context['capsulas'] = capsulas_list
6132
+ context['total_resultados'] = len(capsulas_list)
6133
+
6134
+ # Passar as choices para o template
6135
+ context['opcoes_filtros'] = {
6136
+ 'tipos_deficiencia': CapsulaPage.TIPOS_DEFICIENCIA_CHOICES,
6137
+ 'formatos_acao': CapsulaPage.FORMATOS_ACAO_CHOICES,
6138
+ 'tipos_recurso': CapsulaPage.TIPOS_RECURSO_CHOICES,
6139
+ 'perfis_profissionais': CapsulaPage.PERFIS_PROFISSIONAIS_CHOICES,
6140
+ }
6141
+
6142
+ # Filtros ativos
6143
+ context['filtros_ativos'] = {
6144
+ 'tipo_deficiencia': tipos_def,
6145
+ 'formato_acao': formatos,
6146
+ 'tipo_recurso': recursos,
6147
+ 'perfil_profissional': perfis,
6148
+ 'prioridade': prioridade,
6149
+ }
6150
+
6151
+ # Calcular contadores
6152
+ context['contadores'] = self._calcular_contadores(capsulas)
6153
+
6154
+ return context
6155
+
6156
+ def _calcular_contadores(self, queryset):
6157
+ """Calcula quantas cápsulas têm cada tag - versão compatível com SQLite"""
6158
+ contadores = {
6159
+ 'tipos_deficiencia': {},
6160
+ 'formatos_acao': {},
6161
+ 'tipos_recurso': {},
6162
+ 'perfis_profissionais': {},
6163
+ }
6164
+
6165
+ # Converter queryset para lista
6166
+ capsulas_list = list(queryset)
6167
+
6168
+ # Tipos de Deficiência
6169
+ for value, label in CapsulaPage.TIPOS_DEFICIENCIA_CHOICES:
6170
+ count = sum(1 for c in capsulas_list if c.tipos_deficiencia and value in c.tipos_deficiencia)
6171
+ contadores['tipos_deficiencia'][value] = count
6172
+
6173
+ # Formatos de Ação
6174
+ for value, label in CapsulaPage.FORMATOS_ACAO_CHOICES:
6175
+ count = sum(1 for c in capsulas_list if c.formatos_acao and value in c.formatos_acao)
6176
+ contadores['formatos_acao'][value] = count
6177
+
6178
+ # Tipos de Recurso
6179
+ for value, label in CapsulaPage.TIPOS_RECURSO_CHOICES:
6180
+ count = sum(1 for c in capsulas_list if c.tipos_recurso and value in c.tipos_recurso)
6181
+ contadores['tipos_recurso'][value] = count
6182
+
6183
+ # Perfis Profissionais
6184
+ for value, label in CapsulaPage.PERFIS_PROFISSIONAIS_CHOICES:
6185
+ count = sum(1 for c in capsulas_list if c.perfis_profissionais and value in c.perfis_profissionais)
6186
+ contadores['perfis_profissionais'][value] = count
6187
+
6188
+ return contadores
6189
+
6190
+
6191
+ # ============================================
6192
+ # PÁGINA FILHA - CÁPSULA INDIVIDUAL
6193
+ # ==================from django.db import models
6194
+ from django import forms
6195
+
6196
+
6197
+ class CapsulaPage(Page):
6198
+ """
6199
+ Página individual de cada cápsula de acessibilidade
6200
+ Compatível com PostgreSQL (produção) e SQLite (desenvolvimento)
6201
+ """
6202
+ template = "enap_designsystem/pages/capsula_page.html"
6203
+
6204
+ # ==========================================
6205
+ # CHOICES FIXAS PARA OS FILTROS
6206
+ # ==========================================
6207
+
6208
+ TIPOS_DEFICIENCIA_CHOICES = [
6209
+ ('visual', 'Visual'),
6210
+ ('auditiva', 'Auditiva'),
6211
+ ('motora', 'Motora'),
6212
+ ('cognitiva', 'Cognitiva'),
6213
+ ]
6214
+
6215
+ FORMATOS_ACAO_CHOICES = [
6216
+ ('distancia_sincrono', 'A distância síncrono'),
6217
+ ('distancia_assincrono', 'A distância assíncrono'),
6218
+ ('presencial', 'Presencial'),
6219
+ ('semipresencial', 'Semipresencial'),
6220
+ ]
6221
+
6222
+ TIPOS_RECURSO_CHOICES = [
6223
+ ('imagem', 'Imagem'),
6224
+ ('video', 'Vídeo'),
6225
+ ('tabela', 'Tabela'),
6226
+ ('grafico', 'Gráfico'),
6227
+ ('botao', 'Botão'),
6228
+ ('texto', 'Texto'),
6229
+ ('audio', 'Áudio'),
6230
+ ('hiperlink', 'Hiperlink'),
6231
+ ('videoconferencia', 'Videoconferência'),
6232
+ ]
6233
+
6234
+ PERFIS_PROFISSIONAIS_CHOICES = [
6235
+ ('designer_instrucional', 'Designer instrucional'),
6236
+ ('designer_grafico', 'Designer gráfico'),
6237
+ ('implementador_web', 'Implementador web'),
6238
+ ('editor_video', 'Editor de vídeo'),
6239
+ ('docente', 'Docente'),
6240
+ ('curador_conteudo', 'Curador de conteúdo'),
6241
+ ('conteudista', 'Conteudista'),
6242
+ ]
6243
+
6244
+ PRIORIDADE_CHOICES = [
6245
+ ('obrigatorio', 'Obrigatório'),
6246
+ ('recomendado', 'Recomendado'),
6247
+ ]
6248
+
6249
+ # ==========================================
6250
+ # CAMPOS DE CLASSIFICAÇÃO (FILTROS)
6251
+ # ==========================================
6252
+
6253
+ tipos_deficiencia = models.JSONField(
6254
+ blank=True,
6255
+ default=list,
6256
+ verbose_name="Tipos de Deficiência",
6257
+ help_text="Selecione um ou mais tipos"
6258
+ )
6259
+
6260
+ formatos_acao = models.JSONField(
6261
+ blank=True,
6262
+ default=list,
6263
+ verbose_name="Formatos de Ação",
6264
+ help_text="Selecione um ou mais formatos"
6265
+ )
6266
+
6267
+ tipos_recurso = models.JSONField(
6268
+ blank=True,
6269
+ default=list,
6270
+ verbose_name="Tipos de Recurso",
6271
+ help_text="Selecione um ou mais tipos"
6272
+ )
6273
+
6274
+ perfis_profissionais = models.JSONField(
6275
+ blank=True,
6276
+ default=list,
6277
+ verbose_name="Perfis Profissionais",
6278
+ help_text="Selecione um ou mais perfis"
6279
+ )
6280
+
6281
+ prioridade = models.CharField(
6282
+ max_length=20,
6283
+ choices=PRIORIDADE_CHOICES,
6284
+ default='recomendado',
6285
+ verbose_name="Prioridade"
6286
+ )
6287
+
6288
+ # ==========================================
6289
+ # CONTEÚDO DA CÁPSULA
6290
+ # ==========================================
6291
+
6292
+ resumo_card = models.TextField(
6293
+ max_length=300,
6294
+ blank=True,
6295
+ verbose_name="Resumo para o card",
6296
+ help_text="Texto curto para exibir na listagem (máx. 300 caracteres)"
6297
+ )
6298
+
6299
+ # ==========================================
6300
+ # 1. O QUE É? - COM IMAGEM (StreamField)
6301
+ # ==========================================
6302
+
6303
+ o_que_e_imagem = StreamField(
6304
+ [
6305
+ ('image', ImageChooserBlock(required=False, label="Imagem principal"))
6306
+ ],
6307
+ blank=True,
6308
+ use_json_field=True,
6309
+ max_num=1,
6310
+ verbose_name="Imagem",
6311
+ help_text="Imagem ilustrativa da seção 'O que é?'"
6312
+ )
6313
+
6314
+ o_que_e_resumo = RichTextField(
6315
+ verbose_name="O que é? (Resumo)",
6316
+ help_text="Texto curto exibido inicialmente",
6317
+ features=['bold', 'italic', 'link']
6318
+ )
6319
+
6320
+ o_que_e_detalhado = RichTextField(
6321
+ blank=True,
6322
+ verbose_name="O que é? (Detalhado)",
6323
+ help_text="Texto completo - aparece ao clicar em 'Leia mais'",
6324
+ features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul']
6325
+ )
6326
+
6327
+ # 2. Por que é importante?
6328
+ por_que_importante_resumo = RichTextField(
6329
+ verbose_name="Por que é importante? (Resumo)",
6330
+ help_text="Texto curto exibido inicialmente",
6331
+ features=['bold', 'italic', 'link']
6332
+ )
6333
+
6334
+ por_que_importante_detalhado = RichTextField(
6335
+ blank=True,
6336
+ verbose_name="Por que é importante? (Detalhado)",
6337
+ help_text="Texto completo - aparece ao clicar em 'Leia mais'",
6338
+ features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul']
6339
+ )
6340
+
6341
+ # ==========================================
6342
+ # 3. QUANDO UTILIZAR? - STREAMFIELD DE CARDS
6343
+ # ==========================================
6344
+
6345
+ quando_utilizar_resumo = RichTextField(
6346
+ verbose_name="Quando utilizar? (Resumo)",
6347
+ help_text="Texto curto exibido inicialmente",
6348
+ features=['bold', 'italic', 'link']
6349
+ )
6350
+
6351
+ quando_utilizar_cards = StreamField(
6352
+ [
6353
+ ("enap_cardgrid", EnapCardGridBlock([
6354
+ ("card", CardBlock()),
6355
+ ]))
6356
+ ],
6357
+ blank=True,
6358
+ use_json_field=True,
6359
+ verbose_name="Cards de cenários",
6360
+ help_text="Adicione cards explicando diferentes cenários de uso"
6361
+ )
6362
+
6363
+ # ==========================================
6364
+ # 4. COMO APLICAR - COM IMAGEM (StreamField)
6365
+ # ==========================================
6366
+
6367
+ como_aplicar_imagem = StreamField(
6368
+ [
6369
+ ('image', ImageChooserBlock(required=False, label="Imagem do passo a passo"))
6370
+ ],
6371
+ blank=True,
6372
+ use_json_field=True,
6373
+ max_num=1,
6374
+ verbose_name="Imagem",
6375
+ help_text="Imagem ilustrativa de como aplicar"
6376
+ )
6377
+
6378
+ como_aplicar_resumo = RichTextField(
6379
+ verbose_name="Como aplicar (Resumo)",
6380
+ help_text="Texto curto exibido inicialmente",
6381
+ features=['bold', 'italic', 'link']
6382
+ )
6383
+
6384
+ como_aplicar_detalhado = RichTextField(
6385
+ blank=True,
6386
+ verbose_name="Como aplicar (Detalhado)",
6387
+ help_text="Texto completo - aparece ao clicar em 'Leia mais'",
6388
+ features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'code']
6389
+ )
6390
+
6391
+ # ==========================================
6392
+ # 5. MÉTODO DE VERIFICAÇÃO - STREAMFIELD DE CARDS
6393
+ # ==========================================
6394
+
6395
+ metodo_verificacao_resumo = RichTextField(
6396
+ verbose_name="Método de verificação (Resumo)",
6397
+ help_text="Texto curto exibido inicialmente",
6398
+ features=['bold', 'italic', 'link']
6399
+ )
6400
+
6401
+ metodo_verificacao_cards = StreamField(
6402
+ [
6403
+ ("enap_cardgrid", EnapCardGridBlock([
6404
+ ("card", CardBlock()),
6405
+ ]))
6406
+ ],
6407
+ blank=True,
6408
+ use_json_field=True,
6409
+ verbose_name="Cards de métodos",
6410
+ help_text="Adicione cards com diferentes métodos de verificação"
6411
+ )
6412
+
6413
+ # 6. Exemplos
6414
+ exemplos_resumo = RichTextField(
6415
+ verbose_name="Exemplos (Resumo)",
6416
+ help_text="Texto curto exibido inicialmente",
6417
+ features=['bold', 'italic', 'link']
6418
+ )
6419
+
6420
+ exemplos_detalhado = RichTextField(
6421
+ blank=True,
6422
+ verbose_name="Exemplos (Detalhado)",
6423
+ help_text="Texto completo - aparece ao clicar em 'Leia mais'",
6424
+ features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'code', 'image']
6425
+ )
6426
+
6427
+ # 7. O que não fazer?
6428
+ o_que_nao_fazer_resumo = RichTextField(
6429
+ verbose_name="O que não fazer? (Resumo)",
6430
+ help_text="Texto curto exibido inicialmente",
6431
+ features=['bold', 'italic', 'link']
6432
+ )
6433
+
6434
+ o_que_nao_fazer_detalhado = RichTextField(
6435
+ blank=True,
6436
+ verbose_name="O que não fazer? (Detalhado)",
6437
+ help_text="Texto completo - aparece ao clicar em 'Leia mais'",
6438
+ features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul']
6439
+ )
6440
+
6441
+ # 8. Normas de referência
6442
+ normas_referencia = RichTextField(
6443
+ blank=True,
6444
+ verbose_name="Normas de referência",
6445
+ help_text="Lista de normas técnicas principais (WCAG, NBR, etc.)",
6446
+ features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul']
6447
+ )
6448
+
6449
+ # Metadados
6450
+ destaque_home = models.BooleanField(
6451
+ default=False,
6452
+ verbose_name="Destacar na Home",
6453
+ help_text="Marque para exibir esta cápsula na página inicial"
6454
+ )
6455
+
6456
+ data_atualizacao = models.DateField(
6457
+ null=True,
6458
+ blank=True,
6459
+ verbose_name="Data de atualização",
6460
+ help_text="Data da última revisão do conteúdo"
6461
+ )
6462
+
6463
+ # ==========================================
6464
+ # PAINÉIS DO WAGTAIL ADMIN
6465
+ # ==========================================
6466
+
6467
+ content_panels = Page.content_panels + [
6468
+
6469
+ MultiFieldPanel([
6470
+ FieldPanel('resumo_card'),
6471
+ FieldPanel('prioridade'),
6472
+ FieldPanel('destaque_home'),
6473
+ FieldPanel('data_atualizacao'),
6474
+ ], heading="📋 Informações Gerais", classname="collapsible"),
6475
+
6476
+ MultiFieldPanel([
6477
+ FieldPanel('tipos_deficiencia', widget=forms.CheckboxSelectMultiple(choices=TIPOS_DEFICIENCIA_CHOICES)),
6478
+ FieldPanel('formatos_acao', widget=forms.CheckboxSelectMultiple(choices=FORMATOS_ACAO_CHOICES)),
6479
+ FieldPanel('tipos_recurso', widget=forms.CheckboxSelectMultiple(choices=TIPOS_RECURSO_CHOICES)),
6480
+ FieldPanel('perfis_profissionais', widget=forms.CheckboxSelectMultiple(choices=PERFIS_PROFISSIONAIS_CHOICES)),
6481
+ ], heading="🔖 Classificação (Tags para Filtros)", classname="collapsible"),
6482
+
6483
+ MultiFieldPanel([
6484
+ FieldPanel('o_que_e_imagem'),
6485
+ FieldPanel('o_que_e_resumo'),
6486
+ FieldPanel('o_que_e_detalhado'),
6487
+ ], heading="1️⃣ O que é?", classname="collapsible collapsed"),
6488
+
6489
+ MultiFieldPanel([
6490
+ FieldPanel('por_que_importante_resumo'),
6491
+ FieldPanel('por_que_importante_detalhado'),
6492
+ ], heading="2️⃣ Por que é importante?", classname="collapsible collapsed"),
6493
+
6494
+ MultiFieldPanel([
6495
+ FieldPanel('quando_utilizar_resumo'),
6496
+ FieldPanel('quando_utilizar_cards'),
6497
+ ], heading="3️⃣ Quando utilizar?", classname="collapsible collapsed"),
6498
+
6499
+ MultiFieldPanel([
6500
+ FieldPanel('como_aplicar_imagem'),
6501
+ FieldPanel('como_aplicar_resumo'),
6502
+ FieldPanel('como_aplicar_detalhado'),
6503
+ ], heading="4️⃣ Como aplicar", classname="collapsible collapsed"),
6504
+
6505
+ MultiFieldPanel([
6506
+ FieldPanel('metodo_verificacao_resumo'),
6507
+ FieldPanel('metodo_verificacao_cards'),
6508
+ ], heading="5️⃣ Método de verificação", classname="collapsible collapsed"),
6509
+
6510
+ MultiFieldPanel([
6511
+ FieldPanel('exemplos_resumo'),
6512
+ FieldPanel('exemplos_detalhado'),
6513
+ ], heading="6️⃣ Exemplos", classname="collapsible collapsed"),
6514
+
6515
+ MultiFieldPanel([
6516
+ FieldPanel('o_que_nao_fazer_resumo'),
6517
+ FieldPanel('o_que_nao_fazer_detalhado'),
6518
+ ], heading="7️⃣ O que não fazer?", classname="collapsible collapsed"),
6519
+
6520
+ MultiFieldPanel([
6521
+ FieldPanel('normas_referencia'),
6522
+ ], heading="8️⃣ Normas de referência", classname="collapsible collapsed"),
6523
+ ]
6524
+
6525
+ # Configuração de hierarquia
6526
+ parent_page_types = ['enap_designsystem.CapsulaIndexPage']
6527
+ subpage_types = []
6528
+
6529
+ class Meta:
6530
+ verbose_name = "Cápsula de Acessibilidade"
6531
+ verbose_name_plural = "Cápsulas de Acessibilidade"
6532
+
6533
+ # ==========================================
6534
+ # MÉTODOS HELPERS
6535
+ # ==========================================
6536
+
6537
+ def get_context(self, request):
6538
+ context = super().get_context(request)
6539
+
6540
+ # Buscar cápsulas relacionadas (mesmos tipos de deficiência)
6541
+ capsulas_relacionadas = CapsulaPage.objects.live().public().exclude(id=self.id)
6542
+
6543
+ if self.tipos_deficiencia:
6544
+ # SOLUÇÃO: Buscar manualmente em Python ao invés de query no banco
6545
+ # Para SQLite, fazemos a filtragem após recuperar os dados
6546
+ todas_capsulas = list(capsulas_relacionadas)
6547
+ capsulas_filtradas = []
6548
+
6549
+ for capsula in todas_capsulas:
6550
+ # Verifica se há interseção entre os tipos de deficiência
6551
+ if capsula.tipos_deficiencia:
6552
+ tipos_em_comum = set(self.tipos_deficiencia) & set(capsula.tipos_deficiencia)
6553
+ if tipos_em_comum:
6554
+ capsulas_filtradas.append(capsula)
6555
+
6556
+ capsulas_relacionadas = capsulas_filtradas[:3]
6557
+ else:
6558
+ capsulas_relacionadas = list(capsulas_relacionadas)[:3]
6559
+
6560
+ context['capsulas_relacionadas'] = capsulas_relacionadas
6561
+
6562
+ return context