wagtail-enap-designsystem 1.2.1.119__py3-none-any.whl → 1.2.1.121__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.

@@ -0,0 +1,33 @@
1
+ # Generated by Django 5.1.6 on 2025-09-02 19:00
2
+
3
+ import wagtail.fields
4
+ from django.db import migrations
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ("enap_designsystem", "0379_categoriavotacao_sistemavotacaopage_and_more"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name="sistemavotacaopage",
16
+ name="imagem_fundo",
17
+ field=wagtail.fields.StreamField(
18
+ [("image", 0)],
19
+ blank=True,
20
+ block_lookup={
21
+ 0: (
22
+ "wagtail.images.blocks.ImageChooserBlock",
23
+ (),
24
+ {
25
+ "help_text": "Selecione uma imagem para usar como fundo da página",
26
+ "required": False,
27
+ },
28
+ )
29
+ },
30
+ verbose_name="Imagem de Fundo",
31
+ ),
32
+ ),
33
+ ]
@@ -0,0 +1,36 @@
1
+ # Generated by Django 5.1.6 on 2025-09-02 20:50
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ("enap_designsystem", "0380_sistemavotacaopage_imagem_fundo"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name="sistemavotacaopage",
16
+ name="footer",
17
+ field=models.ForeignKey(
18
+ blank=True,
19
+ null=True,
20
+ on_delete=django.db.models.deletion.SET_NULL,
21
+ related_name="+",
22
+ to="enap_designsystem.enapfootersnippet",
23
+ ),
24
+ ),
25
+ migrations.AddField(
26
+ model_name="sistemavotacaopage",
27
+ name="navbar",
28
+ field=models.ForeignKey(
29
+ blank=True,
30
+ null=True,
31
+ on_delete=django.db.models.deletion.SET_NULL,
32
+ related_name="+",
33
+ to="enap_designsystem.enapnavbarsnippet",
34
+ ),
35
+ ),
36
+ ]
@@ -36,11 +36,13 @@ from datetime import datetime
36
36
  from .utils.sso import get_valid_access_token
37
37
  from wagtail.blocks import PageChooserBlock, StructBlock, CharBlock, BooleanBlock, ListBlock, IntegerBlock
38
38
  from django.conf import settings
39
-
39
+ from django.utils import timezone
40
40
  from .blocks.layout_blocks import EnapAccordionBlock
41
41
 
42
42
  from wagtail.blocks import StreamBlock, StructBlock, CharBlock, ChoiceBlock, RichTextBlock, ChooserBlock, ListBlock
43
43
 
44
+ import uuid
45
+ from django.utils import timezone
44
46
 
45
47
  from django.db import models
46
48
  from django.contrib.auth.models import Group
@@ -148,8 +150,6 @@ from enap_designsystem.blocks import CARD_CARDS_STREAMBLOCKS
148
150
 
149
151
  class ENAPComponentes(Page):
150
152
  """Página personalizada independente do CoderedWebPage."""
151
-
152
- parent_page_types = ['MbaEspecializacao']
153
153
 
