feat: implementa sistema de responsabilidades e instâncias - Adiciona responsabilidades de Finanças e Imprensa para todas as instâncias - Cria templates genéricos para gerenciamento de instâncias - Implementa sistema de permissões baseado em RBAC - Adiciona status de Aspirante com avaliação obrigatória - Atualiza documentação com novas regras e responsabilidades - Cria testes para validação das permissões - Adiciona migração para novos campos no banco de dados
This commit is contained in:
161
README.md
161
README.md
@@ -1,43 +1,134 @@
|
|||||||
# Sistema de Controles
|
# Sistema de Controle de Militantes
|
||||||
|
|
||||||
## Para instalar
|
Sistema para gerenciamento de militantes, células, setores e comitês regionais.
|
||||||
|
|
||||||
```bash
|
## Estrutura de Permissões (RBAC)
|
||||||
make install
|
|
||||||
|
O sistema utiliza um sistema de controle de acesso baseado em papéis (RBAC) com a seguinte hierarquia:
|
||||||
|
|
||||||
|
### Níveis de Papéis
|
||||||
|
|
||||||
|
1. **Militante Básico** (Nível 1)
|
||||||
|
- Visualizar próprios dados
|
||||||
|
- Editar próprios dados
|
||||||
|
- Visualizar dados da célula
|
||||||
|
|
||||||
|
2. **Secretário de Célula** (Nível 2)
|
||||||
|
- Todas as permissões do Militante Básico
|
||||||
|
- Gerenciar membros da célula
|
||||||
|
- Criar membros na célula
|
||||||
|
- Visualizar relatórios da célula
|
||||||
|
|
||||||
|
3. **Membro de Setor** (Nível 3)
|
||||||
|
- Todas as permissões do Secretário de Célula
|
||||||
|
- Visualizar relatórios do setor
|
||||||
|
|
||||||
|
4. **Secretário de Setor** (Nível 4)
|
||||||
|
- Todas as permissões do Membro de Setor
|
||||||
|
- Gerenciar células do setor
|
||||||
|
- Criar células no setor
|
||||||
|
|
||||||
|
5. **Membro de CR** (Nível 5)
|
||||||
|
- Todas as permissões do Secretário de Setor
|
||||||
|
- Visualizar relatórios do CR
|
||||||
|
|
||||||
|
6. **Secretário de CR** (Nível 6)
|
||||||
|
- Todas as permissões do Membro de CR
|
||||||
|
- Gerenciar setores do CR
|
||||||
|
- Criar setores no CR
|
||||||
|
|
||||||
|
7. **Membro do CC** (Nível 7)
|
||||||
|
- Todas as permissões do Secretário de CR
|
||||||
|
- Visualizar relatórios nacionais
|
||||||
|
|
||||||
|
8. **Secretário Geral** (Nível 8)
|
||||||
|
- Todas as permissões do Membro do CC
|
||||||
|
- Gerenciar CRs
|
||||||
|
- Criar CRs
|
||||||
|
- Configurar sistema
|
||||||
|
|
||||||
|
## Instalação
|
||||||
|
|
||||||
|
1. Clone o repositório
|
||||||
|
2. Crie um ambiente virtual:
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # Linux/Mac
|
||||||
|
# ou
|
||||||
|
venv\Scripts\activate # Windows
|
||||||
|
```
|
||||||
|
3. Instale as dependências:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
4. Execute as migrações do banco de dados:
|
||||||
|
```bash
|
||||||
|
python sql/migrate_db.py
|
||||||
|
```
|
||||||
|
5. Configure as variáveis de ambiente no arquivo `.env`:
|
||||||
|
```
|
||||||
|
FLASK_APP=app.py
|
||||||
|
FLASK_ENV=development
|
||||||
|
SECRET_KEY=sua_chave_secreta
|
||||||
|
MAIL_SERVER=seu_servidor_smtp
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USE_TLS=True
|
||||||
|
MAIL_USERNAME=seu_email
|
||||||
|
MAIL_PASSWORD=sua_senha
|
||||||
|
```
|
||||||
|
6. Execute o aplicativo:
|
||||||
|
```bash
|
||||||
|
flask run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uso
|
||||||
|
|
||||||
|
### Decoradores de Permissão
|
||||||
|
|
||||||
|
O sistema fornece três decoradores para controle de acesso:
|
||||||
|
|
||||||
|
1. `@require_permission(permission_name)`
|
||||||
|
- Verifica se o usuário tem uma permissão específica
|
||||||
|
- Exemplo: `@require_permission('create_cell_member')`
|
||||||
|
|
||||||
|
2. `@require_role(role_name)`
|
||||||
|
- Verifica se o usuário tem um papel específico
|
||||||
|
- Exemplo: `@require_role('Secretário de Célula')`
|
||||||
|
|
||||||
|
3. `@require_minimum_role(min_level)`
|
||||||
|
- Verifica se o usuário tem um papel com nível mínimo
|
||||||
|
- Exemplo: `@require_minimum_role(Role.SECRETARIO_CR)`
|
||||||
|
|
||||||
|
### Verificando Permissões no Código
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Verificar se um usuário tem uma permissão
|
||||||
|
if user.has_permission('create_cell_member'):
|
||||||
|
# Faça algo
|
||||||
|
|
||||||
|
# Verificar se um usuário tem um papel
|
||||||
|
if user.has_role('Secretário de Célula'):
|
||||||
|
# Faça algo
|
||||||
|
|
||||||
|
# Obter o papel mais alto do usuário
|
||||||
|
highest_role = user.get_highest_role()
|
||||||
|
if highest_role and highest_role.nivel >= Role.SECRETARIO_CR:
|
||||||
|
# Faça algo
|
||||||
```
|
```
|
||||||
|
|
||||||
## Sobre o QR Code de Autenticação (admin_qr.png)
|
## Estrutura do Banco de Dados
|
||||||
|
|
||||||
O arquivo `admin_qr.png` é um QR code gerado automaticamente para configuração da autenticação de dois fatores (2FA) do usuário administrador. Este arquivo é:
|
O sistema utiliza as seguintes tabelas para o RBAC:
|
||||||
|
|
||||||
- Gerado na raiz do projeto quando:
|
- `roles`: Armazena os papéis disponíveis
|
||||||
- O comando `make reset-admin` é executado
|
- `permissions`: Armazena as permissões disponíveis
|
||||||
- O servidor é iniciado com `make run` e existe um usuário admin
|
- `role_permissions`: Mapeia papéis para permissões
|
||||||
- Um novo usuário é criado através da interface web
|
- `user_roles`: Mapeia usuários para papéis
|
||||||
|
|
||||||
- Usado para:
|
## Segurança
|
||||||
- Configurar a autenticação 2FA no aplicativo autenticador (Google Authenticator, Microsoft Authenticator, etc.)
|
|
||||||
- Gerar os códigos OTP necessários para fazer login no sistema
|
|
||||||
|
|
||||||
### Importante:
|
- Todas as senhas são armazenadas com hash bcrypt
|
||||||
- O QR code é atualizado sempre que um novo usuário é criado ou quando o sistema é reiniciado
|
- Sessões expiram após período de inatividade
|
||||||
- Cada QR code é único e corresponde ao segredo OTP atual do usuário
|
- Controle de acesso granular baseado em papéis
|
||||||
- Se você recriar o banco de dados ou resetar o admin, será necessário reconfigurar o aplicativo autenticador com o novo QR code
|
- Proteção contra CSRF
|
||||||
- Mantenha este arquivo seguro, pois ele contém informações sensíveis de autenticação
|
- Validação de entrada de dados
|
||||||
|
|
||||||
### Como usar:
|
|
||||||
1. Instale um aplicativo autenticador no seu celular (Google Authenticator, Microsoft Authenticator, etc.)
|
|
||||||
2. Escaneie o QR code (`admin_qr.png`) com o aplicativo
|
|
||||||
3. O aplicativo irá gerar códigos de 6 dígitos a cada 30 segundos
|
|
||||||
4. Use estes códigos junto com seu usuário e senha para fazer login no sistema
|
|
||||||
|
|
||||||
### Comandos relacionados:
|
|
||||||
```bash
|
|
||||||
# Limpar banco e criar novo admin com novo QR code
|
|
||||||
make reset-admin
|
|
||||||
|
|
||||||
# Iniciar o servidor (também gera novo QR code se necessário)
|
|
||||||
make run
|
|
||||||
```
|
|
||||||
|
|
||||||
Acesse por: http://127.0.0.1:5000
|
|
||||||
|
|||||||
1
admin_qr.txt
Normal file
1
admin_qr.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
otpauth://totp/Sistema%20de%20Controles:admin?secret=27NESPSPWKWIXVIDBUJPTK7MPAKGF4WG&issuer=Sistema%20de%20Controles
|
||||||
@@ -1 +0,0 @@
|
|||||||
SECRET_KEY = 'sua_chave_secreta_aqui' # Use uma chave segura em produção
|
|
||||||
183
create_admin.py
183
create_admin.py
@@ -1,136 +1,91 @@
|
|||||||
from functions.database import init_database, Usuario, get_db_connection
|
|
||||||
import qrcode
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from functions.database import get_db_connection, Usuario
|
||||||
|
from functions.rbac import Role
|
||||||
import pyotp
|
import pyotp
|
||||||
|
import qrcode
|
||||||
|
import base64
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
def generate_qr_code(user):
|
def create_admin():
|
||||||
"""
|
"""Cria o usuário admin se não existir"""
|
||||||
Gera o QR code para um usuário específico
|
db = get_db_connection()
|
||||||
|
|
||||||
Args:
|
|
||||||
user: Instância do modelo Usuario
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path: Caminho do arquivo QR code gerado
|
|
||||||
"""
|
|
||||||
import qrcode
|
|
||||||
|
|
||||||
# Gerar QR Code apenas na raiz do projeto
|
|
||||||
qr_path = Path('admin_qr.png')
|
|
||||||
|
|
||||||
# Remover arquivo antigo se existir
|
|
||||||
if qr_path.exists():
|
|
||||||
os.remove(str(qr_path))
|
|
||||||
|
|
||||||
# Gerar e salvar QR Code
|
|
||||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
|
||||||
qr.add_data(user.get_otp_uri())
|
|
||||||
qr.make(fit=True)
|
|
||||||
img = qr.make_image(fill_color="black", back_color="white")
|
|
||||||
img.save(str(qr_path))
|
|
||||||
|
|
||||||
print(f"\nQR Code gerado em: {os.path.abspath(qr_path)}")
|
|
||||||
|
|
||||||
return qr_path
|
|
||||||
|
|
||||||
def create_admin_user():
|
|
||||||
try:
|
try:
|
||||||
# Inicializar o banco de dados
|
# Verificar se o admin já existe
|
||||||
init_database()
|
admin = db.query(Usuario).filter_by(username='admin').first()
|
||||||
|
|
||||||
# Obter a sessão
|
|
||||||
db_session = get_db_connection()
|
|
||||||
|
|
||||||
# Verificar se o admin foi criado
|
|
||||||
admin = db_session.query(Usuario).filter_by(username="admin").first()
|
|
||||||
|
|
||||||
if admin:
|
if admin:
|
||||||
print("\n=== Usuário Admin Encontrado ===")
|
print("Usuário admin já existe")
|
||||||
# Atualizar QR code mesmo se o usuário já existir
|
|
||||||
qr_path = generate_qr_code(admin)
|
# Verificar se o arquivo admin_qr.png existe
|
||||||
|
if os.path.exists('admin_qr.png'):
|
||||||
|
print("Usando OTP existente do arquivo admin_qr.png")
|
||||||
|
# Extrair o OTP secret do QR code existente
|
||||||
|
with open('admin_qr.png', 'rb') as f:
|
||||||
|
qr_data = f.read()
|
||||||
|
# Aqui você precisaria implementar a lógica para extrair o OTP secret do QR code
|
||||||
|
# Por enquanto, vamos apenas manter o OTP existente
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print("Gerando novo OTP para o admin...")
|
||||||
|
# Gerar novo OTP
|
||||||
|
otp_secret = pyotp.random_base32()
|
||||||
|
admin.otp_secret = otp_secret
|
||||||
|
db.commit()
|
||||||
else:
|
else:
|
||||||
print("\n=== Criando Novo Usuário Admin ===")
|
print("Criando usuário admin...")
|
||||||
# Criar usuário admin com novo segredo OTP
|
# Criar usuário admin
|
||||||
admin = Usuario(
|
admin = Usuario(
|
||||||
username="admin",
|
username='admin',
|
||||||
password="admin123",
|
password='admin123',
|
||||||
is_admin=True
|
is_admin=True
|
||||||
)
|
)
|
||||||
admin.email = "admin@example.com"
|
admin.email = 'admin@controles.com'
|
||||||
|
db.add(admin)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
# Adicionar e fazer commit para obter o ID
|
# Gerar OTP
|
||||||
db_session.add(admin)
|
otp_secret = pyotp.random_base32()
|
||||||
db_session.commit()
|
admin.otp_secret = otp_secret
|
||||||
|
db.commit()
|
||||||
|
|
||||||
# Recarregar o usuário do banco para garantir dados atualizados
|
# Atribuir role de Secretário Geral
|
||||||
admin = db_session.query(Usuario).filter_by(username="admin").first()
|
admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
|
||||||
|
if admin_role:
|
||||||
|
admin.roles.append(admin_role)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
# Verificar e mostrar informações do OTP
|
# Gerar QR code
|
||||||
print("\n=== Informações do Usuário Admin ===")
|
totp = pyotp.TOTP(otp_secret)
|
||||||
print(f"Username: admin")
|
provisioning_uri = totp.provisioning_uri(admin.username, issuer_name="Sistema de Controles")
|
||||||
print(f"Senha: admin123")
|
|
||||||
print(f"Email: {admin.email}")
|
|
||||||
print(f"Segredo OTP atual: {admin.otp_secret}")
|
|
||||||
|
|
||||||
# Gerar URI do OTP usando o segredo armazenado
|
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||||
totp = pyotp.TOTP(admin.otp_secret)
|
qr.add_data(provisioning_uri)
|
||||||
otp_uri = totp.provisioning_uri(
|
qr.make(fit=True)
|
||||||
name=admin.username,
|
|
||||||
issuer_name="Sistema de Controles"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Usar a função extraída para gerar o QR code
|
img = qr.make_image(fill_color="black", back_color="white")
|
||||||
qr_path = generate_qr_code(admin)
|
|
||||||
|
|
||||||
print("\n=== QR Code Gerado ===")
|
# Salvar QR code como base64
|
||||||
print(f"QR Code salvo em: {qr_path}")
|
buffered = BytesIO()
|
||||||
print(f"URI do OTP: {otp_uri}")
|
img.save(buffered, format="PNG")
|
||||||
|
qr_base64 = base64.b64encode(buffered.getvalue()).decode()
|
||||||
|
|
||||||
# Gerar código atual para verificação
|
# Salvar QR code como arquivo
|
||||||
current_code = totp.now()
|
img.save('admin_qr.png')
|
||||||
print("\n=== Verificação do OTP ===")
|
|
||||||
print(f"Código OTP atual: {current_code}")
|
|
||||||
print(f"Verificação do código: {totp.verify(current_code)}")
|
|
||||||
|
|
||||||
print("\n=== Instruções para Configuração ===")
|
print("\nConfiguração do OTP para o admin:")
|
||||||
print("1. Instale um aplicativo autenticador no seu celular")
|
print(f"OTP Secret: {otp_secret}")
|
||||||
print(" (Google Authenticator, Microsoft Authenticator, etc)")
|
print("\nInstruções:")
|
||||||
print("2. Abra o aplicativo")
|
print("1. Use um aplicativo autenticador (como Google Authenticator ou Authy)")
|
||||||
print("3. Selecione a opção para adicionar uma nova conta")
|
print("2. Escaneie o QR code ou insira o OTP Secret manualmente")
|
||||||
print("4. Escaneie o QR Code salvo em:", qr_path)
|
print("3. Use o código gerado para fazer login")
|
||||||
print("\nOU configure manualmente:")
|
print("\nQR code salvo em 'admin_qr.png'")
|
||||||
print(f"- Nome da conta: {admin.username}")
|
|
||||||
print(f"- Segredo: {admin.otp_secret}")
|
|
||||||
print("- Tipo: Baseado em tempo (TOTP)")
|
|
||||||
print("- Algoritmo: SHA1")
|
|
||||||
print("- Dígitos: 6")
|
|
||||||
print("- Intervalo: 30 segundos")
|
|
||||||
|
|
||||||
# Verificação final
|
|
||||||
print("\n=== Teste de Verificação ===")
|
|
||||||
test_code = totp.now()
|
|
||||||
print(f"Código de teste: {test_code}")
|
|
||||||
is_valid = admin.verify_otp(test_code)
|
|
||||||
print(f"Verificação do código: {'Sucesso' if is_valid else 'Falha'}")
|
|
||||||
|
|
||||||
if not is_valid:
|
|
||||||
print("\nALERTA: Verificação do OTP falhou!")
|
|
||||||
print("Por favor, verifique se o segredo OTP está correto.")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\nErro durante a execução: {e}")
|
print(f"Erro ao criar admin: {str(e)}")
|
||||||
import traceback
|
db.rollback()
|
||||||
traceback.print_exc()
|
raise
|
||||||
finally:
|
finally:
|
||||||
if 'db_session' in locals():
|
db.close()
|
||||||
db_session.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
# Remover banco de dados existente para começar limpo
|
create_admin()
|
||||||
db_path = Path.home() / '.local' / 'share' / 'controles' / 'database.db'
|
|
||||||
if db_path.exists():
|
|
||||||
os.remove(db_path)
|
|
||||||
print("Banco de dados antigo removido.")
|
|
||||||
|
|
||||||
create_admin_user()
|
|
||||||
104
create_test_users.py
Normal file
104
create_test_users.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
from functions.database import get_db_connection, Usuario
|
||||||
|
from functions.rbac import Role
|
||||||
|
import pyotp
|
||||||
|
import qrcode
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
def create_test_users():
|
||||||
|
"""Cria usuários de teste se não existirem"""
|
||||||
|
db = get_db_connection()
|
||||||
|
try:
|
||||||
|
# Usuários de teste
|
||||||
|
test_users = [
|
||||||
|
{
|
||||||
|
'username': 'teste',
|
||||||
|
'password': 'admin123', # Mesma senha do admin
|
||||||
|
'email': 'teste@controles.com',
|
||||||
|
'is_admin': True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'username': 'aligner',
|
||||||
|
'password': 'Test123!@#',
|
||||||
|
'email': 'aligner@controles.com',
|
||||||
|
'is_admin': False
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'username': 'tester',
|
||||||
|
'password': 'Test123!@#',
|
||||||
|
'email': 'tester@controles.com',
|
||||||
|
'is_admin': False
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'username': 'deployer',
|
||||||
|
'password': 'Test123!@#',
|
||||||
|
'email': 'deployer@controles.com',
|
||||||
|
'is_admin': False
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Obter o OTP secret do admin se existir
|
||||||
|
admin = db.query(Usuario).filter_by(username='admin').first()
|
||||||
|
admin_otp_secret = admin.otp_secret if admin else None
|
||||||
|
|
||||||
|
for user_data in test_users:
|
||||||
|
# Verificar se o usuário já existe
|
||||||
|
user = db.query(Usuario).filter_by(username=user_data['username']).first()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
print(f"Criando usuário {user_data['username']}...")
|
||||||
|
# Criar usuário
|
||||||
|
user = Usuario(
|
||||||
|
username=user_data['username'],
|
||||||
|
password=user_data['password'],
|
||||||
|
is_admin=user_data['is_admin']
|
||||||
|
)
|
||||||
|
user.email = user_data['email']
|
||||||
|
db.add(user)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Se for o usuário teste, usar o mesmo OTP do admin
|
||||||
|
if user_data['username'] == 'teste' and admin_otp_secret:
|
||||||
|
user.otp_secret = admin_otp_secret
|
||||||
|
db.commit()
|
||||||
|
else:
|
||||||
|
# Gerar novo OTP para outros usuários
|
||||||
|
otp_secret = pyotp.random_base32()
|
||||||
|
user.otp_secret = otp_secret
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Atribuir role de Secretário Geral para o usuário teste
|
||||||
|
if user_data['username'] == 'teste':
|
||||||
|
admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
|
||||||
|
if admin_role:
|
||||||
|
user.roles.append(admin_role)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
print(f"Usuário {user_data['username']} criado com sucesso!")
|
||||||
|
else:
|
||||||
|
print(f"Usuário {user_data['username']} já existe")
|
||||||
|
|
||||||
|
# Se for o usuário teste e não tiver o OTP do admin, atualizar
|
||||||
|
if user_data['username'] == 'teste' and admin_otp_secret and user.otp_secret != admin_otp_secret:
|
||||||
|
user.otp_secret = admin_otp_secret
|
||||||
|
db.commit()
|
||||||
|
print(f"OTP do usuário teste atualizado para o mesmo do admin")
|
||||||
|
|
||||||
|
# Verificar se o usuário teste tem a role de Secretário Geral
|
||||||
|
if user_data['username'] == 'teste':
|
||||||
|
admin_role = db.query(Role).filter_by(nivel=Role.SECRETARIO_GERAL).first()
|
||||||
|
if admin_role and admin_role not in user.roles:
|
||||||
|
user.roles.append(admin_role)
|
||||||
|
db.commit()
|
||||||
|
print(f"Role de Secretário Geral atribuída ao usuário teste")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao criar usuários de teste: {str(e)}")
|
||||||
|
db.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
create_test_users()
|
||||||
8
dao.py
8
dao.py
@@ -1,8 +0,0 @@
|
|||||||
from functions.database import execute_query
|
|
||||||
|
|
||||||
def get_user_by_email(email):
|
|
||||||
query = "SELECT * FROM users WHERE email = %s"
|
|
||||||
cursor = execute_query(query, (email,))
|
|
||||||
if cursor:
|
|
||||||
return cursor.fetchone()
|
|
||||||
return None
|
|
||||||
99
database.sql
99
database.sql
@@ -1,99 +0,0 @@
|
|||||||
-- Tabela de Militantes
|
|
||||||
CREATE TABLE militantes (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
nome VARCHAR(100) NOT NULL,
|
|
||||||
cpf VARCHAR(14) UNIQUE,
|
|
||||||
email VARCHAR(100) UNIQUE,
|
|
||||||
telefone VARCHAR(15),
|
|
||||||
endereco VARCHAR(255),
|
|
||||||
filiado BOOLEAN DEFAULT false
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Cotas Mensais
|
|
||||||
CREATE TABLE cotas_mensais (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
militante_id INT,
|
|
||||||
valor_antigo DECIMAL(10, 2) NOT NULL,
|
|
||||||
valor_novo DECIMAL(10, 2) NOT NULL,
|
|
||||||
data_alteracao DATE NOT NULL,
|
|
||||||
FOREIGN KEY (militante_id) REFERENCES militantes(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Pagamentos
|
|
||||||
CREATE TABLE tipos_pagamento (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
descricao VARCHAR(100) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE pagamentos (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
militante_id INT,
|
|
||||||
tipo_pagamento_id INT,
|
|
||||||
valor DECIMAL(10, 2) NOT NULL,
|
|
||||||
data_pagamento DATE NOT NULL,
|
|
||||||
FOREIGN KEY (militante_id) REFERENCES militantes(id),
|
|
||||||
FOREIGN KEY (tipo_pagamento_id) REFERENCES tipos_pagamento(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Tipos de Materiais
|
|
||||||
CREATE TABLE tipos_materiais (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
descricao VARCHAR(100) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Materiais Vendidos
|
|
||||||
CREATE TABLE materiais_vendidos (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
militante_id INT,
|
|
||||||
tipo_material_id INT,
|
|
||||||
descricao VARCHAR(255) NOT NULL,
|
|
||||||
valor DECIMAL(10, 2) NOT NULL,
|
|
||||||
data_venda DATE NOT NULL,
|
|
||||||
FOREIGN KEY (militante_id) REFERENCES militantes(id),
|
|
||||||
FOREIGN KEY (tipo_material_id) REFERENCES tipos_materiais(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Vendas de Jornais Avulsos
|
|
||||||
CREATE TABLE vendas_jornais_avulsos (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
militante_id INT,
|
|
||||||
quantidade INT NOT NULL,
|
|
||||||
valor_total DECIMAL(10, 2) NOT NULL,
|
|
||||||
data_venda DATE NOT NULL,
|
|
||||||
FOREIGN KEY (militante_id) REFERENCES militantes(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Assinaturas Anuais
|
|
||||||
CREATE TABLE assinaturas_anuais (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
militante_id INT,
|
|
||||||
tipo_material_id INT,
|
|
||||||
quantidade INT NOT NULL,
|
|
||||||
valor_total DECIMAL(10, 2) NOT NULL,
|
|
||||||
data_inicio DATE NOT NULL,
|
|
||||||
data_fim DATE NOT NULL,
|
|
||||||
FOREIGN KEY (militante_id) REFERENCES militantes(id),
|
|
||||||
FOREIGN KEY (tipo_material_id) REFERENCES tipos_materiais(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Relatório de Cotas Mensais
|
|
||||||
CREATE TABLE relatorio_cotas_mensais (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
setor_id INT,
|
|
||||||
comite_id INT,
|
|
||||||
total_cotas DECIMAL(10, 2) NOT NULL,
|
|
||||||
data_relatorio DATE NOT NULL,
|
|
||||||
FOREIGN KEY (setor_id) REFERENCES setores(id),
|
|
||||||
FOREIGN KEY (comite_id) REFERENCES comites_centrais(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela de Relatório de Vendas de Materiais
|
|
||||||
CREATE TABLE relatorio_vendas_materiais (
|
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
setor_id INT,
|
|
||||||
comite_id INT,
|
|
||||||
total_vendas DECIMAL(10, 2) NOT NULL,
|
|
||||||
data_relatorio DATE NOT NULL,
|
|
||||||
FOREIGN KEY (setor_id) REFERENCES setores(id),
|
|
||||||
FOREIGN KEY (comite_id) REFERENCES comites_centrais(id)
|
|
||||||
);
|
|
||||||
165
docs/README.md
Normal file
165
docs/README.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# Sistema de Controle OCI
|
||||||
|
|
||||||
|
## Hierarquia e Permissões
|
||||||
|
|
||||||
|
### Níveis de Acesso
|
||||||
|
|
||||||
|
1. **Militante Básico**
|
||||||
|
- Pode ver apenas os membros da sua própria célula
|
||||||
|
- Não pode alterar níveis de outros usuários
|
||||||
|
|
||||||
|
2. **Secretário de Célula**
|
||||||
|
- Pode ver e gerenciar apenas os membros da sua célula
|
||||||
|
- Não pode alterar níveis de outros usuários
|
||||||
|
|
||||||
|
3. **Membro de Setor**
|
||||||
|
- Pode ver apenas os dados do setor ao qual pertence
|
||||||
|
- Não pode alterar níveis de outros usuários
|
||||||
|
|
||||||
|
4. **Secretário de Setor**
|
||||||
|
- Pode ver e gerenciar todos os dados do seu setor
|
||||||
|
- Pode alterar níveis de militantes do setor, transformando-os em secretários
|
||||||
|
- Não pode alterar níveis de membros de outros setores
|
||||||
|
|
||||||
|
5. **Membro de CR**
|
||||||
|
- Pode ver apenas os dados do CR ao qual pertence
|
||||||
|
- Não pode alterar níveis de outros usuários
|
||||||
|
|
||||||
|
6. **Secretário de CR**
|
||||||
|
- Pode ver e gerenciar todos os dados do seu CR
|
||||||
|
- Pode alterar níveis de membros do CR
|
||||||
|
- Não pode alterar níveis de membros de outros CRs
|
||||||
|
|
||||||
|
7. **Membro do CC**
|
||||||
|
- Pode ver todos os dados do sistema
|
||||||
|
- Não pode alterar níveis de outros usuários
|
||||||
|
|
||||||
|
8. **Secretário Geral e Secretário de Organização**
|
||||||
|
- Pode ver todos os dados do sistema
|
||||||
|
- Pode alterar níveis de qualquer usuário em qualquer instância
|
||||||
|
|
||||||
|
### Regras de Visualização
|
||||||
|
|
||||||
|
- Cada militante só pode ver os membros da sua própria célula
|
||||||
|
- Membros de setor só veem dados do setor ao qual pertencem
|
||||||
|
- Membros de CR só veem informações do CR ao qual pertencem
|
||||||
|
- Membros do CC podem ver todas as informações do sistema
|
||||||
|
|
||||||
|
### Regras de Edição
|
||||||
|
|
||||||
|
- Apenas o Secretário Geral e o Secretário de Organização podem alterar níveis em todas as instâncias
|
||||||
|
- Secretários de CR podem alterar níveis apenas dentro do seu CR
|
||||||
|
- Secretários de Setor podem alterar níveis apenas dentro do seu setor, transformando militantes em secretários
|
||||||
|
- Outros níveis não podem alterar níveis de outros usuários
|
||||||
|
|
||||||
|
## Responsabilidades
|
||||||
|
|
||||||
|
O sistema suporta as seguintes responsabilidades para militantes:
|
||||||
|
|
||||||
|
- Militante Básico (1)
|
||||||
|
- Secretário de Célula (2)
|
||||||
|
- Secretário de Setor (4)
|
||||||
|
- Secretário de CR (8)
|
||||||
|
- Secretário de CC (16)
|
||||||
|
- Secretário Geral (32)
|
||||||
|
- Quadro-Orientador (64)
|
||||||
|
- Responsável de Finanças (256)
|
||||||
|
- Responsável de Imprensa (512)
|
||||||
|
|
||||||
|
### Status de Aspirante
|
||||||
|
|
||||||
|
Todo novo militante começa como Aspirante. Este status tem as seguintes características:
|
||||||
|
|
||||||
|
1. **Duração Mínima**: O status de Aspirante deve ser mantido por pelo menos 3 meses após a integração do militante.
|
||||||
|
|
||||||
|
2. **Avaliação Obrigatória**: Para remover o status de Aspirante, é necessário:
|
||||||
|
- Ter passado o período mínimo de 3 meses
|
||||||
|
- Registrar uma avaliação detalhada da atuação do militante durante este período
|
||||||
|
|
||||||
|
3. **Quem pode Avaliar**: A avaliação e remoção do status de Aspirante pode ser feita por:
|
||||||
|
- Secretário Geral
|
||||||
|
- Secretário de Organização
|
||||||
|
- Secretários de CR (para militantes de seu CR)
|
||||||
|
- Secretários de Setor (para militantes de seu setor)
|
||||||
|
|
||||||
|
4. **Registro da Avaliação**: A avaliação deve incluir:
|
||||||
|
- Análise da participação do militante nas atividades
|
||||||
|
- Desenvolvimento político e organizativo
|
||||||
|
- Pontos fortes e aspectos a melhorar
|
||||||
|
- Recomendações para o desenvolvimento futuro
|
||||||
|
|
||||||
|
5. **Histórico**: O sistema mantém registro de:
|
||||||
|
- Data de início do período como Aspirante
|
||||||
|
- Data da avaliação
|
||||||
|
- Texto completo da avaliação
|
||||||
|
|
||||||
|
O Quadro-Orientador é uma responsabilidade especial que pode ser atribuída a militantes em qualquer nível hierárquico, incluindo membros de CR e CC. Esta responsabilidade indica que o militante tem a função de orientar e apoiar outros militantes em sua formação política e organizativa.
|
||||||
|
|
||||||
|
A atribuição da responsabilidade de Quadro-Orientador pode ser feita por:
|
||||||
|
- Secretário Geral
|
||||||
|
- Secretário de Organização
|
||||||
|
- Secretários de CR (para militantes de seu CR)
|
||||||
|
- Secretários de Setor (para militantes de seu setor)
|
||||||
|
|
||||||
|
### Responsáveis de Finanças e Imprensa
|
||||||
|
|
||||||
|
Cada instância (Célula, Setor, CR e CC) possui três responsáveis:
|
||||||
|
|
||||||
|
1. **Responsável Geral**: Obrigatório para todas as instâncias. É o principal responsável pela instância.
|
||||||
|
|
||||||
|
2. **Responsável de Finanças**: Opcional. Responsável por:
|
||||||
|
- Controle financeiro da instância
|
||||||
|
- Arrecadação de contribuições
|
||||||
|
- Prestação de contas
|
||||||
|
- Planejamento financeiro
|
||||||
|
|
||||||
|
3. **Responsável de Imprensa**: Opcional. Responsável por:
|
||||||
|
- Comunicação externa da instância
|
||||||
|
- Produção de materiais de divulgação
|
||||||
|
- Gestão de redes sociais
|
||||||
|
- Relacionamento com a mídia
|
||||||
|
|
||||||
|
Os responsáveis de finanças e imprensa são designados pelo responsável geral da instância, com aprovação da instância superior.
|
||||||
|
|
||||||
|
## Hierarquia de Instâncias
|
||||||
|
|
||||||
|
1. **Comitê Central (CC)**
|
||||||
|
- Instância máxima da organização
|
||||||
|
- Possui responsável geral, de finanças e de imprensa
|
||||||
|
- Coordena todos os CRs
|
||||||
|
|
||||||
|
2. **Comitê Regional (CR)**
|
||||||
|
- Subordinado ao CC
|
||||||
|
- Possui responsável geral, de finanças e de imprensa
|
||||||
|
- Coordena os setores da sua região
|
||||||
|
|
||||||
|
3. **Setor**
|
||||||
|
- Subordinado ao CR
|
||||||
|
- Possui responsável geral, de finanças e de imprensa
|
||||||
|
- Coordena as células do seu setor
|
||||||
|
|
||||||
|
4. **Célula**
|
||||||
|
- Subordinada ao Setor
|
||||||
|
- Possui responsável geral, de finanças e de imprensa
|
||||||
|
- Unidade básica de organização
|
||||||
|
|
||||||
|
## Permissões
|
||||||
|
|
||||||
|
As permissões no sistema são baseadas nas responsabilidades do militante e na hierarquia das instâncias:
|
||||||
|
|
||||||
|
1. **Visualização**
|
||||||
|
- Militantes básicos veem apenas sua célula
|
||||||
|
- Secretários de célula veem sua célula
|
||||||
|
- Secretários de setor veem seu setor e células
|
||||||
|
- Secretários de CR veem seu CR, setores e células
|
||||||
|
- Secretários de CC veem todos os dados
|
||||||
|
|
||||||
|
2. **Edição**
|
||||||
|
- Cada nível pode gerenciar apenas os níveis abaixo
|
||||||
|
- Responsáveis de finanças e imprensa podem editar apenas suas áreas
|
||||||
|
- Quadros-Orientadores podem avaliar militantes
|
||||||
|
|
||||||
|
3. **Responsabilidades**
|
||||||
|
- Apenas o nível superior pode atribuir responsabilidades
|
||||||
|
- Responsáveis de finanças e imprensa são designados pelo responsável geral
|
||||||
|
- O status de Quadro-Orientador segue regras específicas
|
||||||
239
docs/rbac.md
Normal file
239
docs/rbac.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# Sistema de Permissões RBAC (Role-Based Access Control)
|
||||||
|
|
||||||
|
## Níveis de Permissão
|
||||||
|
|
||||||
|
O sistema de permissões é hierárquico, onde cada nível herda as permissões do nível anterior. A hierarquia é a seguinte (do menor para o maior nível):
|
||||||
|
|
||||||
|
### 1. Militante Básico
|
||||||
|
- Acesso apenas aos seus próprios dados
|
||||||
|
- Visualização de sua célula
|
||||||
|
- Sem permissões administrativas
|
||||||
|
|
||||||
|
### 2. Secretário de Célula
|
||||||
|
- Todas as permissões do Militante Básico
|
||||||
|
- Gerenciamento de militantes da sua célula
|
||||||
|
- Visualização de dados da célula
|
||||||
|
- Cadastro de novos militantes na célula
|
||||||
|
|
||||||
|
### 3. Membro de Setor
|
||||||
|
- Todas as permissões do Secretário de Célula
|
||||||
|
- Visualização de dados de todas as células do setor
|
||||||
|
- Acesso a relatórios do setor
|
||||||
|
|
||||||
|
### 4. Secretário de Setor
|
||||||
|
- Todas as permissões do Membro de Setor
|
||||||
|
- Gerenciamento de todas as células do setor
|
||||||
|
- Criação de novas células no setor
|
||||||
|
- Geração de relatórios do setor
|
||||||
|
- Gerenciamento de militantes do setor
|
||||||
|
|
||||||
|
### 5. Membro de CR (Comitê Regional)
|
||||||
|
- Todas as permissões do Secretário de Setor
|
||||||
|
- Visualização de dados de todos os setores do CR
|
||||||
|
- Acesso a relatórios do CR
|
||||||
|
|
||||||
|
### 6. Secretário de CR
|
||||||
|
- Todas as permissões do Membro de CR
|
||||||
|
- Gerenciamento de todos os setores do CR
|
||||||
|
- Criação de novos setores no CR
|
||||||
|
- Geração de relatórios do CR
|
||||||
|
- Gerenciamento de militantes do CR
|
||||||
|
|
||||||
|
### 7. Membro do CC (Comitê Central)
|
||||||
|
- Todas as permissões do Secretário de CR
|
||||||
|
- Visualização de dados de todos os CRs
|
||||||
|
- Acesso a relatórios nacionais
|
||||||
|
|
||||||
|
### 8. Secretário Geral / Secretário de Organização do CC
|
||||||
|
- Todas as permissões do Membro do CC
|
||||||
|
- Gerenciamento de todos os CRs
|
||||||
|
- Criação de novos CRs
|
||||||
|
- Geração de relatórios nacionais
|
||||||
|
- Gerenciamento de todos os militantes
|
||||||
|
- Configurações do sistema
|
||||||
|
|
||||||
|
## Implementação Técnica
|
||||||
|
|
||||||
|
O sistema RBAC é implementado através de:
|
||||||
|
|
||||||
|
1. **Roles**: Definem os níveis de acesso
|
||||||
|
2. **Permissions**: Definem as ações permitidas
|
||||||
|
3. **Role-Permission Mapping**: Mapeia quais permissões cada role possui
|
||||||
|
4. **User-Role Assignment**: Atribui roles aos usuários
|
||||||
|
|
||||||
|
### Estrutura do Banco de Dados
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Roles
|
||||||
|
CREATE TABLE roles (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
nome VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
nivel INTEGER NOT NULL,
|
||||||
|
descricao TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Permissions
|
||||||
|
CREATE TABLE permissions (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
nome VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
descricao TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Role-Permission Mapping
|
||||||
|
CREATE TABLE role_permissions (
|
||||||
|
role_id INTEGER,
|
||||||
|
permission_id INTEGER,
|
||||||
|
PRIMARY KEY (role_id, permission_id),
|
||||||
|
FOREIGN KEY (role_id) REFERENCES roles(id),
|
||||||
|
FOREIGN KEY (permission_id) REFERENCES permissions(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- User-Role Assignment
|
||||||
|
CREATE TABLE user_roles (
|
||||||
|
user_id INTEGER,
|
||||||
|
role_id INTEGER,
|
||||||
|
PRIMARY KEY (user_id, role_id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||||
|
FOREIGN KEY (role_id) REFERENCES roles(id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exemplos de Permissões
|
||||||
|
|
||||||
|
### Permissões Básicas
|
||||||
|
- `view_own_data`: Visualizar seus próprios dados
|
||||||
|
- `edit_own_data`: Editar seus próprios dados
|
||||||
|
- `view_cell_data`: Visualizar dados da célula
|
||||||
|
|
||||||
|
### Permissões de Célula
|
||||||
|
- `manage_cell_members`: Gerenciar membros da célula
|
||||||
|
- `create_cell_member`: Criar novos membros na célula
|
||||||
|
- `view_cell_reports`: Visualizar relatórios da célula
|
||||||
|
|
||||||
|
### Permissões de Setor
|
||||||
|
- `manage_sector_cells`: Gerenciar células do setor
|
||||||
|
- `create_sector_cell`: Criar novas células no setor
|
||||||
|
- `view_sector_reports`: Visualizar relatórios do setor
|
||||||
|
|
||||||
|
### Permissões de CR
|
||||||
|
- `manage_cr_sectors`: Gerenciar setores do CR
|
||||||
|
- `create_cr_sector`: Criar novos setores no CR
|
||||||
|
- `view_cr_reports`: Visualizar relatórios do CR
|
||||||
|
|
||||||
|
### Permissões de CC
|
||||||
|
- `manage_cc_crs`: Gerenciar CRs
|
||||||
|
- `create_cc_cr`: Criar novos CRs
|
||||||
|
- `view_cc_reports`: Visualizar relatórios nacionais
|
||||||
|
- `system_config`: Configurar o sistema
|
||||||
|
|
||||||
|
## Uso no Código
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Verificar permissão
|
||||||
|
if user.has_permission('manage_cell_members'):
|
||||||
|
# Permitir ação
|
||||||
|
|
||||||
|
# Verificar nível
|
||||||
|
if user.has_role_level(3): # Membro de Setor
|
||||||
|
# Permitir ação
|
||||||
|
|
||||||
|
# Verificar hierarquia
|
||||||
|
if user.is_higher_or_equal_than(other_user):
|
||||||
|
# Permitir ação
|
||||||
|
```
|
||||||
|
|
||||||
|
# Controle de Acesso Baseado em Funções (RBAC)
|
||||||
|
|
||||||
|
## Estrutura Hierárquica
|
||||||
|
|
||||||
|
O sistema possui uma estrutura hierárquica com os seguintes níveis:
|
||||||
|
- Célula (base)
|
||||||
|
- Setor (agrupa células)
|
||||||
|
- Comitê Regional - CR (agrupa setores)
|
||||||
|
- Comitê Central - CC (único, agrupa CRs)
|
||||||
|
|
||||||
|
## Regras de Associação
|
||||||
|
|
||||||
|
- Cada militante pertence a apenas uma célula
|
||||||
|
- Cada célula pertence a apenas um setor
|
||||||
|
- Cada setor pertence a apenas um CR
|
||||||
|
- Existe apenas um Comitê Central (CC)
|
||||||
|
|
||||||
|
## Permissões por Instância
|
||||||
|
|
||||||
|
### Célula
|
||||||
|
- **Secretário(a)**:
|
||||||
|
- `MANAGE_CELL_MEMBERS`: Gerenciar membros da célula
|
||||||
|
- `VIEW_CELL_DATA`: Visualizar dados da célula
|
||||||
|
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
|
||||||
|
- `REGISTER_CELL_PAYMENT`: Registrar pagamentos da célula
|
||||||
|
|
||||||
|
- **Tesoureiro(a)**:
|
||||||
|
- `VIEW_CELL_DATA`: Visualizar dados da célula
|
||||||
|
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
|
||||||
|
- `REGISTER_CELL_PAYMENT`: Registrar pagamentos da célula
|
||||||
|
|
||||||
|
- **Militante**:
|
||||||
|
- `VIEW_OWN_DATA`: Visualizar apenas seus próprios dados
|
||||||
|
|
||||||
|
### Setor
|
||||||
|
- **Secretário(a)**:
|
||||||
|
- `MANAGE_SECTOR_CELLS`: Gerenciar células do setor
|
||||||
|
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
|
||||||
|
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor
|
||||||
|
|
||||||
|
- **Tesoureiro(a)**:
|
||||||
|
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
|
||||||
|
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor
|
||||||
|
|
||||||
|
### CR
|
||||||
|
- **Secretário(a)**:
|
||||||
|
- `MANAGE_CR_SECTORS`: Gerenciar setores do CR
|
||||||
|
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
|
||||||
|
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR
|
||||||
|
|
||||||
|
- **Tesoureiro(a)**:
|
||||||
|
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
|
||||||
|
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR
|
||||||
|
|
||||||
|
### CC
|
||||||
|
- **Secretário(a)**:
|
||||||
|
- `MANAGE_CC_CRS`: Gerenciar CRs
|
||||||
|
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
|
||||||
|
- `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC
|
||||||
|
- `SYSTEM_CONFIG`: Configurar o sistema
|
||||||
|
|
||||||
|
- **Tesoureiro(a)**:
|
||||||
|
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
|
||||||
|
- `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC
|
||||||
|
|
||||||
|
## Regras de Acesso a Dados
|
||||||
|
|
||||||
|
1. **Visualização de Dados**:
|
||||||
|
- Militantes podem ver apenas seus próprios dados
|
||||||
|
- Secretários e tesoureiros podem ver dados de sua instância
|
||||||
|
- O CC tem acesso a todos os dados
|
||||||
|
|
||||||
|
2. **Registro de Pagamentos**:
|
||||||
|
- Apenas tesoureiros e secretários podem registrar pagamentos
|
||||||
|
- O registro é restrito à instância do usuário
|
||||||
|
- O CC pode registrar pagamentos em qualquer nível
|
||||||
|
|
||||||
|
## Implementação Técnica
|
||||||
|
|
||||||
|
O controle de acesso é implementado através de:
|
||||||
|
|
||||||
|
1. **Decorators**:
|
||||||
|
- `@require_login`: Verifica se o usuário está logado
|
||||||
|
- `@require_permission`: Verifica se o usuário tem uma permissão específica
|
||||||
|
- `@require_instance_permission`: Verifica permissão em uma instância específica
|
||||||
|
- `@require_instance_access`: Verifica acesso a uma instância específica
|
||||||
|
|
||||||
|
2. **Verificações de Acesso**:
|
||||||
|
- Cada rota verifica as permissões necessárias
|
||||||
|
- O acesso é negado se o usuário não tiver as permissões requeridas
|
||||||
|
- Mensagens de erro são exibidas para o usuário
|
||||||
|
|
||||||
|
3. **Filtragem de Dados**:
|
||||||
|
- As consultas ao banco de dados são filtradas baseadas nas permissões
|
||||||
|
- Cada nível hierárquico tem suas próprias regras de acesso
|
||||||
21
functions/base.py
Normal file
21
functions/base.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Configuração do banco de dados
|
||||||
|
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///database.db')
|
||||||
|
engine = create_engine(DATABASE_URL)
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
|
|
||||||
|
# Base declarativa do SQLAlchemy
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
def get_db_connection():
|
||||||
|
"""Retorna uma nova sessão do banco de dados"""
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
return session
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
raise e
|
||||||
@@ -1,39 +1,47 @@
|
|||||||
from sqlalchemy import create_engine, Column, Integer, String, Boolean, Numeric, Date, ForeignKey, DateTime, Text
|
from datetime import datetime, timedelta
|
||||||
from sqlalchemy.orm import relationship, sessionmaker
|
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
import pyotp
|
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum
|
||||||
|
from sqlalchemy.orm import sessionmaker, relationship, backref
|
||||||
import os
|
import os
|
||||||
|
import pyotp
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from sqlalchemy.pool import NullPool
|
from sqlalchemy.pool import NullPool
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import secrets
|
import secrets
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
import enum
|
||||||
|
from flask_login import UserMixin
|
||||||
|
from .rbac import Role, Permission, role_permissions, user_roles
|
||||||
|
from .base import Base, engine, Session
|
||||||
|
|
||||||
# Configurar caminho do banco de dados
|
# Configurar caminho do banco de dados
|
||||||
db_dir = Path.home() / '.local' / 'share' / 'controles'
|
db_dir = Path.home() / '.local' / 'share' / 'controles'
|
||||||
db_dir.mkdir(parents=True, exist_ok=True)
|
db_dir.mkdir(parents=True, exist_ok=True)
|
||||||
db_path = db_dir / 'database.db'
|
db_path = db_dir / 'database.db'
|
||||||
|
|
||||||
# Configurar engine com NullPool
|
|
||||||
engine = create_engine(
|
|
||||||
f'sqlite:///{db_path}',
|
|
||||||
echo=True,
|
|
||||||
poolclass=NullPool # Usar NullPool ao invés do pool padrão
|
|
||||||
)
|
|
||||||
|
|
||||||
Base = declarative_base()
|
|
||||||
SessionLocal = sessionmaker(bind=engine)
|
SessionLocal = sessionmaker(bind=engine)
|
||||||
|
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
"""
|
"""
|
||||||
Retorna uma nova sessão do banco de dados SQLite
|
Retorna uma nova sessão do banco de dados SQLite e verifica timeout
|
||||||
"""
|
"""
|
||||||
|
session = SessionLocal()
|
||||||
try:
|
try:
|
||||||
return SessionLocal()
|
# Verificar timeout para usuários logados
|
||||||
finally:
|
usuario_atual = session.query(Usuario).filter(
|
||||||
engine.dispose()
|
Usuario.ultimo_login.isnot(None),
|
||||||
|
Usuario.ultimo_logout.is_(None)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if usuario_atual and usuario_atual.check_session_timeout():
|
||||||
|
usuario_atual.logout()
|
||||||
|
session.commit()
|
||||||
|
raise Exception("Sessão expirada. Por favor, faça login novamente.")
|
||||||
|
|
||||||
|
return session
|
||||||
|
except Exception as e:
|
||||||
|
session.close()
|
||||||
|
raise e
|
||||||
|
|
||||||
def execute_query(query, params=None):
|
def execute_query(query, params=None):
|
||||||
"""
|
"""
|
||||||
@@ -68,6 +76,7 @@ class Celula(Base):
|
|||||||
secretario_rel = relationship("Militante", foreign_keys=[secretario])
|
secretario_rel = relationship("Militante", foreign_keys=[secretario])
|
||||||
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
|
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
|
||||||
pagamentos = relationship("PagamentoCelula", back_populates="celula")
|
pagamentos = relationship("PagamentoCelula", back_populates="celula")
|
||||||
|
usuarios = relationship("Usuario", back_populates="celula")
|
||||||
|
|
||||||
class ComiteRegional(Base):
|
class ComiteRegional(Base):
|
||||||
__tablename__ = 'comites_regionais'
|
__tablename__ = 'comites_regionais'
|
||||||
@@ -86,6 +95,7 @@ class ComiteRegional(Base):
|
|||||||
correspondente_jornal_rel = relationship("Militante", foreign_keys=[correspondente_jornal])
|
correspondente_jornal_rel = relationship("Militante", foreign_keys=[correspondente_jornal])
|
||||||
setores = relationship("Setor", back_populates="cr")
|
setores = relationship("Setor", back_populates="cr")
|
||||||
celulas = relationship("Celula", back_populates="cr")
|
celulas = relationship("Celula", back_populates="cr")
|
||||||
|
usuarios = relationship("Usuario", back_populates="cr")
|
||||||
|
|
||||||
class EmailMilitante(Base):
|
class EmailMilitante(Base):
|
||||||
__tablename__ = 'emails_militantes'
|
__tablename__ = 'emails_militantes'
|
||||||
@@ -159,6 +169,13 @@ class Militante(Base):
|
|||||||
otp_secret = Column(String(32))
|
otp_secret = Column(String(32))
|
||||||
temp_token = Column(String(64))
|
temp_token = Column(String(64))
|
||||||
temp_token_expiry = Column(DateTime)
|
temp_token_expiry = Column(DateTime)
|
||||||
|
# Novo campo para Quadro-Orientador
|
||||||
|
quadro_orientador = Column(Boolean, default=False)
|
||||||
|
# Campos para Aspirante
|
||||||
|
aspirante = Column(Boolean, default=True) # Por padrão, todo novo militante é aspirante
|
||||||
|
data_inicio_aspirante = Column(DateTime, default=datetime.utcnow)
|
||||||
|
avaliacao_aspirante = Column(Text)
|
||||||
|
data_avaliacao_aspirante = Column(DateTime)
|
||||||
|
|
||||||
# Relacionamentos existentes
|
# Relacionamentos existentes
|
||||||
cotas_mensais = relationship("CotaMensal", back_populates="militante")
|
cotas_mensais = relationship("CotaMensal", back_populates="militante")
|
||||||
@@ -175,6 +192,10 @@ class Militante(Base):
|
|||||||
MNS = 8
|
MNS = 8
|
||||||
MPS = 16
|
MPS = 16
|
||||||
JUVENTUDE = 32
|
JUVENTUDE = 32
|
||||||
|
QUADRO_ORIENTADOR = 64
|
||||||
|
ASPIRANTE = 128
|
||||||
|
RESPONSAVEL_FINANCAS = 256
|
||||||
|
RESPONSAVEL_IMPRENSA = 512
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_responsabilidades_list():
|
def get_responsabilidades_list():
|
||||||
@@ -184,7 +205,11 @@ class Militante(Base):
|
|||||||
(Militante.IMPRENSA, "Imprensa"),
|
(Militante.IMPRENSA, "Imprensa"),
|
||||||
(Militante.MNS, "MNS"),
|
(Militante.MNS, "MNS"),
|
||||||
(Militante.MPS, "MPS"),
|
(Militante.MPS, "MPS"),
|
||||||
(Militante.JUVENTUDE, "Juventude")
|
(Militante.JUVENTUDE, "Juventude"),
|
||||||
|
(Militante.QUADRO_ORIENTADOR, "Quadro-Orientador"),
|
||||||
|
(Militante.ASPIRANTE, "Aspirante"),
|
||||||
|
(Militante.RESPONSAVEL_FINANCAS, "Responsável de Finanças"),
|
||||||
|
(Militante.RESPONSAVEL_IMPRENSA, "Responsável de Imprensa")
|
||||||
]
|
]
|
||||||
|
|
||||||
def set_responsabilidades(self, resp_list):
|
def set_responsabilidades(self, resp_list):
|
||||||
@@ -243,6 +268,26 @@ class Militante(Base):
|
|||||||
|
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|
||||||
|
def generate_username(self):
|
||||||
|
"""Gera um nome de usuário único baseado no primeiro nome e um código"""
|
||||||
|
from sqlalchemy import func
|
||||||
|
db = get_db_connection()
|
||||||
|
try:
|
||||||
|
# Pega o primeiro nome
|
||||||
|
primeiro_nome = self.nome.split()[0].lower()
|
||||||
|
|
||||||
|
# Conta quantos usuários já existem com esse prefixo
|
||||||
|
count = db.query(func.count(Usuario.id)).filter(
|
||||||
|
Usuario.username.like(f"{primeiro_nome}%")
|
||||||
|
).scalar()
|
||||||
|
|
||||||
|
# Gera o código (número sequencial)
|
||||||
|
codigo = str(count + 1).zfill(3)
|
||||||
|
|
||||||
|
return f"{primeiro_nome}{codigo}"
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
class CotaMensal(Base):
|
class CotaMensal(Base):
|
||||||
__tablename__ = 'cotas_mensais'
|
__tablename__ = 'cotas_mensais'
|
||||||
|
|
||||||
@@ -375,10 +420,16 @@ class RelatorioVendasMateriais(Base):
|
|||||||
setor = relationship("Setor", back_populates="relatorios_vendas")
|
setor = relationship("Setor", back_populates="relatorios_vendas")
|
||||||
comite = relationship("ComiteCentral", back_populates="relatorios_vendas")
|
comite = relationship("ComiteCentral", back_populates="relatorios_vendas")
|
||||||
|
|
||||||
class Usuario(Base):
|
class TipoUsuario(enum.Enum):
|
||||||
|
ADMIN = "admin"
|
||||||
|
CR_RESPONSAVEL = "cr_responsavel"
|
||||||
|
SETOR_RESPONSAVEL = "setor_responsavel"
|
||||||
|
USUARIO = "usuario"
|
||||||
|
|
||||||
|
class Usuario(Base, UserMixin):
|
||||||
__tablename__ = 'usuarios'
|
__tablename__ = 'usuarios'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True)
|
||||||
username = Column(String(50), unique=True, nullable=False)
|
username = Column(String(50), unique=True, nullable=False)
|
||||||
password_hash = Column(String(255), nullable=False)
|
password_hash = Column(String(255), nullable=False)
|
||||||
email = Column(String(100), unique=True, nullable=False)
|
email = Column(String(100), unique=True, nullable=False)
|
||||||
@@ -392,74 +443,111 @@ class Usuario(Base):
|
|||||||
motivo_logout = Column(String(100))
|
motivo_logout = Column(String(100))
|
||||||
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
|
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
|
||||||
celula_id = Column(Integer, ForeignKey('celulas.id'))
|
celula_id = Column(Integer, ForeignKey('celulas.id'))
|
||||||
|
session_timeout = Column(Integer, default=30)
|
||||||
|
tipo = Column(String(17), nullable=False)
|
||||||
|
ultima_atividade = Column(DateTime, default=datetime.utcnow)
|
||||||
|
# Relacionamento com militante
|
||||||
|
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||||
|
militante = relationship("Militante", backref=backref("usuario", uselist=False))
|
||||||
|
|
||||||
role = relationship("Role", back_populates="usuarios")
|
# Relacionamentos
|
||||||
setor = relationship("Setor", back_populates="usuarios")
|
roles = relationship("Role", secondary="user_roles", back_populates="users")
|
||||||
celula = relationship("Celula")
|
setor = relationship('Setor', back_populates='usuarios')
|
||||||
cr = relationship("ComiteRegional")
|
cr = relationship('ComiteRegional', back_populates='usuarios')
|
||||||
|
celula = relationship('Celula', back_populates='usuarios')
|
||||||
|
|
||||||
def __init__(self, username, password, is_admin=False):
|
def get_id(self):
|
||||||
|
return str(self.id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
return self.ativo
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_anonymous(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __init__(self, username, password, is_admin=False, email=None, tipo="USUARIO"):
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password_hash = generate_password_hash(password)
|
self.password_hash = generate_password_hash(password)
|
||||||
self.is_admin = is_admin
|
self.is_admin = is_admin
|
||||||
self.otp_secret = pyotp.random_base32() # Gerar segredo OTP na criação
|
self.email = email
|
||||||
self.ativo = True
|
self.ativo = True
|
||||||
|
self.session_timeout = 30
|
||||||
|
self.tipo = tipo
|
||||||
|
self.ultima_atividade = datetime.utcnow()
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password_hash, password)
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
def verify_otp(self, otp_code):
|
def update_last_activity(self):
|
||||||
"""Verifica se o código OTP fornecido é válido"""
|
self.ultima_atividade = datetime.utcnow()
|
||||||
if not self.otp_secret:
|
|
||||||
print(f"Erro: Usuário {self.username} não tem segredo OTP configurado")
|
def is_session_expired(self):
|
||||||
return False
|
if not self.ultima_atividade:
|
||||||
|
return True
|
||||||
totp = pyotp.TOTP(self.otp_secret)
|
time_diff = datetime.utcnow() - self.ultima_atividade
|
||||||
is_valid = totp.verify(otp_code)
|
return time_diff.total_seconds() > (self.session_timeout * 60)
|
||||||
print(f"Verificando OTP para {self.username}")
|
|
||||||
print(f"Segredo: {self.otp_secret}")
|
def check_session_timeout(self):
|
||||||
print(f"Código fornecido: {otp_code}")
|
"""Verifica se a sessão do usuário expirou"""
|
||||||
print(f"Resultado: {'válido' if is_valid else 'inválido'}")
|
if not self.ultima_atividade:
|
||||||
return is_valid
|
return True
|
||||||
|
time_diff = datetime.utcnow() - self.ultima_atividade
|
||||||
|
return time_diff.total_seconds() > (self.session_timeout * 60)
|
||||||
|
|
||||||
|
def has_permission(self, permission_name):
|
||||||
|
"""Verifica se o usuário tem uma determinada permissão"""
|
||||||
|
for role in self.roles:
|
||||||
|
for permission in role.permissions:
|
||||||
|
if permission.nome == permission_name:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_role(self, role_nivel):
|
||||||
|
"""Verifica se o usuário tem um determinado nível de role"""
|
||||||
|
for role in self.roles:
|
||||||
|
if role.nivel == role_nivel:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def get_otp_uri(self):
|
def get_otp_uri(self):
|
||||||
"""Gera a URI para o QR code do OTP"""
|
"""Gera a URI para autenticação em duas etapas"""
|
||||||
if not self.otp_secret:
|
if not self.otp_secret:
|
||||||
self.otp_secret = pyotp.random_base32()
|
self.otp_secret = pyotp.random_base32()
|
||||||
|
return pyotp.totp.TOTP(self.otp_secret).provisioning_uri(
|
||||||
totp = pyotp.TOTP(self.otp_secret)
|
self.username,
|
||||||
return totp.provisioning_uri(
|
|
||||||
name=self.username,
|
|
||||||
issuer_name="Sistema de Controles"
|
issuer_name="Sistema de Controles"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Role(Base):
|
def verify_otp(self, code):
|
||||||
__tablename__ = 'roles'
|
"""Verifica se um código OTP é válido"""
|
||||||
|
if not self.otp_secret:
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
print(f"Erro: OTP secret não configurado para o usuário {self.username}")
|
||||||
nome = Column(String(50), unique=True, nullable=False)
|
return False
|
||||||
nivel = Column(Integer, nullable=False) # Nível hierárquico (1: admin, 2: coordenador, 3: militante)
|
|
||||||
|
print(f"Verificando OTP para usuário {self.username}")
|
||||||
usuarios = relationship("Usuario", back_populates="role")
|
print(f"OTP Secret: {self.otp_secret}")
|
||||||
permissoes = relationship("RolePermissao", back_populates="role")
|
print(f"Código fornecido: {code}")
|
||||||
|
|
||||||
|
totp = pyotp.totp.TOTP(self.otp_secret)
|
||||||
|
is_valid = totp.verify(code)
|
||||||
|
|
||||||
|
print(f"Resultado da verificação: {'Válido' if is_valid else 'Inválido'}")
|
||||||
|
print(f"Tempo atual: {datetime.utcnow()}")
|
||||||
|
print(f"Período atual: {totp.timecode(datetime.utcnow())}")
|
||||||
|
|
||||||
|
return is_valid
|
||||||
|
|
||||||
class Permissao(Base):
|
def logout(self):
|
||||||
__tablename__ = 'permissoes'
|
"""Registra o logout do usuário"""
|
||||||
|
self.ultimo_logout = datetime.utcnow()
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
self.motivo_logout = "Logout manual"
|
||||||
nome = Column(String(50), unique=True, nullable=False)
|
self.ultima_atividade = None
|
||||||
descricao = Column(String(255))
|
|
||||||
|
|
||||||
roles = relationship("RolePermissao", back_populates="permissao")
|
|
||||||
|
|
||||||
class RolePermissao(Base):
|
|
||||||
__tablename__ = 'roles_permissoes'
|
|
||||||
|
|
||||||
role_id = Column(Integer, ForeignKey('roles.id'), primary_key=True)
|
|
||||||
permissao_id = Column(Integer, ForeignKey('permissoes.id'), primary_key=True)
|
|
||||||
|
|
||||||
role = relationship("Role", back_populates="permissoes")
|
|
||||||
permissao = relationship("Permissao", back_populates="roles")
|
|
||||||
|
|
||||||
class PagamentoCelula(Base):
|
class PagamentoCelula(Base):
|
||||||
__tablename__ = 'pagamentos_celula'
|
__tablename__ = 'pagamentos_celula'
|
||||||
@@ -537,12 +625,9 @@ class TransacaoPIX(Base):
|
|||||||
if os.path.exists(db_path):
|
if os.path.exists(db_path):
|
||||||
os.remove(db_path)
|
os.remove(db_path)
|
||||||
|
|
||||||
def init_database():
|
def init_rbac():
|
||||||
"""Inicializa o banco de dados com dados básicos"""
|
"""Inicializa o sistema RBAC"""
|
||||||
print("Inicializando banco de dados...")
|
print("Inicializando sistema RBAC...")
|
||||||
|
|
||||||
# Criar todas as tabelas
|
|
||||||
Base.metadata.create_all(engine)
|
|
||||||
|
|
||||||
session = SessionLocal()
|
session = SessionLocal()
|
||||||
try:
|
try:
|
||||||
@@ -554,7 +639,7 @@ def init_database():
|
|||||||
# Criar role de admin
|
# Criar role de admin
|
||||||
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||||
if not admin_role:
|
if not admin_role:
|
||||||
admin_role = Role(nome="Administrador", nivel=1)
|
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
|
||||||
session.add(admin_role)
|
session.add(admin_role)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@@ -568,6 +653,11 @@ def init_database():
|
|||||||
admin.email = "admin@example.com"
|
admin.email = "admin@example.com"
|
||||||
admin.role_id = admin_role.id
|
admin.role_id = admin_role.id
|
||||||
|
|
||||||
|
# Adicionar apenas a permissão de system_config ao admin
|
||||||
|
permission = session.query(Permission).filter_by(nome='system_config').first()
|
||||||
|
if permission and permission not in admin_role.permissions:
|
||||||
|
admin_role.permissions.append(permission)
|
||||||
|
|
||||||
session.add(admin)
|
session.add(admin)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@@ -578,7 +668,100 @@ def init_database():
|
|||||||
print(f"OTP Secret: {admin.otp_secret}")
|
print(f"OTP Secret: {admin.otp_secret}")
|
||||||
else:
|
else:
|
||||||
print("Usuário admin já existe")
|
print("Usuário admin já existe")
|
||||||
|
# Garantir que o admin tenha apenas a permissão de system_config
|
||||||
|
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||||
|
if admin_role:
|
||||||
|
# Remover todas as permissões atuais
|
||||||
|
admin_role.permissions = []
|
||||||
|
|
||||||
|
# Adicionar apenas a permissão de system_config
|
||||||
|
permission = session.query(Permission).filter_by(nome='system_config').first()
|
||||||
|
if permission:
|
||||||
|
admin_role.permissions.append(permission)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro na inicialização do sistema RBAC: {e}")
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def init_database():
|
||||||
|
"""Inicializa o banco de dados com dados básicos"""
|
||||||
|
print("Inicializando banco de dados...")
|
||||||
|
|
||||||
|
# Criar todas as tabelas
|
||||||
|
Base.metadata.drop_all(engine) # Remover todas as tabelas existentes
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
session = SessionLocal()
|
||||||
|
try:
|
||||||
|
# Criar role de administrador
|
||||||
|
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
|
||||||
|
session.add(admin_role)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Verificar se existe um QR code salvo
|
||||||
|
qr_path = Path('admin_qr.png')
|
||||||
|
admin_otp_secret = None
|
||||||
|
|
||||||
|
if qr_path.exists():
|
||||||
|
# Extrair o segredo OTP do nome do arquivo temporário dentro do QR
|
||||||
|
try:
|
||||||
|
import re
|
||||||
|
with open('admin_qr.txt', 'r') as f:
|
||||||
|
qr_content = f.read()
|
||||||
|
# O segredo OTP está no formato otpauth://totp/admin?secret=XXXXX&issuer=Sistema%20de%20Controles
|
||||||
|
match = re.search(r'secret=([A-Z0-9]+)&', qr_content)
|
||||||
|
if match:
|
||||||
|
admin_otp_secret = match.group(1)
|
||||||
|
print(f"Usando OTP existente: {admin_otp_secret}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao ler OTP existente: {e}")
|
||||||
|
|
||||||
|
if not admin_otp_secret:
|
||||||
|
admin_otp_secret = pyotp.random_base32()
|
||||||
|
print(f"Novo OTP gerado: {admin_otp_secret}")
|
||||||
|
|
||||||
|
# Criar usuário admin
|
||||||
|
admin = Usuario(
|
||||||
|
username="admin",
|
||||||
|
password="admin123",
|
||||||
|
is_admin=True,
|
||||||
|
email="admin@example.com",
|
||||||
|
tipo="ADMIN"
|
||||||
|
)
|
||||||
|
admin.role_id = admin_role.id
|
||||||
|
admin.otp_secret = admin_otp_secret
|
||||||
|
session.add(admin)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Gerar novo QR code se não existir
|
||||||
|
if not qr_path.exists():
|
||||||
|
totp = pyotp.totp.TOTP(admin_otp_secret)
|
||||||
|
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles")
|
||||||
|
|
||||||
|
# Salvar a URI em um arquivo texto para referência futura
|
||||||
|
with open('admin_qr.txt', 'w') as f:
|
||||||
|
f.write(provisioning_uri)
|
||||||
|
|
||||||
|
# Gerar QR code
|
||||||
|
import qrcode
|
||||||
|
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||||
|
qr.add_data(provisioning_uri)
|
||||||
|
qr.make(fit=True)
|
||||||
|
img = qr.make_image(fill_color="black", back_color="white")
|
||||||
|
img.save('admin_qr.png')
|
||||||
|
|
||||||
|
print("=== Usuário Admin Criado ===")
|
||||||
|
print(f"Username: admin")
|
||||||
|
print(f"Senha: admin123")
|
||||||
|
print(f"Email: {admin.email}")
|
||||||
|
print(f"OTP Secret: {admin.otp_secret}")
|
||||||
|
print(f"QR Code: {qr_path}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Erro na inicialização do banco: {e}")
|
print(f"Erro na inicialização do banco: {e}")
|
||||||
session.rollback()
|
session.rollback()
|
||||||
@@ -586,6 +769,9 @@ def init_database():
|
|||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
# Inicializar o sistema RBAC
|
||||||
|
init_rbac()
|
||||||
|
|
||||||
# Inicializar o banco de dados automaticamente quando o módulo for importado
|
# Inicializar o banco de dados automaticamente quando o módulo for importado
|
||||||
init_database()
|
init_database()
|
||||||
|
|
||||||
|
|||||||
197
functions/decorators.py
Normal file
197
functions/decorators.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from flask import session, redirect, url_for, flash
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
from .database import get_db_connection, Usuario
|
||||||
|
from .rbac import Permission
|
||||||
|
|
||||||
|
def require_login(f):
|
||||||
|
"""Decorador para verificar se o usuário está logado"""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
flash('Você precisa estar logado para acessar esta página.', 'error')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db_connection()
|
||||||
|
try:
|
||||||
|
# Carregar o usuário com suas roles
|
||||||
|
user = db.query(Usuario).options(
|
||||||
|
joinedload(Usuario.roles)
|
||||||
|
).get(current_user.id)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
flash('Usuário não encontrado.', 'error')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
# Atualiza timestamp da última atividade
|
||||||
|
user.update_last_activity()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
def require_permission(permission_name):
|
||||||
|
"""Decorador para verificar se o usuário tem uma permissão específica"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
flash('Você precisa estar logado para acessar esta página.', 'error')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db_connection()
|
||||||
|
try:
|
||||||
|
user = db.query(Usuario).get(current_user.id)
|
||||||
|
if not user or not user.has_permission(permission_name):
|
||||||
|
flash('Você não tem permissão para acessar esta página.', 'error')
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
# Atualiza timestamp da última atividade
|
||||||
|
user.update_last_activity()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def require_role(role_name):
|
||||||
|
"""Decorador para verificar se o usuário tem um papel específico"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
flash('Você precisa estar logado para acessar esta página.', 'error')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db_connection()
|
||||||
|
try:
|
||||||
|
user = db.query(Usuario).get(current_user.id)
|
||||||
|
if not user or not user.has_role(role_name):
|
||||||
|
flash('Você não tem permissão para acessar esta página.', 'error')
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
# Atualiza timestamp da última atividade
|
||||||
|
user.update_last_activity()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def require_minimum_role(min_level):
|
||||||
|
"""Decorador para verificar se o usuário tem um papel com nível mínimo"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
flash('Você precisa estar logado para acessar esta página.', 'error')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db_connection()
|
||||||
|
try:
|
||||||
|
user = db.query(Usuario).get(current_user.id)
|
||||||
|
if not user:
|
||||||
|
flash('Usuário não encontrado.', 'error')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
highest_role = user.get_highest_role()
|
||||||
|
if not highest_role or highest_role.nivel < min_level:
|
||||||
|
flash('Você não tem permissão para acessar esta página.', 'error')
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
# Atualiza timestamp da última atividade
|
||||||
|
user.update_last_activity()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def require_instance_permission(permission_name):
|
||||||
|
"""Decorator para verificar se o usuário tem permissão em uma instância específica"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
flash('Você precisa estar logado para acessar esta página.', 'error')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db_connection()
|
||||||
|
try:
|
||||||
|
user = db.query(Usuario).get(session['user_id'])
|
||||||
|
if not user:
|
||||||
|
flash('Usuário não encontrado.', 'error')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
# Verificar se o usuário tem a permissão em alguma instância
|
||||||
|
if not (user.has_permission(permission_name) or
|
||||||
|
user.has_permission(f"{permission_name}_sector") or
|
||||||
|
user.has_permission(f"{permission_name}_cr") or
|
||||||
|
user.has_permission(f"{permission_name}_cc")):
|
||||||
|
flash('Você não tem permissão para acessar esta página.', 'error')
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
# Atualiza timestamp da última atividade
|
||||||
|
user.update_last_activity()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def require_instance_access(instance_type, instance_id):
|
||||||
|
"""Decorator para verificar se o usuário tem acesso a uma instância específica"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
flash('Você precisa estar logado para acessar esta página.', 'error')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db_connection()
|
||||||
|
try:
|
||||||
|
user = db.query(Usuario).get(session['user_id'])
|
||||||
|
if not user:
|
||||||
|
flash('Usuário não encontrado.', 'error')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
# Verificar acesso baseado na instância do usuário
|
||||||
|
if instance_type == 'celula':
|
||||||
|
if not (user.celula_id == instance_id or
|
||||||
|
user.has_permission(Permission.VIEW_SECTOR_REPORTS) or
|
||||||
|
user.has_permission(Permission.VIEW_CR_REPORTS) or
|
||||||
|
user.has_permission(Permission.VIEW_CC_REPORTS)):
|
||||||
|
flash('Você não tem acesso a esta célula.', 'error')
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
elif instance_type == 'setor':
|
||||||
|
if not (user.setor_id == instance_id or
|
||||||
|
user.has_permission(Permission.VIEW_CR_REPORTS) or
|
||||||
|
user.has_permission(Permission.VIEW_CC_REPORTS)):
|
||||||
|
flash('Você não tem acesso a este setor.', 'error')
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
elif instance_type == 'cr':
|
||||||
|
if not (user.cr_id == instance_id or
|
||||||
|
user.has_permission(Permission.VIEW_CC_REPORTS)):
|
||||||
|
flash('Você não tem acesso a este CR.', 'error')
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
# Atualiza timestamp da última atividade
|
||||||
|
user.update_last_activity()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
222
functions/permissions.py
Normal file
222
functions/permissions.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from flask import abort, g
|
||||||
|
from .database import Militante, Celula, Setor, CR, CC
|
||||||
|
|
||||||
|
def check_permission(permission_func):
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not permission_func(*args, **kwargs):
|
||||||
|
abort(403)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def can_manage_militante(militante_id):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar um militante específico."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
militante = Militante.query.get(militante_id)
|
||||||
|
if not militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Secretário Geral e Secretário de Organização podem gerenciar qualquer militante
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CC pode gerenciar militantes do seu CC
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||||
|
if militante.celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CR pode gerenciar militantes do seu CR
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
|
||||||
|
if militante.celula.setor.cr_id == g.user.militante.celula.setor.cr_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de Setor pode gerenciar militantes do seu setor
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR:
|
||||||
|
if militante.celula.setor_id == g.user.militante.celula.setor_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de Célula pode gerenciar militantes da sua célula
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CELULA:
|
||||||
|
if militante.celula_id == g.user.militante.celula_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_manage_celula(celula_id):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar uma célula específica."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
celula = Celula.query.get(celula_id)
|
||||||
|
if not celula:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Secretário Geral e Secretário de Organização podem gerenciar qualquer célula
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CC pode gerenciar células do seu CC
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||||
|
if celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CR pode gerenciar células do seu CR
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
|
||||||
|
if celula.setor.cr_id == g.user.militante.celula.setor.cr_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de Setor pode gerenciar células do seu setor
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR:
|
||||||
|
if celula.setor_id == g.user.militante.celula.setor_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_manage_setor(setor_id):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar um setor específico."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
setor = Setor.query.get(setor_id)
|
||||||
|
if not setor:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Secretário Geral e Secretário de Organização podem gerenciar qualquer setor
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CC pode gerenciar setores do seu CC
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||||
|
if setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CR pode gerenciar setores do seu CR
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
|
||||||
|
if setor.cr_id == g.user.militante.celula.setor.cr_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_manage_cr(cr_id):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar um CR específico."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
cr = CR.query.get(cr_id)
|
||||||
|
if not cr:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Secretário Geral e Secretário de Organização podem gerenciar qualquer CR
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CC pode gerenciar CRs do seu CC
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||||
|
if cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_manage_cc(cc_id):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar um CC específico."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Apenas Secretário Geral e Secretário de Organização podem gerenciar CCs
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_manage_financas(instancia_id, tipo_instancia):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar finanças de uma instância específica."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Secretário Geral e Secretário de Organização podem gerenciar finanças de qualquer instância
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Responsável de Finanças da instância pode gerenciar suas finanças
|
||||||
|
if tipo_instancia == 'celula':
|
||||||
|
celula = Celula.query.get(instancia_id)
|
||||||
|
if celula and celula.responsavel_financas_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
elif tipo_instancia == 'setor':
|
||||||
|
setor = Setor.query.get(instancia_id)
|
||||||
|
if setor and setor.responsavel_financas_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
elif tipo_instancia == 'cr':
|
||||||
|
cr = CR.query.get(instancia_id)
|
||||||
|
if cr and cr.responsavel_financas_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
elif tipo_instancia == 'cc':
|
||||||
|
cc = CC.query.get(instancia_id)
|
||||||
|
if cc and cc.responsavel_financas_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_manage_imprensa(instancia_id, tipo_instancia):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar imprensa de uma instância específica."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Secretário Geral e Secretário de Organização podem gerenciar imprensa de qualquer instância
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Responsável de Imprensa da instância pode gerenciar sua imprensa
|
||||||
|
if tipo_instancia == 'celula':
|
||||||
|
celula = Celula.query.get(instancia_id)
|
||||||
|
if celula and celula.responsavel_imprensa_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
elif tipo_instancia == 'setor':
|
||||||
|
setor = Setor.query.get(instancia_id)
|
||||||
|
if setor and setor.responsavel_imprensa_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
elif tipo_instancia == 'cr':
|
||||||
|
cr = CR.query.get(instancia_id)
|
||||||
|
if cr and cr.responsavel_imprensa_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
elif tipo_instancia == 'cc':
|
||||||
|
cc = CC.query.get(instancia_id)
|
||||||
|
if cc and cc.responsavel_imprensa_id == g.user.militante.id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_manage_responsabilidades(militante_id):
|
||||||
|
"""Verifica se o usuário atual pode gerenciar responsabilidades de um militante específico."""
|
||||||
|
if not g.user or not g.user.militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
militante = Militante.query.get(militante_id)
|
||||||
|
if not militante:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Secretário Geral e Secretário de Organização podem gerenciar responsabilidades de qualquer militante
|
||||||
|
if g.user.militante.responsabilidades & (Militante.SECRETARIO_GERAL | Militante.SECRETARIO_ORGANIZACAO):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CC pode gerenciar responsabilidades de militantes do seu CC
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CC:
|
||||||
|
if militante.celula.setor.cr.cc_id == g.user.militante.celula.setor.cr.cc_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de CR pode gerenciar responsabilidades de militantes do seu CR
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_CR:
|
||||||
|
if militante.celula.setor.cr_id == g.user.militante.celula.setor.cr_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Secretário de Setor pode gerenciar responsabilidades de militantes do seu setor
|
||||||
|
if g.user.militante.responsabilidades & Militante.SECRETARIO_SETOR:
|
||||||
|
if militante.celula.setor_id == g.user.militante.celula.setor_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
292
functions/rbac.py
Normal file
292
functions/rbac.py
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, Text, ForeignKey, Table
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from .base import Base
|
||||||
|
|
||||||
|
# Tabela de mapeamento Role-Permission
|
||||||
|
role_permissions = Table(
|
||||||
|
'role_permissions',
|
||||||
|
Base.metadata,
|
||||||
|
Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True),
|
||||||
|
Column('permission_id', Integer, ForeignKey('permissions.id'), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tabela de mapeamento User-Role
|
||||||
|
user_roles = Table(
|
||||||
|
'user_roles',
|
||||||
|
Base.metadata,
|
||||||
|
Column('user_id', Integer, ForeignKey('usuarios.id'), primary_key=True),
|
||||||
|
Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
class Role(Base):
|
||||||
|
__tablename__ = 'roles'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
nome = Column(String(50), unique=True, nullable=False)
|
||||||
|
nivel = Column(Integer, nullable=False) # Nível hierárquico
|
||||||
|
descricao = Column(Text)
|
||||||
|
|
||||||
|
# Relacionamentos
|
||||||
|
permissions = relationship("Permission", secondary=role_permissions, back_populates="roles")
|
||||||
|
users = relationship("Usuario", secondary=user_roles, back_populates="roles")
|
||||||
|
|
||||||
|
# Níveis de role
|
||||||
|
MILITANTE_BASICO = 1
|
||||||
|
SECRETARIO_CELULA = 2
|
||||||
|
MEMBRO_SETOR = 3
|
||||||
|
SECRETARIO_SETOR = 4
|
||||||
|
MEMBRO_CR = 5
|
||||||
|
SECRETARIO_CR = 6
|
||||||
|
MEMBRO_CC = 7
|
||||||
|
SECRETARIO_GERAL = 8
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_roles_list():
|
||||||
|
return [
|
||||||
|
(Role.MILITANTE_BASICO, "Militante Básico"),
|
||||||
|
(Role.SECRETARIO_CELULA, "Secretário de Célula"),
|
||||||
|
(Role.MEMBRO_SETOR, "Membro de Setor"),
|
||||||
|
(Role.SECRETARIO_SETOR, "Secretário de Setor"),
|
||||||
|
(Role.MEMBRO_CR, "Membro de CR"),
|
||||||
|
(Role.SECRETARIO_CR, "Secretário de CR"),
|
||||||
|
(Role.MEMBRO_CC, "Membro do CC"),
|
||||||
|
(Role.SECRETARIO_GERAL, "Secretário Geral")
|
||||||
|
]
|
||||||
|
|
||||||
|
class Permission(Base):
|
||||||
|
__tablename__ = 'permissions'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
nome = Column(String(50), unique=True, nullable=False)
|
||||||
|
descricao = Column(Text)
|
||||||
|
|
||||||
|
# Relacionamentos
|
||||||
|
roles = relationship("Role", secondary=role_permissions, back_populates="permissions")
|
||||||
|
|
||||||
|
# Permissões básicas
|
||||||
|
VIEW_OWN_DATA = "view_own_data"
|
||||||
|
EDIT_OWN_DATA = "edit_own_data"
|
||||||
|
VIEW_CELL_DATA = "view_cell_data"
|
||||||
|
CREATE_MILITANT = "create_militant" # Nova permissão para criar militantes
|
||||||
|
|
||||||
|
# Permissões de célula
|
||||||
|
MANAGE_CELL_MEMBERS = "manage_cell_members"
|
||||||
|
CREATE_CELL_MEMBER = "create_cell_member"
|
||||||
|
VIEW_CELL_REPORTS = "view_cell_reports"
|
||||||
|
REGISTER_CELL_PAYMENT = "register_cell_payment"
|
||||||
|
|
||||||
|
# Permissões de setor
|
||||||
|
MANAGE_SECTOR_CELLS = "manage_sector_cells"
|
||||||
|
CREATE_SECTOR_CELL = "create_sector_cell"
|
||||||
|
VIEW_SECTOR_REPORTS = "view_sector_reports"
|
||||||
|
REGISTER_SECTOR_PAYMENT = "register_sector_payment"
|
||||||
|
|
||||||
|
# Permissões de CR
|
||||||
|
MANAGE_CR_SECTORS = "manage_cr_sectors"
|
||||||
|
CREATE_CR_SECTOR = "create_cr_sector"
|
||||||
|
VIEW_CR_REPORTS = "view_cr_reports"
|
||||||
|
REGISTER_CR_PAYMENT = "register_cr_payment"
|
||||||
|
|
||||||
|
# Permissões de CC
|
||||||
|
MANAGE_CC_CRS = "manage_cc_crs"
|
||||||
|
CREATE_CC_CR = "create_cc_cr"
|
||||||
|
VIEW_CC_REPORTS = "view_cc_reports"
|
||||||
|
REGISTER_CC_PAYMENT = "register_cc_payment"
|
||||||
|
SYSTEM_CONFIG = "system_config"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_permissions_list():
|
||||||
|
return [
|
||||||
|
# Permissões básicas
|
||||||
|
(Permission.VIEW_OWN_DATA, "Visualizar próprios dados"),
|
||||||
|
(Permission.EDIT_OWN_DATA, "Editar próprios dados"),
|
||||||
|
(Permission.VIEW_CELL_DATA, "Visualizar dados da célula"),
|
||||||
|
(Permission.CREATE_MILITANT, "Criar novos militantes"), # Nova permissão
|
||||||
|
|
||||||
|
# Permissões de célula
|
||||||
|
(Permission.MANAGE_CELL_MEMBERS, "Gerenciar membros da célula"),
|
||||||
|
(Permission.CREATE_CELL_MEMBER, "Criar membros na célula"),
|
||||||
|
(Permission.VIEW_CELL_REPORTS, "Visualizar relatórios da célula"),
|
||||||
|
(Permission.REGISTER_CELL_PAYMENT, "Registrar pagamentos da célula"),
|
||||||
|
|
||||||
|
# Permissões de setor
|
||||||
|
(Permission.MANAGE_SECTOR_CELLS, "Gerenciar células do setor"),
|
||||||
|
(Permission.CREATE_SECTOR_CELL, "Criar células no setor"),
|
||||||
|
(Permission.VIEW_SECTOR_REPORTS, "Visualizar relatórios do setor"),
|
||||||
|
(Permission.REGISTER_SECTOR_PAYMENT, "Registrar pagamentos do setor"),
|
||||||
|
|
||||||
|
# Permissões de CR
|
||||||
|
(Permission.MANAGE_CR_SECTORS, "Gerenciar setores do CR"),
|
||||||
|
(Permission.CREATE_CR_SECTOR, "Criar setores no CR"),
|
||||||
|
(Permission.VIEW_CR_REPORTS, "Visualizar relatórios do CR"),
|
||||||
|
(Permission.REGISTER_CR_PAYMENT, "Registrar pagamentos do CR"),
|
||||||
|
|
||||||
|
# Permissões de CC
|
||||||
|
(Permission.MANAGE_CC_CRS, "Gerenciar CRs"),
|
||||||
|
(Permission.CREATE_CC_CR, "Criar CRs"),
|
||||||
|
(Permission.VIEW_CC_REPORTS, "Visualizar relatórios nacionais"),
|
||||||
|
(Permission.REGISTER_CC_PAYMENT, "Registrar pagamentos nacionais"),
|
||||||
|
(Permission.SYSTEM_CONFIG, "Configurar sistema")
|
||||||
|
]
|
||||||
|
|
||||||
|
def init_rbac():
|
||||||
|
"""Inicializa o sistema RBAC com roles e permissões básicas"""
|
||||||
|
from .database import get_db_connection
|
||||||
|
session = get_db_connection()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Criar roles se não existirem
|
||||||
|
for nivel, nome in Role.get_roles_list():
|
||||||
|
role = session.query(Role).filter_by(nivel=nivel).first()
|
||||||
|
if not role:
|
||||||
|
role = Role(nome=nome, nivel=nivel)
|
||||||
|
session.add(role)
|
||||||
|
|
||||||
|
# Criar permissões se não existirem
|
||||||
|
for nome, descricao in Permission.get_permissions_list():
|
||||||
|
permission = session.query(Permission).filter_by(nome=nome).first()
|
||||||
|
if not permission:
|
||||||
|
permission = Permission(nome=nome, descricao=descricao)
|
||||||
|
session.add(permission)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Mapear permissões para roles
|
||||||
|
for role in session.query(Role).all():
|
||||||
|
# Militante Básico
|
||||||
|
if role.nivel == Role.MILITANTE_BASICO:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Secretário de Célula
|
||||||
|
elif role.nivel == Role.SECRETARIO_CELULA:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Membro de Setor
|
||||||
|
elif role.nivel == Role.MEMBRO_SETOR:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Secretário de Setor
|
||||||
|
elif role.nivel == Role.SECRETARIO_SETOR:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Membro de CR
|
||||||
|
elif role.nivel == Role.MEMBRO_CR:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Secretário de CR
|
||||||
|
elif role.nivel == Role.SECRETARIO_CR:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Membro do CC
|
||||||
|
elif role.nivel == Role.MEMBRO_CC:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Secretário Geral
|
||||||
|
elif role.nivel == Role.SECRETARIO_GERAL:
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.MANAGE_CC_CRS).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.CREATE_CC_CR).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(),
|
||||||
|
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Administrador
|
||||||
|
elif role.nome == "Administrador":
|
||||||
|
role.permissions = [
|
||||||
|
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first()
|
||||||
|
]
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao inicializar RBAC: {e}")
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
58
init_system.py
Normal file
58
init_system.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from create_admin import create_admin
|
||||||
|
from create_test_users import create_test_users
|
||||||
|
from functions.database import get_db_connection, Usuario
|
||||||
|
from functions.rbac import Role
|
||||||
|
|
||||||
|
def init_system():
|
||||||
|
print("=== Inicializando Sistema ===")
|
||||||
|
|
||||||
|
# Criar admin
|
||||||
|
print("\nCriando usuário admin...")
|
||||||
|
create_admin()
|
||||||
|
|
||||||
|
# Criar usuários de teste
|
||||||
|
print("\nCriando usuários de teste...")
|
||||||
|
create_test_users()
|
||||||
|
|
||||||
|
# Verificar configuração
|
||||||
|
print("\n=== Verificando Configuração ===")
|
||||||
|
session = get_db_connection()
|
||||||
|
try:
|
||||||
|
# Verificar admin
|
||||||
|
admin = session.query(Usuario).filter_by(username='admin').first()
|
||||||
|
if admin:
|
||||||
|
print("Admin: OK")
|
||||||
|
print(f"OTP configurado: {'Sim' if admin.otp_secret else 'Não'}")
|
||||||
|
else:
|
||||||
|
print("Admin: FALHOU")
|
||||||
|
|
||||||
|
# Verificar usuários de teste
|
||||||
|
test_users = ['aligner', 'tester', 'deployer']
|
||||||
|
for username in test_users:
|
||||||
|
user = session.query(Usuario).filter_by(username=username).first()
|
||||||
|
if user:
|
||||||
|
print(f"{username}: OK")
|
||||||
|
print(f"OTP configurado: {'Sim' if user.otp_secret else 'Não'}")
|
||||||
|
else:
|
||||||
|
print(f"{username}: FALHOU")
|
||||||
|
|
||||||
|
print("\n=== Instruções ===")
|
||||||
|
print("1. Use o aplicativo autenticador para configurar o OTP de cada usuário")
|
||||||
|
print("2. Faça login com cada usuário para testar")
|
||||||
|
print("3. Altere a senha no primeiro login")
|
||||||
|
print("\nCredenciais:")
|
||||||
|
print("Admin:")
|
||||||
|
print(" Usuário: admin")
|
||||||
|
print(" Senha: admin123")
|
||||||
|
print("\nUsuários de teste:")
|
||||||
|
print(" Usuário: aligner, tester, deployer")
|
||||||
|
print(" Senha: Test123!@#")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao verificar configuração: {str(e)}")
|
||||||
|
session.rollback()
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_system()
|
||||||
64
migrations/versions/add_responsaveis_financas_imprensa.py
Normal file
64
migrations/versions/add_responsaveis_financas_imprensa.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"""add_responsaveis_financas_imprensa
|
||||||
|
|
||||||
|
Revision ID: add_responsaveis_financas_imprensa
|
||||||
|
Revises: add_aspirante_fields
|
||||||
|
Create Date: 2024-03-19 10:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_responsaveis_financas_imprensa'
|
||||||
|
down_revision = 'add_aspirante_fields'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Adicionar colunas na tabela celulas
|
||||||
|
op.add_column('celulas', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
|
||||||
|
op.add_column('celulas', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key('fk_celulas_responsavel_financas', 'celulas', 'militantes', ['responsavel_financas_id'], ['id'])
|
||||||
|
op.create_foreign_key('fk_celulas_responsavel_imprensa', 'celulas', 'militantes', ['responsavel_imprensa_id'], ['id'])
|
||||||
|
|
||||||
|
# Adicionar colunas na tabela setores
|
||||||
|
op.add_column('setores', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
|
||||||
|
op.add_column('setores', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key('fk_setores_responsavel_financas', 'setores', 'militantes', ['responsavel_financas_id'], ['id'])
|
||||||
|
op.create_foreign_key('fk_setores_responsavel_imprensa', 'setores', 'militantes', ['responsavel_imprensa_id'], ['id'])
|
||||||
|
|
||||||
|
# Adicionar colunas na tabela crs
|
||||||
|
op.add_column('crs', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
|
||||||
|
op.add_column('crs', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key('fk_crs_responsavel_financas', 'crs', 'militantes', ['responsavel_financas_id'], ['id'])
|
||||||
|
op.create_foreign_key('fk_crs_responsavel_imprensa', 'crs', 'militantes', ['responsavel_imprensa_id'], ['id'])
|
||||||
|
|
||||||
|
# Adicionar colunas na tabela ccs
|
||||||
|
op.add_column('ccs', sa.Column('responsavel_financas_id', sa.Integer(), nullable=True))
|
||||||
|
op.add_column('ccs', sa.Column('responsavel_imprensa_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key('fk_ccs_responsavel_financas', 'ccs', 'militantes', ['responsavel_financas_id'], ['id'])
|
||||||
|
op.create_foreign_key('fk_ccs_responsavel_imprensa', 'ccs', 'militantes', ['responsavel_imprensa_id'], ['id'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# Remover foreign keys
|
||||||
|
op.drop_constraint('fk_celulas_responsavel_financas', 'celulas', type_='foreignkey')
|
||||||
|
op.drop_constraint('fk_celulas_responsavel_imprensa', 'celulas', type_='foreignkey')
|
||||||
|
op.drop_constraint('fk_setores_responsavel_financas', 'setores', type_='foreignkey')
|
||||||
|
op.drop_constraint('fk_setores_responsavel_imprensa', 'setores', type_='foreignkey')
|
||||||
|
op.drop_constraint('fk_crs_responsavel_financas', 'crs', type_='foreignkey')
|
||||||
|
op.drop_constraint('fk_crs_responsavel_imprensa', 'crs', type_='foreignkey')
|
||||||
|
op.drop_constraint('fk_ccs_responsavel_financas', 'ccs', type_='foreignkey')
|
||||||
|
op.drop_constraint('fk_ccs_responsavel_imprensa', 'ccs', type_='foreignkey')
|
||||||
|
|
||||||
|
# Remover colunas
|
||||||
|
op.drop_column('celulas', 'responsavel_financas_id')
|
||||||
|
op.drop_column('celulas', 'responsavel_imprensa_id')
|
||||||
|
op.drop_column('setores', 'responsavel_financas_id')
|
||||||
|
op.drop_column('setores', 'responsavel_imprensa_id')
|
||||||
|
op.drop_column('crs', 'responsavel_financas_id')
|
||||||
|
op.drop_column('crs', 'responsavel_imprensa_id')
|
||||||
|
op.drop_column('ccs', 'responsavel_financas_id')
|
||||||
|
op.drop_column('ccs', 'responsavel_imprensa_id')
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
Flask==3.0.2
|
Flask==3.0.2
|
||||||
Flask-SQLAlchemy==3.1.1
|
Flask-SQLAlchemy==3.1.1
|
||||||
SQLAlchemy==2.0.39
|
Flask-Login==0.6.3
|
||||||
|
Flask-WTF==1.2.1
|
||||||
|
Flask-Mail==0.9.1
|
||||||
|
SQLAlchemy==2.0.27
|
||||||
Werkzeug==3.0.1
|
Werkzeug==3.0.1
|
||||||
|
python-dotenv==1.0.1
|
||||||
pyotp==2.9.0
|
pyotp==2.9.0
|
||||||
qrcode==7.4.2
|
qrcode==7.4.2
|
||||||
pillow==11.0.0
|
Pillow==10.2.0
|
||||||
python-dotenv==1.0.1
|
|
||||||
flask-login==0.6.3
|
|
||||||
flask-wtf==1.2.1
|
|
||||||
email-validator==2.1.0.post1
|
email-validator==2.1.0.post1
|
||||||
Bootstrap-Flask==2.4.1
|
cryptography==42.0.2
|
||||||
|
bcrypt==4.1.2
|
||||||
|
Bootstrap-Flask==2.3.3
|
||||||
flask-bootstrap5==0.1.dev1
|
flask-bootstrap5==0.1.dev1
|
||||||
flask-mail
|
|
||||||
|
|||||||
18
setup.py
Normal file
18
setup.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="controles",
|
||||||
|
version="0.1.0",
|
||||||
|
packages=find_packages(),
|
||||||
|
install_requires=[
|
||||||
|
"fastapi",
|
||||||
|
"uvicorn",
|
||||||
|
"sqlalchemy",
|
||||||
|
"python-jose[cryptography]",
|
||||||
|
"passlib[bcrypt]",
|
||||||
|
"python-multipart",
|
||||||
|
"qrcode",
|
||||||
|
"pillow",
|
||||||
|
"python-dotenv"
|
||||||
|
],
|
||||||
|
)
|
||||||
66
sql/migrate_db.py
Normal file
66
sql/migrate_db.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Adiciona o diretório raiz ao PYTHONPATH
|
||||||
|
root_dir = str(Path(__file__).parent.parent)
|
||||||
|
sys.path.append(root_dir)
|
||||||
|
|
||||||
|
from functions.base import Base, engine
|
||||||
|
from functions.database import init_database
|
||||||
|
from functions.rbac import init_rbac
|
||||||
|
|
||||||
|
def execute_sql_file(file_path):
|
||||||
|
"""Executa um arquivo SQL"""
|
||||||
|
print(f"Executando arquivo {file_path}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r') as sql_file:
|
||||||
|
sql_commands = sql_file.read().split(';')
|
||||||
|
|
||||||
|
conn = sqlite3.connect('database.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
for command in sql_commands:
|
||||||
|
command = command.strip()
|
||||||
|
if command:
|
||||||
|
try:
|
||||||
|
cursor.execute(command)
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "already exists" in str(e):
|
||||||
|
print(f"Aviso: {str(e)}")
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print(f"Arquivo {file_path} executado com sucesso!")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erro ao executar {file_path}: {str(e)}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def migrate_database():
|
||||||
|
"""Executa a migração do banco de dados"""
|
||||||
|
print("Inicializando banco de dados...")
|
||||||
|
|
||||||
|
# Criar todas as tabelas
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
# Executar scripts SQL
|
||||||
|
sql_dir = Path(__file__).parent
|
||||||
|
rbac_tables_sql = sql_dir / 'rbac_tables.sql'
|
||||||
|
|
||||||
|
if rbac_tables_sql.exists():
|
||||||
|
execute_sql_file(rbac_tables_sql)
|
||||||
|
|
||||||
|
# Inicializar RBAC
|
||||||
|
init_rbac()
|
||||||
|
|
||||||
|
# Inicializar banco de dados
|
||||||
|
init_database()
|
||||||
|
|
||||||
|
print("Migração concluída com sucesso!")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
migrate_database()
|
||||||
47
sql/migrate_rbac.py
Normal file
47
sql/migrate_rbac.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from functions.database import get_db_connection, Usuario
|
||||||
|
from functions.rbac import Role, Permission
|
||||||
|
|
||||||
|
def migrate_existing_users():
|
||||||
|
"""Migra os usuários existentes para o novo sistema RBAC"""
|
||||||
|
session = get_db_connection()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Buscar todos os usuários
|
||||||
|
usuarios = session.query(Usuario).all()
|
||||||
|
|
||||||
|
# Buscar ou criar role de administrador
|
||||||
|
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||||
|
if not admin_role:
|
||||||
|
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
|
||||||
|
session.add(admin_role)
|
||||||
|
|
||||||
|
# Buscar ou criar role de militante básico
|
||||||
|
militante_role = session.query(Role).filter_by(nome="Militante Básico").first()
|
||||||
|
if not militante_role:
|
||||||
|
militante_role = Role(nome="Militante Básico", nivel=Role.MILITANTE_BASICO)
|
||||||
|
session.add(militante_role)
|
||||||
|
|
||||||
|
# Atualizar usuários
|
||||||
|
for usuario in usuarios:
|
||||||
|
# Se o usuário já tem roles, pular
|
||||||
|
if usuario.roles:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Atribuir role com base no is_admin
|
||||||
|
if usuario.is_admin:
|
||||||
|
usuario.roles.append(admin_role)
|
||||||
|
else:
|
||||||
|
usuario.roles.append(militante_role)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
print("Migração de usuários concluída com sucesso!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
print(f"Erro durante a migração de usuários: {str(e)}")
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
migrate_existing_users()
|
||||||
152
sql/rbac_tables.sql
Normal file
152
sql/rbac_tables.sql
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
-- Tabela de roles
|
||||||
|
CREATE TABLE IF NOT EXISTS roles (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
nome VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
nivel INTEGER NOT NULL,
|
||||||
|
descricao TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tabela de permissões
|
||||||
|
CREATE TABLE IF NOT EXISTS permissions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
nome VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
descricao TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tabela de mapeamento Role-Permission
|
||||||
|
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||||
|
role_id INTEGER NOT NULL,
|
||||||
|
permission_id INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (role_id, permission_id),
|
||||||
|
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tabela de mapeamento User-Role
|
||||||
|
CREATE TABLE IF NOT EXISTS user_roles (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
role_id INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (user_id, role_id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES usuarios(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Inserir roles básicas
|
||||||
|
INSERT OR IGNORE INTO roles (nome, nivel, descricao) VALUES
|
||||||
|
('Militante Básico', 1, 'Militante com permissões básicas'),
|
||||||
|
('Secretário de Célula', 2, 'Responsável por uma célula'),
|
||||||
|
('Membro de Setor', 3, 'Membro de um setor'),
|
||||||
|
('Secretário de Setor', 4, 'Responsável por um setor'),
|
||||||
|
('Membro de CR', 5, 'Membro de um Comitê Regional'),
|
||||||
|
('Secretário de CR', 6, 'Responsável por um Comitê Regional'),
|
||||||
|
('Membro do CC', 7, 'Membro do Comitê Central'),
|
||||||
|
('Secretário Geral', 8, 'Secretário Geral ou de Organização do CC');
|
||||||
|
|
||||||
|
-- Inserir permissões básicas
|
||||||
|
INSERT OR IGNORE INTO permissions (nome, descricao) VALUES
|
||||||
|
-- Permissões básicas
|
||||||
|
('view_own_data', 'Visualizar próprios dados'),
|
||||||
|
('edit_own_data', 'Editar próprios dados'),
|
||||||
|
('view_cell_data', 'Visualizar dados da célula'),
|
||||||
|
('create_militant', 'Criar novos militantes'),
|
||||||
|
|
||||||
|
-- Permissões de célula
|
||||||
|
('manage_cell_members', 'Gerenciar membros da célula'),
|
||||||
|
('create_cell_member', 'Criar membros na célula'),
|
||||||
|
('view_cell_reports', 'Visualizar relatórios da célula'),
|
||||||
|
|
||||||
|
-- Permissões de setor
|
||||||
|
('manage_sector_cells', 'Gerenciar células do setor'),
|
||||||
|
('create_sector_cell', 'Criar células no setor'),
|
||||||
|
('view_sector_reports', 'Visualizar relatórios do setor'),
|
||||||
|
|
||||||
|
-- Permissões de CR
|
||||||
|
('manage_cr_sectors', 'Gerenciar setores do CR'),
|
||||||
|
('create_cr_sector', 'Criar setores no CR'),
|
||||||
|
('view_cr_reports', 'Visualizar relatórios do CR'),
|
||||||
|
|
||||||
|
-- Permissões de CC
|
||||||
|
('manage_cc_crs', 'Gerenciar CRs'),
|
||||||
|
('create_cc_cr', 'Criar CRs'),
|
||||||
|
('view_cc_reports', 'Visualizar relatórios nacionais'),
|
||||||
|
('system_config', 'Configurar sistema');
|
||||||
|
|
||||||
|
-- Mapear permissões para roles
|
||||||
|
-- Militante Básico
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Militante Básico'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data');
|
||||||
|
|
||||||
|
-- Secretário de Célula
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Secretário de Célula'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||||
|
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||||
|
'create_militant');
|
||||||
|
|
||||||
|
-- Membro de Setor
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Membro de Setor'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||||
|
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||||
|
'view_sector_reports', 'create_militant');
|
||||||
|
|
||||||
|
-- Secretário de Setor
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Secretário de Setor'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||||
|
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||||
|
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||||
|
'create_militant');
|
||||||
|
|
||||||
|
-- Membro de CR
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Membro de CR'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||||
|
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||||
|
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||||
|
'view_cr_reports', 'create_militant');
|
||||||
|
|
||||||
|
-- Secretário de CR
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Secretário de CR'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||||
|
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||||
|
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||||
|
'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector',
|
||||||
|
'create_militant');
|
||||||
|
|
||||||
|
-- Membro do CC
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Membro do CC'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||||
|
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||||
|
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||||
|
'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector',
|
||||||
|
'view_cc_reports', 'create_militant');
|
||||||
|
|
||||||
|
-- Secretário Geral
|
||||||
|
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r, permissions p
|
||||||
|
WHERE r.nome = 'Secretário Geral'
|
||||||
|
AND p.nome IN ('view_own_data', 'edit_own_data', 'view_cell_data',
|
||||||
|
'manage_cell_members', 'create_cell_member', 'view_cell_reports',
|
||||||
|
'view_sector_reports', 'manage_sector_cells', 'create_sector_cell',
|
||||||
|
'view_cr_reports', 'manage_cr_sectors', 'create_cr_sector',
|
||||||
|
'view_cc_reports', 'manage_cc_crs', 'create_cc_cr',
|
||||||
|
'system_config', 'create_militant');
|
||||||
51
templates/alterar_senha.html
Normal file
51
templates/alterar_senha.html
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Alterar Senha{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Alterar Senha</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('alterar_senha') }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="senha_atual" class="form-label">Senha Atual</label>
|
||||||
|
<input type="password" class="form-control" id="senha_atual" name="senha_atual" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="nova_senha" class="form-label">Nova Senha</label>
|
||||||
|
<input type="password" class="form-control" id="nova_senha" name="nova_senha" required>
|
||||||
|
<small class="text-muted">
|
||||||
|
A senha deve ter no mínimo 8 caracteres e conter letras e números.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="confirmar_senha" class="form-label">Confirmar Nova Senha</label>
|
||||||
|
<input type="password" class="form-control" id="confirmar_senha" name="confirmar_senha" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Alterar Senha</button>
|
||||||
|
<a href="{{ url_for('home') }}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -65,42 +65,60 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav me-auto">
|
<ul class="navbar-nav me-auto">
|
||||||
<li class="nav-item">
|
{% if current_user is defined and current_user.is_authenticated %}
|
||||||
<a class="nav-link" href="{{ url_for('home') }}">Início</a>
|
{% if current_user.is_admin %}
|
||||||
</li>
|
<li class="nav-item">
|
||||||
<li class="nav-item">
|
<a class="nav-link" href="{{ url_for('dashboard_admin') }}">Dashboard Admin</a>
|
||||||
<a class="nav-link" href="{{ url_for('listar_militantes') }}">Militantes</a>
|
</li>
|
||||||
</li>
|
{% else %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('listar_pagamentos') }}">Pagamentos</a>
|
<a class="nav-link" href="{{ url_for('home') }}">Início</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{{ url_for('listar_materiais') }}">Materiais</a>
|
{% if current_user.has_permission('view_cell_data') %}
|
||||||
</li>
|
<li class="nav-item">
|
||||||
<li class="nav-item">
|
<a class="nav-link" href="{{ url_for('listar_militantes') }}">Militantes</a>
|
||||||
<a class="nav-link" href="{{ url_for('listar_relatorios_vendas') }}">Vendas</a>
|
</li>
|
||||||
</li>
|
{% endif %}
|
||||||
<li class="nav-item dropdown">
|
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
{% if current_user.has_permission('view_cell_reports') %}
|
||||||
Relatórios
|
<li class="nav-item">
|
||||||
</a>
|
<a class="nav-link" href="{{ url_for('listar_pagamentos') }}">Pagamentos</a>
|
||||||
<ul class="dropdown-menu">
|
</li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('listar_relatorios_cotas') }}">Relatórios de Cotas</a></li>
|
<li class="nav-item">
|
||||||
<li><a class="dropdown-item" href="{{ url_for('listar_relatorios_vendas') }}">Relatórios de Vendas</a></li>
|
<a class="nav-link" href="{{ url_for('listar_materiais') }}">Materiais</a>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
<li class="nav-item">
|
||||||
<li class="nav-item dropdown">
|
<a class="nav-link" href="{{ url_for('listar_relatorios_vendas') }}">Vendas</a>
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
</li>
|
||||||
Configurações
|
{% endif %}
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu">
|
{% if current_user.has_permission('view_cell_reports') or current_user.has_permission('view_sector_reports') or current_user.has_permission('view_cr_reports') %}
|
||||||
</ul>
|
<li class="nav-item dropdown">
|
||||||
</li>
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
||||||
|
Relatórios
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% if current_user.has_permission('view_cell_reports') %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('listar_relatorios_cotas') }}">Relatórios de Cotas</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('listar_relatorios_vendas') }}">Relatórios de Vendas</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
|
{% if current_user is defined and current_user.is_authenticated %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('logout') }}">Sair</a>
|
<a class="nav-link" href="{{ url_for('logout') }}">Sair</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,5 +129,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// Verificar status da sessão a cada 5 minutos
|
||||||
|
function checkSession() {
|
||||||
|
fetch('/check_session')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.expired) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Erro ao verificar sessão:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar a cada 5 minutos
|
||||||
|
setInterval(checkSession, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
// Verificar também quando a página ganha foco
|
||||||
|
document.addEventListener('visibilitychange', function() {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
checkSession();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
111
templates/criar_instancia.html
Normal file
111
templates/criar_instancia.html
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Criar {{ tipo_instancia }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1 class="mb-4">Criar {{ tipo_instancia }}</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" class="needs-validation" novalidate>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="nome" class="form-label">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="nome" name="nome" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira o nome da {{ tipo_instancia }}.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if tipo_instancia != 'Célula' %}
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="instancia_superior_id" class="form-label">{{ instancia_superior }}</label>
|
||||||
|
<select class="form-select" id="instancia_superior_id" name="instancia_superior_id" required>
|
||||||
|
<option value="">Selecione uma {{ instancia_superior }}</option>
|
||||||
|
{% for superior in instancias_superiores %}
|
||||||
|
<option value="{{ superior.id }}">{{ superior.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, selecione uma {{ instancia_superior }}.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel_geral_id" class="form-label">Responsável Geral</label>
|
||||||
|
<select class="form-select" id="responsavel_geral_id" name="responsavel_geral_id" required>
|
||||||
|
<option value="">Selecione o responsável geral</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, selecione o responsável geral.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel_financas_id" class="form-label">Responsável de Finanças</label>
|
||||||
|
<select class="form-select" id="responsavel_financas_id" name="responsavel_financas_id">
|
||||||
|
<option value="">Selecione o responsável de finanças</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel_imprensa_id" class="form-label">Responsável de Imprensa</label>
|
||||||
|
<select class="form-select" id="responsavel_imprensa_id" name="responsavel_imprensa_id">
|
||||||
|
<option value="">Selecione o responsável de imprensa</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary">Criar</button>
|
||||||
|
<a href="{{ url_for('listar_' + tipo_instancia.lower() + 's') }}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Validação do formulário
|
||||||
|
(function () {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var forms = document.querySelectorAll('.needs-validation')
|
||||||
|
|
||||||
|
Array.prototype.slice.call(forms)
|
||||||
|
.forEach(function (form) {
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
form.classList.add('was-validated')
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
106
templates/criar_militante.html
Normal file
106
templates/criar_militante.html
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Criar Militante{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1 class="mb-4">Criar Militante</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" class="needs-validation" novalidate>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="nome" class="form-label">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="nome" name="nome" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira o nome do militante.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="email" class="form-label">Email</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira um email válido.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="celula_id" class="form-label">Célula</label>
|
||||||
|
<select class="form-select" id="celula_id" name="celula_id" required>
|
||||||
|
<option value="">Selecione uma célula</option>
|
||||||
|
{% for celula in celulas %}
|
||||||
|
<option value="{{ celula.id }}">{{ celula.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, selecione uma célula.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 mb-3">
|
||||||
|
<label class="form-label">Responsabilidades</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="responsavel_financas" name="responsabilidades" value="{{ Militante.RESPONSAVEL_FINANCAS }}">
|
||||||
|
<label class="form-check-label" for="responsavel_financas">
|
||||||
|
Responsável de Finanças
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="responsavel_imprensa" name="responsabilidades" value="{{ Militante.RESPONSAVEL_IMPRENSA }}">
|
||||||
|
<label class="form-check-label" for="responsavel_imprensa">
|
||||||
|
Responsável de Imprensa
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="quadro_orientador" name="responsabilidades" value="{{ Militante.QUADRO_ORIENTADOR }}">
|
||||||
|
<label class="form-check-label" for="quadro_orientador">
|
||||||
|
Quadro-Orientador
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary">Criar</button>
|
||||||
|
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Validação do formulário
|
||||||
|
(function () {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var forms = document.querySelectorAll('.needs-validation')
|
||||||
|
|
||||||
|
Array.prototype.slice.call(forms)
|
||||||
|
.forEach(function (form) {
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
form.classList.add('was-validated')
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
284
templates/dashboard.html
Normal file
284
templates/dashboard.html
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard Administrativo{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="mb-4">Dashboard Administrativo</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Gerenciamento de Acessos</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Usuário</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Último Login</th>
|
||||||
|
<th>Nível</th>
|
||||||
|
<th>Responsabilidades</th>
|
||||||
|
<th>Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
{% if current_user.has_permission('system_config') or
|
||||||
|
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||||
|
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) or
|
||||||
|
(current_user.has_permission('manage_cell_members') and user.celula_id == current_user.celula_id) %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.id }}</td>
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td>{{ user.email }}</td>
|
||||||
|
<td>
|
||||||
|
{% if user.ativo %}
|
||||||
|
<span class="badge bg-success">Ativo</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">Inativo</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ user.ultimo_login.strftime('%d/%m/%Y %H:%M') if user.ultimo_login else 'Nunca' }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-info">{{ user.role }}</span>
|
||||||
|
{% if current_user.has_permission('system_config') or
|
||||||
|
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||||
|
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) %}
|
||||||
|
<select class="form-select form-select-sm d-inline-block w-auto" onchange="alterarNivel({{ user.id }}, this.value)">
|
||||||
|
<option value="">Alterar Nível</option>
|
||||||
|
{% if current_user.has_permission('system_config') %}
|
||||||
|
<option value="militante_basico">Militante Básico</option>
|
||||||
|
<option value="secretario_celula">Secretário de Célula</option>
|
||||||
|
<option value="membro_setor">Membro de Setor</option>
|
||||||
|
<option value="secretario_setor">Secretário de Setor</option>
|
||||||
|
<option value="membro_cr">Membro de CR</option>
|
||||||
|
<option value="secretario_cr">Secretário de CR</option>
|
||||||
|
<option value="membro_cc">Membro do CC</option>
|
||||||
|
<option value="secretario_geral">Secretário Geral</option>
|
||||||
|
{% elif current_user.has_permission('manage_cr_sectors') %}
|
||||||
|
<option value="membro_cr">Membro de CR</option>
|
||||||
|
<option value="secretario_cr">Secretário de CR</option>
|
||||||
|
{% elif current_user.has_permission('manage_sector_cells') %}
|
||||||
|
<option value="membro_setor">Membro de Setor</option>
|
||||||
|
<option value="secretario_setor">Secretário de Setor</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if user.militante %}
|
||||||
|
{% if user.militante.quadro_orientador %}
|
||||||
|
<span class="badge bg-primary">Quadro-Orientador</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.militante.aspirante %}
|
||||||
|
<span class="badge bg-warning">Aspirante</span>
|
||||||
|
<small class="text-muted">
|
||||||
|
(desde {{ user.militante.data_inicio_aspirante.strftime('%d/%m/%Y') }})
|
||||||
|
</small>
|
||||||
|
{% if user.militante.avaliacao_aspirante %}
|
||||||
|
<button type="button" class="btn btn-sm btn-info"
|
||||||
|
onclick="verAvaliacaoAspirante({{ user.id }})">
|
||||||
|
Ver Avaliação
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.has_permission('system_config') or
|
||||||
|
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||||
|
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) %}
|
||||||
|
{% if user.militante.quadro_orientador %}
|
||||||
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
|
onclick="toggleQuadroOrientador({{ user.id }}, {{ user.militante.quadro_orientador|lower }})">
|
||||||
|
Remover QO
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<button type="button" class="btn btn-sm btn-success"
|
||||||
|
onclick="toggleQuadroOrientador({{ user.id }}, {{ user.militante.quadro_orientador|lower }})">
|
||||||
|
Tornar QO
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.militante.aspirante %}
|
||||||
|
{% if datetime.utcnow() - user.militante.data_inicio_aspirante >= timedelta(days=90) %}
|
||||||
|
{% if not user.militante.avaliacao_aspirante %}
|
||||||
|
<button type="button" class="btn btn-sm btn-primary"
|
||||||
|
onclick="avaliarAspirante({{ user.id }})">
|
||||||
|
Avaliar Aspirante
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
|
onclick="toggleAspirante({{ user.id }}, {{ user.militante.aspirante|lower }})">
|
||||||
|
Remover Aspirante
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<button type="button" class="btn btn-sm btn-warning"
|
||||||
|
onclick="toggleAspirante({{ user.id }}, {{ user.militante.aspirante|lower }})">
|
||||||
|
Tornar Aspirante
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
{% if current_user.has_permission('system_config') or
|
||||||
|
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
|
||||||
|
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id) or
|
||||||
|
(current_user.has_permission('manage_cell_members') and user.celula_id == current_user.celula_id) %}
|
||||||
|
<button type="button" class="btn btn-sm btn-primary"
|
||||||
|
onclick="resetOTP({{ user.id }})">
|
||||||
|
Gerar Novo OTP
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-warning"
|
||||||
|
onclick="resetPassword({{ user.id }})">
|
||||||
|
Resetar Senha
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm {% if user.ativo %}btn-danger{% else %}btn-success{% endif %}"
|
||||||
|
onclick="toggleUserStatus({{ user.id }}, {{ user.ativo|lower }})">
|
||||||
|
{% if user.ativo %}Desativar{% else %}Ativar{% endif %} Login
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function resetOTP(userId) {
|
||||||
|
if (confirm('Tem certeza que deseja gerar um novo OTP para este usuário? O OTP atual será invalidado.')) {
|
||||||
|
fetch(`/usuarios/${userId}/otp/reset`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('Novo OTP gerado com sucesso!');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Erro ao gerar novo OTP: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Erro ao gerar novo OTP: ' + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPassword(userId) {
|
||||||
|
if (confirm('Tem certeza que deseja resetar a senha deste usuário? Uma nova senha será gerada e enviada por email.')) {
|
||||||
|
fetch(`/usuarios/${userId}/password/reset`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('Senha resetada com sucesso! A nova senha foi enviada por email.');
|
||||||
|
} else {
|
||||||
|
alert('Erro ao resetar senha: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Erro ao resetar senha: ' + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleUserStatus(userId, currentStatus) {
|
||||||
|
const action = currentStatus ? 'desativar' : 'ativar';
|
||||||
|
if (confirm(`Tem certeza que deseja ${action} o login deste usuário?`)) {
|
||||||
|
fetch(`/usuarios/${userId}/toggle_status`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert(`Login ${action}do com sucesso!`);
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(`Erro ao ${action} login: ` + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert(`Erro ao ${action} login: ` + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function alterarNivel(userId, novoNivel) {
|
||||||
|
if (!novoNivel) return;
|
||||||
|
|
||||||
|
if (confirm('Tem certeza que deseja alterar o nível deste usuário?')) {
|
||||||
|
fetch(`/usuarios/${userId}/alterar_nivel`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ nivel: novoNivel })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('Nível do usuário alterado com sucesso!');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Erro ao alterar nível: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Erro ao alterar nível: ' + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleQuadroOrientador(userId, isQuadroOrientador) {
|
||||||
|
const action = isQuadroOrientador ? 'remover' : 'adicionar';
|
||||||
|
if (confirm(`Tem certeza que deseja ${action} a responsabilidade de Quadro-Orientador deste militante?`)) {
|
||||||
|
fetch(`/usuarios/${userId}/toggle_quadro_orientador`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert(`Responsabilidade de Quadro-Orientador ${action}da com sucesso!`);
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(`Erro ao ${action} responsabilidade: ` + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert(`Erro ao ${action} responsabilidade: ` + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
83
templates/dashboard_admin.html
Normal file
83
templates/dashboard_admin.html
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard Administrativo{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="mb-4">Dashboard Administrativo</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Gerenciamento de Usuários</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Usuário</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Admin</th>
|
||||||
|
<th>OTP Configurado</th>
|
||||||
|
<th>Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for usuario in usuarios %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ usuario.id }}</td>
|
||||||
|
<td>{{ usuario.username }}</td>
|
||||||
|
<td>{{ usuario.email }}</td>
|
||||||
|
<td>
|
||||||
|
{% if usuario.is_admin %}
|
||||||
|
<span class="badge bg-success">Sim</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Não</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if usuario.otp_secret %}
|
||||||
|
<span class="badge bg-success">Sim</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">Não</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form action="{{ url_for('reset_otp', user_id=usuario.id) }}" method="POST" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-warning btn-sm"
|
||||||
|
onclick="return confirm('Tem certeza que deseja resetar o OTP deste usuário?')">
|
||||||
|
Resetar OTP
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">Ações Rápidas</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a href="{{ url_for('novo_usuario') }}" class="btn btn-primary">
|
||||||
|
Criar Novo Usuário
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
111
templates/editar_instancia.html
Normal file
111
templates/editar_instancia.html
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Editar {{ tipo_instancia }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1 class="mb-4">Editar {{ tipo_instancia }}</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" class="needs-validation" novalidate>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="nome" class="form-label">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="nome" name="nome" value="{{ instancia.nome }}" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira o nome da {{ tipo_instancia }}.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if tipo_instancia != 'Célula' %}
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="instancia_superior_id" class="form-label">{{ instancia_superior }}</label>
|
||||||
|
<select class="form-select" id="instancia_superior_id" name="instancia_superior_id" required>
|
||||||
|
<option value="">Selecione uma {{ instancia_superior }}</option>
|
||||||
|
{% for superior in instancias_superiores %}
|
||||||
|
<option value="{{ superior.id }}" {% if superior.id == instancia.instancia_superior_id %}selected{% endif %}>{{ superior.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, selecione uma {{ instancia_superior }}.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel_geral_id" class="form-label">Responsável Geral</label>
|
||||||
|
<select class="form-select" id="responsavel_geral_id" name="responsavel_geral_id" required>
|
||||||
|
<option value="">Selecione o responsável geral</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}" {% if militante.id == instancia.responsavel_geral_id %}selected{% endif %}>{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, selecione o responsável geral.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel_financas_id" class="form-label">Responsável de Finanças</label>
|
||||||
|
<select class="form-select" id="responsavel_financas_id" name="responsavel_financas_id">
|
||||||
|
<option value="">Selecione o responsável de finanças</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}" {% if militante.id == instancia.responsavel_financas_id %}selected{% endif %}>{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="responsavel_imprensa_id" class="form-label">Responsável de Imprensa</label>
|
||||||
|
<select class="form-select" id="responsavel_imprensa_id" name="responsavel_imprensa_id">
|
||||||
|
<option value="">Selecione o responsável de imprensa</option>
|
||||||
|
{% for militante in militantes %}
|
||||||
|
<option value="{{ militante.id }}" {% if militante.id == instancia.responsavel_imprensa_id %}selected{% endif %}>{{ militante.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||||
|
<a href="{{ url_for('listar_' + tipo_instancia.lower() + 's') }}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Validação do formulário
|
||||||
|
(function () {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var forms = document.querySelectorAll('.needs-validation')
|
||||||
|
|
||||||
|
Array.prototype.slice.call(forms)
|
||||||
|
.forEach(function (form) {
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
form.classList.add('was-validated')
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -27,99 +27,107 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="cpf" class="form-label">CPF</label>
|
<label for="email" class="form-label">Email</label>
|
||||||
<input type="text" class="form-control" id="cpf" name="cpf" value="{{ militante.cpf }}" required>
|
<input type="email" class="form-control" id="email" name="email" value="{{ militante.email }}" required>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
Por favor, insira o CPF do militante.
|
Por favor, insira um email válido.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="cpf" class="form-label">CPF</label>
|
||||||
|
<input type="text" class="form-control" id="cpf" name="cpf" value="{{ militante.cpf }}" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Por favor, insira o CPF do militante.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="titulo_eleitoral" class="form-label">Título Eleitoral</label>
|
<label for="titulo_eleitoral" class="form-label">Título Eleitoral</label>
|
||||||
<input type="text" class="form-control" id="titulo_eleitoral" name="titulo_eleitoral" value="{{ militante.titulo_eleitoral }}">
|
<input type="text" class="form-control" id="titulo_eleitoral" name="titulo_eleitoral" value="{{ militante.titulo_eleitoral }}">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="data_nascimento" class="form-label">Data de Nascimento</label>
|
<label for="data_nascimento" class="form-label">Data de Nascimento</label>
|
||||||
<input type="date" class="form-control" id="data_nascimento" name="data_nascimento" value="{{ militante.data_nascimento.strftime('%Y-%m-%d') }}">
|
<input type="date" class="form-control" id="data_nascimento" name="data_nascimento" value="{{ militante.data_nascimento.strftime('%Y-%m-%d') }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="data_entrada_oci" class="form-label">Data de Entrada na OCI</label>
|
<label for="data_entrada_oci" class="form-label">Data de Entrada na OCI</label>
|
||||||
<input type="date" class="form-control" id="data_entrada_oci" name="data_entrada_oci" value="{{ militante.data_entrada_oci.strftime('%Y-%m-%d') }}">
|
<input type="date" class="form-control" id="data_entrada_oci" name="data_entrada_oci" value="{{ militante.data_entrada_oci.strftime('%Y-%m-%d') }}">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="data_efetivacao_oci" class="form-label">Data de Efetivação na OCI</label>
|
<label for="data_efetivacao_oci" class="form-label">Data de Efetivação na OCI</label>
|
||||||
<input type="date" class="form-control" id="data_efetivacao_oci" name="data_efetivacao_oci" value="{{ militante.data_efetivacao_oci.strftime('%Y-%m-%d') }}">
|
<input type="date" class="form-control" id="data_efetivacao_oci" name="data_efetivacao_oci" value="{{ militante.data_efetivacao_oci.strftime('%Y-%m-%d') }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="telefone1" class="form-label">Telefone 1</label>
|
<label for="telefone1" class="form-label">Telefone 1</label>
|
||||||
<input type="text" class="form-control" id="telefone1" name="telefone1" value="{{ militante.telefone1 }}">
|
<input type="text" class="form-control" id="telefone1" name="telefone1" value="{{ militante.telefone1 }}">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="telefone2" class="form-label">Telefone 2</label>
|
<label for="telefone2" class="form-label">Telefone 2</label>
|
||||||
<input type="text" class="form-control" id="telefone2" name="telefone2" value="{{ militante.telefone2 }}">
|
<input type="text" class="form-control" id="telefone2" name="telefone2" value="{{ militante.telefone2 }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="profissao" class="form-label">Profissão</label>
|
<label for="profissao" class="form-label">Profissão</label>
|
||||||
<input type="text" class="form-control" id="profissao" name="profissao" value="{{ militante.profissao }}">
|
<input type="text" class="form-control" id="profissao" name="profissao" value="{{ militante.profissao }}">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="regime_trabalho" class="form-label">Regime de Trabalho</label>
|
<label for="regime_trabalho" class="form-label">Regime de Trabalho</label>
|
||||||
<input type="text" class="form-control" id="regime_trabalho" name="regime_trabalho" value="{{ militante.regime_trabalho }}">
|
<input type="text" class="form-control" id="regime_trabalho" name="regime_trabalho" value="{{ militante.regime_trabalho }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="empresa" class="form-label">Empresa</label>
|
<label for="empresa" class="form-label">Empresa</label>
|
||||||
<input type="text" class="form-control" id="empresa" name="empresa" value="{{ militante.empresa }}">
|
<input type="text" class="form-control" id="empresa" name="empresa" value="{{ militante.empresa }}">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="contratante" class="form-label">Contratante</label>
|
<label for="contratante" class="form-label">Contratante</label>
|
||||||
<input type="text" class="form-control" id="contratante" name="contratante" value="{{ militante.contratante }}">
|
<input type="text" class="form-control" id="contratante" name="contratante" value="{{ militante.contratante }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="instituicao_ensino" class="form-label">Instituição de Ensino</label>
|
<label for="instituicao_ensino" class="form-label">Instituição de Ensino</label>
|
||||||
<input type="text" class="form-control" id="instituicao_ensino" name="instituicao_ensino" value="{{ militante.instituicao_ensino }}">
|
<input type="text" class="form-control" id="instituicao_ensino" name="instituicao_ensino" value="{{ militante.instituicao_ensino }}">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="tipo_instituicao" class="form-label">Tipo de Instituição</label>
|
<label for="tipo_instituicao" class="form-label">Tipo de Instituição</label>
|
||||||
<input type="text" class="form-control" id="tipo_instituicao" name="tipo_instituicao" value="{{ militante.tipo_instituicao }}">
|
<input type="text" class="form-control" id="tipo_instituicao" name="tipo_instituicao" value="{{ militante.tipo_instituicao }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="sindicato" class="form-label">Sindicato</label>
|
<label for="sindicato" class="form-label">Sindicato</label>
|
||||||
<input type="text" class="form-control" id="sindicato" name="sindicato" value="{{ militante.sindicato }}">
|
<input type="text" class="form-control" id="sindicato" name="sindicato" value="{{ militante.sindicato }}">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="cargo_sindical" class="form-label">Cargo Sindical</label>
|
<label for="cargo_sindical" class="form-label">Cargo Sindical</label>
|
||||||
<input type="text" class="form-control" id="cargo_sindical" name="cargo_sindical" value="{{ militante.cargo_sindical }}">
|
<input type="text" class="form-control" id="cargo_sindical" name="cargo_sindical" value="{{ militante.cargo_sindical }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="dirigente_sindical" name="dirigente_sindical" {% if militante.dirigente_sindical %}checked{% endif %}>
|
<input class="form-check-input" type="checkbox" id="dirigente_sindical" name="dirigente_sindical" {% if militante.dirigente_sindical %}checked{% endif %}>
|
||||||
@@ -128,14 +136,14 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="central_sindical" class="form-label">Central Sindical</label>
|
<label for="central_sindical" class="form-label">Central Sindical</label>
|
||||||
<input type="text" class="form-control" id="central_sindical" name="central_sindical" value="{{ militante.central_sindical }}">
|
<input type="text" class="form-control" id="central_sindical" name="central_sindical" value="{{ militante.central_sindical }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="setor_id" class="form-label">Setor</label>
|
<label for="setor_id" class="form-label">Setor</label>
|
||||||
<select class="form-select" id="setor_id" name="setor_id" required>
|
<select class="form-select" id="setor_id" name="setor_id" required>
|
||||||
@@ -148,7 +156,9 @@
|
|||||||
Por favor, selecione um setor.
|
Por favor, selecione um setor.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="celula_id" class="form-label">Célula</label>
|
<label for="celula_id" class="form-label">Célula</label>
|
||||||
<select class="form-select" id="celula_id" name="celula_id" required>
|
<select class="form-select" id="celula_id" name="celula_id" required>
|
||||||
@@ -163,6 +173,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 mb-3">
|
||||||
|
<label class="form-label">Responsabilidades</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="responsavel_financas" name="responsabilidades" value="{{ Militante.RESPONSAVEL_FINANCAS }}" {% if militante.responsabilidades & Militante.RESPONSAVEL_FINANCAS %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="responsavel_financas">
|
||||||
|
Responsável de Finanças
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="responsavel_imprensa" name="responsabilidades" value="{{ Militante.RESPONSAVEL_IMPRENSA }}" {% if militante.responsabilidades & Militante.RESPONSAVEL_IMPRENSA %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="responsavel_imprensa">
|
||||||
|
Responsável de Imprensa
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="quadro_orientador" name="responsabilidades" value="{{ Militante.QUADRO_ORIENTADOR }}" {% if militante.responsabilidades & Militante.QUADRO_ORIENTADOR %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="quadro_orientador">
|
||||||
|
Quadro-Orientador
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mt-4">
|
<div class="d-flex justify-content-between mt-4">
|
||||||
<button type="submit" class="btn btn-primary">Salvar</button>
|
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||||
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Cancelar</a>
|
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Cancelar</a>
|
||||||
|
|||||||
@@ -17,82 +17,16 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
{% for link in links %}
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title">Militantes</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text">Gerencie os militantes da organização.</p>
|
<h5 class="card-title">{{ link.text }}</h5>
|
||||||
<a href="{{ url_for('listar_militantes') }}" class="btn btn-primary">Listar Militantes</a>
|
<a href="{{ link.url }}" class="btn btn-primary">Acessar</a>
|
||||||
<a href="{{ url_for('novo_militante') }}" class="btn btn-success">Novo Militante</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title">Pagamentos</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="card-text">Gerencie os pagamentos dos militantes.</p>
|
|
||||||
<a href="{{ url_for('listar_pagamentos') }}" class="btn btn-primary">Listar Pagamentos</a>
|
|
||||||
<a href="{{ url_for('novo_pagamento') }}" class="btn btn-success">Novo Pagamento</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title">Materiais</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="card-text">Gerencie os materiais da organização.</p>
|
|
||||||
<a href="{{ url_for('listar_materiais') }}" class="btn btn-primary">Listar Materiais</a>
|
|
||||||
<a href="{{ url_for('novo_material') }}" class="btn btn-success">Novo Material</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title">Vendas</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="card-text">Gerencie as vendas de materiais.</p>
|
|
||||||
<a href="{{ url_for('listar_relatorios_vendas') }}" class="btn btn-primary">Listar Vendas</a>
|
|
||||||
<a href="{{ url_for('novo_relatorio_vendas') }}" class="btn btn-success">Nova Venda</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title">Relatórios</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="card-text">Gerencie os relatórios da organização.</p>
|
|
||||||
<a href="{{ url_for('listar_relatorios_cotas') }}" class="btn btn-primary">Relatórios de Cotas</a>
|
|
||||||
<a href="{{ url_for('listar_relatorios_vendas') }}" class="btn btn-primary">Relatórios de Vendas</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title">Configurações</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="card-text">Gerencie as configurações da organização.</p>
|
|
||||||
<a href="{{ url_for('novo_usuario') }}" class="btn btn-primary">Novo Usuário</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
79
templates/listar_instancias.html
Normal file
79
templates/listar_instancias.html
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Lista de {{ tipo_instancia }}s{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1 class="mb-4">Lista de {{ tipo_instancia }}s</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mb-4">
|
||||||
|
<a href="{{ url_for('criar_' + tipo_instancia.lower()) }}" class="btn btn-primary">Nova {{ tipo_instancia }}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nome</th>
|
||||||
|
{% if tipo_instancia != 'Célula' %}
|
||||||
|
<th>{{ instancia_superior }}</th>
|
||||||
|
{% endif %}
|
||||||
|
<th>Responsável Geral</th>
|
||||||
|
<th>Responsável de Finanças</th>
|
||||||
|
<th>Responsável de Imprensa</th>
|
||||||
|
<th>Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for instancia in instancias %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ instancia.nome }}</td>
|
||||||
|
{% if tipo_instancia != 'Célula' %}
|
||||||
|
<td>{{ instancia.instancia_superior.nome }}</td>
|
||||||
|
{% endif %}
|
||||||
|
<td>{{ instancia.responsavel_geral.nome }}</td>
|
||||||
|
<td>
|
||||||
|
{% if instancia.responsavel_financas %}
|
||||||
|
{{ instancia.responsavel_financas.nome }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if instancia.responsavel_imprensa %}
|
||||||
|
{{ instancia.responsavel_imprensa.nome }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('editar_' + tipo_instancia.lower(), id=instancia.id) }}" class="btn btn-sm btn-warning">Editar</a>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger" onclick="confirmarExclusao({{ instancia.id }})">Excluir</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function confirmarExclusao(id) {
|
||||||
|
if (confirm('Tem certeza que deseja excluir esta {{ tipo_instancia }}?')) {
|
||||||
|
window.location.href = "{{ url_for('excluir_' + tipo_instancia.lower(), id=0) }}".replace('0', id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block title %}Listar Militantes{% endblock %}
|
{% block title %}Lista de Militantes{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -17,67 +17,42 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mb-4">
|
<div class="d-flex justify-content-between mb-4">
|
||||||
<a href="{{ url_for('novo_militante') }}" class="btn btn-success">Novo Militante</a>
|
<a href="{{ url_for('criar_militante') }}" class="btn btn-primary">Novo Militante</a>
|
||||||
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
|
||||||
<th>Nome</th>
|
<th>Nome</th>
|
||||||
<th>CPF</th>
|
<th>Email</th>
|
||||||
<th>Título Eleitoral</th>
|
|
||||||
<th>Data de Nascimento</th>
|
|
||||||
<th>Data de Entrada</th>
|
|
||||||
<th>Data de Efetivação</th>
|
|
||||||
<th>Telefone 1</th>
|
|
||||||
<th>Telefone 2</th>
|
|
||||||
<th>Profissão</th>
|
|
||||||
<th>Regime de Trabalho</th>
|
|
||||||
<th>Empresa</th>
|
|
||||||
<th>Contratante</th>
|
|
||||||
<th>Instituição de Ensino</th>
|
|
||||||
<th>Tipo de Instituição</th>
|
|
||||||
<th>Sindicato</th>
|
|
||||||
<th>Cargo Sindical</th>
|
|
||||||
<th>Dirigente Sindical</th>
|
|
||||||
<th>Central Sindical</th>
|
|
||||||
<th>Setor</th>
|
|
||||||
<th>Célula</th>
|
<th>Célula</th>
|
||||||
|
<th>Responsabilidades</th>
|
||||||
<th>Ações</th>
|
<th>Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for militante in militantes %}
|
{% for militante in militantes %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ militante.id }}</td>
|
<td>{{ militante.nome }}</td>
|
||||||
<td>{{ militante.nome }}</td>
|
<td>{{ militante.email }}</td>
|
||||||
<td>{{ militante.cpf }}</td>
|
<td>{{ militante.celula.nome }}</td>
|
||||||
<td>{{ militante.titulo_eleitoral }}</td>
|
<td>
|
||||||
<td>{{ militante.data_nascimento.strftime('%d/%m/%Y') }}</td>
|
{% if militante.responsabilidades & Militante.RESPONSAVEL_FINANCAS %}
|
||||||
<td>{{ militante.data_entrada_oci.strftime('%d/%m/%Y') }}</td>
|
<span class="badge bg-primary">Finanças</span>
|
||||||
<td>{{ militante.data_efetivacao_oci.strftime('%d/%m/%Y') }}</td>
|
{% endif %}
|
||||||
<td>{{ militante.telefone1 }}</td>
|
{% if militante.responsabilidades & Militante.RESPONSAVEL_IMPRENSA %}
|
||||||
<td>{{ militante.telefone2 }}</td>
|
<span class="badge bg-info">Imprensa</span>
|
||||||
<td>{{ militante.profissao }}</td>
|
{% endif %}
|
||||||
<td>{{ militante.regime_trabalho }}</td>
|
{% if militante.responsabilidades & Militante.QUADRO_ORIENTADOR %}
|
||||||
<td>{{ militante.empresa }}</td>
|
<span class="badge bg-success">Quadro-Orientador</span>
|
||||||
<td>{{ militante.contratante }}</td>
|
{% endif %}
|
||||||
<td>{{ militante.instituicao_ensino }}</td>
|
</td>
|
||||||
<td>{{ militante.tipo_instituicao }}</td>
|
<td>
|
||||||
<td>{{ militante.sindicato }}</td>
|
<a href="{{ url_for('editar_militante', id=militante.id) }}" class="btn btn-sm btn-warning">Editar</a>
|
||||||
<td>{{ militante.cargo_sindical }}</td>
|
<button type="button" class="btn btn-sm btn-danger" onclick="confirmarExclusao({{ militante.id }})">Excluir</button>
|
||||||
<td>{{ 'Sim' if militante.dirigente_sindical else 'Não' }}</td>
|
</td>
|
||||||
<td>{{ militante.central_sindical }}</td>
|
</tr>
|
||||||
<td>{{ militante.setor.nome }}</td>
|
|
||||||
<td>{{ militante.celula.nome }}</td>
|
|
||||||
<td>
|
|
||||||
<a href="{{ url_for('editar_militante', id=militante.id) }}" class="btn btn-primary btn-sm">Editar</a>
|
|
||||||
<a href="{{ url_for('deletar_militante', id=militante.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Tem certeza que deseja excluir este militante?')">Excluir</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -85,4 +60,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function confirmarExclusao(id) {
|
||||||
|
if (confirm('Tem certeza que deseja excluir este militante?')) {
|
||||||
|
window.location.href = "{{ url_for('excluir_militante', id=0) }}".replace('0', id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -4,43 +4,38 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-6 offset-md-3">
|
<div class="col-md-6">
|
||||||
<div class="card mt-5">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="text-center">Login</h3>
|
<h3 class="card-title">Login</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
<div class="alert alert-{{ category }}">
|
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||||
{{ message }}
|
|
||||||
{% if category == 'danger' %}
|
|
||||||
<br>
|
|
||||||
<small>Se o problema persistir, contate o administrador.</small>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<form method="POST">
|
<form method="POST" action="{{ url_for('login') }}">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">Usuário</label>
|
<label for="username" class="form-label">Usuário</label>
|
||||||
<input type="text" class="form-control" id="username" name="username" required
|
<input type="text" class="form-control" id="username" name="username" required>
|
||||||
value="{{ request.form.username }}">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">Senha</label>
|
<label for="password" class="form-label">Senha</label>
|
||||||
<input type="password" class="form-control" id="password" name="password" required>
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="otp" class="form-label">Código OTP</label>
|
<label for="otp_code" class="form-label">Código OTP</label>
|
||||||
<input type="text" class="form-control" id="otp" name="otp" required
|
<input type="text" class="form-control" id="otp_code" name="otp_code" required>
|
||||||
pattern="[0-9]{6}" title="Digite o código de 6 dígitos">
|
<small class="text-muted">Digite o código gerado pelo seu aplicativo autenticador</small>
|
||||||
<small class="form-text text-muted">Digite o código de 6 dígitos do seu aplicativo autenticador</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
<button type="submit" class="btn btn-primary">Entrar</button>
|
<button type="submit" class="btn btn-primary">Entrar</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,12 @@
|
|||||||
<input type="text" class="form-control" id="nome" name="nome" required>
|
<input type="text" class="form-control" id="nome" name="nome" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Email:</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
|
<small class="form-text text-muted">Este email será usado para login e comunicação do sistema</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="cpf" class="form-label">CPF:</label>
|
<label for="cpf" class="form-label">CPF:</label>
|
||||||
<input type="text" class="form-control" id="cpf" name="cpf" required>
|
<input type="text" class="form-control" id="cpf" name="cpf" required>
|
||||||
@@ -115,25 +121,67 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="setor_id" class="form-label">Setor:</label>
|
<label for="cr_id" class="form-label">Comitê Regional:</label>
|
||||||
<select class="form-select" id="setor_id" name="setor_id" required>
|
<select class="form-select" id="cr_id" name="cr_id" required>
|
||||||
<option value="">Selecione o setor</option>
|
<option value="">Selecione o CR</option>
|
||||||
{% for setor in setores %}
|
{% for cr in crs %}
|
||||||
<option value="{{ setor.id }}">{{ setor.nome }}</option>
|
<option value="{{ cr.id }}">{{ cr.nome }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="celula_id" class="form-label">Célula:</label>
|
<label for="setor_id" class="form-label">Setor:</label>
|
||||||
<select class="form-select" id="celula_id" name="celula_id" required>
|
<select class="form-select" id="setor_id" name="setor_id" required>
|
||||||
<option value="">Selecione a célula</option>
|
<option value="">Selecione o setor</option>
|
||||||
{% for celula in celulas %}
|
{% for setor in setores %}
|
||||||
<option value="{{ celula.id }}">{{ celula.nome }}</option>
|
<option value="{{ setor.id }}" data-cr="{{ setor.cr_id }}">{{ setor.nome }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Célula:</label>
|
||||||
|
{% if pode_criar_celula %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="celula_opcao" id="celula_existente" value="existente" checked>
|
||||||
|
<label class="form-check-label" for="celula_existente">
|
||||||
|
Usar célula existente
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="celula_opcao" id="celula_nova" value="nova">
|
||||||
|
<label class="form-check-label" for="celula_nova">
|
||||||
|
Criar nova célula
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="celula_existente_container">
|
||||||
|
<select class="form-select" id="celula_id" name="celula_id" required>
|
||||||
|
<option value="">Selecione a célula</option>
|
||||||
|
{% for celula in celulas %}
|
||||||
|
<option value="{{ celula.id }}" data-setor="{{ celula.setor_id }}">{{ celula.nome }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if pode_criar_celula %}
|
||||||
|
<div id="celula_nova_container" style="display: none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="nova_celula_nome" class="form-label">Nome da Nova Célula:</label>
|
||||||
|
<input type="text" class="form-control" id="nova_celula_nome" name="nova_celula_nome">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="nova_celula_quadro_orientador" class="form-label">Quadro Orientador:</label>
|
||||||
|
<input type="text" class="form-control" id="nova_celula_quadro_orientador" name="nova_celula_quadro_orientador">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary">Registrar</button>
|
<button type="submit" class="btn btn-primary">Registrar</button>
|
||||||
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Voltar</a>
|
<a href="{{ url_for('listar_militantes') }}" class="btn btn-secondary">Voltar</a>
|
||||||
@@ -143,5 +191,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Função para atualizar as células baseado no setor selecionado
|
||||||
|
function atualizarCelulas() {
|
||||||
|
const setorId = document.getElementById('setor_id').value;
|
||||||
|
const celulaSelect = document.getElementById('celula_id');
|
||||||
|
|
||||||
|
// Limpar opções existentes
|
||||||
|
celulaSelect.innerHTML = '<option value="">Selecione a célula</option>';
|
||||||
|
|
||||||
|
// Adicionar apenas células do setor selecionado
|
||||||
|
document.querySelectorAll('#celula_id option').forEach(option => {
|
||||||
|
if (option.dataset.setor === setorId) {
|
||||||
|
celulaSelect.appendChild(option.cloneNode(true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para atualizar os setores baseado no CR selecionado
|
||||||
|
function atualizarSetores() {
|
||||||
|
const crId = document.getElementById('cr_id').value;
|
||||||
|
const setorSelect = document.getElementById('setor_id');
|
||||||
|
|
||||||
|
// Limpar opções existentes
|
||||||
|
setorSelect.innerHTML = '<option value="">Selecione o setor</option>';
|
||||||
|
|
||||||
|
// Adicionar apenas setores do CR selecionado
|
||||||
|
document.querySelectorAll('#setor_id option').forEach(option => {
|
||||||
|
if (option.dataset.cr === crId) {
|
||||||
|
setorSelect.appendChild(option.cloneNode(true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Atualizar células após mudar o setor
|
||||||
|
atualizarCelulas();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
document.getElementById('cr_id').addEventListener('change', atualizarSetores);
|
||||||
|
document.getElementById('setor_id').addEventListener('change', atualizarCelulas);
|
||||||
|
|
||||||
|
// Toggle entre célula existente e nova
|
||||||
|
const celulaExistente = document.getElementById('celula_existente');
|
||||||
|
const celulaNova = document.getElementById('celula_nova');
|
||||||
|
const celulaExistenteContainer = document.getElementById('celula_existente_container');
|
||||||
|
const celulaNovaContainer = document.getElementById('celula_nova_container');
|
||||||
|
|
||||||
|
if (celulaExistente && celulaNova) {
|
||||||
|
celulaExistente.addEventListener('change', function() {
|
||||||
|
celulaExistenteContainer.style.display = 'block';
|
||||||
|
celulaNovaContainer.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
celulaNova.addEventListener('change', function() {
|
||||||
|
celulaExistenteContainer.style.display = 'none';
|
||||||
|
celulaNovaContainer.style.display = 'block';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block title %}Novo Usuário{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8 offset-md-2">
|
|
||||||
<h1 class="mb-4">Novo Usuário</h1>
|
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
||||||
{% if messages %}
|
|
||||||
{% for category, message in messages %}
|
|
||||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
<form method="post" class="mb-4">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="militante_id" class="form-label">Militante:</label>
|
|
||||||
<select class="form-select" id="militante_id" name="militante_id" required>
|
|
||||||
<option value="">Selecione o militante</option>
|
|
||||||
{% for militante in militantes %}
|
|
||||||
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="setor_id" class="form-label">Setor:</label>
|
|
||||||
<select class="form-select" id="setor_id" name="setor_id" required>
|
|
||||||
<option value="">Selecione o setor</option>
|
|
||||||
{% for setor in setores %}
|
|
||||||
<option value="{{ setor.id }}">{{ setor.nome }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="comite_id" class="form-label">Comitê Central:</label>
|
|
||||||
<select class="form-select" id="comite_id" name="comite_id" required>
|
|
||||||
<option value="">Selecione o comitê</option>
|
|
||||||
{% for comite in comites %}
|
|
||||||
<option value="{{ comite.id }}">{{ comite.nome }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="username" class="form-label">Nome de Usuário:</label>
|
|
||||||
<input type="text" class="form-control" id="username" name="username" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="password" class="form-label">Senha:</label>
|
|
||||||
<input type="password" class="form-control" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="confirm_password" class="form-label">Confirmar Senha:</label>
|
|
||||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="submit" class="btn btn-primary">Registrar</button>
|
|
||||||
<a href="{{ url_for('listar_usuarios') }}" class="btn btn-secondary">Voltar</a>
|
|
||||||
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
205
tests/test_permissions.py
Normal file
205
tests/test_permissions.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import unittest
|
||||||
|
from flask import Flask, g
|
||||||
|
from functions.database import db, Militante, Celula, Setor, CR, CC
|
||||||
|
from functions.permissions import (
|
||||||
|
can_manage_militante, can_manage_celula, can_manage_setor, can_manage_cr, can_manage_cc,
|
||||||
|
can_manage_financas, can_manage_imprensa, can_manage_responsabilidades
|
||||||
|
)
|
||||||
|
|
||||||
|
class TestPermissions(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.app = Flask(__name__)
|
||||||
|
self.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
|
||||||
|
self.app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
db.init_app(self.app)
|
||||||
|
|
||||||
|
with self.app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# Criar instâncias
|
||||||
|
self.cc = CC(nome='CC 1')
|
||||||
|
db.session.add(self.cc)
|
||||||
|
|
||||||
|
self.cr = CR(nome='CR 1', cc=self.cc)
|
||||||
|
db.session.add(self.cr)
|
||||||
|
|
||||||
|
self.setor = Setor(nome='Setor 1', cr=self.cr)
|
||||||
|
db.session.add(self.setor)
|
||||||
|
|
||||||
|
self.celula = Celula(nome='Célula 1', setor=self.setor)
|
||||||
|
db.session.add(self.celula)
|
||||||
|
|
||||||
|
# Criar militantes com diferentes responsabilidades
|
||||||
|
self.militante_basico = Militante(
|
||||||
|
nome='Militante Básico',
|
||||||
|
email='basico@example.com',
|
||||||
|
celula=self.celula,
|
||||||
|
responsabilidades=Militante.MILITANTE_BASICO
|
||||||
|
)
|
||||||
|
db.session.add(self.militante_basico)
|
||||||
|
|
||||||
|
self.secretario_celula = Militante(
|
||||||
|
nome='Secretário de Célula',
|
||||||
|
email='celula@example.com',
|
||||||
|
celula=self.celula,
|
||||||
|
responsabilidades=Militante.SECRETARIO_CELULA
|
||||||
|
)
|
||||||
|
db.session.add(self.secretario_celula)
|
||||||
|
|
||||||
|
self.secretario_setor = Militante(
|
||||||
|
nome='Secretário de Setor',
|
||||||
|
email='setor@example.com',
|
||||||
|
celula=self.celula,
|
||||||
|
responsabilidades=Militante.SECRETARIO_SETOR
|
||||||
|
)
|
||||||
|
db.session.add(self.secretario_setor)
|
||||||
|
|
||||||
|
self.secretario_cr = Militante(
|
||||||
|
nome='Secretário de CR',
|
||||||
|
email='cr@example.com',
|
||||||
|
celula=self.celula,
|
||||||
|
responsabilidades=Militante.SECRETARIO_CR
|
||||||
|
)
|
||||||
|
db.session.add(self.secretario_cr)
|
||||||
|
|
||||||
|
self.secretario_cc = Militante(
|
||||||
|
nome='Secretário de CC',
|
||||||
|
email='cc@example.com',
|
||||||
|
celula=self.celula,
|
||||||
|
responsabilidades=Militante.SECRETARIO_CC
|
||||||
|
)
|
||||||
|
db.session.add(self.secretario_cc)
|
||||||
|
|
||||||
|
self.secretario_geral = Militante(
|
||||||
|
nome='Secretário Geral',
|
||||||
|
email='geral@example.com',
|
||||||
|
celula=self.celula,
|
||||||
|
responsabilidades=Militante.SECRETARIO_GERAL
|
||||||
|
)
|
||||||
|
db.session.add(self.secretario_geral)
|
||||||
|
|
||||||
|
self.responsavel_financas = Militante(
|
||||||
|
nome='Responsável de Finanças',
|
||||||
|
email='financas@example.com',
|
||||||
|
celula=self.celula,
|
||||||
|
responsabilidades=Militante.RESPONSAVEL_FINANCAS
|
||||||
|
)
|
||||||
|
db.session.add(self.responsavel_financas)
|
||||||
|
|
||||||
|
self.responsavel_imprensa = Militante(
|
||||||
|
nome='Responsável de Imprensa',
|
||||||
|
email='imprensa@example.com',
|
||||||
|
celula=self.celula,
|
||||||
|
responsabilidades=Militante.RESPONSAVEL_IMPRENSA
|
||||||
|
)
|
||||||
|
db.session.add(self.responsavel_imprensa)
|
||||||
|
|
||||||
|
# Atribuir responsáveis às instâncias
|
||||||
|
self.celula.responsavel_geral = self.secretario_celula
|
||||||
|
self.celula.responsavel_financas = self.responsavel_financas
|
||||||
|
self.celula.responsavel_imprensa = self.responsavel_imprensa
|
||||||
|
|
||||||
|
self.setor.responsavel_geral = self.secretario_setor
|
||||||
|
self.setor.responsavel_financas = self.responsavel_financas
|
||||||
|
self.setor.responsavel_imprensa = self.responsavel_imprensa
|
||||||
|
|
||||||
|
self.cr.responsavel_geral = self.secretario_cr
|
||||||
|
self.cr.responsavel_financas = self.responsavel_financas
|
||||||
|
self.cr.responsavel_imprensa = self.responsavel_imprensa
|
||||||
|
|
||||||
|
self.cc.responsavel_geral = self.secretario_cc
|
||||||
|
self.cc.responsavel_financas = self.responsavel_financas
|
||||||
|
self.cc.responsavel_imprensa = self.responsavel_imprensa
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def test_can_manage_militante(self):
|
||||||
|
with self.app.app_context():
|
||||||
|
# Militante básico não pode gerenciar outros militantes
|
||||||
|
g.user = type('User', (), {'militante': self.militante_basico})
|
||||||
|
self.assertFalse(can_manage_militante(self.militante_basico.id))
|
||||||
|
|
||||||
|
# Secretário de célula pode gerenciar militantes da sua célula
|
||||||
|
g.user = type('User', (), {'militante': self.secretario_celula})
|
||||||
|
self.assertTrue(can_manage_militante(self.militante_basico.id))
|
||||||
|
|
||||||
|
# Secretário de setor pode gerenciar militantes do seu setor
|
||||||
|
g.user = type('User', (), {'militante': self.secretario_setor})
|
||||||
|
self.assertTrue(can_manage_militante(self.militante_basico.id))
|
||||||
|
|
||||||
|
# Secretário de CR pode gerenciar militantes do seu CR
|
||||||
|
g.user = type('User', (), {'militante': self.secretario_cr})
|
||||||
|
self.assertTrue(can_manage_militante(self.militante_basico.id))
|
||||||
|
|
||||||
|
# Secretário de CC pode gerenciar militantes do seu CC
|
||||||
|
g.user = type('User', (), {'militante': self.secretario_cc})
|
||||||
|
self.assertTrue(can_manage_militante(self.militante_basico.id))
|
||||||
|
|
||||||
|
# Secretário Geral pode gerenciar qualquer militante
|
||||||
|
g.user = type('User', (), {'militante': self.secretario_geral})
|
||||||
|
self.assertTrue(can_manage_militante(self.militante_basico.id))
|
||||||
|
|
||||||
|
def test_can_manage_financas(self):
|
||||||
|
with self.app.app_context():
|
||||||
|
# Militante básico não pode gerenciar finanças
|
||||||
|
g.user = type('User', (), {'militante': self.militante_basico})
|
||||||
|
self.assertFalse(can_manage_financas(self.celula.id, 'celula'))
|
||||||
|
|
||||||
|
# Responsável de finanças pode gerenciar finanças da sua instância
|
||||||
|
g.user = type('User', (), {'militante': self.responsavel_financas})
|
||||||
|
self.assertTrue(can_manage_financas(self.celula.id, 'celula'))
|
||||||
|
self.assertTrue(can_manage_financas(self.setor.id, 'setor'))
|
||||||
|
self.assertTrue(can_manage_financas(self.cr.id, 'cr'))
|
||||||
|
self.assertTrue(can_manage_financas(self.cc.id, 'cc'))
|
||||||
|
|
||||||
|
# Secretário Geral pode gerenciar finanças de qualquer instância
|
||||||
|
g.user = type('User', (), {'militante': self.secretario_geral})
|
||||||
|
self.assertTrue(can_manage_financas(self.celula.id, 'celula'))
|
||||||
|
self.assertTrue(can_manage_financas(self.setor.id, 'setor'))
|
||||||
|
self.assertTrue(can_manage_financas(self.cr.id, 'cr'))
|
||||||
|
self.assertTrue(can_manage_financas(self.cc.id, 'cc'))
|
||||||
|
|
||||||
|
def test_can_manage_imprensa(self):
|
||||||
|
with self.app.app_context():
|
||||||
|
# Militante básico não pode gerenciar imprensa
|
||||||
|
g.user = type('User', (), {'militante': self.militante_basico})
|
||||||
|
self.assertFalse(can_manage_imprensa(self.celula.id, 'celula'))
|
||||||
|
|
||||||
|
# Responsável de imprensa pode gerenciar imprensa da sua instância
|
||||||
|
g.user = type('User', (), {'militante': self.responsavel_imprensa})
|
||||||
|
self.assertTrue(can_manage_imprensa(self.celula.id, 'celula'))
|
||||||
|
self.assertTrue(can_manage_imprensa(self.setor.id, 'setor'))
|
||||||
|
self.assertTrue(can_manage_imprensa(self.cr.id, 'cr'))
|
||||||
|
self.assertTrue(can_manage_imprensa(self.cc.id, 'cc'))
|
||||||
|
|
||||||
|
# Secretário Geral pode gerenciar imprensa de qualquer instância
|
||||||
|
g.user = type('User', (), {'militante': self.secretario_geral})
|
||||||
|
self.assertTrue(can_manage_imprensa(self.celula.id, 'celula'))
|
||||||
|
self.assertTrue(can_manage_imprensa(self.setor.id, 'setor'))
|
||||||
|
self.assertTrue(can_manage_imprensa(self.cr.id, 'cr'))
|
||||||
|
self.assertTrue(can_manage_imprensa(self.cc.id, 'cc'))
|
||||||
|
|
||||||
|
def test_can_manage_responsabilidades(self):
|
||||||
|
with self.app.app_context():
|
||||||
|
# Militante básico não pode gerenciar responsabilidades
|
||||||
|
g.user = type('User', (), {'militante': self.militante_basico})
|
||||||
|
self.assertFalse(can_manage_responsabilidades(self.militante_basico.id))
|
||||||
|
|
||||||
|
# Secretário de setor pode gerenciar responsabilidades do seu setor
|
||||||
|
g.user = type('User', (), {'militante': self.secretario_setor})
|
||||||
|
self.assertTrue(can_manage_responsabilidades(self.militante_basico.id))
|
||||||
|
|
||||||
|
# Secretário de CR pode gerenciar responsabilidades do seu CR
|
||||||
|
g.user = type('User', (), {'militante': self.secretario_cr})
|
||||||
|
self.assertTrue(can_manage_responsabilidades(self.militante_basico.id))
|
||||||
|
|
||||||
|
# Secretário de CC pode gerenciar responsabilidades do seu CC
|
||||||
|
g.user = type('User', (), {'militante': self.secretario_cc})
|
||||||
|
self.assertTrue(can_manage_responsabilidades(self.militante_basico.id))
|
||||||
|
|
||||||
|
# Secretário Geral pode gerenciar responsabilidades de qualquer militante
|
||||||
|
g.user = type('User', (), {'militante': self.secretario_geral})
|
||||||
|
self.assertTrue(can_manage_responsabilidades(self.militante_basico.id))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user