diff --git a/sapl/base/urls.py b/sapl/base/urls.py index 1fd8fe23c..787ac44e3 100644 --- a/sapl/base/urls.py +++ b/sapl/base/urls.py @@ -8,7 +8,7 @@ from django.views.generic.base import RedirectView, TemplateView from sapl.base.views import (AutorCrud, ConfirmarEmailView, TipoAutorCrud, get_estatistica, RecuperarSenhaEmailView, RecuperarSenhaFinalizadoView, RecuperarSenhaConfirmaView, RecuperarSenhaCompletoView, IndexView, UserCrud, - GuiaProjetoView, FluxoProposicoesView) + GuiaProjetoView, FluxoProposicoesView, CriarAutorAjaxView) from sapl.settings import MEDIA_URL, LOGOUT_REDIRECT_URL from .apps import AppConfig from .views import (LoginSapl, AlterarSenha, AppConfigCrud, CasaLegislativaCrud, @@ -50,6 +50,7 @@ urlpatterns = [ url(r'^$', IndexView.as_view(template_name='index.html'), name='sapl_index'), url(r'^sistema/autor/tipo/', include(TipoAutorCrud.get_urls())), + url(r'^sistema/autor/criar-ajax/$', CriarAutorAjaxView.as_view(), name='criar_autor_ajax'), url(r'^sistema/autor/', include(AutorCrud.get_urls())), url(r'^sistema/guia-projeto/$', GuiaProjetoView.as_view(), diff --git a/sapl/base/views.py b/sapl/base/views.py index 07a1097f4..10024940e 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -1587,3 +1587,154 @@ def pesquisa_textual(request): json_dict['resultados'].append(sec_dict) return JsonResponse(json_dict) + + +@method_decorator(ratelimit(key=ratelimit_ip, + rate=RATE_LIMITER_RATE, + method='POST', + block=True), name='dispatch') +class CriarAutorAjaxView(PermissionRequiredMixin, FormView): + """ + View AJAX para criar Autor rapidamente na tela de edição de usuário. + Suporta tanto autores com content_type (Parlamentar, Comissão, etc) + quanto autores genéricos (nome e cargo). + """ + permission_required = ('base.add_autor',) + logger = logging.getLogger(__name__) + + def get(self, request, *args, **kwargs): + """ + Retorna lista de usuários ativos para seleção no modal. + """ + User = get_user_model() + + usuarios = User.objects.filter(is_active=True).order_by('first_name', 'username') + + usuarios_data = [] + for user in usuarios: + nome_completo = f"{user.first_name} {user.last_name}".strip() if user.first_name else user.username + usuarios_data.append({ + 'id': user.id, + 'username': user.username, + 'first_name': user.first_name, + 'last_name': user.last_name, + 'nome_completo': nome_completo + }) + + return JsonResponse({ + 'success': True, + 'count': len(usuarios_data), + 'results': usuarios_data + }) + + def post(self, request, *args, **kwargs): + from django.contrib.contenttypes.models import ContentType + + try: + tipo_autor_id = request.POST.get('tipo_autor') + autor_related_id = request.POST.get('autor_related') + nome = request.POST.get('nome', '').strip() + cargo = request.POST.get('cargo', '').strip() + + if not tipo_autor_id: + return JsonResponse({ + 'success': False, + 'error': 'Tipo de Autor é obrigatório.' + }, status=400) + + # Buscar TipoAutor + try: + tipo_autor = TipoAutor.objects.get(pk=tipo_autor_id) + except TipoAutor.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': 'Tipo de Autor não encontrado.' + }, status=404) + + # Verificar se tipo tem content_type + if tipo_autor.content_type: + # Tipo com content_type (Parlamentar, Comissão, etc) + if not autor_related_id: + return JsonResponse({ + 'success': False, + 'error': f'Selecione um registro de {tipo_autor.descricao}.' + }, status=400) + + # Buscar o registro relacionado + model_class = tipo_autor.content_type.model_class() + try: + related_obj = model_class.objects.get(pk=autor_related_id) + except model_class.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': f'Registro de {tipo_autor.descricao} não encontrado.' + }, status=404) + + # Verificar se já existe autor para este registro + if Autor.objects.filter( + content_type=tipo_autor.content_type, + object_id=autor_related_id + ).exists(): + return JsonResponse({ + 'success': False, + 'error': f'Já existe um autor cadastrado para "{related_obj}".' + }, status=400) + + # Criar autor com content_type + autor = Autor.objects.create( + tipo=tipo_autor, + content_type=tipo_autor.content_type, + object_id=autor_related_id, + nome=str(related_obj) + ) + + self.logger.info( + f"user={request.user.username}. Autor '{autor.nome}' criado " + f"via AJAX vinculado a {tipo_autor.descricao} (ID: {autor.id})" + ) + + else: + # Tipo genérico (sem content_type) + if not nome: + return JsonResponse({ + 'success': False, + 'error': 'Nome do Autor é obrigatório.' + }, status=400) + + # Verificar se já existe autor com este nome + if Autor.objects.filter(nome=nome).exists(): + return JsonResponse({ + 'success': False, + 'error': f'Já existe um autor com o nome "{nome}".' + }, status=400) + + # Criar autor genérico + autor = Autor.objects.create( + tipo=tipo_autor, + nome=nome, + cargo=cargo if cargo else '' + ) + + self.logger.info( + f"user={request.user.username}. Autor genérico '{autor.nome}' " + f"criado via AJAX (ID: {autor.id})" + ) + + return JsonResponse({ + 'success': True, + 'autor': { + 'id': autor.id, + 'nome': autor.nome, + 'tipo': str(autor.tipo), + 'cargo': autor.cargo if autor.cargo else '' + } + }) + + except Exception as e: + self.logger.error( + f"user={request.user.username}. Erro ao criar autor via AJAX: {str(e)}" + ) + return JsonResponse({ + 'success': False, + 'error': f'Erro ao criar autor: {str(e)}' + }, status=500) diff --git a/sapl/materia/urls.py b/sapl/materia/urls.py index 80d909e79..abf15cb20 100644 --- a/sapl/materia/urls.py +++ b/sapl/materia/urls.py @@ -166,9 +166,13 @@ urlpatterns_sistema = [ include(AssuntoMateriaCrud.get_urls())), url(r'^sistema/proposicao/tipo/', include(TipoProposicaoCrud.get_urls())), + url(r'^sistema/materia/tipoproposicao/', + include(TipoProposicaoCrud.get_urls())), url(r'^sistema/materia/tipo/', include(TipoMateriaCrud.get_urls())), url(r'^sistema/materia/regime-tramitacao/', include(RegimeTramitacaoCrud.get_urls())), + url(r'^sistema/materia/regimetramitacao/', + include(RegimeTramitacaoCrud.get_urls())), url(r'^sistema/materia/tipo-documento/', include(TipoDocumentoCrud.get_urls())), url(r'^sistema/materia/tipo-fim-relatoria/', diff --git a/sapl/parlamentares/urls.py b/sapl/parlamentares/urls.py index d67a1f6b1..b3fd32fd5 100644 --- a/sapl/parlamentares/urls.py +++ b/sapl/parlamentares/urls.py @@ -23,7 +23,8 @@ from sapl.parlamentares.views import (CargoMesaCrud, ColigacaoCrud, PesquisarParlamentarView, VincularParlamentarView, get_sessoes_legislatura, FrenteCargoCrud, FrenteParlamentarCrud, get_parlamentar_frentes, PesquisarColigacaoView, PesquisarPartidoView, - BlocoCargoCrud, BlocoMembroCrud) + BlocoCargoCrud, BlocoMembroCrud, criar_parlamentar_ajax, + criar_legislatura_ajax) from .apps import AppConfig @@ -56,6 +57,12 @@ urlpatterns = [ url(r'^parlamentar/vincular-parlamentar/$', VincularParlamentarView.as_view(), name='vincular_parlamentar'), + url(r'^parlamentar/criar-parlamentar-ajax/$', + criar_parlamentar_ajax, name='criar_parlamentar_ajax'), + + url(r'^parlamentar/criar-legislatura-ajax/$', + criar_legislatura_ajax, name='criar_legislatura_ajax'), + url(r'^parlamentar/coligacao-legislatura/', coligacao_legislatura, name="coligacao_legislatura"), url(r'^sistema/coligacao/', include(ColigacaoCrud.get_urls() + diff --git a/sapl/parlamentares/views.py b/sapl/parlamentares/views.py index a46ce2b84..fda75cca2 100644 --- a/sapl/parlamentares/views.py +++ b/sapl/parlamentares/views.py @@ -3,6 +3,7 @@ import json import logging from django.contrib import messages +from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType @@ -1470,6 +1471,163 @@ class VincularParlamentarView(PermissionRequiredMixin, FormView): return HttpResponseRedirect(self.get_success_url()) +@permission_required('parlamentares.add_parlamentar') +def criar_parlamentar_ajax(request): + """View AJAX para criar parlamentar via modal""" + if request.method == 'POST': + try: + nome_completo = request.POST.get('nome_completo', '').strip() + nome_parlamentar = request.POST.get('nome_parlamentar', '').strip() + sexo = request.POST.get('sexo', '').strip() + + # Validações básicas dos campos obrigatórios + if not nome_completo or not nome_parlamentar or not sexo: + return JsonResponse({ + 'success': False, + 'error': 'Nome Completo, Nome Parlamentar e Sexo são obrigatórios.' + }) + + if sexo not in ['F', 'M']: + return JsonResponse({ + 'success': False, + 'error': 'Sexo inválido. Use F (Feminino) ou M (Masculino).' + }) + + # Campos opcionais + cpf = request.POST.get('cpf', '').strip() + rg = request.POST.get('rg', '').strip() + data_nascimento = request.POST.get('data_nascimento', '').strip() + email = request.POST.get('email', '').strip() + telefone = request.POST.get('telefone', '').strip() + telefone_celular = request.POST.get('telefone_celular', '').strip() + + # Processar data de nascimento se fornecida + data_nascimento_obj = None + if data_nascimento: + try: + data_nascimento_obj = datetime.strptime(data_nascimento, '%d/%m/%Y').date() + except ValueError: + return JsonResponse({ + 'success': False, + 'error': 'Formato de data de nascimento inválido. Use DD/MM/AAAA.' + }) + + # Criar parlamentar com todos os campos + parlamentar = Parlamentar.objects.create( + nome_completo=nome_completo, + nome_parlamentar=nome_parlamentar, + sexo=sexo, + cpf=cpf, + rg=rg, + data_nascimento=data_nascimento_obj, + email=email, + telefone=telefone, + telefone_celular=telefone_celular, + ativo=True # Por padrão, marcar como ativo + ) + + return JsonResponse({ + 'success': True, + 'parlamentar': { + 'id': parlamentar.id, + 'nome': str(parlamentar) + } + }) + + except Exception as e: + return JsonResponse({ + 'success': False, + 'error': f'Erro ao criar parlamentar: {str(e)}' + }) + + return JsonResponse({'success': False, 'error': 'Método não permitido'}) + + +@permission_required('parlamentares.add_parlamentar') +def criar_legislatura_ajax(request): + """View AJAX para criar legislatura via modal""" + if request.method == 'POST': + try: + numero = request.POST.get('numero', '').strip() + data_inicio = request.POST.get('data_inicio', '').strip() + data_fim = request.POST.get('data_fim', '').strip() + data_eleicao = request.POST.get('data_eleicao', '').strip() + + # Validações básicas + if not numero or not data_inicio or not data_fim or not data_eleicao: + return JsonResponse({ + 'success': False, + 'error': 'Todos os campos são obrigatórios.' + }) + + try: + numero = int(numero) + if numero <= 0: + raise ValueError + except ValueError: + return JsonResponse({ + 'success': False, + 'error': 'Número deve ser um valor inteiro positivo.' + }) + + # Converter datas do formato brasileiro (DD/MM/YYYY) para YYYY-MM-DD + from datetime import datetime + + try: + data_inicio_obj = datetime.strptime(data_inicio, '%d/%m/%Y').date() + data_fim_obj = datetime.strptime(data_fim, '%d/%m/%Y').date() + data_eleicao_obj = datetime.strptime(data_eleicao, '%d/%m/%Y').date() + except ValueError: + return JsonResponse({ + 'success': False, + 'error': 'Formato de data inválido. Use DD/MM/YYYY.' + }) + + # Validar ordem das datas + if data_inicio_obj >= data_fim_obj: + return JsonResponse({ + 'success': False, + 'error': 'Data de Início deve ser anterior à Data Fim.' + }) + + if data_eleicao_obj >= data_inicio_obj: + return JsonResponse({ + 'success': False, + 'error': 'Data de Eleição deve ser anterior à Data de Início.' + }) + + # Verificar se já existe legislatura com esse número + if Legislatura.objects.filter(numero=numero).exists(): + return JsonResponse({ + 'success': False, + 'error': f'Já existe uma legislatura com o número {numero}.' + }) + + # Criar legislatura + legislatura = Legislatura.objects.create( + numero=numero, + data_inicio=data_inicio_obj, + data_fim=data_fim_obj, + data_eleicao=data_eleicao_obj + ) + + return JsonResponse({ + 'success': True, + 'legislatura': { + 'id': legislatura.id, + 'nome': str(legislatura) + } + }) + + except Exception as e: + return JsonResponse({ + 'success': False, + 'error': f'Erro ao criar legislatura: {str(e)}' + }) + + return JsonResponse({'success': False, 'error': 'Método não permitido'}) + + class BlocoCrud(CrudAux): model = Bloco public = [RP_DETAIL, RP_LIST] diff --git a/sapl/templates/404.html b/sapl/templates/404.html index 82a3dee5e..b174f7a4f 100644 --- a/sapl/templates/404.html +++ b/sapl/templates/404.html @@ -1,147 +1,12 @@ -{% load i18n menus %} -{% load common_tags %} -{% load render_bundle from webpack_loader %} -{% load webpack_static from webpack_loader %} - - - - - - -
- -{% trans 'A página que você está procurando não foi encontrada.' %}
+ {% trans 'Voltar para a página inicial' %} +{% trans 'Desculpe, ocorreu um erro interno no servidor. Tente novamente mais tarde.' %}
+ {% trans 'Voltar para a página inicial' %} +Para que um usuário possa acessar /proposicao/create e criar proposições, ele precisa atender DOIS requisitos obrigatórios:
O usuário precisa estar no grupo "Autor" do sistema.
+Como adicionar:
+/sistema/usuario/
+
+ Acessar
+
+ Clique no nome do usuário (ex: helena)
+Na seção "Grupos", marque "Autor"
+Clique em "Salvar"
+O usuário precisa estar vinculado como operador de um Autor.
+Como vincular:
+/sistema/usuario/{id}/edit
+
+ Ver Lista
+
+ No campo "Autor", selecione o autor correspondente (ex: "Helena Silva")
+Se não existir autor, crie um parlamentar primeiro
+Isso cria automaticamente o vínculo OperadorAutor
+Se o usuário helena possui os grupos:
+Ela NÃO conseguirá criar proposições porque:
+Solução: Adicionar ao grupo "Autor" E vincular a um Autor existente.
+Para vincular rapidamente um usuário e permitir criação de proposições:
+/sistema/usuario/
+
+ Configurar Agora
+
+ Antes de tentar criar uma proposição, certifique-se de que o usuário possui:
+Se faltar algum requisito, você receberá:
++ + Ver Seção de Permissões + + + Configurar Usuário Agora + +
+/sistema/materia/regimetramitacao/ /parlamentar/create /sistema/usuario/create /sistema/usuario/{id}/edit Para cada usuário que precisa criar proposições, você DEVE:
+/sistema/usuario//sistema/usuario/
+
+ Configurar Permissões
+
+ Sem essas duas configurações, o usuário receberá "Acesso Negado" ao tentar acessar /proposicao/create.