154
154
  admin_notes = models.TextField(
155
155
  verbose_name="Anotações Internas",
@@ -4494,3 +4494,484 @@ class GroupPageTypePermission(models.Model):
4494
4494
  valid_content_types = self.get_allowed_content_types()
4495
4495
  self.content_types.set([ct for ct in self.content_types.all() if ct in valid_content_types])
4496
4496
 
4497
+
4498
+
4499
+
4500
+
4501
+
4502
+
4503
+
4504
+
4505
+
4506
+
4507
+ @register_snippet
4508
+ class CategoriaVotacao(models.Model):
4509
+ """
4510
+ Categorias/Tabs do sistema de votação
4511
+ Gerenciadas dinamicamente pelo admin
4512
+ """
4513
+ nome = models.CharField(
4514
+ max_length=100,
4515
+ verbose_name="Nome da Categoria",
4516
+ help_text="Ex: Inovação Tecnológica, Sustentabilidade, etc."
4517
+ )
4518
+
4519
+ descricao = models.TextField(
4520
+ blank=True,
4521
+ verbose_name="Descrição",
4522
+ help_text="Descrição opcional da categoria"
4523
+ )
4524
+
4525
+ ordem = models.PositiveIntegerField(
4526
+ default=0,
4527
+ verbose_name="Ordem de Exibição",
4528
+ help_text="Ordem das tabs (menor número = primeiro)"
4529
+ )
4530
+
4531
+ ativo = models.BooleanField(
4532
+ default=True,
4533
+ verbose_name="Categoria Ativa",
4534
+ help_text="Desmarque para ocultar esta categoria"
4535
+ )
4536
+
4537
+ icone = models.CharField(
4538
+ max_length=50,
4539
+ blank=True,
4540
+ verbose_name="Ícone (classe CSS)",
4541
+ help_text="Ex: fa-microchip, fa-leaf, fa-users"
4542
+ )
4543
+
4544
+ cor_destaque = models.CharField(
4545
+ max_length=7,
4546
+ default="#00E5CC",
4547
+ verbose_name="Cor de Destaque",
4548
+ help_text="Cor hexadecimal para esta categoria"
4549
+ )
4550
+
4551
+ created_at = models.DateTimeField(auto_now_add=True)
4552
+ updated_at = models.DateTimeField(auto_now=True)
4553
+
4554
+ class Meta:
4555
+ verbose_name = "Categoria de Votação"
4556
+ verbose_name_plural = "Categorias de Votação"
4557
+ ordering = ['ordem', 'nome']
4558
+
4559
+ def __str__(self):
4560
+ return self.nome
4561
+
4562
+ @property
4563
+ def total_projetos(self):
4564
+ """Retorna total de projetos ativos nesta categoria"""
4565
+ return self.projetos.filter(ativo=True).count()
4566
+
4567
+ @property
4568
+ def total_votos(self):
4569
+ """Retorna total de votos recebidos nesta categoria"""
4570
+ return VotoRegistrado.objects.filter(
4571
+ projeto__categoria=self,
4572
+ projeto__ativo=True
4573
+ ).count()
4574
+
4575
+
4576
+ @register_snippet
4577
+ class ProjetoVotacao(ClusterableModel):
4578
+ """
4579
+ Projetos participantes da votação
4580
+ Cards dinâmicos configuráveis pelo admin
4581
+ """
4582
+ titulo = models.CharField(
4583
+ max_length=200,
4584
+ verbose_name="Título do Projeto"
4585
+ )
4586
+
4587
+ descricao = RichTextField(
4588
+ verbose_name="Descrição do Projeto",
4589
+ help_text="Descrição completa do projeto"
4590
+ )
4591
+
4592
+ categoria = models.ForeignKey(
4593
+ CategoriaVotacao,
4594
+ on_delete=models.CASCADE,
4595
+ related_name='projetos',
4596
+ verbose_name="Categoria"
4597
+ )
4598
+
4599
+ # Equipe
4600
+ nome_equipe = models.CharField(
4601
+ max_length=150,
4602
+ verbose_name="Nome da Equipe/Organização"
4603
+ )
4604
+
4605
+ icone_equipe = models.ImageField(
4606
+ upload_to='votacao/equipes/',
4607
+ blank=True,
4608
+ null=True,
4609
+ verbose_name="Logo/Ícone da Equipe"
4610
+ )
4611
+
4612
+ # Vídeo
4613
+ video_youtube = models.URLField(
4614
+ blank=True,
4615
+ verbose_name="URL do Vídeo YouTube",
4616
+ help_text="Cole a URL completa do YouTube"
4617
+ )
4618
+
4619
+ video_arquivo = models.FileField(
4620
+ upload_to='votacao/videos/',
4621
+ blank=True,
4622
+ null=True,
4623
+ verbose_name="Arquivo de Vídeo",
4624
+ help_text="Alternativamente, faça upload de um vídeo"
4625
+ )
4626
+
4627
+ # Contato
4628
+ email_contato = models.EmailField(
4629
+ blank=True,
4630
+ verbose_name="Email de Contato",
4631
+ help_text="Email da equipe (opcional)"
4632
+ )
4633
+
4634
+ # Configurações
4635
+ ordem = models.PositiveIntegerField(
4636
+ default=0,
4637
+ verbose_name="Ordem na Categoria",
4638
+ help_text="Ordem do projeto dentro da categoria"
4639
+ )
4640
+
4641
+ ativo = models.BooleanField(
4642
+ default=True,
4643
+ verbose_name="Projeto Ativo",
4644
+ help_text="Desmarque para ocultar este projeto"
4645
+ )
4646
+
4647
+ destacado = models.BooleanField(
4648
+ default=False,
4649
+ verbose_name="Projeto em Destaque",
4650
+ help_text="Marque para destacar este projeto"
4651
+ )
4652
+
4653
+ created_at = models.DateTimeField(auto_now_add=True)
4654
+ updated_at = models.DateTimeField(auto_now=True)
4655
+
4656
+ panels = [
4657
+ MultiFieldPanel([
4658
+ FieldPanel('titulo'),
4659
+ FieldPanel('categoria'),
4660
+ FieldPanel('descricao'),
4661
+ ], heading="Informações Básicas"),
4662
+
4663
+ MultiFieldPanel([
4664
+ FieldPanel('nome_equipe'),
4665
+ FieldPanel('icone_equipe'),
4666
+ FieldPanel('email_contato'),
4667
+ InlinePanel('apresentadores', label="Apresentadores"),
4668
+ ], heading="Equipe"),
4669
+
4670
+ MultiFieldPanel([
4671
+ FieldPanel('video_youtube'),
4672
+ FieldPanel('video_arquivo'),
4673
+ ], heading="Vídeo do Projeto"),
4674
+
4675
+ MultiFieldPanel([
4676
+ FieldPanel('ordem'),
4677
+ FieldPanel('ativo'),
4678
+ FieldPanel('destacado'),
4679
+ ], heading="Configurações"),
4680
+ ]
4681
+
4682
+ class Meta:
4683
+ verbose_name = "Projeto de Votação"
4684
+ verbose_name_plural = "Projetos de Votação"
4685
+ ordering = ['categoria__ordem', 'ordem', 'titulo']
4686
+
4687
+ def __str__(self):
4688
+ return f"{self.titulo} ({self.categoria.nome})"
4689
+
4690
+ @property
4691
+ def total_votos(self):
4692
+ """Retorna total de votos recebidos por este projeto"""
4693
+ return self.votos.count()
4694
+
4695
+ @property
4696
+ def video_embed_url(self):
4697
+ """Converte URL do YouTube para embed"""
4698
+ if self.video_youtube:
4699
+ if "youtube.com/watch?v=" in self.video_youtube:
4700
+ video_id = self.video_youtube.split("watch?v=")[1].split("&")[0]
4701
+ return f"https://www.youtube.com/embed/{video_id}"
4702
+ elif "youtu.be/" in self.video_youtube:
4703
+ video_id = self.video_youtube.split("youtu.be/")[1].split("?")[0]
4704
+ return f"https://www.youtube.com/embed/{video_id}"
4705
+ return None
4706
+
4707
+ def get_apresentadores_list(self):
4708
+ """Retorna lista de apresentadores como badges"""
4709
+ return [ap.nome for ap in self.apresentadores.all()]
4710
+
4711
+
4712
+ class ApresentadorProjeto(Orderable):
4713
+ """
4714
+ Apresentadores de cada projeto
4715
+ Inline para criar badges dinâmicas
4716
+ """
4717
+ projeto = ParentalKey(
4718
+ ProjetoVotacao,
4719
+ related_name='apresentadores',
4720
+ on_delete=models.CASCADE
4721
+ )
4722
+
4723
+ nome = models.CharField(
4724
+ max_length=100,
4725
+ verbose_name="Nome do Apresentador"
4726
+ )
4727
+
4728
+ cargo = models.CharField(
4729
+ max_length=100,
4730
+ blank=True,
4731
+ verbose_name="Cargo/Função",
4732
+ help_text="Ex: Desenvolvedor, Designer, etc."
4733
+ )
4734
+
4735
+ panels = [
4736
+ FieldPanel('nome'),
4737
+ FieldPanel('cargo'),
4738
+ ]
4739
+
4740
+ def __str__(self):
4741
+ return self.nome
4742
+
4743
+
4744
+ class VotoRegistrado(models.Model):
4745
+ """
4746
+ Registro de cada voto realizado
4747
+ Para auditoria e controle básico anti-fraude
4748
+ """
4749
+ id = models.UUIDField(
4750
+ primary_key=True,
4751
+ default=uuid.uuid4,
4752
+ editable=False
4753
+ )
4754
+
4755
+ projeto = models.ForeignKey(
4756
+ ProjetoVotacao,
4757
+ on_delete=models.CASCADE,
4758
+ related_name='votos',
4759
+ verbose_name="Projeto Votado"
4760
+ )
4761
+
4762
+ ip_address = models.GenericIPAddressField(
4763
+ verbose_name="Endereço IP"
4764
+ )
4765
+
4766
+ user_agent = models.TextField(
4767
+ blank=True,
4768
+ verbose_name="User Agent",
4769
+ help_text="Navegador utilizado"
4770
+ )
4771
+
4772
+ timestamp = models.DateTimeField(
4773
+ default=timezone.now,
4774
+ verbose_name="Data/Hora do Voto"
4775
+ )
4776
+
4777
+ # Campos para relatórios
4778
+ categoria_nome = models.CharField(
4779
+ max_length=100,
4780
+ verbose_name="Nome da Categoria",
4781
+ help_text="Cache do nome da categoria no momento do voto"
4782
+ )
4783
+
4784
+ class Meta:
4785
+ verbose_name = "Voto Registrado"
4786
+ verbose_name_plural = "Votos Registrados"
4787
+ ordering = ['-timestamp']
4788
+ indexes = [
4789
+ models.Index(fields=['projeto', 'timestamp']),
4790
+ models.Index(fields=['ip_address', 'timestamp']),
4791
+ models.Index(fields=['categoria_nome', 'timestamp']),
4792
+ ]
4793
+
4794
+ def __str__(self):
4795
+ return f"Voto em {self.projeto.titulo} - {self.timestamp}"
4796
+
4797
+ def save(self, *args, **kwargs):
4798
+ # Cache do nome da categoria
4799
+ if not self.categoria_nome:
4800
+ self.categoria_nome = self.projeto.categoria.nome
4801
+ super().save(*args, **kwargs)
4802
+
4803
+
4804
+ class SistemaVotacaoPage(Page):
4805
+ """
4806
+ Página principal do sistema de votação
4807
+ Configurações gerais editáveis pelo admin
4808
+ """
4809
+
4810
+ navbar = models.ForeignKey(
4811
+ "EnapNavbarSnippet",
4812
+ null=True,
4813
+ blank=True,
4814
+ on_delete=models.SET_NULL,
4815
+ related_name="+",
4816
+ )
4817
+
4818
+ subtitulo = models.CharField(
4819
+ max_length=255,
4820
+ default="Escolha os melhores projetos em cada categoria",
4821
+ verbose_name="Subtítulo",
4822
+ help_text="Texto que aparece abaixo do título"
4823
+ )
4824
+
4825
+ descricao_header = RichTextField(
4826
+ blank=True,
4827
+ verbose_name="Descrição do Header",
4828
+ help_text="Texto adicional no topo da página (opcional)"
4829
+ )
4830
+
4831
+ imagem_fundo = StreamField([
4832
+ ("image", ImageChooserBlock(
4833
+ required=False,
4834
+ help_text="Selecione uma imagem para usar como fundo da página"
4835
+ ))
4836
+ ],
4837
+ blank=True,
4838
+ use_json_field=True,
4839
+ verbose_name="Imagem de Fundo",
4840
+ )
4841
+
4842
+ mostrar_progresso = models.BooleanField(
4843
+ default=True,
4844
+ verbose_name="Mostrar Barra de Progresso",
4845
+ help_text="Exibe progresso de quantas categorias o usuário votou"
4846
+ )
4847
+
4848
+ permitir_multiplos_votos = models.BooleanField(
4849
+ default=True,
4850
+ verbose_name="Permitir Múltiplos Votos",
4851
+ help_text="Usuário pode votar em diferentes projetos de diferentes categorias"
4852
+ )
4853
+
4854
+ ordenacao_projetos = models.CharField(
4855
+ max_length=20,
4856
+ choices=[
4857
+ ('ordem', 'Ordem Manual'),
4858
+ ('votos_desc', 'Mais Votados Primeiro'),
4859
+ ('votos_asc', 'Menos Votados Primeiro'),
4860
+ ('alfabetica', 'Ordem Alfabética'),
4861
+ ('recentes', 'Mais Recentes Primeiro'),
4862
+ ],
4863
+ default='ordem',
4864
+ verbose_name="Ordenação dos Projetos"
4865
+ )
4866
+
4867
+ # Meta configurações
4868
+ votacao_ativa = models.BooleanField(
4869
+ default=True,
4870
+ verbose_name="Votação Ativa",
4871
+ help_text="Desmarque para pausar a votação"
4872
+ )
4873
+
4874
+ data_inicio = models.DateTimeField(
4875
+ blank=True,
4876
+ null=True,
4877
+ verbose_name="Data de Início",
4878
+ help_text="Data/hora de início da votação (opcional)"
4879
+ )
4880
+
4881
+ data_fim = models.DateTimeField(
4882
+ blank=True,
4883
+ null=True,
4884
+ verbose_name="Data de Encerramento",
4885
+ help_text="Data/hora de encerramento da votação (opcional)"
4886
+ )
4887
+
4888
+ footer = models.ForeignKey(
4889
+ "EnapFooterSnippet",
4890
+ null=True,
4891
+ blank=True,
4892
+ on_delete=models.SET_NULL,
4893
+ related_name="+",
4894
+ )
4895
+
4896
+
4897
+ content_panels = Page.content_panels + [
4898
+
4899
+ MultiFieldPanel([
4900
+ FieldPanel('subtitulo'),
4901
+ FieldPanel('descricao_header'),
4902
+ FieldPanel('imagem_fundo'),
4903
+ FieldPanel('navbar'),
4904
+ FieldPanel('footer'),
4905
+ ], heading="Conteúdo do Header"),
4906
+
4907
+ MultiFieldPanel([
4908
+ FieldPanel('mostrar_progresso'),
4909
+ FieldPanel('permitir_multiplos_votos'),
4910
+ FieldPanel('ordenacao_projetos'),
4911
+ ], heading="Configurações de Exibição"),
4912
+
4913
+ MultiFieldPanel([
4914
+ FieldPanel('votacao_ativa'),
4915
+ FieldPanel('data_inicio'),
4916
+ FieldPanel('data_fim'),
4917
+ ], heading="Controle da Votação"),
4918
+ ]
4919
+
4920
+ class Meta:
4921
+ verbose_name = "Sistema de Votação"
4922
+
4923
+ def get_context(self, request):
4924
+ context = super().get_context(request)
4925
+
4926
+ # Buscar categorias ativas
4927
+ categorias = CategoriaVotacao.objects.filter(ativo=True).order_by('ordem')
4928
+
4929
+ # Buscar projetos por categoria
4930
+ projetos_por_categoria = {}
4931
+ for categoria in categorias:
4932
+ projetos = categoria.projetos.filter(ativo=True)
4933
+
4934
+ # Aplicar ordenação
4935
+ if self.ordenacao_projetos == 'votos_desc':
4936
+ projetos = sorted(projetos, key=lambda p: p.total_votos, reverse=True)
4937
+ elif self.ordenacao_projetos == 'votos_asc':
4938
+ projetos = sorted(projetos, key=lambda p: p.total_votos)
4939
+ elif self.ordenacao_projetos == 'alfabetica':
4940
+ projetos = projetos.order_by('titulo')
4941
+ elif self.ordenacao_projetos == 'recentes':
4942
+ projetos = projetos.order_by('-created_at')
4943
+ else: # 'ordem'
4944
+ projetos = projetos.order_by('ordem', 'titulo')
4945
+
4946
+ projetos_por_categoria[categoria] = projetos
4947
+
4948
+ # Estatísticas gerais
4949
+ total_categorias = categorias.count()
4950
+ total_projetos = ProjetoVotacao.objects.filter(ativo=True).count()
4951
+ total_votos = VotoRegistrado.objects.count()
4952
+
4953
+ context.update({
4954
+ 'categorias': categorias,
4955
+ 'projetos_por_categoria': projetos_por_categoria,
4956
+ 'total_categorias': total_categorias,
4957
+ 'total_projetos': total_projetos,
4958
+ 'total_votos': total_votos,
4959
+ 'votacao_ativa': self.is_votacao_ativa(),
4960
+ })
4961
+
4962
+ return context
4963
+
4964
+ def is_votacao_ativa(self):
4965
+ """Verifica se a votação está ativa baseado nas configurações"""
4966
+ if not self.votacao_ativa:
4967
+ return False
4968
+
4969
+ now = timezone.now()
4970
+
4971
+ if self.data_inicio and now < self.data_inicio:
4972
+ return False
4973
+
4974
+ if self.data_fim and now > self.data_fim:
4975
+ return False
4976
+
4977
+ return True