diff --git a/.gitignore b/.gitignore index b30c9723f..38805b350 100644 --- a/.gitignore +++ b/.gitignore @@ -87,8 +87,8 @@ target/ # specific to this project +whoosh_index collected_static bower bower_components media - diff --git a/README.rst b/README.rst index 43df9a125..8717e7086 100644 --- a/README.rst +++ b/README.rst @@ -17,303 +17,37 @@ atual do sistema (2.5), visite a página do `projeto na Interlegis wiki `_ -* Procedimento testado nos seguintes SO's: - * `Ubuntu 16.04 64bits `_; +Instruções para Importação da base mysql 2.5 +============================================ + `Importação da Base do SAPL 2.5 para SAPL 3.1 `_ - * edite e incremente outros, ou ainda, crie outros readme's dentro do projeto para outros SO's e adicione o link aqui. -Instalar as seguintes dependências do sistema:: ----------------------------------------------------------------------------------------- +Instruções para Deploy +====================== + `Deploy SAPL com Nginx + Gunicorn `_ -* :: - sudo apt-get install git nginx python3-dev libpq-dev graphviz-dev graphviz \ - pkg-config postgresql postgresql-contrib pgadmin3 python-psycopg2 \ - software-properties-common build-essential libxml2-dev libjpeg-dev \ - libmysqlclient-dev libssl-dev libffi-dev libxslt1-dev python3-setuptools curl - - sudo easy_install3 pip lxml - - sudo -i - curl -sL https://deb.nodesource.com/setup_5.x | bash - - exit - sudo apt-get install nodejs - - sudo npm install npm -g - sudo npm install -g bower - -Instalar o virtualenv usando python 3 para o projeto. ------------------------------------------------------ - -* Para usar `virtualenvwrapper `_, instale com:: - - sudo pip install virtualenvwrapper - - mkdir ~/Envs - -* Edite o arquivo ``.bashrc`` e adicione ao seu final as configurações abaixo para o virtualenvwrapper:: - - export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3 - export WORKON_HOME=$HOME/.virtualenvs - export PROJECT_HOME=$HOME/Envs - source /usr/local/bin/virtualenvwrapper.sh - -* Saia do terminal e entre novamente para que as configurações do virtualenvwrapper sejam carregadas. - -Clonar o projeto do github, ou fazer um fork e depois clonar ------------------------------------------------------------- - -* Para apenas clonar do repositório do Interlegis:: - - cd ~/Envs - git clone git://github.com/interlegis/sapl - -* Para fazer um fork e depois clonar, siga as instruções em https://help.github.com/articles/fork-a-repo que basicamente são: - - * Criar uma conta no github - é gratuíto. - * Acessar https://github.com/interlegis/sapl e clicar em **Fork**. - - Será criado um domínio pelo qual será possível **clonar, corrigir, customizar, melhorar, contribuir, etc**:: - - cd ~/Envs - git clone git://github.com/[SEU NOME]/sapl - -* As configurações e instruções de uso para o git estão espalhadas pela internet e possui muito coisa bacana. As tarefas básicas de git e suas interações com github são tranquilas de se aprender. - - -Criar o ambiente virtual de desenvolvimento para o SAPL -------------------------------------------------------- -* :: - - mkvirtualenv sapl -a $HOME/Envs/sapl -p /usr/bin/python3 - -Instalação e configuração das dependências do projeto ------------------------------------------------------ - -* **Acesse o terminal e entre no virtualenv**:: - - workon sapl - -* **Instalar dependências python**:: - - pip install -r requirements/dev-requirements.txt - -* **Configurar Postgresql**: - - * Acessar Postrgresql para criar o banco ``sapl`` com a role ``sapl``:: - - sudo su - postgres - psql - - CREATE ROLE sapl LOGIN - ENCRYPTED PASSWORD 'sapl' - NOSUPERUSER INHERIT CREATEDB NOCREATEROLE NOREPLICATION; - - ALTER ROLE sapl VALID UNTIL 'infinity'; - - CREATE DATABASE sapl - WITH OWNER = sapl - ENCODING = 'UTF8' - TABLESPACE = pg_default - LC_COLLATE = 'pt_BR.UTF-8' - LC_CTYPE = 'pt_BR.UTF-8' - CONNECTION LIMIT = -1; - - \q - exit - - * Se você possui uma cópia da base de dados do SAPL, essa é a hora para restaurá-la. - * Obs: no ambiente de desenvolvimento, a role deve ter permissão para criar outro banco. Isso é usado pelos testes automatizados. - * (caso você já possua uma instalação do postrgresql anterior ao processo de instalação do ambiente de desenvolvimento do SAPL em sua máquina e sábia como fazer, esteja livre para proceder como desejar, porém, ao configurar o arquivo ``.env`` no próximo passo, as mesmas definições deverão ser usadas) - -* **Configurar arquivo .env**: - - * Criação da `SECRET_KEY `_: - - É necessário criar um projeto fake para extrair uma chave SECRET_KEY:: - - mkdir ~/Envs/temp - cd ~/Envs/temp - - django-admin startproject sapl_temp - - grep SECRET_KEY sapl_temp/sapl_temp/settings.py - - Copie a linha que aparecerá, volte para a pasta do projeto SAPL e apague sua pasta temporária:: - - cd ~/Envs/sapl - rm -R ~/Envs/temp - - * Criar o arquivo ``.env`` dentro da pasta ~/Envs/sapl/sapl/.env:: - - DATABASE_URL = postgresql://USER:PASSWORD@HOST:PORT/NAME - SECRET_KEY = Gere alguma chave e coloque aqui - DEBUG = [True/False] - EMAIL_USE_TLS = [True/False] - EMAIL_PORT = [Insira este parâmetro] - EMAIL_HOST = [Insira este parâmetro] - EMAIL_HOST_USER = [Insira este parâmetro] - EMAIL_HOST_PASSWORD = [Insira este parâmetro] - - * Uma configuração mínima para atender os procedimentos acima seria:: - - DATABASE_URL = postgresql://sapl:sapl@localhost:5432/sapl - SECRET_KEY = 'Substitua esta linha pela copiada acima' - DEBUG = True - EMAIL_USE_TLS = True - EMAIL_PORT = 587 - EMAIL_HOST = - EMAIL_HOST_USER = - EMAIL_HOST_PASSWORD = - - - -* Instalar as dependências do ``bower``:: - - ./manage.py bower install - -* Atualizar e/ou criar as tabelas da base de dados para refletir o modelo da versão clonada:: - - ./manage.py migrate - -* Atualizar arquivos estáticos:: - - ./manage.py collectstatic --noinput - -* Subir o servidor do django:: - - ./manage.py runserver - -* Acesse o SAPL em:: - - http://localhost:8000/ - -Instruções para criação do super usuário e de usuários de testes -=========================================================================== - -* Criar super usuário do django-contrib-admin (Será solicitado alguns dados para criação):: - - ./manage.py createsuperuser - -* `Os perfis semânticos do SAPL `_ são fixos e atualizados a cada execução do comando: - - ./manage.py migrate - - * Os perfis fixos não aceitam customização via admin, porém outros grupos podem ser criados. O SAPL não interferirá no conjunto de permissões definidas em grupos customizados e se comportará diante de usuários segundo seus grupos e suas permissões. - -* Os usuários de testes de perfil são criados apenas se o SAPL estiver rodando em modo DEBUG=True. Todos com senha "interlegis", serão:: - - operador_administrativo - operador_protocoloadm - operador_comissoes - operador_materia - operador_norma - operador_sessao - operador_painel - operador_geral Instruções para Tradução ======================== - -Nós utilizamos o `Transifex `_ para gerenciar as traduções do projeto. -Se você deseja contribuir, por favor crie uma conta no site e peça para se juntar a nós em `Transifex SAPL Page `_. -Assim que for aceito, você já pode começar a traduzir. - -Para integrar as últimas traduções ao projeto atual, siga estes passos: - -* Siga as instruções em `Development Environment Installation`_. - -* Instale `Transifex Client `_. - -Aviso: - - O Transifex Client armazena senhas em 'plain text' no arquivo ``~/.transifexrc``. - - Nós preferimos logar no site do Transifex por meio de redes sociais (GitHub, Google Plus, Linkedin) e modificar, frequentemente, a senha utilizada pelo client. - -* `Pull translations `_ ou `push translations `_ usando o client. Faça o Pull somente com o repositório vazio, isto é, faça o commit de suas mudanças antes de fazer o Pull de novas traduções. - -* Execute o programa com ``.manage.py runserver`` e cheque o sistema para ver se as traduções tiveram efeito. - -Nota: - - O idioma do browser é utilizado para escolher as traduções que devem mostradas. + `Instruções para Tradução `_ Orientações gerais de implementação =================================== + `Instruções para Implementação `_ -Boas Práticas --------------- - -* Utilize a língua portuguesa em todo o código, nas mensagens de commit e na documentação do projeto. - -* Mensagens de commit seguem o padrão de 50/72 colunas. Comece toda mensagem de commit com o verbo no infinitivo. Para mais informações, clique nos links abaixo: - - Http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html - - Http://stackoverflow.com/questions/2290016/git-commit-messages-50-72-formatting -* Mantenha todo o código de acordo com o padrão da PEP8 (sem exceções). - -* Antes de todo ``git push``: - - - Execute ``git pull --rebase`` (quase sempre). - - Em casos excepcionais, faça somente ``git pull`` para criar um merge. - -* Antes de ``git commit``, sempre: - - - Execute ``./manage.py check`` - - Execute todos os testes com ``py.test`` na pasta raiz do projeto - -* Em caso de Implementação de modelo que envolva a classe ``django.contrib.auth.models.User``, não a use diretamente, use para isso a função ``get_settings_auth_user_model()`` de ``sapl.utils``. Exemplo: - - - no lugar de ``owner = models.ForeignKey(User, ... )`` - - use ``owner = models.ForeignKey(get_settings_auth_user_model(), ... )`` - - - Não use em qualquer modelagem futura, ``ForeignKey`` com ``User`` ou mesmo ``settings.AUTH_USER_MODEL`` sem o import correto que não é o do projeto e sim o que está em ``sapl.utils``, ou seja (``from django.conf import settings``) - - - em https://docs.djangoproject.com/en/1.9/topics/auth/customizing/#referencing-the-user-model é explicado por que ser dessa forma! - - - Já em qualquer uso em implementação de execução, ao fazer uma query, por exemplo: - - - não use ``django.contrib.auth.models.User`` para utilizar as caracteristicas do model, para isso, use esta função: django.contrib.auth.get_user_model() - - - Seguir esses passos simplificará qualquer customização futura que venha a ser feita na autenticação do usuários ao evitar correções de inúmeros import's e ainda, desta forma, torna a funcionalidade de autenticação reimplementável por qualquer outro projeto que venha usar partes ou o todo do SAPL. - -Atenção: - - O usuário do banco de dados ``sapl`` deve ter a permissão ``create database`` no postgres para que os testes tenham sucesso - -* Se você não faz parte da equipe principal, faça o fork deste repositório e envie pull requests. - Todos são bem-vindos para contribuir. Por favor, faça uma pull request separada para cada correção ou criação de novas funcionalidades. - -* Novas funcionalidades estão sujeitas a aprovação, uma vez que elas podem ter impacto em várias pessoas. - Nós sugerimos que você abra uma nova issue para discutir novas funcionalidades. Elas podem ser escritas tanto em Português, quanto em Inglês. - - - -Testes ------- - -* Escrever testes para todas as funcionalidades que você implementar. - -* Manter a cobertura de testes próximo a 100%. - -* Para executar todos os testes você deve entrar em seu virtualenv e executar este comando **na raiz do seu projeto**:: - - py.test - -* Para executar os teste de cobertura use:: - - py.test --cov . --cov-report term --cov-report html && firefox htmlcov/index.html +Orientações gerais sobre o GitHub +=================================== + `Instruções para GitHub `_ -* Na primeira vez que for executar os testes após uma migração (``./manage.py migrate``) use a opção de recriação da base de testes. - É necessário fazer usar esta opção apenas uma vez:: - py.test --create-db Issues ------ diff --git a/docs/deploy.rst b/docs/deploy.rst new file mode 100644 index 000000000..d8f2ec9bb --- /dev/null +++ b/docs/deploy.rst @@ -0,0 +1,110 @@ +============================== +Instruções para fazer o Deploy +============================== + +Para efeitos deste doc, foram consideradas as tecnologias NGINX + GUNICORN para servir a aplicação Django SAPL. + +O NGINX é o servidor WEB, e o GUNICORN é o servidor da aplicação para o servidor WEB. + + + +É altamente recomendável que para produção o SAPL não seja executado em modo debug. +Para isso edite o arquivo ``.env`` criado anteriormente em:: + + sudo nano /var/interlegis/sapl/sapl/.env + +alterando o variável DEBUG para false:: + + DEBUG = False + + +Instalando Pacotes +------------------ + +Instalar o NGINX:: + + sudo apt-get install nginx + + +Instalar o Gunicorn:: + + sudo pip install gunicorn + + +Preparando o NGINX +------------------ +sudo nano /etc/nginx/sites-available/sapl31.conf:: + + upstream ENDERECO_SITE { + server unix:/var/interlegis/sapl/run/gunicorn.sock fail_timeout=0; + } + + server { + + listen 80; + server_name ENDERECO_SITE; + + client_max_body_size 4G; + + access_log /var/log/nginx-access.log; + error_log /var/log/nginx-error.log; + + location /static/ { + alias /var/interlegis/sapl/collected_static/; + } + + location /media/ { + alias /var/interlegis/sapl/media/; + } + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + if (!-f $request_filename) { + proxy_pass http://ENDERECO_SITE; + break; + } + } + + # Error pages + error_page 500 502 503 504 /500.html; + location = /500.html { + root /var/interlegis/sapl/sapl/static/; + } + } + + +Criar link simbólico para ativar o site:: + + sudo ln -s /etc/nginx/sites-available/sapl31.conf /etc/nginx/sites-enabled/sapl3 + +Reiniciar o nginx + + sudo service nginx restart + + +Preparando o Gunicorn +--------------------- +Na raiz do Projeto sapl, existe o arquivo chamado gunicorn_start.sh + +Para definir o parametro NUM_WORKERS utilize a seguinte fórmula: 2 * CPUs 1. +Para uma máquina de CPU única o valor seria 3 + + +Para rodar o gunicorn:: + + workon sapl + + /var/interlegis/sapl/.gunicorn_start.sh + + + +#Referências. + +http://michal.karzynski.pl/blog/2013/06/09/django-nginx-gunicorn-virtualenv-supervisor/ + +Para multiplas aplicações Django. + +http://michal.karzynski.pl/blog/2013/10/29/serving-multiple-django-applications-with-nginx-gunicorn-supervisor/ + diff --git a/docs/howtogit.rst b/docs/howtogit.rst new file mode 100644 index 000000000..926d3193b --- /dev/null +++ b/docs/howtogit.rst @@ -0,0 +1,41 @@ +De forma muito simples e em linhas gerais o básico sobre GIT + +Glosário +--------- + + Git - Sistema de controle de versão de aquivos + + GitHub - É um serviço web que oferece diversas funcionalidades extras aplicadas ao git + + Branch - Significa ramificar seu projeto, criar um snapshot. + + Merge - Significa incorporar seu branch no master + + +Pode ser útil +------------- + +Atualizar a base local: + + git pull --rebase git://github.com/interlegis/sapl + +Exibir informações: + + git status + + +Na base local descartar alguma alteração feita nos arquivos: + + git checkout sapl/legacy_migration_settings.py + +Atualizar para alguma brach especifica (ex:785-atualizar-migracao): + + git checkout 785-atualizar-migracao + +Voltar para a branch master + + git checkout master + +Verificar 5 ultimos comits: + + git log --oneline -n 5 diff --git a/docs/implementacoes.rst b/docs/implementacoes.rst new file mode 100644 index 000000000..0a8aed013 --- /dev/null +++ b/docs/implementacoes.rst @@ -0,0 +1,73 @@ + +Orientações gerais de implementação +------------------------------------ + + +Boas Práticas +-------------- + +* Utilize a língua portuguesa em todo o código, nas mensagens de commit e na documentação do projeto. + +* Mensagens de commit seguem o padrão de 50/72 colunas. Comece toda mensagem de commit com o verbo no infinitivo. Para mais informações, clique nos links abaixo: + + - Http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html + - Http://stackoverflow.com/questions/2290016/git-commit-messages-50-72-formatting + +* Mantenha todo o código de acordo com o padrão da PEP8 (sem exceções). + +* Antes de todo ``git push``: + + - Execute ``git pull --rebase`` (quase sempre). + - Em casos excepcionais, faça somente ``git pull`` para criar um merge. + +* Antes de ``git commit``, sempre: + + - Execute ``./manage.py check`` + - Execute todos os testes com ``py.test`` na pasta raiz do projeto + +* Em caso de Implementação de modelo que envolva a classe ``django.contrib.auth.models.User``, não a use diretamente, use para isso a função ``get_settings_auth_user_model()`` de ``sapl.utils``. Exemplo: + + - no lugar de ``owner = models.ForeignKey(User, ... )`` + - use ``owner = models.ForeignKey(get_settings_auth_user_model(), ... )`` + + - Não use em qualquer modelagem futura, ``ForeignKey`` com ``User`` ou mesmo ``settings.AUTH_USER_MODEL`` sem o import correto que não é o do projeto e sim o que está em ``sapl.utils``, ou seja (``from django.conf import settings``) + + - em https://docs.djangoproject.com/en/1.9/topics/auth/customizing/#referencing-the-user-model é explicado por que ser dessa forma! + + - Já em qualquer uso em implementação de execução, ao fazer uma query, por exemplo: + + - não use ``django.contrib.auth.models.User`` para utilizar as caracteristicas do model, para isso, use esta função: django.contrib.auth.get_user_model() + + - Seguir esses passos simplificará qualquer customização futura que venha a ser feita na autenticação do usuários ao evitar correções de inúmeros import's e ainda, desta forma, torna a funcionalidade de autenticação reimplementável por qualquer outro projeto que venha usar partes ou o todo do SAPL. + +Atenção: + + O usuário do banco de dados ``sapl`` deve ter a permissão ``create database`` no postgres para que os testes tenham sucesso + +* Se você não faz parte da equipe principal, faça o fork deste repositório e envie pull requests. + Todos são bem-vindos para contribuir. Por favor, faça uma pull request separada para cada correção ou criação de novas funcionalidades. + +* Novas funcionalidades estão sujeitas a aprovação, uma vez que elas podem ter impacto em várias pessoas. + Nós sugerimos que você abra uma nova issue para discutir novas funcionalidades. Elas podem ser escritas tanto em Português, quanto em Inglês. + + + +Testes +------ + +* Escrever testes para todas as funcionalidades que você implementar. + +* Manter a cobertura de testes próximo a 100%. + +* Para executar todos os testes você deve entrar em seu virtualenv e executar este comando **na raiz do seu projeto**:: + + py.test + +* Para executar os teste de cobertura use:: + + py.test --cov . --cov-report term --cov-report html && firefox htmlcov/index.html + +* Na primeira vez que for executar os testes após uma migração (``./manage.py migrate``) use a opção de recriação da base de testes. + É necessário fazer usar esta opção apenas uma vez:: + + py.test --create-db diff --git a/docs/importacao_25_31.rst b/docs/importacao_25_31.rst new file mode 100644 index 000000000..62850d1bc --- /dev/null +++ b/docs/importacao_25_31.rst @@ -0,0 +1,31 @@ +Instruções para Importação da base mysql 2.5 +============================================ + + +Para entrar no ambiente virtual:: + + workon sapl + + + +Instalar Dependências:: + + pip3 install -r requirements/migration-requirements.txt + +Criar um arquivo sapl/legacy/.env com o seguinte conteúdo (parametros de acesso ao banco 2.5):: + + DATABASE_URL = mysql://[usuario do mysql]:[senha do myuysql]@[host]:[porta]/[banco] + + +o conteúdo do arquivo será semelhante a isso:: + + DATABASE_URL = mysql://sapl:sapl@localhost:3306/interlegis + + +Posteriormente rodar a seguinte sequencia de comandos estando no ambiente virtual:: + + ./manage.py shell --settings=sapl.legacy_migration_settings + + %run sapl/legacy/migration.py + + migrate() diff --git a/docs/instacao31.rst b/docs/instacao31.rst new file mode 100644 index 000000000..4307a5d6c --- /dev/null +++ b/docs/instacao31.rst @@ -0,0 +1,220 @@ +Instalação do Ambiente de Desenvolvimento +========================================= + +* Procedimento testado nos seguintes SO's: + + * `Ubuntu 16.04 64bits `_; + +* Para esta instalação foi utilizado o usuário de sistema sapl31 + + +Atualizar o sistema:: +---------------------- + + :: + + sudo apt-get update + + sudo apt-get upgrade + + + +Instalar as seguintes dependências do sistema:: +---------------------------------------------------------------------------------------- + +* :: + + sudo apt-get install git python3-dev libpq-dev graphviz-dev graphviz \ + pkg-config postgresql postgresql-contrib pgadmin3 python-psycopg2 \ + software-properties-common build-essential libxml2-dev libjpeg-dev \ + libmysqlclient-dev libssl-dev libffi-dev libxslt1-dev python3-setuptools \ + python3-pip curl + + sudo -i + curl -sL https://deb.nodesource.com/setup_5.x | bash - + exit + sudo apt-get install nodejs + + sudo npm install npm -g + sudo npm install -g bower + +Instalar o virtualenv usando python 3 para o projeto. +----------------------------------------------------- + +* Para usar `virtualenvwrapper `_, instale com:: + + sudo pip3 install virtualenvwrapper + + sudo mkdir -p /var/interlegis/.virtualenvs + +* Ajustar as permissões - onde sapl31 trocar por usuario:: + + sudo chown -R sapl31:sapl31 /var/interlegis/ + + +* Edite o arquivo ``.bashrc`` e adicione ao seu final as configurações abaixo para o virtualenvwrapper:: + + nano /home/sapl31/.bashrc + + export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3 + export WORKON_HOME=/var/interlegis/.virtualenvs + export PROJECT_HOME=/var/interlegis + source /usr/local/bin/virtualenvwrapper.sh + + +* Carregue as configurações do virtualenvwrapper:: + + source /home/sapl31/.bashrc + + + +Clonar o projeto do github, ou fazer um fork e depois clonar +------------------------------------------------------------ + +* Para apenas clonar do repositório do Interlegis:: + + cd /var/interlegis + git clone git://github.com/interlegis/sapl + +* Para fazer um fork e depois clonar, siga as instruções em https://help.github.com/articles/fork-a-repo que basicamente são: + + * Criar uma conta no github - é gratuíto. + * Acessar https://github.com/interlegis/sapl e clicar em **Fork**. + + Será criado um domínio pelo qual será possível **clonar, corrigir, customizar, melhorar, contribuir, etc**:: + + cd /var/interlegis + git clone git://github.com/[SEU NOME]/sapl + +* As configurações e instruções de uso para o git estão espalhadas pela internet e possui muito coisa bacana. As tarefas básicas de git e suas interações com github são tranquilas de se aprender. + + +Criar o ambiente virtual de desenvolvimento para o SAPL +------------------------------------------------------- +* :: + + mkvirtualenv sapl -a /var/interlegis/sapl -p /usr/bin/python3 + +Instalação e configuração das dependências do projeto +----------------------------------------------------- + +* **Acesse o terminal e entre no virtualenv**:: + + workon sapl + +* **Instalar dependências python**:: + + pip install -r /var/interlegis/sapl/requirements/dev-requirements.txt + +* **Configurar Postgresql**:: + + sudo -u postgres psql -c "CREATE ROLE sapl LOGIN ENCRYPTED PASSWORD 'sapl' NOSUPERUSER INHERIT CREATEDB NOCREATEROLE NOREPLICATION;" + + sudo -u postgres psql -c "ALTER ROLE sapl VALID UNTIL 'infinity';" + + sudo -u postgres psql -c "CREATE DATABASE sapl WITH OWNER = sapl ENCODING = 'UTF8' TABLESPACE = pg_default LC_COLLATE = 'pt_BR.UTF-8' LC_CTYPE = 'pt_BR.UTF-8' CONNECTION LIMIT = -1;" + + * Se você possui uma cópia da base de dados do SAPL, essa é a hora para restaurá-la. + * Obs: no ambiente de desenvolvimento, a role deve ter permissão para criar outro banco. Isso é usado pelos testes automatizados. + * (caso você já possua uma instalação do postrgresql anterior ao processo de instalação do ambiente de desenvolvimento do SAPL em sua máquina e sábia como fazer, esteja livre para proceder como desejar, porém, ao configurar o arquivo ``.env`` no próximo passo, as mesmas definições deverão ser usadas) + + +* **Ajustar as permissões - onde sapl31 trocar por usuario**:: + + sudo chown -R sapl31:sapl31 /var/interlegis/ + + + +* **Configurar arquivo .env**:: + + +Criação da `SECRET_KEY `_: + + +* **Criar o arquivo ``.env`` dentro da pasta /var/interlegis/sapl/sapl/.env**:: + + nano /var/interlegis/sapl/sapl/.env + + DATABASE_URL = postgresql://USER:PASSWORD@HOST:PORT/NAME + SECRET_KEY = Gere alguma chave e coloque aqui + DEBUG = [True/False] + EMAIL_USE_TLS = [True/False] + EMAIL_PORT = [Insira este parâmetro] + EMAIL_HOST = [Insira este parâmetro] + EMAIL_HOST_USER = [Insira este parâmetro] + EMAIL_HOST_PASSWORD = [Insira este parâmetro] + DEFAULT_FROM_EMAIL = [Insira este parâmetro] + SERVER_EMAIL = [Insira este parâmetro] + + + * Uma configuração mínima para atender os procedimentos acima seria:: + + DATABASE_URL = postgresql://sapl:sapl@localhost:5432/sapl + SECRET_KEY = 'cole aqui entre as aspas simples a chave gerada pelo comando abaixo' + DEBUG = True + EMAIL_USE_TLS = True + EMAIL_PORT = 587 + EMAIL_HOST = + EMAIL_HOST_USER = + EMAIL_HOST_PASSWORD = + DEFAULT_FROM_EMAIL = + SERVER_EMAIL = + + + + +Rodar o comando abaixo, um detalhe importante, esse comando só funciona com o django extensions, mas ele já está presente no arquivo requirements/requirements.txt desse projeto:: + + python manage.py generate_secret_key + +Copie a chave que aparecerá, edite o arquivo .env e altere o valor do parâmetro SECRET_KEY. + + +* Posicionar-se no diretorio do Projeto:: + + cd /var/interlegis/sapl + + +* Instalar as dependências do ``bower``:: + + ./manage.py bower install + +* Atualizar e/ou criar as tabelas da base de dados para refletir o modelo da versão clonada:: + + ./manage.py migrate + +* Atualizar arquivos estáticos:: + + ./manage.py collectstatic --noinput + +* Subir o servidor do django:: + + ./manage.py runserver 0.0.0.0:8001 + +* Acesse o SAPL em:: + + http://localhost:8001/ + +Instruções para criação do super usuário e de usuários de testes +=========================================================================== + +* Criar super usuário do django-contrib-admin (Será solicitado alguns dados para criação):: + + ./manage.py createsuperuser + +* `Os perfis semânticos do SAPL `_ são fixos e atualizados a cada execução do comando:: + + ./manage.py migrate + +* Os perfis fixos não aceitam customização via admin, porém outros grupos podem ser criados. O SAPL não interferirá no conjunto de permissões definidas em grupos customizados e se comportará diante de usuários segundo seus grupos e suas permissões. + +* Os usuários de testes de perfil são criados apenas se o SAPL estiver rodando em modo DEBUG=True. Todos com senha "interlegis", serão:: + + operador_administrativo + operador_protocoloadm + operador_comissoes + operador_materia + operador_norma + operador_sessao + operador_painel + operador_geral diff --git a/docs/traducao.rst b/docs/traducao.rst new file mode 100644 index 000000000..25173dc4b --- /dev/null +++ b/docs/traducao.rst @@ -0,0 +1,27 @@ + +Instruções para Tradução +======================== + +Nós utilizamos o `Transifex `_ para gerenciar as traduções do projeto. +Se você deseja contribuir, por favor crie uma conta no site e peça para se juntar a nós em `Transifex SAPL Page `_. +Assim que for aceito, você já pode começar a traduzir. + +Para integrar as últimas traduções ao projeto atual, siga estes passos: + +* Siga as instruções em `Development Environment Installation`_. + +* Instale `Transifex Client `_. + +Aviso: + + O Transifex Client armazena senhas em 'plain text' no arquivo ``~/.transifexrc``. + + Nós preferimos logar no site do Transifex por meio de redes sociais (GitHub, Google Plus, Linkedin) e modificar, frequentemente, a senha utilizada pelo client. + +* `Pull translations `_ ou `push translations `_ usando o client. Faça o Pull somente com o repositório vazio, isto é, faça o commit de suas mudanças antes de fazer o Pull de novas traduções. + +* Execute o programa com ``.manage.py runserver`` e cheque o sistema para ver se as traduções tiveram efeito. + +Nota: + + O idioma do browser é utilizado para escolher as traduções que devem mostradas. diff --git a/gunicorn_start.sh b/gunicorn_start.sh index f68613c26..904d76a3a 100755 --- a/gunicorn_start.sh +++ b/gunicorn_start.sh @@ -3,8 +3,8 @@ # As seen in http://tutos.readthedocs.org/en/latest/source/ndg.html NAME="SAPL" # Name of the application (*) -DJANGODIR=/home/sapl31/sapl # Django project directory (*) -SOCKFILE=/home/sapl31/sapl/run/gunicorn.sock # we will communicate using this unix socket (*) +DJANGODIR=/var/interlegis/sapl # Django project directory (*) +SOCKFILE=/var/interlegis/sapl/run/gunicorn.sock # we will communicate using this unix socket (*) USER=`whoami` # the user to run as (*) GROUP=`whoami` # the group to run as (*) NUM_WORKERS=9 # how many worker processes should Gunicorn spawn (*) @@ -16,7 +16,7 @@ echo "Starting $NAME as `whoami`" # Activate the virtual environment cd $DJANGODIR -source ~/.virtualenvs/sapl/bin/activate +source /var/interlegis/.virtualenvs/sapl/bin/activate export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE export PYTHONPATH=$DJANGODIR:$PYTHONPATH diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 4387cb37d..c6e9f2266 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,9 @@ dj-database-url==0.4.1 -django==1.9.7 -django-admin-bootstrapped==2.5.7 +django>=1.9,<1.10 +# TODO O django-admin-bootstrapped 2.5.7 não inseriu a mudança que permite +# a compatibilidade com Django 1.9+. A linha abaixo será mudada quando uma +# nova versão do django-admin-bootstrapped for lançada +git+git://github.com/django-admin-bootstrapped/django-admin-bootstrapped.git django-bootstrap3==7.0.1 django-bower==5.1.0 django-braces==1.9.0 diff --git a/sapl/api/serializers.py b/sapl/api/serializers.py index bd3fb7def..a7ff559e3 100644 --- a/sapl/api/serializers.py +++ b/sapl/api/serializers.py @@ -51,3 +51,4 @@ class MateriaLegislativaSerializer(serializers.ModelSerializer): class Meta: model = MateriaLegislativa + fields = '__all__' diff --git a/sapl/api/views.py b/sapl/api/views.py index 41d327bff..5dd86cf09 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework.filters import DjangoFilterBackend from rest_framework.generics import ListAPIView from rest_framework.mixins import ListModelMixin, RetrieveModelMixin -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly from rest_framework.viewsets import GenericViewSet from sapl.api.forms import AutorChoiceFilterSet @@ -59,10 +59,6 @@ class AutorListView(ListAPIView): de Autores mas feito para Possíveis Autores armazenados segundo o ContentType associado ao Tipo de Autor via relacionamento genérico. -<<<<<<< HEAD - -======= ->>>>>>> master Busca feita sem django-filter processada no get_queryset -> processo no cadastro de autores para seleção e busca dos possíveis autores @@ -87,7 +83,7 @@ class AutorListView(ListAPIView): TR_AUTOR_SERIALIZER = 3 # FIXME aplicar permissão correta de usuário - permission_classes = (IsAuthenticated,) + permission_classes = (IsAuthenticatedOrReadOnly,) queryset = Autor.objects.all() model = Autor @@ -116,7 +112,6 @@ class AutorListView(ListAPIView): desativa o django-filter se a busca for por possiveis autores parametro tr = TR_CHOICE_SERIALIZER """ - if self.tr == AutorListView.TR_CHOICE_SERIALIZER: self.filter_class = None self.filter_backends = [] diff --git a/sapl/base/admin.py b/sapl/base/admin.py index ae581e031..02ccd3c60 100644 --- a/sapl/base/admin.py +++ b/sapl/base/admin.py @@ -8,6 +8,9 @@ register_all_models_in_admin(__name__) admin.site.unregister(ProblemaMigracao) +admin.site.site_title = 'Administração - SAPL' +admin.site.site_header = 'Administração - SAPL' + @admin.register(ProblemaMigracao) class ProblemaMigracaoAdmin(admin.ModelAdmin): diff --git a/sapl/base/forms.py b/sapl/base/forms.py index 5b4d9f734..7f1eef180 100644 --- a/sapl/base/forms.py +++ b/sapl/base/forms.py @@ -1,19 +1,20 @@ -import django_filters from crispy_forms.bootstrap import FieldWithButtons, InlineRadios, StrictButton from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Button, Div, Field, Fieldset, Layout, Row from django import forms from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.auth.forms import AuthenticationForm -from django.contrib.auth.models import Group +from django.contrib.auth.forms import (AuthenticationForm, PasswordResetForm, + SetPasswordForm) +from django.contrib.auth.models import Group, User from django.contrib.auth.password_validation import validate_password from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db import models, transaction from django.forms import ModelForm -from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat +from django.utils.translation import ugettext_lazy as _ +import django_filters from sapl.base.models import Autor, TipoAutor from sapl.crispy_layout_mixin import (SaplFormLayout, form_actions, to_column, @@ -27,6 +28,7 @@ from sapl.utils import (RANGE_ANOS, ChoiceWithoutValidationField, from .models import AppConfig, CasaLegislativa + ACTION_CREATE_USERS_AUTOR_CHOICE = [ ('C', _('Criar novo Usuário')), ('A', _('Associar um usuário existente')), @@ -96,7 +98,7 @@ class AutorForm(ModelForm): label=_('Confirmar Email')) username = forms.CharField(label=get_user_model()._meta.get_field( - 'username').verbose_name.capitalize(), + get_user_model().USERNAME_FIELD).verbose_name.capitalize(), required=False, max_length=50) @@ -188,26 +190,36 @@ class AutorForm(ModelForm): self.fields['autor_related'].initial = self.instance.autor_related if self.instance.user: - self.fields['username'].initial = self.instance.user.username + self.fields['username'].initial = getattr( + self.instance.user, + get_user_model().USERNAME_FIELD) self.fields['action_user'].initial = 'A' self.fields['username'].label = string_concat( self.fields['username'].label, - ' (', self.instance.user.username, ')') + ' (', getattr( + self.instance.user, + get_user_model().USERNAME_FIELD), ')') if 'status_user' in self.Meta.fields: self.fields['status_user'].initial = 'R' self.fields['status_user'].label = string_concat( self.fields['status_user'].label, - ' (', self.instance.user.username, ')') + ' (', getattr( + self.instance.user, + get_user_model().USERNAME_FIELD), ')') self.fields['username'].widget.attrs.update({ - 'data': self.instance.user.username + 'data': getattr( + self.instance.user, + get_user_model().USERNAME_FIELD) if self.instance.user else ''}) if 'status_user' in self.Meta.fields: self.fields['status_user'].widget.attrs.update({ - 'data': self.instance.user.username + 'data': getattr( + self.instance.user, + get_user_model().USERNAME_FIELD) if self.instance.user else ''}) def valida_igualdade(self, texto1, texto2, msg): @@ -225,7 +237,9 @@ class AutorForm(ModelForm): if 'status_user' in self.Meta.fields: if self.instance.pk and self.instance.user_id: - if self.instance.user.username != cd['username']: + if getattr( + self.instance.user.username, + get_user_model().USERNAME_FIELD) != cd['username']: if 'status_user' not in cd or not cd['status_user']: raise ValidationError( _('Foi trocado ou removido o usuário deste Autor, ' @@ -241,7 +255,8 @@ class AutorForm(ModelForm): qs_user = qs_user.exclude(pk=self.instance.user.pk) if cd['action_user'] == 'C': - if User.objects.filter(username=cd['username']).exists(): + param_username = {get_user_model().USERNAME_FIELD: cd['username']} + if User.objects.filter(**param_username).exists(): raise ValidationError( _('Já existe usuário com o username "%s". ' 'Para utilizar esse username você deve selecionar ' @@ -275,7 +290,8 @@ class AutorForm(ModelForm): _('Já existe um Autor com este email.')) elif cd['action_user'] == 'A': - if not User.objects.filter(username=cd['username']).exists(): + param_username = {get_user_model().USERNAME_FIELD: cd['username']} + if not User.objects.filter(**param_username).exists(): raise ValidationError( _('Não existe usuário com username "%s". ' 'Para utilizar esse username você deve selecionar ' @@ -286,7 +302,9 @@ class AutorForm(ModelForm): if 'username' not in cd or not cd['username']: raise ValidationError(_('O username deve ser informado.')) - if qs_autor.filter(user__username=cd['username']).exists(): + param_username = { + 'user__' + get_user_model().USERNAME_FIELD: cd['username']} + if qs_autor.filter(**param_username).exists(): raise ValidationError( _('Já existe um Autor para este usuário.')) @@ -332,16 +350,23 @@ class AutorForm(ModelForm): user_old = autor.user if autor.user_id else None u = None + param_username = { + get_user_model().USERNAME_FIELD: self.cleaned_data['username']} if self.cleaned_data['action_user'] == 'A': - u = get_user_model().objects.get( - username=self.cleaned_data['username']) + u = get_user_model().objects.get(**param_username) if not u.is_active: u.is_active = settings.DEBUG u.save() elif self.cleaned_data['action_user'] == 'C': - u = get_user_model().objects.create( - username=self.cleaned_data['username'], - email=self.cleaned_data['email']) + + param_username = { + get_user_model().USERNAME_FIELD: self.cleaned_data['username']} + + if get_user_model().USERNAME_FIELD != 'email': + param_username['email'] = self.cleaned_data['email'] + + u = get_user_model().objects.create(**param_username) + u.set_password(self.cleaned_data['senha']) # Define usuário como ativo em ambiente de desenvolvimento # pode logar sem a necessidade de passar pela validação de email @@ -671,3 +696,42 @@ class ConfiguracoesAppForm(ModelForm): 'texto_articulado_materia', 'texto_articulado_norma', 'proposicao_incorporacao_obrigatoria'] + + +class RecuperarSenhaForm(PasswordResetForm): + def __init__(self, *args, **kwargs): + row1 = to_row( + [('email', 12)]) + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset(_('Insira o e-mail cadastrado com a sua conta'), + row1, + form_actions(save_label='Enviar')) + ) + + super(RecuperarSenhaForm, self).__init__(*args, **kwargs) + + def clean(self): + email_existente = User.objects.filter( + email=self.data['email']).exists() + + if not email_existente: + msg = 'Não existe nenhum usuário cadastrado com este e-mail.' + raise ValidationError(msg) + + return self.cleaned_data + + +class NovaSenhaForm(SetPasswordForm): + def __init__(self, user, *args, **kwargs): + self.user = user + super(NovaSenhaForm, self).__init__(user, *args, **kwargs) + + row1 = to_row( + [('new_password1', 6), + ('new_password2', 6)]) + + self.helper = FormHelper() + self.helper.layout = Layout( + row1, + form_actions(save_label='Enviar')) diff --git a/sapl/base/urls.py b/sapl/base/urls.py index c2a9fdc4a..08f456f3b 100644 --- a/sapl/base/urls.py +++ b/sapl/base/urls.py @@ -1,12 +1,17 @@ from django.conf.urls import include, url from django.contrib.auth import views +from django.contrib.auth.views import (password_reset, + password_reset_done, + password_reset_confirm, + password_reset_complete) from django.contrib.auth.decorators import permission_required from django.views.generic.base import TemplateView from sapl.base.views import AutorCrud, ConfirmarEmailView, TipoAutorCrud from .apps import AppConfig -from .forms import LoginForm +from .forms import LoginForm, NovaSenhaForm, RecuperarSenhaForm +from sapl.settings import EMAIL_SEND_USER from .views import (AppConfigCrud, CasaLegislativaCrud, HelpView, RelatorioAtasView, RelatorioHistoricoTramitacaoView, RelatorioMateriasPorAnoAutorTipoView, @@ -16,6 +21,35 @@ from .views import (AppConfigCrud, CasaLegislativaCrud, HelpView, app_name = AppConfig.name +recuperar_senha = [ + url(r'^recuperar-senha/email/$', + password_reset, + {'post_reset_redirect': 'sapl.base:recuperar_senha_finalizado', + 'email_template_name': 'base/recuperar_senha_email.html', + 'html_email_template_name': 'base/recuperar_senha_email.html', + 'template_name': 'base/recuperar_senha_email_form.html', + 'from_email': EMAIL_SEND_USER, + 'password_reset_form': RecuperarSenhaForm}, + name='recuperar_senha_email'), + + url(r'^recuperar-senha/finalizado/$', + password_reset_done, + {'template_name': 'base/recupera_senha_email_enviado.html'}, + name='recuperar_senha_finalizado'), + + url(r'^recuperar-senha/(?P[0-9A-Za-z_\-]+)/(?P.+)/$', + password_reset_confirm, + {'post_reset_redirect': 'sapl.base:recuperar_senha_completo', + 'template_name': 'base/nova_senha_form.html', + 'set_password_form': NovaSenhaForm}, + name='recuperar_senha_confirma'), + + url(r'^recuperar-senha/completo/$', + password_reset_complete, + {'template_name': 'base/recuperar_senha_completo.html'}, + name='recuperar_senha_completo'), +] + urlpatterns = [ url(r'^sistema/autor/tipo/', include(TipoAutorCrud.get_urls())), @@ -66,4 +100,4 @@ urlpatterns = [ name='login'), url(r'^logout/$', views.logout, {'next_page': '/login'}, name='logout'), -] +] + recuperar_senha diff --git a/sapl/base/views.py b/sapl/base/views.py index 4de2da9fc..9d50c7176 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -62,7 +62,7 @@ class AutorCrud(CrudAux): help_path = 'autor' class BaseMixin(CrudAux.BaseMixin): - list_field_names = ['tipo', 'nome', 'user__username'] + list_field_names = ['tipo', 'nome', 'user'] class DeleteView(CrudAux.DeleteView): diff --git a/sapl/compilacao/compilacao_data_tables.sql b/sapl/compilacao/compilacao_data_tables.sql index d0d815b58..663dbc682 100644 --- a/sapl/compilacao/compilacao_data_tables.sql +++ b/sapl/compilacao/compilacao_data_tables.sql @@ -1,6 +1,10 @@ -INSERT INTO compilacao_perfilestruturaltextoarticulado (id, sigla, nome, padrao) VALUES (2, 'LC95-v', 'Lei Complementar 95 com Variação', false); -INSERT INTO compilacao_perfilestruturaltextoarticulado (id, sigla, nome, padrao) VALUES (1, 'LC95', 'Lei Complementar 95', true); -SELECT pg_catalog.setval('compilacao_perfilestruturaltextoarticulado_id_seq', 2, true); +INSERT INTO compilacao_perfilestruturaltextoarticulado (id, sigla, nome, padrao, parent_id) VALUES (1, 'LC95', 'Lei Complementar 95', true, null); +INSERT INTO compilacao_perfilestruturaltextoarticulado (id, sigla, nome, padrao, parent_id) VALUES (2, 'LC95-v', 'Lei Complementar 95 com Variação', false, null); +INSERT INTO compilacao_perfilestruturaltextoarticulado (id, sigla, nome, padrao, parent_id) VALUES (3, 'PLOL', 'Projeto de Lei Ordinária do Legislativo', false, 1); +INSERT INTO compilacao_perfilestruturaltextoarticulado (id, sigla, nome, padrao, parent_id) VALUES (4, 'REQ', 'Requerimento', false, null); +SELECT pg_catalog.setval('compilacao_perfilestruturaltextoarticulado_id_seq', 4, true); +update compilacao_perfilestruturaltextoarticulado set nome = 'Projeto de Lei Ordinária', sigla = 'PLO' where id = 3; + INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (1, 'Articulação', 'articulacao', '', '', 0, '', '', '', '', '', '', true, 'N', 'N', 'N', 'N', 'N', 'N', '-', '-', '-', '-', '-', true, false); INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (2, 'Ementa', 'ementa', '', '', 0, '', '', '', '', '', '', false, 'N', '1', '1', '1', '1', '1', '-', '-', '-', '-', '-', false, false); @@ -19,12 +23,14 @@ INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (117, 'SubSeção', 'subsecao', '', 'SubSeção ', 0, '', '
', '', '
', '
', '', false, '1', '1', '1', '1', '1', '1', '-', '-', '-', '-', '-', false, false); INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (119, 'Artigo', 'artigo', '', 'Art. ', 9, '.', ' – ', '', '', '', '', true, '1', 'A', '1', '1', '1', '1', '-', '-', '-', '-', '-', true, false); INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (120, 'Caput', 'caput', '', '', 0, '', '', '', '', '', '', false, 'N', 'N', 'N', 'N', 'N', 'N', '-', '-', '-', '-', '-', false, false); -INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (121, 'Parágrafo', 'paragrafo', '', '§ ;Parágrafo Único ', 9, '', ' – ', '', '', '', '', false, '1', '1', '1', '1', '1', '1', '-', '-', '-', '-', '-', false, false); -INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (122, 'Inciso', 'inciso', '', '', 0, '', ' – ', '', '', '', '', false, 'I', '1', '1', '1', '1', '1', '-', '-', '-', '-', '-', false, false); -INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (123, 'Alinea', 'alinea', '', '', 0, ')', ' – ', '', '', '', '', false, 'a', '1', '1', '1', '1', '1', '-', '-', '-', '-', '-', false, false); -INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (124, 'Item', 'item', '', '', 0, '', ' – ', '', '', '', '', false, '1', '1', '1', '1', '1', '1', '.', '.', '.', '.', '.', false, false); +INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (121, 'Parágrafo', 'paragrafo indent', '', '§ ;Parágrafo Único ', 9, '', ' – ', '', '', '', '', false, '1', '1', '1', '1', '1', '1', '-', '-', '-', '-', '-', false, false); +INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (122, 'Inciso', 'inciso indent', '', '', 0, '', ' – ', '', '', '', '', false, 'I', '1', '1', '1', '1', '1', '-', '-', '-', '-', '-', false, false); +INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (123, 'Alinea', 'alinea indent', '', '', 0, ')', ' – ', '', '', '', '', false, 'a', '1', '1', '1', '1', '1', '-', '-', '-', '-', '-', false, false); +INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (124, 'Item', 'item indent', '', '', 0, '', ' – ', '', '', '', '', false, '1', '1', '1', '1', '1', '1', '.', '.', '.', '.', '.', false, false); INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (125, 'Texto Não Estruturado', 'texto_n_estruturado', '', '', 0, '', '', '', '', '', '', true, 'N', 'N', 'N', 'N', 'N', 'N', '-', '-', '-', '-', '-', false, false); -SELECT pg_catalog.setval('compilacao_tipodispositivo_id_seq', 125, true); +INSERT INTO compilacao_tipodispositivo (id, nome, class_css, rotulo_prefixo_html, rotulo_prefixo_texto, rotulo_ordinal, rotulo_sufixo_texto, rotulo_sufixo_html, texto_prefixo_html, texto_sufixo_html, nota_automatica_prefixo_html, nota_automatica_sufixo_html, contagem_continua, formato_variacao0, formato_variacao1, formato_variacao2, formato_variacao3, formato_variacao4, formato_variacao5, rotulo_separador_variacao01, rotulo_separador_variacao12, rotulo_separador_variacao23, rotulo_separador_variacao34, rotulo_separador_variacao45, dispositivo_de_articulacao, dispositivo_de_alteracao) VALUES (126, 'Justificativa', 'justificativa', '', '', 0, '', '', '', '
Justificativa
', '', '', true, 'N', 'N', 'N', 'N', 'N', 'N', '-', '-', '-', '-', '-', false, false); + +SELECT pg_catalog.setval('compilacao_tipodispositivo_id_seq', 126, true); delete from compilacao_tipodispositivorelationship; @@ -245,6 +251,18 @@ INSERT INTO compilacao_tipodispositivorelationship (filho_permitido_id, pai_id, INSERT INTO compilacao_tipodispositivorelationship (filho_permitido_id, pai_id, filho_de_insercao_automatica, perfil_id, quantidade_permitida, permitir_variacao) VALUES (125, 104, false, 1, -1, false); INSERT INTO compilacao_tipodispositivorelationship (filho_permitido_id, pai_id, filho_de_insercao_automatica, perfil_id, quantidade_permitida, permitir_variacao) VALUES (125, 104, false, 2, -1, false); +INSERT INTO compilacao_tipodispositivorelationship (filho_permitido_id, pai_id, filho_de_insercao_automatica, perfil_id, quantidade_permitida, permitir_variacao) VALUES (126, 1, false, 3, -1, false); + +INSERT INTO compilacao_tipodispositivorelationship (filho_permitido_id, pai_id, filho_de_insercao_automatica, perfil_id, quantidade_permitida, permitir_variacao) VALUES (2, 1, false, 4, -1, false); +INSERT INTO compilacao_tipodispositivorelationship (filho_permitido_id, pai_id, filho_de_insercao_automatica, perfil_id, quantidade_permitida, permitir_variacao) VALUES (125, 1, false, 4, -1, false); +INSERT INTO compilacao_tipodispositivorelationship (filho_permitido_id, pai_id, filho_de_insercao_automatica, perfil_id, quantidade_permitida, permitir_variacao) VALUES (126, 1, false, 4, -1, false); + +INSERT INTO compilacao_tipodispositivorelationship (filho_permitido_id, pai_id, filho_de_insercao_automatica, perfil_id, quantidade_permitida, permitir_variacao) VALUES (122, 119, false, 1, -1, false); +INSERT INTO compilacao_tipodispositivorelationship (filho_permitido_id, pai_id, filho_de_insercao_automatica, perfil_id, quantidade_permitida, permitir_variacao) VALUES (122, 119, false, 1, -1, false); + +INSERT INTO compilacao_tipodispositivorelationship (filho_permitido_id, pai_id, filho_de_insercao_automatica, perfil_id, quantidade_permitida, permitir_variacao) VALUES (122, 119, false, 2, -1, true); +INSERT INTO compilacao_tipodispositivorelationship (filho_permitido_id, pai_id, filho_de_insercao_automatica, perfil_id, quantidade_permitida, permitir_variacao) VALUES (122, 119, false, 2, -1, true); + INSERT INTO compilacao_tiponota (id, sigla, nome, modelo) VALUES (1, 'NE', 'Nota Explicativa', ''); diff --git a/sapl/compilacao/forms.py b/sapl/compilacao/forms.py index 1559b97ef..0374846fc 100644 --- a/sapl/compilacao/forms.py +++ b/sapl/compilacao/forms.py @@ -8,6 +8,7 @@ from crispy_forms.layout import (HTML, Button, Column, Div, Field, Fieldset, from django import forms from django.core.exceptions import NON_FIELD_ERRORS, ValidationError from django.db.models import Q +from django.forms import widgets from django.forms.forms import Form from django.forms.models import ModelForm from django.template import defaultfilters @@ -24,6 +25,7 @@ from sapl.compilacao.utils import DISPOSITIVO_SELECT_RELATED from sapl.crispy_layout_mixin import SaplFormLayout, to_column, to_row from sapl.utils import YES_NO_CHOICES + error_messages = { 'required': _('Este campo é obrigatório'), 'invalid': _('URL inválida.') @@ -62,9 +64,12 @@ class TipoTaForm(ModelForm): 'descricao', 'content_type', 'participacao_social', - 'publicacao_func' + 'publicacao_func', + 'perfis' ] + widgets = {'perfis': widgets.CheckboxSelectMultiple()} + def __init__(self, *args, **kwargs): row1 = to_row([ @@ -75,6 +80,7 @@ class TipoTaForm(ModelForm): row2 = to_row([ (InlineRadios('participacao_social'), 3), (InlineRadios('publicacao_func'), 3), + ('perfis', 12), ]) self.helper = FormHelper() diff --git a/sapl/compilacao/migrations/0070_perfilestruturaltextoarticulado_parent.py b/sapl/compilacao/migrations/0070_perfilestruturaltextoarticulado_parent.py new file mode 100644 index 000000000..480d9c15c --- /dev/null +++ b/sapl/compilacao/migrations/0070_perfilestruturaltextoarticulado_parent.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-11 13:01 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0069_auto_20161107_1932'), + ] + + operations = [ + migrations.AddField( + model_name='perfilestruturaltextoarticulado', + name='parent', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='perfil_parent_set', to='compilacao.PerfilEstruturalTextoArticulado', verbose_name='Perfil Herdado'), + ), + ] diff --git a/sapl/compilacao/migrations/0071_tipotextoarticulado_perfis.py b/sapl/compilacao/migrations/0071_tipotextoarticulado_perfis.py new file mode 100644 index 000000000..ee2639bc5 --- /dev/null +++ b/sapl/compilacao/migrations/0071_tipotextoarticulado_perfis.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-12 14:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0070_perfilestruturaltextoarticulado_parent'), + ] + + operations = [ + migrations.AddField( + model_name='tipotextoarticulado', + name='perfis', + field=models.ManyToManyField(blank=True, help_text='\n Apenas os perfis selecionados aqui estarão disponíveis\n para o editor de Textos Articulados cujo Tipo seja este\n em edição.\n ', to='compilacao.PerfilEstruturalTextoArticulado', verbose_name='Perfis Estruturais de Textos Articulados'), + ), + ] diff --git a/sapl/compilacao/migrations/0072_auto_20161112_1553.py b/sapl/compilacao/migrations/0072_auto_20161112_1553.py new file mode 100644 index 000000000..421215496 --- /dev/null +++ b/sapl/compilacao/migrations/0072_auto_20161112_1553.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-12 15:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0071_tipotextoarticulado_perfis'), + ] + + operations = [ + migrations.AlterField( + model_name='tipodispositivo', + name='nota_automatica_prefixo_html', + field=models.TextField(blank=True, verbose_name='Prefixo html da nota automática'), + ), + migrations.AlterField( + model_name='tipodispositivo', + name='nota_automatica_sufixo_html', + field=models.TextField(blank=True, verbose_name='Sufixo html da nota automática'), + ), + migrations.AlterField( + model_name='tipodispositivo', + name='rotulo_prefixo_html', + field=models.TextField(blank=True, verbose_name='Prefixo html do rótulo'), + ), + migrations.AlterField( + model_name='tipodispositivo', + name='rotulo_prefixo_texto', + field=models.TextField(blank=True, verbose_name='Prefixo de Edição do rótulo'), + ), + migrations.AlterField( + model_name='tipodispositivo', + name='rotulo_sufixo_html', + field=models.TextField(blank=True, verbose_name='Sufixo html do rótulo'), + ), + migrations.AlterField( + model_name='tipodispositivo', + name='rotulo_sufixo_texto', + field=models.TextField(blank=True, verbose_name='Sufixo de Edição do rótulo'), + ), + migrations.AlterField( + model_name='tipodispositivo', + name='texto_prefixo_html', + field=models.TextField(blank=True, verbose_name='Prefixo html do texto'), + ), + migrations.AlterField( + model_name='tipodispositivo', + name='texto_sufixo_html', + field=models.TextField(blank=True, verbose_name='Sufixo html do texto'), + ), + ] diff --git a/sapl/compilacao/models.py b/sapl/compilacao/models.py index 9e589f65b..c131564aa 100644 --- a/sapl/compilacao/models.py +++ b/sapl/compilacao/models.py @@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models import F, Q from django.db.models.aggregates import Max +from django.db.models.deletion import PROTECT from django.http.response import Http404 from django.template import defaultfilters from django.utils.decorators import classonlymethod @@ -74,6 +75,31 @@ class BaseModel(models.Model): update_fields=update_fields) +class PerfilEstruturalTextoArticulado(BaseModel): + sigla = models.CharField( + max_length=10, unique=True, verbose_name=_('Sigla')) + nome = models.CharField(max_length=50, verbose_name=_('Nome')) + padrao = models.BooleanField( + default=False, + choices=YES_NO_CHOICES, verbose_name=_('Padrão')) + + parent = models.ForeignKey( + 'self', + blank=True, null=True, default=None, + related_name='perfil_parent_set', + on_delete=PROTECT, + verbose_name=_('Perfil Herdado')) + + class Meta: + verbose_name = _('Perfil Estrutural de Texto Articulado') + verbose_name_plural = _('Perfis Estruturais de Textos Articulados') + + ordering = ['-padrao', 'sigla'] + + def __str__(self): + return self.nome + + class TipoTextoArticulado(models.Model): sigla = models.CharField(max_length=3, verbose_name=_('Sigla')) descricao = models.CharField(max_length=50, verbose_name=_('Descrição')) @@ -93,6 +119,15 @@ class TipoTextoArticulado(models.Model): choices=YES_NO_CHOICES, verbose_name=_('Histórico de Publicação')) + perfis = models.ManyToManyField( + PerfilEstruturalTextoArticulado, + blank=True, verbose_name=_('Perfis Estruturais de Textos Articulados'), + help_text=_(""" + Apenas os perfis selecionados aqui estarão disponíveis + para o editor de Textos Articulados cujo Tipo seja este + em edição. + """)) + class Meta: verbose_name = _('Tipo de Texto Articulado') verbose_name_plural = _('Tipos de Texto Articulados') @@ -538,13 +573,11 @@ class TipoDispositivo(BaseModel): blank=True, max_length=20, verbose_name=_('Classe CSS')) - rotulo_prefixo_html = models.CharField( + rotulo_prefixo_html = models.TextField( blank=True, - max_length=100, verbose_name=_('Prefixo html do rótulo')) - rotulo_prefixo_texto = models.CharField( + rotulo_prefixo_texto = models.TextField( blank=True, - max_length=30, verbose_name=_('Prefixo de Edição do rótulo')) rotulo_ordinal = models.IntegerField( choices=TIPO_NUMERO_ROTULO, @@ -574,29 +607,23 @@ class TipoDispositivo(BaseModel): max_length=1, default="-", verbose_name=_('Separador entre Variação 4 e Variação 5')) - rotulo_sufixo_texto = models.CharField( + rotulo_sufixo_texto = models.TextField( blank=True, - max_length=30, verbose_name=_('Sufixo de Edição do rótulo')) - rotulo_sufixo_html = models.CharField( + rotulo_sufixo_html = models.TextField( blank=True, - max_length=100, verbose_name=_('Sufixo html do rótulo')) - texto_prefixo_html = models.CharField( + texto_prefixo_html = models.TextField( blank=True, - max_length=100, verbose_name=_('Prefixo html do texto')) - texto_sufixo_html = models.CharField( + texto_sufixo_html = models.TextField( blank=True, - max_length=100, verbose_name=_('Sufixo html do texto')) - nota_automatica_prefixo_html = models.CharField( + nota_automatica_prefixo_html = models.TextField( blank=True, - max_length=100, verbose_name=_('Prefixo html da nota automática')) - nota_automatica_sufixo_html = models.CharField( + nota_automatica_sufixo_html = models.TextField( blank=True, - max_length=100, verbose_name=_('Sufixo html da nota automática')) contagem_continua = models.BooleanField( choices=YES_NO_CHOICES, verbose_name=_('Contagem contínua')) @@ -657,58 +684,49 @@ class TipoDispositivo(BaseModel): def permitido_inserir_in( self, pai_relativo, include_relative_autos=True, perfil_pk=None): + perfil = PerfilEstruturalTextoArticulado.objects.all() if not perfil_pk: - perfis = PerfilEstruturalTextoArticulado.objects.filter( - padrao=True)[:1] - - if not perfis.exists(): - return False - - perfil_pk = perfis[0].pk + perfil = perfil.filter(padrao=True) - pp = self.possiveis_pais.filter(pai=pai_relativo, perfil_id=perfil_pk) - if pp.exists(): - if not include_relative_autos: - if pp[0].filho_de_insercao_automatica: - return False - return True - return False - - def permitido_variacao( - self, base, perfil_pk=None): - - if not perfil_pk: - perfis = PerfilEstruturalTextoArticulado.objects.filter( - padrao=True)[:1] + else: + perfil = perfil.filter(pk=perfil_pk) - if not perfis.exists(): - return False + if not perfil.exists(): + return False - perfil_pk = perfis[0].pk + perfil = perfil[0] - pp = self.possiveis_pais.filter(pai=base, perfil_id=perfil_pk) - if pp.exists(): - if pp[0].permitir_variacao: + while perfil: + pp = self.possiveis_pais.filter(pai=pai_relativo, perfil=perfil) + if pp.exists(): + if not include_relative_autos: + if pp[0].filho_de_insercao_automatica: + return False return True + perfil = perfil.parent return False + def permitido_variacao(self, base, perfil_pk=None): -class PerfilEstruturalTextoArticulado(BaseModel): - sigla = models.CharField( - max_length=10, unique=True, verbose_name=_('Sigla')) - nome = models.CharField(max_length=50, verbose_name=_('Nome')) - padrao = models.BooleanField( - default=False, - choices=YES_NO_CHOICES, verbose_name=_('Padrão')) + perfil = PerfilEstruturalTextoArticulado.objects.all() + if not perfil_pk: + perfil = perfil.filter(padrao=True) - class Meta: - verbose_name = _('Perfil Estrutural de Texto Articulado') - verbose_name_plural = _('Perfis Estruturais de Textos Articulados') + else: + perfil = perfil.filter(pk=perfil_pk) - ordering = ['-padrao', 'sigla'] + if not perfil.exists(): + return False - def __str__(self): - return self.nome + perfil = perfil[0] + + while perfil: + pp = self.possiveis_pais.filter(pai=base, perfil=perfil) + if pp.exists(): + if pp[0].permitir_variacao: + return True + perfil = perfil.parent + return False class TipoDispositivoRelationship(BaseModel): diff --git a/sapl/compilacao/views.py b/sapl/compilacao/views.py index 1d5e6d459..bfb0903d9 100644 --- a/sapl/compilacao/views.py +++ b/sapl/compilacao/views.py @@ -162,7 +162,7 @@ class IntegracaoTaView(TemplateView): (request.user.has_perm( 'compilacao.change_your_dispositivo_edicao_dinamica') and ta_values.get('privacidade', STATUS_TA_EDITION - ) == STATUS_TA_PUBLIC)): + ) == STATUS_TA_PRIVATE)): """ o texto articulado será criado/atualizado se: - texto articulado já foi criado. @@ -1154,9 +1154,10 @@ class TextEditView(CompMixin, TemplateView): result = Dispositivo.objects.filter(ta_id=self.kwargs['ta_id']) if not result.exists(): + # FIXME a inserção básica deve ser refatorada para não depender + # das classes css ta = self.object - td = TipoDispositivo.objects.filter(class_css='articulacao')[0] a = Dispositivo() a.nivel = 0 @@ -1169,6 +1170,8 @@ class TextEditView(CompMixin, TemplateView): a.inicio_eficacia = ta.data a.save() + return + td = TipoDispositivo.objects.filter(class_css='ementa')[0] e = Dispositivo() e.nivel = 1 @@ -1638,9 +1641,6 @@ class ActionDispositivoCreateMixin(ActionsCommonsMixin): 'action': 'json_add_prior', 'itens': []}] - if request and 'perfil_estrutural' not in request.session: - self.set_perfil_in_session(request) - perfil_pk = request.session['perfil_estrutural'] prox_possivel = Dispositivo.objects.filter( @@ -2222,42 +2222,47 @@ class ActionsEditMixin(ActionDragAndMoveDispositivoAlteradoMixin, return JsonResponse(data, safe=False) - def get_queryset_perfil_estrutural(self): - perfis = PerfilEstruturalTextoArticulado.objects.all() - return perfis - def json_get_perfis(self, context): - - if 'perfil_pk' in self.request.GET: - self.set_perfil_in_session( - self.request, self.request.GET['perfil_pk']) - elif 'perfil_estrutural' not in self.request.session: - self.set_perfil_in_session(request=self.request) - data = {'pk': self.kwargs['dispositivo_id'], 'pai': [self.kwargs['dispositivo_id'], ]} return data - def set_perfil_in_session(self, request=None, perfil_id=0): - if not request: - return None + def update_perfis(self): + qs = PerfilEstruturalTextoArticulado.objects.all() + request = self.request - if perfil_id: - perfil = PerfilEstruturalTextoArticulado.objects.get( - pk=perfil_id) - request.session['perfil_estrutural'] = perfil.pk - return perfil.pk + ta = None + if hasattr(self, 'object') and isinstance(self.object, Dispositivo): + ta = self.object.ta + elif hasattr(self, 'object') and isinstance( + self.object, TextoArticulado): + ta = self.object else: - perfis = PerfilEstruturalTextoArticulado.objects.filter( - padrao=True)[:1] + ta_id = self.kwargs.get('ta_id', 0) + if ta_id: + ta = TextoArticulado.objects.get(pk=ta_id) - if not perfis.exists(): - request.session.pop('perfil_estrutural') + if ta: + if ta.content_object and hasattr(ta.content_object, 'perfis'): + qs = ta.content_object.perfis else: - request.session['perfil_estrutural'] = perfis[0].pk - return perfis[0].pk - return None + qs = ta.tipo_ta.perfis.all() + + perfil_get = request.GET.get('perfil_pk', 0) + if perfil_get and qs.filter(id=perfil_get).exists(): + request.session['perfil_estrutural'] = int(perfil_get) + return qs + + perfil_session = request.session.get('perfil_estrutural', perfil_get) + if perfil_session and qs.filter(id=perfil_session).exists(): + request.session['perfil_estrutural'] = int(perfil_session) + return qs + + if qs.exists(): + request.session['perfil_estrutural'] = qs.first().id + + return qs def json_add_next_registra_inclusao( self, context, local_add='json_add_next'): @@ -2417,9 +2422,11 @@ class ActionsEditMixin(ActionDragAndMoveDispositivoAlteradoMixin, dispositivos_do_bloco = \ bloco_alteracao.dispositivos_alterados_set.order_by( 'ordem_bloco_atualizador') - if dispositivos_do_bloco.exists: + if dispositivos_do_bloco.exists(): ndp.ordem_bloco_atualizador = dispositivos_do_bloco.last( ).ordem_bloco_atualizador + Dispositivo.INTERVALO_ORDEM + else: + ndp.ordem_bloco_atualizador = Dispositivo.INTERVALO_ORDEM ndp.save() p.dispositivo_subsequente = ndp @@ -2500,6 +2507,9 @@ class DispositivoDinamicEditView( self.template_name = 'compilacao/text_edit_bloco.html' return TextEditView.get(self, request, *args, **kwargs) + self.object = Dispositivo.objects.get(pk=self.kwargs['dispositivo_id']) + perfil_estrutural_list = self.update_perfis() + self.template_name = 'compilacao/ajax_form.html' self.action = request.GET['action'] @@ -2519,9 +2529,6 @@ class DispositivoDinamicEditView( elif self.action.startswith('get_actions'): self.form_class = None - self.object = Dispositivo.objects.get( - pk=self.kwargs['dispositivo_id']) - ta_id = self.kwargs['ta_id'] context = {} @@ -2537,17 +2544,9 @@ class DispositivoDinamicEditView( 'ajax_actions_dinamic_edit.html') if ta_id == str(self.object.ta_id): + context['perfil_estrutural_list'] = perfil_estrutural_list context['allowed_inserts'] = self.allowed_inserts() - if 'perfil_pk' in request.GET: - self.set_perfil_in_session( - request, request.GET['perfil_pk']) - elif 'perfil_estrutural' not in request.session: - self.set_perfil_in_session(request=request) - - context['perfil_estrutural_list' - ] = PerfilEstruturalTextoArticulado.objects.all() - return self.render_to_response(context) elif self.action.startswith('json_'): diff --git a/sapl/crispy_layout_mixin.py b/sapl/crispy_layout_mixin.py index 028301502..555a04f19 100644 --- a/sapl/crispy_layout_mixin.py +++ b/sapl/crispy_layout_mixin.py @@ -61,8 +61,12 @@ def get_field_display(obj, fieldname): try: field = obj._meta.get_field(fieldname) except: - value = getattr(obj, fieldname) - return '', str(value) + field = getattr(obj, fieldname) + if 'ManyRelatedManager' not in str(type(field))\ + and 'RelatedManager' not in str(type(field))\ + and 'GenericRelatedObjectManager' not in str(type(field)): + return '', str(field) + verbose_name = str(field.verbose_name)\ if hasattr(field, 'verbose_name') else '' if hasattr(field, 'choices') and field.choices: @@ -98,7 +102,11 @@ def get_field_display(obj, fieldname): display += '
  • %s
  • ' % str(v) display += '' if not verbose_name: - verbose_name = str(field.related_model._meta.verbose_name_plural) + if hasattr(field, 'related_model'): + verbose_name = str( + field.related_model._meta.verbose_name_plural) + elif hasattr(field, 'model'): + verbose_name = str(field.model._meta.verbose_name_plural) else: display = str(value) return verbose_name, display diff --git a/sapl/crud/base.py b/sapl/crud/base.py index cc84c7b4e..7c39850bf 100644 --- a/sapl/crud/base.py +++ b/sapl/crud/base.py @@ -180,7 +180,15 @@ class PermissionRequiredContainerCrudMixin(PermissionRequiredMixin): perms = self.get_permission_required() # Torna a view pública se não possuir conteudo # no atributo permission_required - return self.request.user.has_perms(perms) if len(perms) else True + if not len(perms): + return True + + for perm in perms: + if self.request.user.has_perm(perm): + return True + return False + + # return self.request.user.has_perms(perms) if len(perms) else True def dispatch(self, request, *args, **kwargs): if not self.has_permission(): @@ -257,9 +265,7 @@ class CrudBaseMixin(CrispyLayoutFormMixin): self.permission_required = list( set(self.permission_required) - set(obj.public)) else: - obj.public = list( - set(self.permission_required) - - set((RP_LIST, RP_DETAIL, RP_ADD, RP_CHANGE, RP_DELETE))) + obj.public = [] self.permission_required = tuple(( self.permission(pr) for pr in self.permission_required)) diff --git a/sapl/legacy/migration.py b/sapl/legacy/migration.py index aed78fd88..ffe48cd9a 100644 --- a/sapl/legacy/migration.py +++ b/sapl/legacy/migration.py @@ -4,7 +4,7 @@ import pkg_resources import yaml from django.apps import apps from django.apps.config import AppConfig -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist from django.db import connections, models from django.db.models import CharField, TextField, ProtectedError @@ -12,8 +12,8 @@ from django.db.models.base import ModelBase from model_mommy import mommy from model_mommy.mommy import foreign_key_required, make -from sapl.base.models import Autor, ProblemaMigracao, TipoAutor -from sapl.comissoes.models import Composicao, Participacao +from sapl.base.models import Autor, ProblemaMigracao +from sapl.comissoes.models import Comissao, Composicao, Participacao from sapl.materia.models import (Proposicao, StatusTramitacao, TipoDocumento, TipoMateriaLegislativa, TipoProposicao, Tramitacao) @@ -332,22 +332,24 @@ class DataMigrator: # warning: model/app migration order is of utmost importance self.to_delete = [] ProblemaMigracao.objects.all().delete() - User.objects.all().delete() + get_user_model().objects.exclude(is_superuser=True).delete() info('Começando migração: %s...' % obj) self._do_migrate(obj) # exclude logically deleted in legacy base info('Deletando models com ind_excluido...') - for obj in self.to_delete: - try: - obj.delete() - except ProtectedError: - msg = 'A entrada de PK %s da model %s não pode ser excluida' %\ - (obj.pk, obj._meta.model_name) - descricao = 'Um ou mais objetos protegidos ' - warn(msg + ' => ' + descricao) - save_relation(obj=obj, problema=msg, - descricao=descricao, eh_stub=False) + while self.to_delete: + for obj in self.to_delete: + try: + obj.delete() + self.to_delete.remove(obj) + except ProtectedError: + msg = 'A entrada de PK %s da model %s não pode ser ' \ + 'excluida' % (obj.pk, obj._meta.model_name) + descricao = 'Um ou mais objetos protegidos ' + warn(msg + ' => ' + descricao) + save_relation(obj=obj, problema=msg, + descricao=descricao, eh_stub=False) info('Deletando stubs desnecessários...') while self.delete_stubs(): @@ -532,14 +534,21 @@ def adjust_normajuridica_depois_salvar(new, old): def adjust_autor(new, old): - new.autor_related = TipoAutor.objects.get(pk=old.tip_autor) + if old.cod_parlamentar: + new.autor_related = Parlamentar.objects.get(pk=old.cod_parlamentar) + elif old.cod_comissao: + new.autor_related = Comissao.objects.get(pk=old.cod_comissao) + if old.col_username: - if not User.objects.filter(username=old.col_username).exists(): - user = User(username=old.col_username, password=12345) + if not get_user_model().objects.filter( + username=old.col_username).exists(): + user = get_user_model()( + username=old.col_username, password=12345) user.save() new.user = user else: - new.user = User.objects.filter(username=old.col_username)[0] + new.user = get_user_model().objects.filter( + username=old.col_username)[0] AJUSTE_ANTES_SALVAR = { diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index 9f107a87d..84a60dcb6 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -1,12 +1,14 @@ -from datetime import date, datetime import os +from datetime import date, datetime +from itertools import chain +import django_filters from crispy_forms.bootstrap import (Alert, FormActions, InlineCheckboxes, InlineRadios) from crispy_forms.helper import FormHelper -from crispy_forms.layout import (HTML, Button, Column, Field, Fieldset, Layout, - Submit) +from crispy_forms.layout import (HTML, Button, Column, Div, Field, Fieldset, + Layout, Submit) from django import forms from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist, ValidationError @@ -16,17 +18,23 @@ from django.db import models, transaction from django.db.models import Max from django.forms import ModelForm, widgets from django.forms.forms import Form +from django.forms.widgets import Select +from django.utils.encoding import force_text +from django.utils.html import format_html +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ -import django_filters +import sapl from sapl.base.models import Autor from sapl.comissoes.models import Comissao -from sapl.compilacao.models import STATUS_TA_PRIVATE,\ - STATUS_TA_IMMUTABLE_PUBLIC, TextoArticulado, STATUS_TA_PUBLIC +from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC, + STATUS_TA_PRIVATE, STATUS_TA_PUBLIC, + PerfilEstruturalTextoArticulado, + TextoArticulado) from sapl.crispy_layout_mixin import (SaplFormLayout, form_actions, to_column, to_row) -from sapl.materia.models import TipoProposicao, MateriaLegislativa,\ - RegimeTramitacao, TipoDocumento +from sapl.materia.models import (MateriaLegislativa, RegimeTramitacao, + TipoDocumento, TipoProposicao) from sapl.norma.models import (LegislacaoCitada, NormaJuridica, TipoNormaJuridica) from sapl.parlamentares.models import Parlamentar @@ -36,12 +44,10 @@ from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, ChoiceWithoutValidationField, MateriaPesquisaOrderingFilter, RangeWidgetOverride, autor_label, autor_modal, models_with_gr_for_model) -import sapl from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial, - DocumentoAcessorio, Numeracao, - Proposicao, Relatoria, TipoMateriaLegislativa, Tramitacao, - UnidadeTramitacao) + DocumentoAcessorio, Numeracao, Proposicao, Relatoria, + TipoMateriaLegislativa, Tramitacao, UnidadeTramitacao) def ANO_CHOICES(): @@ -472,7 +478,7 @@ class MateriaLegislativaFilterSet(django_filters.FilterSet): ementa = django_filters.CharFilter(lookup_expr='icontains') em_tramitacao = django_filters.ChoiceFilter(required=False, - label=u'Ano da Matéria', + label=u'Em tramitação', choices=em_tramitacao) o = MateriaPesquisaOrderingFilter() @@ -583,6 +589,8 @@ def filtra_tramitacao_destino_and_status(status, destino): class DespachoInicialForm(ModelForm): + comissao = forms.ModelChoiceField( + queryset=Comissao.objects.filter(ativa=True)) class Meta: model = DespachoInicial @@ -673,7 +681,7 @@ class PrimeiraTramitacaoEmLoteFilterSet(django_filters.FilterSet): self.filters['tipo'].label = 'Tipo de Matéria' self.filters['data_apresentacao'].label = 'Data (Inicial - Final)' self.form.fields['tipo'].required = True - self.form.fields['data_apresentacao'].required = True + self.form.fields['data_apresentacao'].required = False row1 = to_row([('tipo', 12)]) row2 = to_row([('data_apresentacao', 12)]) @@ -708,7 +716,7 @@ class TramitacaoEmLoteFilterSet(django_filters.FilterSet): self.filters['tramitacao__unidade_tramitacao_destino' ].label = 'Unidade Destino (Último Destino)' self.form.fields['tipo'].required = True - self.form.fields['data_apresentacao'].required = True + self.form.fields['data_apresentacao'].required = False self.form.fields['tramitacao__status'].required = True self.form.fields[ 'tramitacao__unidade_tramitacao_destino'].required = True @@ -747,16 +755,20 @@ class TipoProposicaoForm(ModelForm): fields = ['descricao', 'content_type', 'tipo_conteudo_related_radio', - 'tipo_conteudo_related'] + 'tipo_conteudo_related', + 'perfis'] - widgets = {'tipo_conteudo_related': forms.HiddenInput()} + widgets = {'tipo_conteudo_related': forms.HiddenInput(), + 'perfis': widgets.CheckboxSelectMultiple()} def __init__(self, *args, **kwargs): tipo_select = Fieldset(TipoProposicao._meta.verbose_name, - to_column(('descricao', 5)), - to_column(('content_type', 7)), - to_column(('tipo_conteudo_related_radio', 12))) + Div(to_column(('descricao', 5)), + to_column(('content_type', 7)), css_class='clearfix'), + to_column(('tipo_conteudo_related_radio', 6)), + + to_column(('perfis', 6))) self.helper = FormHelper() self.helper.layout = SaplFormLayout(tipo_select) @@ -795,7 +807,7 @@ class TipoProposicaoForm(ModelForm): @transaction.atomic def save(self, commit=False): - tipo_proposicao = super(TipoProposicaoForm, self).save(commit) + tipo_proposicao = self.instance assert tipo_proposicao.content_type @@ -803,9 +815,45 @@ class TipoProposicaoForm(ModelForm): tipo_proposicao.content_type.model_class( ).objects.get(pk=self.cleaned_data['tipo_conteudo_related']) - tipo_proposicao.save() + return super().save(True) + - return tipo_proposicao +class TipoProposicaoSelect(Select): + + def render_tipo_option(self, selected_choices, option_value, option_label, + data_has_perfil=False): + if option_value is None: + option_value = '' + option_value = force_text(option_value) + if option_value in selected_choices: + selected_html = mark_safe(' selected="selected"') + if not self.allow_multiple_selected: + # Only allow for a single selection. + selected_choices.remove(option_value) + else: + selected_html = '' + return format_html('', + option_value, + selected_html, + str(data_has_perfil), + force_text(option_label)) + + def render_options(self, choices, selected_choices): + # Normalize to strings. + selected_choices = set(force_text(v) for v in selected_choices) + output = [] + output.append( + self.render_tipo_option( + selected_choices, '', self.choices.field.empty_label)) + + for tipo in self.choices.queryset.all(): + output.append( + self.render_tipo_option( + selected_choices, + str(tipo.pk), + str(tipo), + data_has_perfil=tipo.perfis.exists())) + return '\n'.join(output) class ProposicaoForm(forms.ModelForm): @@ -827,11 +875,11 @@ class ProposicaoForm(forms.ModelForm): ano_materia = forms.CharField( label='Ano', required=False) - tipo_texto = forms.MultipleChoiceField( + tipo_texto = forms.ChoiceField( label=_('Tipo do Texto da Proposição'), required=False, choices=TIPO_TEXTO_CHOICE, - widget=widgets.CheckboxSelectMultiple()) + widget=widgets.RadioSelect()) materia_de_vinculo = forms.ModelChoiceField( queryset=MateriaLegislativa.objects.all(), @@ -851,7 +899,8 @@ class ProposicaoForm(forms.ModelForm): 'tipo_texto'] widgets = { - 'descricao': widgets.Textarea(attrs={'rows': 4})} + 'descricao': widgets.Textarea(attrs={'rows': 4}), + 'tipo': TipoProposicaoSelect()} def __init__(self, *args, **kwargs): self.texto_articulado_proposicao = sapl.base.models.AppConfig.attr( @@ -882,7 +931,7 @@ class ProposicaoForm(forms.ModelForm): if self.texto_articulado_proposicao: fields.append( - to_column((InlineCheckboxes('tipo_texto'), 5)),) + to_column((InlineRadios('tipo_texto'), 5)),) fields.append(to_column(( 'texto_original', 7 if self.texto_articulado_proposicao else 12))) @@ -893,12 +942,12 @@ class ProposicaoForm(forms.ModelForm): super(ProposicaoForm, self).__init__(*args, **kwargs) if self.instance.pk: - self.fields['tipo_texto'].initial = [] + self.fields['tipo_texto'].initial = '' if self.instance.texto_original: - self.fields['tipo_texto'].initial.append('D') + self.fields['tipo_texto'].initial = 'D' if self.texto_articulado_proposicao: if self.instance.texto_articulado.exists(): - self.fields['tipo_texto'].initial.append('T') + self.fields['tipo_texto'].initial = 'T' if self.instance.materia_de_vinculo: self.fields[ @@ -939,21 +988,35 @@ class ProposicaoForm(forms.ModelForm): return cd def save(self, commit=True): + cd = self.cleaned_data + inst = self.instance + if inst.pk: + if 'tipo_texto' in cd: + + if cd['tipo_texto'] == 'T' and inst.texto_original: + inst.texto_original.delete() + + elif cd['tipo_texto'] != 'T': + inst.texto_articulado.all().delete() + + if 'texto_original' in cd and\ + not cd['texto_original'] and \ + inst.texto_original: + inst.texto_original.delete() - if self.instance.pk: return super().save(commit) - self.instance.ano = datetime.now().year + inst.ano = datetime.now().year numero__max = Proposicao.objects.filter( - autor=self.instance.autor, + autor=inst.autor, ano=datetime.now().year).aggregate(Max('numero_proposicao')) numero__max = numero__max['numero_proposicao__max'] - self.instance.numero_proposicao = ( + inst.numero_proposicao = ( numero__max + 1) if numero__max else 1 - self.instance.save() + inst.save() - return self.instance + return inst class ConfirmarProposicaoForm(ProposicaoForm): @@ -1103,7 +1166,7 @@ class ConfirmarProposicaoForm(ProposicaoForm): if 'regime_tramitacao' not in cd or\ not cd['regime_tramitacao']: raise ValidationError( - _('Regimente de Tramitação deve ser informado.')) + _('Regime de Tramitação deve ser informado.')) elif self.instance.tipo.content_type.model_class( ) == TipoDocumento and not cd['materia_de_vinculo']: @@ -1154,6 +1217,7 @@ class ConfirmarProposicaoForm(ProposicaoForm): self.instance.justificativa_devolucao = '' self.instance.data_devolucao = None self.instance.data_recebimento = datetime.now() + self.instance.materia_de_vinculo = cd['materia_de_vinculo'] if self.instance.texto_articulado.exists(): ta = self.instance.texto_articulado.first() @@ -1320,19 +1384,19 @@ class ConfirmarProposicaoForm(ProposicaoForm): protocolo.timestamp = datetime.now() protocolo.tipo_protocolo = '1' - # 1 Processo Legislativo - # 0 Processo Administrativo - protocolo.tipo_processo = '1' protocolo.interessado = str(proposicao.autor) protocolo.autor = proposicao.autor + protocolo.assunto_ementa = proposicao.descricao protocolo.numero_paginas = cd['numero_de_paginas'] protocolo.anulado = False if self.instance.tipo.content_type.model_class( ) == TipoMateriaLegislativa: protocolo.tipo_materia = proposicao.tipo.tipo_conteudo_related + protocolo.tipo_processo = '1' elif self.instance.tipo.content_type.model_class() == TipoDocumento: protocolo.tipo_documento = proposicao.tipo.tipo_conteudo_related + protocolo.tipo_processo = '0' protocolo.save() @@ -1341,6 +1405,7 @@ class ConfirmarProposicaoForm(ProposicaoForm): # FIXME qdo protocoloadm estiver homologado, verifique a necessidade # de redirecionamento para o protocolo. + # complete e libere código abaixo para tal. """ self.instance.results['url'] = reverse( diff --git a/sapl/materia/migrations/0068_auto_20161110_0910.py b/sapl/materia/migrations/0068_auto_20161110_0910.py new file mode 100644 index 000000000..5d4bc7b36 --- /dev/null +++ b/sapl/materia/migrations/0068_auto_20161110_0910.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-10 09:10 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0067_auto_20161025_1630'), + ] + + operations = [ + migrations.AlterField( + model_name='materialegislativa', + name='ano', + field=models.PositiveSmallIntegerField(choices=[('', 'Selecione'), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano'), + ), + migrations.AlterField( + model_name='materialegislativa', + name='ano_origem_externa', + field=models.PositiveSmallIntegerField(blank=True, choices=[('', 'Selecione'), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], null=True, verbose_name='Ano'), + ), + migrations.AlterField( + model_name='numeracao', + name='ano_materia', + field=models.PositiveSmallIntegerField(choices=[('', 'Selecione'), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano'), + ), + migrations.AlterField( + model_name='proposicao', + name='ano', + field=models.PositiveSmallIntegerField(blank=True, choices=[('', 'Selecione'), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], default=None, null=True, verbose_name='Ano'), + ), + ] diff --git a/sapl/materia/migrations/0069_tipoproposicao_perfis.py b/sapl/materia/migrations/0069_tipoproposicao_perfis.py new file mode 100644 index 000000000..e6bcb3076 --- /dev/null +++ b/sapl/materia/migrations/0069_tipoproposicao_perfis.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-11 10:05 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0069_auto_20161107_1932'), + ('materia', '0068_auto_20161110_0910'), + ] + + operations = [ + migrations.AddField( + model_name='tipoproposicao', + name='perfis', + field=models.ManyToManyField(blank=True, to='compilacao.PerfilEstruturalTextoArticulado', verbose_name='Perfis Estruturais de Textos Articulados'), + ), + ] diff --git a/sapl/materia/migrations/0070_auto_20161111_1301.py b/sapl/materia/migrations/0070_auto_20161111_1301.py new file mode 100644 index 000000000..38a53d9cd --- /dev/null +++ b/sapl/materia/migrations/0070_auto_20161111_1301.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-11 13:01 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0069_tipoproposicao_perfis'), + ] + + operations = [ + migrations.AlterField( + model_name='tipoproposicao', + name='perfis', + field=models.ManyToManyField(blank=True, help_text='\n Mesmo que em Configurações da Aplicação nas\n Tabelas Auxiliares esteja definido que Proposições possam\n utilizar Textos Articulados, ao gerar uma proposição,\n a solução de Textos Articulados será disponibilizada se\n o Tipo escolhido para a Proposição estiver associado a ao\n menos um Perfil Estrutural de Texto Articulado.\n ', + to='compilacao.PerfilEstruturalTextoArticulado', verbose_name='Perfis Estruturais de Textos Articulados'), + ), + ] diff --git a/sapl/materia/migrations/0071_auto_20161130_1001.py b/sapl/materia/migrations/0071_auto_20161130_1001.py new file mode 100644 index 000000000..c1b688121 --- /dev/null +++ b/sapl/materia/migrations/0071_auto_20161130_1001.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2016-11-30 10:01 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0070_auto_20161111_1301'), + ] + + operations = [ + migrations.AlterModelOptions( + name='proposicao', + options={'permissions': (('detail_proposicao_enviada', 'Pode acessar detalhes de uma proposição enviada.'), ('detail_proposicao_devolvida', 'Pode acessar detalhes de uma proposição devolvida.')), 'verbose_name': 'Proposição', 'verbose_name_plural': 'Proposições'}, + ), + ] diff --git a/sapl/materia/migrations/0072_auto_20161130_1618.py b/sapl/materia/migrations/0072_auto_20161130_1618.py new file mode 100644 index 000000000..da265d3ff --- /dev/null +++ b/sapl/materia/migrations/0072_auto_20161130_1618.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2016-11-30 16:18 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0071_auto_20161130_1001'), + ] + + operations = [ + migrations.AlterModelOptions( + name='proposicao', + options={'permissions': (('detail_proposicao_enviada', 'Pode acessar detalhes de uma proposição enviada.'), ('detail_proposicao_devolvida', 'Pode acessar detalhes de uma proposição devolvida.'), ('detail_proposicao_incorporada', 'Pode acessar detalhes de uma proposição incorporada.')), 'verbose_name': 'Proposição', 'verbose_name_plural': 'Proposições'}, + ), + ] diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 589f039ce..f991e58df 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -1,19 +1,24 @@ +from datetime import datetime + from django.contrib.auth.models import Group from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models.deletion import PROTECT +from django.utils import formats from django.utils.translation import ugettext_lazy as _ from model_utils import Choices from sapl.base.models import Autor from sapl.comissoes.models import Comissao -from sapl.compilacao.models import TextoArticulado +from sapl.compilacao.models import TextoArticulado,\ + PerfilEstruturalTextoArticulado from sapl.parlamentares.models import Parlamentar from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericForeignKey, SaplGenericRelation, restringe_tipos_de_arquivo_txt, texto_upload_path) + EM_TRAMITACAO = [(1, 'Sim'), (0, 'Não')] @@ -38,19 +43,17 @@ class TipoProposicao(models.Model): tipo_conteudo_related = SaplGenericForeignKey( 'content_type', 'object_id', verbose_name=_('Seleção de Tipo')) - """materia_ou_documento = models.CharField( - max_length=1, verbose_name=_('Gera'), choices=MAT_OU_DOC_CHOICES) - modelo = models.CharField(max_length=50, verbose_name=_('Modelo XML')) - - # mutually exclusive (depend on materia_ou_documento) - tipo_materia = models.ForeignKey( - TipoMateriaLegislativa, - blank=True, - null=True, - verbose_name=_('Tipo de Matéria')) - tipo_documento = models.ForeignKey( - TipoDocumento, blank=True, null=True, - verbose_name=_('Tipo de Documento'))""" + perfis = models.ManyToManyField( + PerfilEstruturalTextoArticulado, + blank=True, verbose_name=_('Perfis Estruturais de Textos Articulados'), + help_text=_(""" + Mesmo que em Configurações da Aplicação nas + Tabelas Auxiliares esteja definido que Proposições possam + utilizar Textos Articulados, ao gerar uma proposição, + a solução de Textos Articulados será disponibilizada se + o Tipo escolhido para a Proposição estiver associado a ao + menos um Perfil Estrutural de Texto Articulado. + """)) class Meta: verbose_name = _('Tipo de Proposição') @@ -80,6 +83,7 @@ class TipoMateriaLegislativa(models.Model): class Meta: verbose_name = _('Tipo de Matéria Legislativa') verbose_name_plural = _('Tipos de Matérias Legislativas') + ordering = ['descricao'] def __str__(self): return self.descricao @@ -197,6 +201,13 @@ class MateriaLegislativa(models.Model): return _('%(tipo)s nº %(numero)s de %(ano)s') % { 'tipo': self.tipo, 'numero': self.numero, 'ano': self.ano} + def delete(self, using=None, keep_parents=False): + if self.texto_original: + self.texto_original.delete() + + return models.Model.delete( + self, using=using, keep_parents=keep_parents) + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): @@ -284,6 +295,8 @@ class AssuntoMateria(models.Model): class DespachoInicial(models.Model): # TODO M2M? + # TODO Despachos não são necessáriamente comissoes, podem ser outros + # órgãos, ex: procuradorias materia = models.ForeignKey(MateriaLegislativa) comissao = models.ForeignKey(Comissao) @@ -343,6 +356,13 @@ class DocumentoAcessorio(models.Model): 'data': self.data, 'autor': self.autor} + def delete(self, using=None, keep_parents=False): + if self.arquivo: + self.arquivo.delete() + + return models.Model.delete( + self, using=using, keep_parents=keep_parents) + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): @@ -395,10 +415,9 @@ class Numeracao(models.Model): 'data_materia',) def __str__(self): - return _('Nº%(numero)s %(tipo)s - %(data)s') % { + return _('%(numero)s/%(ano)s') % { 'numero': self.numero_materia, - 'tipo': self.tipo_materia, - 'data': self.data_materia} + 'ano': self.data_materia.year} class Orgao(models.Model): @@ -566,16 +585,42 @@ class Proposicao(models.Model): documento_gerado = models.ForeignKey( DocumentoAcessorio, blank=True, null=True)""" + @property + def perfis(self): + return self.tipo.perfis.all() + + @property + def title_type(self): + return '%s nº _____ %s' % ( + self.tipo, formats.date_format( + self.data_envio if self.data_envio else datetime.now(), + "\d\e d \d\e F \d\e Y")) + class Meta: verbose_name = _('Proposição') verbose_name_plural = _('Proposições') unique_together = (('content_type', 'object_id'), ) + permissions = ( + ('detail_proposicao_enviada', + _('Pode acessar detalhes de uma proposição enviada.')), + ('detail_proposicao_devolvida', + _('Pode acessar detalhes de uma proposição devolvida.')), + ('detail_proposicao_incorporada', + _('Pode acessar detalhes de uma proposição incorporada.')), + ) def __str__(self): return '%s %s/%s' % (Proposicao._meta.verbose_name, self.numero_proposicao, self.ano) + def delete(self, using=None, keep_parents=False): + if self.texto_original: + self.texto_original.delete() + + return models.Model.delete( + self, using=using, keep_parents=keep_parents) + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): @@ -608,6 +653,7 @@ class StatusTramitacao(models.Model): class Meta: verbose_name = _('Status de Tramitação') verbose_name_plural = _('Status de Tramitação') + ordering = ['descricao'] def __str__(self): return _('%(descricao)s') % { diff --git a/sapl/materia/tests/test_materia.py b/sapl/materia/tests/test_materia.py index a8aaaf2ec..4f88a8b79 100644 --- a/sapl/materia/tests/test_materia.py +++ b/sapl/materia/tests/test_materia.py @@ -159,6 +159,7 @@ def test_despacho_inicial_submit(admin_client): comissao = mommy.make(Comissao, tipo=tipo_comissao, nome='Teste', + ativa=True, sigla='T', data_criacao='2016-03-18') @@ -172,6 +173,7 @@ def test_despacho_inicial_submit(admin_client): # Verifica se o despacho foi criado despacho = DespachoInicial.objects.first() + assert despacho.comissao == comissao assert despacho.materia == materia_principal diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 63d456a3e..e177c9301 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -12,19 +12,21 @@ from django.core.mail import send_mail from django.core.urlresolvers import reverse from django.http import JsonResponse from django.http.response import Http404, HttpResponseRedirect -from django.shortcuts import redirect, get_object_or_404 +from django.shortcuts import get_object_or_404, redirect from django.template import Context, loader from django.utils import formats from django.utils.translation import ugettext_lazy as _ -from django.views.generic import CreateView, ListView, TemplateView, UpdateView +from django.views.generic import (CreateView, DetailView, ListView, + TemplateView, UpdateView) from django.views.generic.base import RedirectView from django.views.generic.edit import FormView from django_filters.views import FilterView +import sapl from sapl.base.models import Autor, CasaLegislativa -from sapl.compilacao.models import STATUS_TA_PRIVATE, STATUS_TA_EDITION,\ - STATUS_TA_IMMUTABLE_RESTRICT - +from sapl.compilacao.models import (STATUS_TA_EDITION, + STATUS_TA_IMMUTABLE_RESTRICT, + STATUS_TA_PRIVATE) from sapl.compilacao.views import IntegracaoTaView from sapl.crispy_layout_mixin import SaplFormLayout, form_actions from sapl.crud.base import (ACTION_CREATE, ACTION_DELETE, ACTION_DETAIL, @@ -39,9 +41,9 @@ from sapl.protocoloadm.models import Protocolo from sapl.utils import (TURNO_TRAMITACAO_CHOICES, YES_NO_CHOICES, autor_label, autor_modal, gerar_hash_arquivo, get_base_url, montar_row_autor) -import sapl from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm, + DespachoInicialForm, DocumentoAcessorioForm, MateriaLegislativaFilterSet, MateriaSimplificadaForm, PrimeiraTramitacaoEmLoteFilterSet, ReceberProposicaoForm, TramitacaoEmLoteFilterSet, @@ -55,7 +57,6 @@ from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial, TipoMateriaLegislativa, TipoProposicao, Tramitacao, UnidadeTramitacao) - OrigemCrud = Crud.build(Origem, '') TipoMateriaCrud = CrudAux.build( @@ -251,7 +252,7 @@ class ProposicaoDevolvida(PermissionRequiredMixin, ListView): model = Proposicao ordering = ['data_envio'] paginate_by = 10 - permission_required = ('materia.list_proposicao', ) + permission_required = ('materia.detail_proposicao_devolvida', ) def get_queryset(self): return Proposicao.objects.filter( @@ -275,7 +276,7 @@ class ProposicaoPendente(PermissionRequiredMixin, ListView): model = Proposicao ordering = ['data_envio', 'autor', 'tipo', 'descricao'] paginate_by = 10 - permission_required = ('materia.list_proposicao', ) + permission_required = ('materia.detail_proposicao_enviada', ) def get_queryset(self): return Proposicao.objects.filter( @@ -300,7 +301,7 @@ class ProposicaoRecebida(PermissionRequiredMixin, ListView): model = Proposicao ordering = ['data_envio'] paginate_by = 10 - permission_required = ('materia.list_proposicao', ) + permission_required = 'materia.detail_proposicao_incorporada' def get_queryset(self): return Proposicao.objects.filter( @@ -472,10 +473,16 @@ class ProposicaoCrud(Crud): class DetailView(Crud.DetailView): layout_key = 'Proposicao' + permission_required = (RP_DETAIL, 'materia.detail_proposicao_enviada', + 'materia.detail_proposicao_devolvida', + 'materia.detail_proposicao_incorporada') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['subnav_template_name'] = '' + + context['title'] = '%s (%s)' % ( + self.object, self.object.autor) return context def get(self, request, *args, **kwargs): @@ -535,6 +542,37 @@ class ProposicaoCrud(Crud): return redirect(reverse('sapl.materia:proposicao_detail', kwargs={'pk': kwargs['pk']})) + def dispatch(self, request, *args, **kwargs): + + try: + p = Proposicao.objects.get(id=kwargs['pk']) + except: + raise Http404() + + if not self.has_permission(): + return self.handle_no_permission() + + if p.autor.user != request.user: + if not p.data_envio and not p.data_devolucao: + raise Http404() + + if p.data_devolucao and not request.user.has_perm( + 'materia.detail_proposicao_devolvida'): + raise Http404() + + if p.data_envio and not p.data_recebimento\ + and not request.user.has_perm( + 'materia.detail_proposicao_enviada'): + raise Http404() + + if p.data_envio and p.data_recebimento\ + and not request.user.has_perm( + 'materia.detail_proposicao_incorporada'): + raise Http404() + + return super(PermissionRequiredMixin, self).dispatch( + request, *args, **kwargs) + class DeleteView(BaseLocalMixin, Crud.DeleteView): def _action_is_valid(self, request, *args, **kwargs): @@ -624,18 +662,19 @@ class ProposicaoCrud(Crud): def get_rows(self, object_list): for obj in object_list: + if obj.data_recebimento is None: + obj.data_recebimento = 'Não recebida'\ + if obj.data_envio else 'Não enviada' + else: + obj.data_recebimento = formats.date_format( + obj.data_recebimento, "DATETIME_FORMAT") + if obj.data_envio is None: obj.data_envio = 'Em elaboração...' else: obj.data_envio = formats.date_format( obj.data_envio, "DATETIME_FORMAT") - if obj.data_recebimento is None: - obj.data_recebimento = 'Não recebida' - else: - obj.data_envio = formats.date_format( - obj.data_recebimento, "DATETIME_FORMAT") - return [self._as_row(obj) for obj in object_list] @@ -843,6 +882,12 @@ class DespachoInicialCrud(MasterDetailCrud): help_path = '' public = [RP_LIST, RP_DETAIL] + class CreateView(MasterDetailCrud.CreateView): + form_class = DespachoInicialForm + + class UpdateView(MasterDetailCrud.UpdateView): + form_class = DespachoInicialForm + class LegislacaoCitadaCrud(MasterDetailCrud): model = LegislacaoCitada @@ -987,6 +1032,17 @@ class MateriaLegislativaCrud(Crud): def cancel_url(self): return self.search_url + class DeleteView(Crud.DeleteView): + + def get_success_url(self): + return self.search_url + + class DetailView(Crud.DetailView): + + @property + def layout_key(self): + return 'MateriaLegislativaDetail' + class ListView(Crud.ListView, RedirectView): def get_redirect_url(self, *args, **kwargs): @@ -1100,6 +1156,9 @@ class MateriaLegislativaPesquisaView(FilterView): lista = filtra_tramitacao_destino(unidade_destino) qs = qs.filter(id__in=lista).distinct() + if 'o' in self.request.GET and not self.request.GET['o']: + qs = qs.order_by('-ano', '-numero') + kwargs.update({ 'queryset': qs, }) @@ -1460,6 +1519,9 @@ class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView): context['unidade_local'] = [UnidadeTramitacao.objects.get( id=qr['tramitacao__unidade_tramitacao_destino'])] + context['object_list'] = context['object_list'].order_by( + 'ano', 'numero') + context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' return context diff --git a/sapl/norma/forms.py b/sapl/norma/forms.py index 4fd8c8579..68b059436 100644 --- a/sapl/norma/forms.py +++ b/sapl/norma/forms.py @@ -4,15 +4,18 @@ from crispy_forms.helper import FormHelper from crispy_forms.layout import Fieldset, Layout from django import forms from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.db import models from django.forms import ModelForm, widgets from django.utils.translation import ugettext_lazy as _ +import django_filters from sapl.crispy_layout_mixin import form_actions, to_row from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.settings import MAX_DOC_UPLOAD_SIZE -from sapl.utils import RANGE_ANOS +from sapl.utils import RANGE_ANOS, RangeWidgetOverride -from .models import AssuntoNorma, NormaJuridica +from .models import (AssuntoNorma, NormaJuridica, NormaRelacionada, + TipoNormaJuridica) def get_esferas(): @@ -30,94 +33,45 @@ ORDENACAO_CHOICES = [('', '---------'), ('data,tipo,ano,numero', _('Data/Tipo/Ano/Número'))] -# TODO termos, pesquisa textual, assunto(M2M) -class NormaJuridicaPesquisaForm(ModelForm): - - periodo_inicial = forms.DateField(label=u'Período Inicial', - input_formats=['%d/%m/%Y'], - required=False, - widget=forms.DateInput( - format='%d/%m/%Y', - attrs={'class': 'dateinput'})) - - periodo_final = forms.DateField(label=u'Período Final', - input_formats=['%d/%m/%Y'], - required=False, - widget=forms.DateInput( - format='%d/%m/%Y', - attrs={'class': 'dateinput'})) - - publicacao_inicial = forms.DateField(label=u'Publicação Inicial', - input_formats=['%d/%m/%Y'], - required=False, - widget=forms.DateInput( - format='%d/%m/%Y', - attrs={'class': 'dateinput'})) - - publicacao_final = forms.DateField(label=u'Publicação Final', - input_formats=['%d/%m/%Y'], - required=False, - widget=forms.DateInput( - format='%d/%m/%Y', - attrs={'class': 'dateinput'})) - - ano = forms.ModelChoiceField( - label='Ano', - required=False, - queryset=NormaJuridica.objects.order_by('-ano').values_list( - 'ano', flat=True).distinct(), - empty_label='Selecione' - ) +class NormaFilterSet(django_filters.FilterSet): - em_vigencia = forms.ChoiceField( - label='Em vigência?', - choices=YES_NO_CHOICES, - required=False) + RANGE_ANOS.insert(0, ('', 'Selecione')) - ordenacao = forms.ChoiceField( - label='Ordenação', - choices=ORDENACAO_CHOICES, - required=False) + filter_overrides = {models.DateField: { + 'filter_class': django_filters.DateFromToRangeFilter, + 'extra': lambda f: { + 'label': '%s (%s)' % (f.verbose_name, _('Inicial - Final')), + 'widget': RangeWidgetOverride} + }} - numero = forms.IntegerField(required=False) + ano = django_filters.ChoiceFilter(required=False, + label=u'Ano', + choices=RANGE_ANOS) - assunto = forms.ModelChoiceField( - label='Assunto', - required=False, - queryset=AssuntoNorma.objects.all(), - empty_label='Selecione' - ) + ementa = django_filters.CharFilter(lookup_expr='icontains') + + assuntos = django_filters.ModelChoiceFilter( + queryset=AssuntoNorma.objects.all()) class Meta: model = NormaJuridica - fields = ['tipo', - 'numero', - 'ano', - 'periodo_inicial', - 'periodo_final', - 'publicacao_inicial', - 'publicacao_final', - 'assunto'] + fields = ['tipo', 'numero', 'ano', 'data', + 'data_publicacao', 'ementa', 'assuntos'] def __init__(self, *args, **kwargs): - - row1 = to_row([('tipo', 12)]) - - row2 = to_row([('numero', 6), ('ano', 6)]) - - row3 = to_row([('periodo_inicial', 6), ('periodo_final', 6)]) - - row4 = to_row([('publicacao_inicial', 6), ('publicacao_final', 6)]) - - row5 = to_row([('em_vigencia', 4), ('ordenacao', 4), ('assunto', 4)]) - - self.helper = FormHelper() - self.helper.layout = Layout( - Fieldset('Pesquisa Norma Juridica', - row1, row2, row3, row4, row5), - form_actions(save_label='Pesquisar') + super(NormaFilterSet, self).__init__(*args, **kwargs) + + row1 = to_row([('tipo', 4), ('numero', 4), ('ano', 4)]) + row2 = to_row([('data', 6), ('data_publicacao', 6)]) + row3 = to_row([('ementa', 8), ('assuntos', 4)]) + + self.form.helper = FormHelper() + self.form.helper.form_method = 'GET' + self.form.helper.layout = Layout( + Fieldset(_('Pesquisa de Norma'), + row1, row2, row3, + form_actions(save_label='Pesquisar')) ) - super(NormaJuridicaPesquisaForm, self).__init__(*args, **kwargs) class NormaJuridicaForm(ModelForm): @@ -195,3 +149,49 @@ class NormaJuridicaForm(ModelForm): norma.materia = self.cleaned_data['materia'] norma = super(NormaJuridicaForm, self).save(commit=True) return norma + + +class NormaRelacionadaForm(ModelForm): + + tipo = forms.ModelChoiceField( + label='Tipo', + required=True, + queryset=TipoNormaJuridica.objects.all(), + empty_label='----------', + ) + numero = forms.CharField(label='Número', required=True) + ano = forms.CharField(label='Ano', required=True) + ementa = forms.CharField( + required=False, + widget=forms.Textarea(attrs={'disabled': 'disabled'})) + + class Meta: + model = NormaRelacionada + fields = ['tipo', 'numero', 'ano', 'ementa', 'tipo_vinculo'] + + def __init__(self, *args, **kwargs): + super(NormaRelacionadaForm, self).__init__(*args, **kwargs) + + def clean(self): + if self.errors: + return self.errors + cleaned_data = self.cleaned_data + + try: + norma_relacionada = NormaJuridica.objects.get( + numero=cleaned_data['numero'], + ano=cleaned_data['ano'], + tipo=cleaned_data['tipo']) + except ObjectDoesNotExist: + msg = _('A norma a ser relacionada não existe.') + raise ValidationError(msg) + else: + cleaned_data['norma_relacionada'] = norma_relacionada + + return cleaned_data + + def save(self, commit=False): + relacionada = super(NormaRelacionadaForm, self).save(commit) + relacionada.norma_relacionada = self.cleaned_data['norma_relacionada'] + relacionada.save() + return relacionada diff --git a/sapl/norma/migrations/0022_auto_20161110_0910.py b/sapl/norma/migrations/0022_auto_20161110_0910.py new file mode 100644 index 000000000..c440abb8a --- /dev/null +++ b/sapl/norma/migrations/0022_auto_20161110_0910.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-10 09:10 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('norma', '0021_auto_20161028_1335'), + ] + + operations = [ + migrations.AlterField( + model_name='normajuridica', + name='ano', + field=models.PositiveSmallIntegerField(choices=[('', 'Selecione'), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano'), + ), + ] diff --git a/sapl/norma/migrations/0023_auto_20161123_1359.py b/sapl/norma/migrations/0023_auto_20161123_1359.py new file mode 100644 index 000000000..450071cc8 --- /dev/null +++ b/sapl/norma/migrations/0023_auto_20161123_1359.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-23 13:59 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('norma', '0022_auto_20161110_0910'), + ] + + operations = [ + migrations.AlterModelOptions( + name='vinculonormajuridica', + options={'verbose_name': 'Tipo de Vínculo entre Normas Jurídicas', 'verbose_name_plural': 'Tipos de Vínculos entre Normas Jurídicas'}, + ), + migrations.RemoveField( + model_name='vinculonormajuridica', + name='norma_referente', + ), + migrations.RemoveField( + model_name='vinculonormajuridica', + name='norma_referida', + ), + migrations.RemoveField( + model_name='vinculonormajuridica', + name='tipo_vinculo', + ), + migrations.AddField( + model_name='vinculonormajuridica', + name='descricao', + field=models.CharField(blank=True, max_length=20, verbose_name='Descrição'), + ), + migrations.AddField( + model_name='vinculonormajuridica', + name='sigla', + field=models.CharField(blank=True, max_length=1, verbose_name='Nome'), + ), + ] diff --git a/sapl/norma/migrations/0024_auto_20161123_1430.py b/sapl/norma/migrations/0024_auto_20161123_1430.py new file mode 100644 index 000000000..e531c2a38 --- /dev/null +++ b/sapl/norma/migrations/0024_auto_20161123_1430.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-23 14:30 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('norma', '0023_auto_20161123_1359'), + ] + + operations = [ + migrations.AlterField( + model_name='vinculonormajuridica', + name='descricao', + field=models.CharField(blank=True, max_length=50, verbose_name='Descrição'), + ), + migrations.AlterField( + model_name='vinculonormajuridica', + name='sigla', + field=models.CharField(blank=True, max_length=1, verbose_name='Sigla'), + ), + ] diff --git a/sapl/norma/migrations/0025_normarelacionada.py b/sapl/norma/migrations/0025_normarelacionada.py new file mode 100644 index 000000000..1cfafedb2 --- /dev/null +++ b/sapl/norma/migrations/0025_normarelacionada.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-23 14:44 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('norma', '0024_auto_20161123_1430'), + ] + + operations = [ + migrations.CreateModel( + name='NormaRelacionada', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('norma_principal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='norma_principal', to='norma.NormaJuridica')), + ('norma_relacionada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='norma_relacionada', to='norma.NormaJuridica')), + ('tipo_vinculo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='norma.VinculoNormaJuridica')), + ], + options={ + 'verbose_name': 'Norma Relacionada', + 'verbose_name_plural': 'Normas Relacionadas', + }, + ), + ] diff --git a/sapl/norma/migrations/0026_auto_20161123_1450.py b/sapl/norma/migrations/0026_auto_20161123_1450.py new file mode 100644 index 000000000..6fda19247 --- /dev/null +++ b/sapl/norma/migrations/0026_auto_20161123_1450.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-23 14:50 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('norma', '0025_normarelacionada'), + ] + + operations = [ + migrations.AlterField( + model_name='normarelacionada', + name='tipo_vinculo', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='norma.VinculoNormaJuridica', verbose_name='Tipo de Vínculo'), + ), + ] diff --git a/sapl/norma/migrations/0027_auto_20161123_1538.py b/sapl/norma/migrations/0027_auto_20161123_1538.py new file mode 100644 index 000000000..f44348b0e --- /dev/null +++ b/sapl/norma/migrations/0027_auto_20161123_1538.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-23 15:38 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('norma', '0026_auto_20161123_1450'), + ] + + operations = [ + migrations.AlterField( + model_name='normarelacionada', + name='norma_principal', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='norma_principal', to='norma.NormaJuridica', verbose_name='Norma Principal'), + ), + migrations.AlterField( + model_name='normarelacionada', + name='norma_relacionada', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='norma_relacionada', to='norma.NormaJuridica', verbose_name='Norma Relacionada'), + ), + ] diff --git a/sapl/norma/migrations/0028_auto_20161202_1025.py b/sapl/norma/migrations/0028_auto_20161202_1025.py new file mode 100644 index 000000000..2bfebb011 --- /dev/null +++ b/sapl/norma/migrations/0028_auto_20161202_1025.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-12-02 10:25 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('norma', '0027_auto_20161123_1538'), + ] + + operations = [ + migrations.AlterField( + model_name='normajuridica', + name='materia', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa', verbose_name='Matéria'), + ), + ] diff --git a/sapl/norma/models.py b/sapl/norma/models.py index debbdadc9..4d9780038 100644 --- a/sapl/norma/models.py +++ b/sapl/norma/models.py @@ -50,6 +50,7 @@ class TipoNormaJuridica(models.Model): class Meta: verbose_name = _('Tipo de Norma Jurídica') verbose_name_plural = _('Tipos de Norma Jurídica') + ordering = ['descricao'] def __str__(self): return self.descricao @@ -68,7 +69,8 @@ class NormaJuridica(models.Model): verbose_name=_('Texto Integral')) tipo = models.ForeignKey( TipoNormaJuridica, verbose_name=_('Tipo da Norma Juridica')) - materia = models.ForeignKey(MateriaLegislativa, blank=True, null=True) + materia = models.ForeignKey( + MateriaLegislativa, blank=True, null=True, verbose_name=_('Matéria')) numero = models.PositiveIntegerField(verbose_name=_('Número')) ano = models.PositiveSmallIntegerField(verbose_name=_('Ano'), choices=RANGE_ANOS) @@ -116,6 +118,13 @@ class NormaJuridica(models.Model): 'numero': self.numero, 'data': defaultfilters.date(self.data, "d \d\e F \d\e Y")} + def delete(self, using=None, keep_parents=False): + if self.texto_integral: + self.texto_integral.delete() + + return models.Model.delete( + self, using=using, keep_parents=keep_parents) + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): @@ -171,37 +180,37 @@ class LegislacaoCitada(models.Model): class VinculoNormaJuridica(models.Model): - TIPO_VINCULO_CHOICES = ( - ('A', _('Altera a norma')), - ('R', _('Revoga integralmente a norma')), - ('P', _('Revoga parcialmente a norma')), - ('T', _('Revoga integralmente por consolidação')), - ('C', _('Norma correlata')), - ('S', _('Ressalva a norma')), - ('E', _('Reedita a norma')), - ('I', _('Reedita a norma com alteração')), - ('G', _('Regulamenta a norma')), - ('K', _('Suspende parcialmente a norma')), - ('L', _('Suspende integralmente a norma')), - ('N', _('Julgada integralmente inconstitucional')), - ('O', _('Julgada parcialmente inconstitucional')), - ) + sigla = models.CharField( + max_length=1, blank=True, verbose_name=_('Sigla')) + descricao = models.CharField( + max_length=50, blank=True, verbose_name=_('Descrição')) + + class Meta: + verbose_name = _('Tipo de Vínculo entre Normas Jurídicas') + verbose_name_plural = _('Tipos de Vínculos entre Normas Jurídicas') + + def __str__(self): + return self.descricao + - # TODO M2M ??? - norma_referente = models.ForeignKey( - NormaJuridica, related_name='norma_referente_set') - norma_referida = models.ForeignKey( - NormaJuridica, related_name='norma_referida_set') - tipo_vinculo = models.CharField( - max_length=1, blank=True, choices=TIPO_VINCULO_CHOICES) +class NormaRelacionada(models.Model): + norma_principal = models.ForeignKey( + NormaJuridica, + related_name='norma_principal', + verbose_name=_('Norma Principal')) + norma_relacionada = models.ForeignKey( + NormaJuridica, + related_name='norma_relacionada', + verbose_name=_('Norma Relacionada')) + tipo_vinculo = models.ForeignKey( + VinculoNormaJuridica, verbose_name=_('Tipo de Vínculo')) class Meta: - verbose_name = _('Vínculo entre Normas Jurídicas') - verbose_name_plural = _('Vínculos entre Normas Jurídicas') + verbose_name = _('Norma Relacionada') + verbose_name_plural = _('Normas Relacionadas') def __str__(self): - return _('Referente: %(referente)s \n' - 'Referida: %(referida)s \nVínculo: %(vinculo)s') % { - 'referente': self.norma_referente, - 'referida': self.norma_referida, - 'vinculo': self.tipo_vinculo} + return _('Principal: %(norma_principal)s' + ' - Relacionada: %(norma_relacionada)s') % { + 'norma_principal': self.norma_principal, + 'norma_relacionada': self.norma_relacionada} diff --git a/sapl/norma/urls.py b/sapl/norma/urls.py index a8d8c59d7..59d8f3473 100644 --- a/sapl/norma/urls.py +++ b/sapl/norma/urls.py @@ -1,8 +1,8 @@ from django.conf.urls import include, url -from sapl.norma.views import (AssuntoNormaCrud, - NormaCrud, NormaPesquisaView, NormaTaView, - PesquisaNormaListView, TipoNormaCrud) +from sapl.norma.views import (AssuntoNormaCrud, NormaCrud, NormaPesquisaView, + NormaRelacionadaCrud, NormaTaView, TipoNormaCrud, + VinculoNormaJuridicaCrud, recuperar_norma) from .apps import AppConfig @@ -10,15 +10,20 @@ app_name = AppConfig.name urlpatterns = [ - url(r'^norma/', include(NormaCrud.get_urls())), + url(r'^norma/', include(NormaCrud.get_urls() + + NormaRelacionadaCrud.get_urls())), # Integração com Compilação url(r'^norma/(?P[0-9]+)/ta$', NormaTaView.as_view(), name='norma_ta'), url(r'^sistema/norma/tipo/', include(TipoNormaCrud.get_urls())), url(r'^sistema/norma/assunto/', include(AssuntoNormaCrud.get_urls())), - url(r'^norma/pesquisa$', + url(r'^sistema/norma/vinculo/', include( + VinculoNormaJuridicaCrud.get_urls())), + + url(r'^norma/pesquisar$', NormaPesquisaView.as_view(), name='norma_pesquisa'), - url(r'^norma/pesquisa-resultado$', - PesquisaNormaListView.as_view(), name='list_pesquisa_norma'), + + url(r'^norma/recuperar-norma/', recuperar_norma), + ] diff --git a/sapl/norma/views.py b/sapl/norma/views.py index 1cddc0ca9..f206b7dd7 100644 --- a/sapl/norma/views.py +++ b/sapl/norma/views.py @@ -1,19 +1,21 @@ from datetime import datetime from django.core.urlresolvers import reverse +from django.http import JsonResponse from django.shortcuts import redirect +from django.utils.translation import ugettext_lazy as _ from django.views.generic import FormView, ListView from django.views.generic.base import RedirectView +from django_filters.views import FilterView from sapl.base.models import AppConfig from sapl.compilacao.views import IntegracaoTaView from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux, MasterDetailCrud, make_pagination) -from sapl.norma.forms import NormaJuridicaForm -from .forms import NormaJuridicaPesquisaForm -from .models import (AssuntoNorma, NormaJuridica, - TipoNormaJuridica) +from .forms import NormaFilterSet, NormaJuridicaForm, NormaRelacionadaForm +from .models import (AssuntoNorma, NormaJuridica, NormaRelacionada, + TipoNormaJuridica, VinculoNormaJuridica) # LegislacaoCitadaCrud = Crud.build(LegislacaoCitada, '') AssuntoNormaCrud = CrudAux.build(AssuntoNorma, 'assunto_norma_juridica', @@ -23,6 +25,63 @@ AssuntoNormaCrud = CrudAux.build(AssuntoNorma, 'assunto_norma_juridica', TipoNormaCrud = CrudAux.build( TipoNormaJuridica, 'tipo_norma_juridica', list_field_names=['sigla', 'descricao', 'equivalente_lexml']) +VinculoNormaJuridicaCrud = CrudAux.build( + VinculoNormaJuridica, '', list_field_names=['sigla', 'descricao']) + + +class NormaRelacionadaCrud(MasterDetailCrud): + model = NormaRelacionada + parent_field = 'norma_principal' + help_path = '' + public = [RP_LIST, RP_DETAIL] + + class BaseMixin(MasterDetailCrud.BaseMixin): + list_field_names = ['norma_relacionada'] + + class CreateView(MasterDetailCrud.CreateView): + form_class = NormaRelacionadaForm + + class UpdateView(MasterDetailCrud.UpdateView): + form_class = NormaRelacionadaForm + + def get_initial(self): + self.initial['tipo'] = self.object.norma_relacionada.tipo.id + self.initial['numero'] = self.object.norma_relacionada.numero + self.initial['ano'] = self.object.norma_relacionada.ano + self.initial['ementa'] = self.object.norma_relacionada.ementa + return self.initial + + class DetailView(MasterDetailCrud.DetailView): + + @property + def layout_key(self): + return 'NormaRelacionadaDetail' + + +class NormaPesquisaView(FilterView): + model = NormaJuridica + filterset_class = NormaFilterSet + paginate_by = 10 + + def get_context_data(self, **kwargs): + context = super(NormaPesquisaView, self).get_context_data(**kwargs) + + context['title'] = _('Pesquisar Norma Jurídica') + + qr = self.request.GET.copy() + + if 'page' in qr: + del qr['page'] + + paginator = context['paginator'] + page_obj = context['page_obj'] + + context['page_range'] = make_pagination( + page_obj.number, paginator.num_pages) + + context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + + return context class NormaTaView(IntegracaoTaView): @@ -69,6 +128,11 @@ class NormaCrud(Crud): namespace = self.model._meta.app_config.name return reverse('%s:%s' % (namespace, 'norma_pesquisa')) + class DeleteView(Crud.DeleteView): + + def get_success_url(self): + return self.search_url + class CreateView(Crud.CreateView): form_class = NormaJuridicaForm @@ -105,111 +169,18 @@ class NormaCrud(Crud): return self.initial.copy() -class NormaPesquisaView(FormView): - template_name = "norma/pesquisa.html" - success_url = "norma:norma_pesquisa" - form_class = NormaJuridicaPesquisaForm - - def post(self, request, *args, **kwargs): - form = NormaJuridicaPesquisaForm(request.POST) - - if form.data['tipo']: - kwargs['tipo'] = form.data['tipo'] - if form.data['numero']: - kwargs['numero'] = form.data['numero'] - if form.data['ano']: - kwargs['ano'] = form.data['ano'] - if form.data['periodo_inicial'] and form.data['periodo_final']: - kwargs['periodo_inicial'] = form.data['periodo_inicial'] - kwargs['periodo_final'] = form.data['periodo_final'] - if form.data['publicacao_inicial'] and form.data['publicacao_final']: - kwargs['publicacao_inicial'] = form.data['publicacao_inicial'] - kwargs['publicacao_final'] = form.data['publicacao_final'] - if form.data['ordenacao']: - kwargs['ordenacao'] = form.data['ordenacao'] - if form.data['em_vigencia']: - kwargs['em_vigencia'] = form.data['em_vigencia'] - if form.data['assunto']: - kwargs['assunto'] = form.data['assunto'] - - request.session['kwargs'] = kwargs - return redirect('sapl.norma:list_pesquisa_norma') - - -class PesquisaNormaListView(ListView): - template_name = 'norma/list_pesquisa.html' - model = NormaJuridica - paginate_by = 10 - - def get_queryset(self): - kwargs = self.request.session['kwargs'] - - if 'ordenacao' in kwargs: - ordenacao = kwargs.pop('ordenacao').split(',') - for o in ordenacao: - normas = NormaJuridica.objects.all().order_by(o) - else: - normas = NormaJuridica.objects.all() - - if 'em_vigencia' in kwargs: - del kwargs['em_vigencia'] - normas = normas.filter( - data_vigencia__lte=datetime.now().date()) - - if 'periodo_inicial' and 'publicacao_inicial' in kwargs: - periodo_inicial = datetime.strptime( - kwargs['periodo_inicial'], - '%d/%m/%Y').strftime('%Y-%m-%d') - periodo_final = datetime.strptime( - kwargs['periodo_final'], - '%d/%m/%Y').strftime('%Y-%m-%d') - publicacao_inicial = datetime.strptime( - kwargs['publicacao_inicial'], - '%d/%m/%Y').strftime('%Y-%m-%d') - publicacao_final = datetime.strptime( - kwargs['publicacao_final'], - '%d/%m/%Y').strftime('%Y-%m-%d') - - normas = normas.filter( - data__range=(periodo_inicial, periodo_final), - data_publicacao__range=(publicacao_inicial, publicacao_final)) - - if 'periodo_inicial' in kwargs: - inicial = datetime.strptime(kwargs['periodo_inicial'], - '%d/%m/%Y').strftime('%Y-%m-%d') - final = datetime.strptime(kwargs['periodo_inicial'], - '%d/%m/%Y').strftime('%Y-%m-%d') - - normas = normas.filter(data__range=(inicial, final)) - - if 'publicacao_inicial' in kwargs: - inicial = datetime.strptime(kwargs['publicacao_inicial'], - '%d/%m/%Y').strftime('%Y-%m-%d') - final = datetime.strptime(kwargs['publicacao_final'], - '%d/%m/%Y').strftime('%Y-%m-%d') - - normas = normas.filter(data_publicacao__range=(inicial, final)) - if 'tipo' in kwargs: - normas = normas.filter(tipo=kwargs['tipo']) - - if 'numero' in kwargs: - normas = normas.filter(numero=kwargs['numero']) - - if 'ano' in kwargs: - normas = normas.filter(ano=kwargs['ano']) - - if 'assunto' in kwargs: - normas = normas.filter(assuntos=kwargs['assunto']) - - return normas +def recuperar_norma(request): + tipo = TipoNormaJuridica.objects.get(pk=request.GET['tipo']) + numero = request.GET['numero'] + ano = request.GET['ano'] - def get_context_data(self, **kwargs): - context = super(PesquisaNormaListView, self).get_context_data( - **kwargs) + try: + norma = NormaJuridica.objects.get(tipo=tipo, + ano=ano, + numero=numero) + response = JsonResponse({'ementa': norma.ementa, + 'id': norma.id}) + except ObjectDoesNotExist: + response = JsonResponse({'ementa': '', 'id': 0}) - paginator = context['paginator'] - page_obj = context['page_obj'] - - context['page_range'] = make_pagination( - page_obj.number, paginator.num_pages) - return context + return response diff --git a/sapl/painel/views.py b/sapl/painel/views.py index 6c43708a4..d285137a0 100644 --- a/sapl/painel/views.py +++ b/sapl/painel/views.py @@ -84,21 +84,8 @@ def get_cronometro_status(request, name): def get_materia_aberta(pk): - try: - materia = OrdemDia.objects.filter( - sessao_plenaria_id=pk, votacao_aberta=True).last() - return materia - except ObjectDoesNotExist: - return False - - -def get_last_materia(pk): - try: - materia = OrdemDia.objects.filter( - sessao_plenaria_id=pk).last() - return materia - except ObjectDoesNotExist: - return None + return OrdemDia.objects.filter( + sessao_plenaria_id=pk, votacao_aberta=True).last() def get_presentes(pk, response, materia): @@ -138,11 +125,14 @@ def get_presentes(pk, response, materia): num_presentes_ordem_dia = len(presentes_ordem_dia) if materia.tipo_votacao == 1: - tipo_votacao = 'Simbólica' + tipo_votacao = str(_('Simbólica')) + response = get_votos(response, materia) elif materia.tipo_votacao == 2: tipo_votacao = 'Nominal' + response = get_votos_nominal(response, materia) elif materia.tipo_votacao == 3: tipo_votacao = 'Secreta' + response = get_votos(response, materia) response.update({ 'presentes_ordem_dia': presentes_ordem_dia, @@ -151,10 +141,6 @@ def get_presentes(pk, response, materia): 'num_presentes_sessao_plenaria': num_presentes_sessao_plen, 'status_painel': 'ABERTO', 'msg_painel': str(_('Votação aberta!')), - 'numero_votos_sim': 0, - 'numero_votos_nao': 0, - 'numero_abstencoes': 0, - 'total_votos': 0, 'tipo_resultado': tipo_votacao, 'observacao_materia': materia.observacao, 'materia_legislativa_texto': str(materia.materia)}) @@ -166,21 +152,8 @@ def get_presentes(pk, response, materia): def get_materia_expediente_aberta(pk): - try: - materia = ExpedienteMateria.objects.filter( - sessao_plenaria_id=pk, votacao_aberta=True).last() - return materia - except ObjectDoesNotExist: - return False - - -def get_last_materia_expediente(pk): - try: - materia = ExpedienteMateria.objects.filter( - sessao_plenaria_id=pk).last() - return materia - except ObjectDoesNotExist: - return None + return ExpedienteMateria.objects.filter( + sessao_plenaria_id=pk, votacao_aberta=True).last() def get_presentes_expediente(pk, response, materia): @@ -221,10 +194,14 @@ def get_presentes_expediente(pk, response, materia): if materia.tipo_votacao == 1: tipo_votacao = 'Simbólica' + response = get_votos(response, materia) elif materia.tipo_votacao == 2: tipo_votacao = 'Nominal' + response = get_votos_nominal(response, materia) elif materia.tipo_votacao == 3: tipo_votacao = 'Secreta' + response = get_votos(response, materia) + response.update({ 'presentes_expediente': presentes_expediente, 'num_presentes_expediente': num_presentes_expediente, @@ -232,10 +209,6 @@ def get_presentes_expediente(pk, response, materia): 'num_presentes_sessao_plenaria': num_presentes_sessao_plen, 'status_painel': str(_('ABERTO')), 'msg_painel': str(_('Votação aberta!')), - 'numero_votos_sim': 0, - 'numero_votos_nao': 0, - 'numero_abstencoes': 0, - 'total_votos': 0, 'tipo_resultado': tipo_votacao, 'observacao_materia': materia.observacao, 'materia_legislativa_texto': str(materia.materia)}) @@ -245,16 +218,14 @@ def get_presentes_expediente(pk, response, materia): # ##########################GENERAL FUNCTIONS############################# -def response_null_materia(response): +def response_nenhuma_materia(response): response.update({ 'status_painel': 'FECHADO', - 'msg_painel': str(_('Nenhuma matéria disponivel para votação.')) - }) + 'msg_painel': str(_('Nenhuma matéria disponivel para votação.'))}) return JsonResponse(response) def get_votos(response, materia): - if materia.tipo_votacao == 1: tipo_votacao = 'Simbólica' elif materia.tipo_votacao == 2: @@ -262,19 +233,34 @@ def get_votos(response, materia): elif materia.tipo_votacao == 3: tipo_votacao = 'Secreta' - registro = RegistroVotacao.objects.filter( - ordem=materia, materia=materia.materia).last() - total = (registro.numero_votos_sim + - registro.numero_votos_nao + - registro.numero_abstencoes) - response.update({ - 'numero_votos_sim': registro.numero_votos_sim, - 'numero_votos_nao': registro.numero_votos_nao, - 'numero_abstencoes': registro.numero_abstencoes, - 'total_votos': total, - 'tipo_votacao': tipo_votacao, - 'tipo_resultado': registro.tipo_resultado_votacao.nome, - }) + if type(materia) == OrdemDia: + registro = RegistroVotacao.objects.filter( + ordem=materia, materia=materia.materia).last() + else: + registro = RegistroVotacao.objects.filter( + expediente=materia, materia=materia.materia).last() + + if registro: + total = (registro.numero_votos_sim + + registro.numero_votos_nao + + registro.numero_abstencoes) + response.update({ + 'numero_votos_sim': registro.numero_votos_sim, + 'numero_votos_nao': registro.numero_votos_nao, + 'numero_abstencoes': registro.numero_abstencoes, + 'total_votos': total, + 'tipo_votacao': tipo_votacao, + 'tipo_resultado': registro.tipo_resultado_votacao.nome, + }) + else: + response.update({ + 'numero_votos_sim': 0, + 'numero_votos_nao': 0, + 'numero_abstencoes': 0, + 'total_votos': 0, + 'tipo_votacao': tipo_votacao, + 'tipo_resultado': 'Ainda não foi votada.', + }) return response @@ -288,48 +274,65 @@ def get_votos_nominal(response, materia): elif materia.tipo_votacao == 3: tipo_votacao = 'Secreta' - registro = RegistroVotacao.objects.get( - ordem=materia, materia=materia.materia) - - votos_parlamentares = VotoParlamentar.objects.filter( - votacao_id=registro.id) - - filiacao = Filiacao.objects.filter( - data_desfiliacao__isnull=True, parlamentar__ativo=True) - parlamentar_partido = {} - for f in filiacao: - parlamentar_partido[ - f.parlamentar.nome_parlamentar] = f.partido.sigla - - for v in votos_parlamentares: - try: - parlamentar_partido[v.parlamentar.nome_parlamentar] - except KeyError: - votos.update({v.parlamentar.id: { - 'parlamentar': v.parlamentar.nome_parlamentar, - 'voto': str(v.voto), - 'partido': str(_('Sem Registro')) - }}) - else: - votos.update({v.parlamentar.id: { - 'parlamentar': v.parlamentar.nome_parlamentar, - 'voto': str(v.voto), - 'partido': parlamentar_partido[v.parlamentar.nome_parlamentar] - }}) - - total = (registro.numero_votos_sim + - registro.numero_votos_nao + - registro.numero_abstencoes) + if type(materia) == OrdemDia: + registro = RegistroVotacao.objects.filter( + ordem=materia, materia=materia.materia).last() + else: + registro = RegistroVotacao.objects.filter( + expediente=materia, materia=materia.materia).last() + + if not registro: + response.update({ + 'numero_votos_sim': 0, + 'numero_votos_nao': 0, + 'numero_abstencoes': 0, + 'total_votos': 0, + 'tipo_votacao': tipo_votacao, + 'tipo_resultado': 'Não foi votado ainda', + 'votos': None + }) - response.update({ - 'numero_votos_sim': registro.numero_votos_sim, - 'numero_votos_nao': registro.numero_votos_nao, - 'numero_abstencoes': registro.numero_abstencoes, - 'total_votos': total, - 'tipo_votacao': tipo_votacao, - 'tipo_resultado': registro.tipo_resultado_votacao.nome, - 'votos': votos - }) + else: + votos_parlamentares = VotoParlamentar.objects.filter( + votacao_id=registro.id) + + filiacao = Filiacao.objects.filter( + data_desfiliacao__isnull=True, parlamentar__ativo=True) + parlamentar_partido = {} + for f in filiacao: + parlamentar_partido[ + f.parlamentar.nome_parlamentar] = f.partido.sigla + + for v in votos_parlamentares: + try: + parlamentar_partido[v.parlamentar.nome_parlamentar] + except KeyError: + votos.update({v.parlamentar.id: { + 'parlamentar': v.parlamentar.nome_parlamentar, + 'voto': str(v.voto), + 'partido': str(_('Sem Registro')) + }}) + else: + votos.update({v.parlamentar.id: { + 'parlamentar': v.parlamentar.nome_parlamentar, + 'voto': str(v.voto), + 'partido': parlamentar_partido[ + v.parlamentar.nome_parlamentar] + }}) + + total = (registro.numero_votos_sim + + registro.numero_votos_nao + + registro.numero_abstencoes) + + response.update({ + 'numero_votos_sim': registro.numero_votos_sim, + 'numero_votos_nao': registro.numero_votos_nao, + 'numero_abstencoes': registro.numero_abstencoes, + 'total_votos': total, + 'tipo_votacao': tipo_votacao, + 'tipo_resultado': registro.tipo_resultado_votacao.nome, + 'votos': votos + }) return response @@ -358,37 +361,57 @@ def get_dados_painel(request, pk): elif expediente: return JsonResponse(get_presentes_expediente(pk, response, expediente)) - ultima_ordem = get_last_materia(pk) - - if ultima_ordem: - if ultima_ordem.resultado: - if ultima_ordem.tipo_votacao in [1, 3]: - return JsonResponse( - get_votos(get_presentes( - pk, response, ultima_ordem), ultima_ordem)) - elif ultima_ordem.tipo_votacao == 2: - return JsonResponse( - get_votos_nominal(get_presentes( - pk, response, ultima_ordem), ultima_ordem)) - else: - return JsonResponse(get_presentes(pk, response, ultima_ordem)) - - ultimo_expediente = get_last_materia_expediente(pk) - - if ultimo_expediente: - if ultimo_expediente.resultado: - if ultimo_expediente.tipo_votacao in [1, 3]: - return JsonResponse( - get_votos(get_presentes( - pk, response, ultimo_expediente), - ultimo_expediente)) - elif ultimo_expediente.tipo_votacao == 2: - return JsonResponse( - get_votos_nominal(get_presentes( - pk, response, ultimo_expediente), - ultimo_expediente)) - else: + # Ultimo voto em ordem e ultimo voto em expediente + last_ordem_voto = RegistroVotacao.objects.filter( + ordem__sessao_plenaria=sessao).last() + last_expediente_voto = RegistroVotacao.objects.filter( + expediente__sessao_plenaria=sessao).last() + + # Ultimas materias votadas + if last_ordem_voto: + ultima_ordem_votada = last_ordem_voto.ordem + if last_expediente_voto: + ultimo_expediente_votado = last_expediente_voto.expediente + + # Caso não tenha nenhuma votação aberta + if last_ordem_voto or last_expediente_voto: + + # Se alguma ordem E algum expediente já tiver sido votado... + if last_ordem_voto and last_expediente_voto: + # Verifica se o último resultado é um uma ordem do dia + if last_ordem_voto.pk >= last_expediente_voto.pk: + if ultima_ordem_votada.tipo_votacao in [1, 3]: + return JsonResponse( + get_votos(get_presentes( + pk, response, ultima_ordem_votada), + ultima_ordem_votada)) + elif ultima_ordem_votada.tipo_votacao == 2: + return JsonResponse( + get_votos_nominal(get_presentes( + pk, response, ultima_ordem_votada), + ultima_ordem_votada)) + # Caso não seja, verifica se é um expediente + else: + if ultimo_expediente_votado.tipo_votacao in [1, 3]: + return JsonResponse( + get_votos(get_presentes( + pk, response, ultimo_expediente_votado), + ultimo_expediente_votado)) + elif ultimo_expediente_votado.tipo_votacao == 2: + return JsonResponse( + get_votos_nominal(get_presentes( + pk, response, + ultimo_expediente_votado), + ultimo_expediente_votado)) + + # Caso somente um deles tenha resultado, prioriza a Ordem do Dia + if last_ordem_voto: + return JsonResponse(get_presentes( + pk, response, ultima_ordem_votada)) + # Caso a Ordem do dia não tenha resultado, mostra o último expediente + if last_expediente_voto: return JsonResponse(get_presentes(pk, response, - ultimo_expediente)) - else: - return response_null_materia(response) + ultimo_expediente_votado)) + + # Retorna que não há nenhuma matéria já votada ou aberta + return response_nenhuma_materia(response) diff --git a/sapl/parlamentares/models.py b/sapl/parlamentares/models.py index 2703b2416..1509e2425 100644 --- a/sapl/parlamentares/models.py +++ b/sapl/parlamentares/models.py @@ -294,7 +294,7 @@ class Parlamentar(models.Model): @property def avatar_html(self): return ''if self.fotografia else '' + + self.fotografia.url + '>'if self.fotografia else '' class TipoDependente(models.Model): diff --git a/sapl/parlamentares/views.py b/sapl/parlamentares/views.py index b461cdbee..21c171c68 100644 --- a/sapl/parlamentares/views.py +++ b/sapl/parlamentares/views.py @@ -416,12 +416,13 @@ class MesaDiretoraView(FormView): [p.parlamentar for p in parlamentares]) - set( parlamentares_ocupados)) - sessao_selecionada = SessaoLegislativa.objects.get( + sessao_sel = SessaoLegislativa.objects.get( id=int(request.POST['sessao'])) - if str(sessao_selecionada.legislatura_id) != int( - request.POST['legislatura']): - sessao_selecionada = SessaoLegislativa.objects.filter( + + if str(sessao_sel.legislatura_id) != request.POST['legislatura']: + sessao_sel = SessaoLegislativa.objects.filter( legislatura=Legislatura.objects.first()).first() + return self.render_to_response( {'legislaturas': Legislatura.objects.all( ).order_by('-numero'), @@ -429,7 +430,7 @@ class MesaDiretoraView(FormView): id=int(request.POST['legislatura'])), 'sessoes': SessaoLegislativa.objects.filter( legislatura_id=int(request.POST['legislatura'])), - 'sessao_selecionada': sessao_selecionada, + 'sessao_selecionada': sessao_sel, 'composicao_mesa': mesa, 'parlamentares': parlamentares_vagos, 'cargos_vagos': cargos_vagos diff --git a/sapl/protocoloadm/forms.py b/sapl/protocoloadm/forms.py index 3e8a44ec5..375229710 100644 --- a/sapl/protocoloadm/forms.py +++ b/sapl/protocoloadm/forms.py @@ -12,7 +12,7 @@ from django.utils.translation import ugettext_lazy as _ from sapl.base.models import Autor from sapl.crispy_layout_mixin import form_actions, to_row -from sapl.materia.models import UnidadeTramitacao +from sapl.materia.models import TipoMateriaLegislativa, UnidadeTramitacao from sapl.utils import (RANGE_ANOS, AnoNumeroOrderingFilter, RangeWidgetOverride, autor_label, autor_modal) @@ -275,7 +275,7 @@ class ProtocoloDocumentForm(ModelForm): tipo_documento = forms.ModelChoiceField( label=_('Tipo de Documento'), - required=False, + required=True, queryset=TipoDocumentoAdministrativo.objects.all(), empty_label='Selecione', ) @@ -333,6 +333,21 @@ class ProtocoloDocumentForm(ModelForm): class ProtocoloMateriaForm(ModelForm): autor = forms.IntegerField(widget=forms.HiddenInput(), required=False) + tipo_materia = forms.ModelChoiceField( + label=_('Tipo de Matéria'), + required=True, + queryset=TipoMateriaLegislativa.objects.all(), + empty_label='Selecione', + ) + + numero_paginas = forms.CharField(label=_('Núm. Páginas'), required=True) + + observacao = forms.CharField(required=False, + widget=forms.Textarea, label='Observação') + + assunto_ementa = forms.CharField(required=True, + widget=forms.Textarea, label='Ementa') + def clean_autor(self): autor_field = self.cleaned_data['autor'] try: @@ -348,6 +363,7 @@ class ProtocoloMateriaForm(ModelForm): fields = ['tipo_materia', 'numero_paginas', 'autor', + 'assunto_ementa', 'observacao'] def __init__(self, *args, **kwargs): @@ -364,19 +380,15 @@ class ProtocoloMateriaForm(ModelForm): 'limpar Autor', css_class='btn btn-primary btn-sm'), 10)]) row3 = to_row( + [('assunto_ementa', 12)]) + row4 = to_row( [('observacao', 12)]) self.helper = FormHelper() self.helper.layout = Layout( Fieldset(_('Identificação da Matéria'), - row1, - HTML(autor_label), - HTML(autor_modal), - row2, - row3, - form_actions(save_label='Protocolar Matéria') - ) - ) + row1, HTML(autor_label), HTML(autor_modal), row2, row3, + row4, form_actions(save_label='Protocolar Matéria'))) super(ProtocoloMateriaForm, self).__init__( *args, **kwargs) diff --git a/sapl/protocoloadm/migrations/0007_auto_20161110_0910.py b/sapl/protocoloadm/migrations/0007_auto_20161110_0910.py new file mode 100644 index 000000000..79aa42f23 --- /dev/null +++ b/sapl/protocoloadm/migrations/0007_auto_20161110_0910.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-11-10 09:10 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('protocoloadm', '0006_auto_20161103_1721'), + ] + + operations = [ + migrations.AlterField( + model_name='documentoadministrativo', + name='ano', + field=models.PositiveSmallIntegerField(choices=[('', 'Selecione'), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano'), + ), + migrations.AlterField( + model_name='protocolo', + name='ano', + field=models.PositiveSmallIntegerField(choices=[('', 'Selecione'), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano do Protocolo'), + ), + ] diff --git a/sapl/protocoloadm/models.py b/sapl/protocoloadm/models.py index 68401a7d8..f3d0a56c2 100644 --- a/sapl/protocoloadm/models.py +++ b/sapl/protocoloadm/models.py @@ -14,6 +14,7 @@ class TipoDocumentoAdministrativo(models.Model): class Meta: verbose_name = _('Tipo de Documento Administrativo') verbose_name_plural = _('Tipos de Documento Administrativo') + ordering = ['descricao'] def __str__(self): return self.descricao @@ -81,6 +82,30 @@ class DocumentoAdministrativo(models.Model): 'tipo': self.tipo, 'assunto': self.assunto } + def delete(self, using=None, keep_parents=False): + if self.texto_integral: + self.texto_integral.delete() + + return models.Model.delete( + self, using=using, keep_parents=keep_parents) + + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + + if not self.pk and self.texto_integral: + texto_integral = self.texto_integral + self.texto_integral = None + models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + self.texto_integral = texto_integral + + return models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + class DocumentoAcessorioAdministrativo(models.Model): documento = models.ForeignKey(DocumentoAdministrativo) @@ -106,6 +131,30 @@ class DocumentoAcessorioAdministrativo(models.Model): def __str__(self): return self.nome + def delete(self, using=None, keep_parents=False): + if self.arquivo: + self.arquivo.delete() + + return models.Model.delete( + self, using=using, keep_parents=keep_parents) + + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + + if not self.pk and self.arquivo: + arquivo = self.arquivo + self.arquivo = None + models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + self.arquivo = arquivo + + return models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + class Protocolo(models.Model): numero = models.PositiveIntegerField( diff --git a/sapl/protocoloadm/views.py b/sapl/protocoloadm/views.py index e45c943b4..3c9f592dd 100644 --- a/sapl/protocoloadm/views.py +++ b/sapl/protocoloadm/views.py @@ -92,6 +92,9 @@ class ProtocoloPesquisaView(PermissionRequiredMixin, FilterView): qs = self.get_queryset().order_by('ano', 'numero') qs = qs.distinct() + + if 'o' in self.request.GET and not self.request.GET['o']: + qs = qs.order_by('-ano', '-numero') kwargs.update({ 'queryset': qs, @@ -220,12 +223,9 @@ class ProtocoloDocumentoView(PermissionRequiredMixin, elif numeracao == 'U': numero = Protocolo.objects.all().aggregate(Max('numero')) - if numero['numero__max'] is None: - numero['numero__max'] = 0 - f.tipo_processo = '0' # TODO validar o significado f.anulado = False - f.numero = numero['numero__max'] + 1 + f.numero = (numero['numero__max'] + 1) if numero['numero__max'] else 1 f.ano = datetime.now().year f.data = datetime.now().strftime('%Y-%m-%d') f.hora = datetime.now().strftime('%H:%M') @@ -356,19 +356,22 @@ class ProtocoloMateriaView(PermissionRequiredMixin, CreateView): protocolo = Protocolo() - protocolo.numero = numero['numero__max'] + 1 + protocolo.numero = ( + numero['numero__max'] + 1) if numero['numero__max'] else 1 protocolo.ano = datetime.now().year protocolo.data = datetime.now().strftime("%Y-%m-%d") protocolo.hora = datetime.now().strftime("%H:%M") protocolo.timestamp = datetime.now().strftime("%Y-%m-%d %H:%M") - protocolo.tipo_processo = '0' # TODO validar o significado + protocolo.tipo_processo = '1' # TODO validar o significado + protocolo.anulado = False + if form.cleaned_data['autor']: protocolo.autor = form.cleaned_data['autor'] - protocolo.anulado = False protocolo.tipo_materia = TipoMateriaLegislativa.objects.get( id=self.request.POST['tipo_materia']) protocolo.numero_paginas = self.request.POST['numero_paginas'] protocolo.observacao = self.request.POST['observacao'] + protocolo.save() return redirect(self.get_success_url(protocolo)) @@ -403,6 +406,9 @@ class PesquisarDocumentoAdministrativoView(DocumentoAdministrativoMixin, qs = self.get_queryset() qs = qs.distinct() + + if 'o' in self.request.GET and not self.request.GET['o']: + qs = qs.order_by('-ano', '-numero') kwargs.update({ 'queryset': qs, diff --git a/sapl/relatorios/views.py b/sapl/relatorios/views.py index ac10778aa..5ad572c51 100644 --- a/sapl/relatorios/views.py +++ b/sapl/relatorios/views.py @@ -920,7 +920,7 @@ def get_etiqueta_protocolos(prots): dic['titulo'] = str(p.numero) + '/' + str(p.ano) dic['data'] = 'Data: ' + p.data.strftime( - "%d/%m/%Y") + ' - Horário: ' + p.hora.strftime("%H:%m") + "%d/%m/%Y") + ' - Horário: ' + p.hora.strftime("%H:%M") dic['txt_assunto'] = p.assunto_ementa dic['txt_interessado'] = p.interessado diff --git a/sapl/rules/__init__.py b/sapl/rules/__init__.py index 407b77ba1..6c5987c64 100644 --- a/sapl/rules/__init__.py +++ b/sapl/rules/__init__.py @@ -2,6 +2,21 @@ from django.utils.translation import ugettext_lazy as _ default_app_config = 'sapl.rules.apps.AppConfig' +""" +Os cinco radicais de permissão completa são: + + RP_LIST, RP_DETAIL, RP_ADD, RP_CHANGE, RP_DELETE =\ + '.list_', '.detail_', '.add_', '.change_', '.delete_', + +Tanto a app crud quanto a app rules estão sempre ligadas a um model. Ao lidar +com permissões, sempre é analisado se é apenas um radical ou permissão +completa, sendo apenas um radical, a permissão completa é montada com base +no model associado. +""" + +RP_LIST, RP_DETAIL, RP_ADD, RP_CHANGE, RP_DELETE =\ + '.list_', '.detail_', '.add_', '.change_', '.delete_', + SAPL_GROUP_ADMINISTRATIVO = _("Operador Administrativo") SAPL_GROUP_PROTOCOLO = _("Operador de Protocolo Administrativo") diff --git a/sapl/rules/apps.py b/sapl/rules/apps.py index affce7bec..66fd06632 100644 --- a/sapl/rules/apps.py +++ b/sapl/rules/apps.py @@ -1,6 +1,5 @@ from builtins import LookupError -import django from django.apps import apps from django.conf import settings from django.contrib.auth import get_user_model @@ -8,8 +7,9 @@ from django.contrib.auth.management import _get_all_permissions from django.core import exceptions from django.db import models, router from django.db.utils import DEFAULT_DB_ALIAS -from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat +from django.utils.translation import ugettext_lazy as _ +import django from sapl.rules import (SAPL_GROUP_ADMINISTRATIVO, SAPL_GROUP_COMISSOES, SAPL_GROUP_GERAL, SAPL_GROUP_MATERIA, SAPL_GROUP_NORMA, @@ -205,8 +205,9 @@ def update_groups(app_config, verbosity=2, interactive=True, def cria_usuario(self, nome, grupo): nome_usuario = nome + param_username = {get_user_model().USERNAME_FIELD: nome_usuario} usuario = get_user_model().objects.get_or_create( - username=nome_usuario)[0] + **param_username)[0] usuario.set_password('interlegis') usuario.save() grupo.user_set.add(usuario) @@ -214,7 +215,7 @@ def update_groups(app_config, verbosity=2, interactive=True, def update_groups(self): print('') print(string_concat('\033[93m\033[1m', - _('Atualizando grupos:'), + _('Atualizando grupos do SAPL:'), '\033[0m')) for rules_group in self.rules_patterns: group_name = rules_group['group'] diff --git a/sapl/rules/map_rules.py b/sapl/rules/map_rules.py index ff5208f4a..f5565b372 100644 --- a/sapl/rules/map_rules.py +++ b/sapl/rules/map_rules.py @@ -12,7 +12,9 @@ from sapl.rules import (SAPL_GROUP_ADMINISTRATIVO, SAPL_GROUP_ANONYMOUS, SAPL_GROUP_GERAL, SAPL_GROUP_LOGIN_SOCIAL, SAPL_GROUP_MATERIA, SAPL_GROUP_NORMA, SAPL_GROUP_PAINEL, SAPL_GROUP_PARLAMENTAR, - SAPL_GROUP_PROTOCOLO, SAPL_GROUP_SESSAO) + SAPL_GROUP_PROTOCOLO, SAPL_GROUP_SESSAO, + RP_LIST, RP_DETAIL, RP_ADD, RP_CHANGE, RP_DELETE) + from sapl.sessao import models as sessao """ @@ -42,20 +44,8 @@ arquivo (sapl.rules.map_rules.py) e criar os grupos definidos na regra de negócio trabalham com os cinco radiais de permissão e com qualquer outro tipo de permissão customizada, nesta ordem de precedência. -Os cinco radicais de permissão completa são: - - RP_LIST, RP_DETAIL, RP_ADD, RP_CHANGE, RP_DELETE =\ - '.list_', '.detail_', '.add_', '.change_', '.delete_', - -Tanto a app crud quanto a app rules estão sempre ligadas a um model. Ao lidar -com permissões, sempre é analisado se é apenas um radical ou permissão -completa, sendo apenas um radical, a permissão completa é montada com base -no model associado. """ -RP_LIST, RP_DETAIL, RP_ADD, RP_CHANGE, RP_DELETE =\ - '.list_', '.detail_', '.add_', '.change_', '.delete_', - __base__ = [RP_LIST, RP_DETAIL, RP_ADD, RP_CHANGE, RP_DELETE] __listdetailchange__ = [RP_LIST, RP_DETAIL, RP_CHANGE] @@ -83,7 +73,9 @@ rules_group_protocolo = { (materia.Anexada, __base__), (materia.Autoria, __base__), - (materia.Proposicao, __listdetailchange__), + (materia.Proposicao, ['detail_proposicao_enviada', + 'detail_proposicao_devolvida', + 'detail_proposicao_incorporada']), (compilacao.TextoArticulado, ['view_restricted_textoarticulado']) ] } @@ -128,7 +120,7 @@ rules_group_norma = { 'group': SAPL_GROUP_NORMA, 'rules': [ (norma.NormaJuridica, __base__), - (norma.VinculoNormaJuridica, __base__), + (norma.NormaRelacionada, __base__), # Publicacao está com permissão apenas para norma e não para matéria # e proposições apenas por análise do contexto, não é uma limitação @@ -221,6 +213,7 @@ rules_group_geral = { (norma.AssuntoNorma, __base__), (norma.TipoNormaJuridica, __base__), + (norma.VinculoNormaJuridica, __base__), (parlamentares.Legislatura, __base__), (parlamentares.SessaoLegislativa, __base__), @@ -259,7 +252,8 @@ rules_group_geral = { # este model é um espelho do model integrado e sua edição pode # confundir Autores, operadores de matéria e/ou norma. # Por isso está adicionado apenas para o operador geral - (compilacao.TextoArticulado, __base__ + ['lock_unlock_textoarticulado']), + (compilacao.TextoArticulado, + __base__ + ['lock_unlock_textoarticulado']), # estes tres models são complexos e a principio apenas o admin tem perm (compilacao.TipoDispositivo, []), diff --git a/sapl/sessao/forms.py b/sapl/sessao/forms.py index 1ade99710..f0856330c 100644 --- a/sapl/sessao/forms.py +++ b/sapl/sessao/forms.py @@ -79,11 +79,26 @@ class ExpedienteMateriaForm(ModelForm): fields = ['data_ordem', 'numero_ordem', 'tipo_materia', 'observacao', 'numero_materia', 'ano_materia', 'tipo_votacao'] + def clean_numero_ordem(self): + sessao = self.instance.sessao_plenaria + + ex = ExpedienteMateria.objects.filter( + sessao_plenaria=sessao, + numero_ordem=self.cleaned_data['numero_ordem']).count() + + if ex >= 1: + msg = _('Esse número de ordem já existe.') + raise ValidationError(msg) + + return self.cleaned_data['numero_ordem'] + def clean_data_ordem(self): - return datetime.now() + return self.instance.sessao_plenaria.data_inicio def clean(self): cleaned_data = self.cleaned_data + sessao = self.instance.sessao_plenaria + try: materia = MateriaLegislativa.objects.get( numero=self.cleaned_data['numero_materia'], @@ -96,6 +111,14 @@ class ExpedienteMateriaForm(ModelForm): else: cleaned_data['materia'] = materia + ex = ExpedienteMateria.objects.filter( + sessao_plenaria=sessao, + materia=materia).count() + + if ex >= 1: + msg = _('Essa matéria já foi cadastrada.') + raise ValidationError(msg) + return cleaned_data def save(self, commit=False): @@ -113,10 +136,12 @@ class OrdemDiaForm(ExpedienteMateriaForm): 'numero_materia', 'ano_materia', 'tipo_votacao'] def clean_data_ordem(self): - return datetime.now() + return self.instance.sessao_plenaria.data_inicio def clean(self): cleaned_data = self.cleaned_data + sessao = self.instance.sessao_plenaria + try: materia = MateriaLegislativa.objects.get( numero=self.cleaned_data['numero_materia'], @@ -129,6 +154,14 @@ class OrdemDiaForm(ExpedienteMateriaForm): else: cleaned_data['materia'] = materia + ex = ExpedienteMateria.objects.filter( + sessao_plenaria=sessao, + materia=materia).count() + + if ex >= 1: + msg = _('Essa matéria já foi cadastrada.') + raise ValidationError(msg) + return cleaned_data def save(self, commit=False): diff --git a/sapl/sessao/models.py b/sapl/sessao/models.py index cf74399b5..2ad10beed 100644 --- a/sapl/sessao/models.py +++ b/sapl/sessao/models.py @@ -7,7 +7,7 @@ from sapl.materia.models import MateriaLegislativa from sapl.parlamentares.models import (CargoMesa, Legislatura, Parlamentar, Partido, SessaoLegislativa) from sapl.utils import (YES_NO_CHOICES, SaplGenericRelation, - restringe_tipos_de_arquivo_txt) + restringe_tipos_de_arquivo_txt, texto_upload_path) class CargoBancada(models.Model): @@ -31,7 +31,7 @@ class Bancada(models.Model): legislatura = models.ForeignKey(Legislatura, verbose_name=_('Legislatura')) nome = models.CharField( max_length=80, - verbose_name=_('Nome da Bancada, Bloco, ou Frente')) + verbose_name=_('Nome da Bancada')) partido = models.ForeignKey(Partido, blank=True, null=True, verbose_name=_('Partido')) data_criacao = models.DateField(blank=True, null=True, @@ -77,11 +77,13 @@ def get_sessao_media_path(instance, subpath, filename): def pauta_upload_path(instance, filename): - return get_sessao_media_path(instance, 'pauta', filename) + return texto_upload_path(instance, filename, subpath='pauta') + # return get_sessao_media_path(instance, 'pauta', filename) def ata_upload_path(instance, filename): - return get_sessao_media_path(instance, 'ata', filename) + return texto_upload_path(instance, filename, subpath='ata') + # return get_sessao_media_path(instance, 'ata', filename) class SessaoPlenaria(models.Model): @@ -144,6 +146,37 @@ class SessaoPlenaria(models.Model): # XXX check if it shouldn't be legislatura.numero 'legislatura_id': self.legislatura.numero} + def delete(self, using=None, keep_parents=False): + if self.upload_pauta: + self.upload_pauta.delete() + + if self.upload_ata: + self.upload_ata.delete() + + return models.Model.delete( + self, using=using, keep_parents=keep_parents) + + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + + if not self.pk and (self.upload_pauta or self.upload_ata): + upload_pauta = self.upload_pauta + upload_ata = self.upload_ata + self.upload_pauta = None + self.upload_ata = None + models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + + self.upload_pauta = upload_pauta + self.upload_ata = upload_ata + + return models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + class AbstractOrdemDia(models.Model): TIPO_VOTACAO_CHOICES = Choices( diff --git a/sapl/sessao/views.py b/sapl/sessao/views.py index d5ce1bdad..3ccd0f4b0 100644 --- a/sapl/sessao/views.py +++ b/sapl/sessao/views.py @@ -3,7 +3,7 @@ from re import sub from django.contrib import messages from django.contrib.auth.decorators import permission_required -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.core.urlresolvers import reverse from django.forms.utils import ErrorList from django.http import JsonResponse @@ -80,10 +80,14 @@ def reordernar_materias_ordem(request, pk): @permission_required('sessao.change_expedientemateria') def abrir_votacao_expediente_view(request, pk, spk): - existe_votacao_aberta = ExpedienteMateria.objects.filter( + existe_expediente_aberto = ExpedienteMateria.objects.filter( sessao_plenaria_id=spk, votacao_aberta=True ).exists() - if existe_votacao_aberta: + existe_ordem_aberta = OrdemDia.objects.filter( + sessao_plenaria_id=spk, votacao_aberta=True + ).exists() + + if existe_expediente_aberto or existe_ordem_aberta: msg = _('Já existe uma matéria com votação aberta. Para abrir ' 'outra, termine ou feche a votação existente.') messages.add_message(request, messages.INFO, msg) @@ -97,10 +101,14 @@ def abrir_votacao_expediente_view(request, pk, spk): @permission_required('sessao.change_ordemdia') def abrir_votacao_ordem_view(request, pk, spk): - existe_votacao_aberta = OrdemDia.objects.filter( + existe_ordem_aberta = OrdemDia.objects.filter( sessao_plenaria_id=spk, votacao_aberta=True ).exists() - if existe_votacao_aberta: + existe_expediente_aberto = ExpedienteMateria.objects.filter( + sessao_plenaria_id=spk, votacao_aberta=True + ).exists() + + if existe_ordem_aberta or existe_expediente_aberto: msg = _('Já existe uma matéria com votação aberta. Para abrir ' 'outra, termine ou feche a votação existente.') messages.add_message(request, messages.INFO, msg) @@ -125,6 +133,11 @@ class MateriaOrdemDiaCrud(MasterDetailCrud): class CreateView(MasterDetailCrud.CreateView): form_class = OrdemDiaForm + def get_initial(self): + self.initial['data_ordem'] = SessaoPlenaria.objects.get( + pk=self.kwargs['pk']).data_inicio.strftime('%d/%m/%Y') + return self.initial + def get_success_url(self): return reverse('sapl.sessao:ordemdia_list', kwargs={'pk': self.kwargs['pk']}) @@ -323,6 +336,11 @@ class ExpedienteMateriaCrud(MasterDetailCrud): class CreateView(MasterDetailCrud.CreateView): form_class = ExpedienteMateriaForm + def get_initial(self): + self.initial['data_ordem'] = SessaoPlenaria.objects.get( + pk=self.kwargs['pk']).data_inicio.strftime('%d/%m/%Y') + return self.initial + def get_success_url(self): return reverse('sapl.sessao:expedientemateria_list', kwargs={'pk': self.kwargs['pk']}) @@ -1854,9 +1872,14 @@ class VotacaoExpedienteEditView(SessaoPermissionMixin): 'ementa': expediente.observacao} context.update({'materia': materia}) - votacao = RegistroVotacao.objects.get( - materia_id=materia_id, - expediente_id=expediente_id) + try: + votacao = RegistroVotacao.objects.get( + materia_id=materia_id, + expediente_id=expediente_id) + except MultipleObjectsReturned: + votacao = RegistroVotacao.objects.filter( + materia_id=materia_id, + expediente_id=expediente_id).last() votacao_existente = {'observacao': sub( ' ', ' ', strip_tags(votacao.observacao)), 'tipo_resultado': @@ -1875,9 +1898,14 @@ class VotacaoExpedienteEditView(SessaoPermissionMixin): expediente_id = kwargs['mid'] if(int(request.POST['anular_votacao']) == 1): - RegistroVotacao.objects.get( - materia_id=materia_id, - expediente_id=expediente_id).delete() + try: + RegistroVotacao.objects.get( + materia_id=materia_id, + expediente_id=expediente_id).delete() + except MultipleObjectsReturned: + RegistroVotacao.objects.filter( + materia_id=materia_id, + expediente_id=expediente_id).last().delete() expediente = ExpedienteMateria.objects.get( sessao_plenaria_id=self.object.id, diff --git a/sapl/settings.py b/sapl/settings.py index cfeefdd0f..bcf61fd56 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -32,6 +32,8 @@ SECRET_KEY = config('SECRET_KEY', default='') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = config('DEBUG', default=False, cast=bool) +MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' + ALLOWED_HOSTS = ['*'] LOGIN_REDIRECT_URL = '/' @@ -165,6 +167,8 @@ EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='') EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='') EMAIL_USE_TLS = config('EMAIL_USE_TLS', cast=bool, default=True) EMAIL_SEND_USER = config('EMAIL_SEND_USER', cast=str, default='') +DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', cast=str, default='') +SERVER_EMAIL = config('SERVER_EMAIL', cast=str, default='') MAX_DOC_UPLOAD_SIZE = 5 * 1024 * 1024 # 5MB MAX_IMAGE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB diff --git a/sapl/static/js/app.js b/sapl/static/js/app.js index 9b7f2297d..139806ffb 100644 --- a/sapl/static/js/app.js +++ b/sapl/static/js/app.js @@ -13,9 +13,9 @@ function initTinymce(elements, readonly=false) { } if (readonly) { - config_tinymce.readonly = 1, - config_tinymce.menubar = false, - config_tinymce.toolbar = false + config_tinymce.readonly = 1; + config_tinymce.menubar = false; + config_tinymce.toolbar = false; } if (elements != null) { diff --git a/sapl/static/js/compilacao.js b/sapl/static/js/compilacao.js index 6d797ea41..fb9104917 100644 --- a/sapl/static/js/compilacao.js +++ b/sapl/static/js/compilacao.js @@ -22,12 +22,8 @@ function insertWaitAjax(element) { jQuery(element).append('
    '); } - - function DispostivoSearch(opts) { - $(function() { - var container_ds = $('body').children("#container_ds"); if (container_ds.length > 0) $(container_ds).remove(); diff --git a/sapl/static/js/compilacao_view.js b/sapl/static/js/compilacao_view.js index ffb96dd8b..e4860bb76 100644 --- a/sapl/static/js/compilacao_view.js +++ b/sapl/static/js/compilacao_view.js @@ -142,6 +142,7 @@ function textoVigente(item, link) { } $(document).ready(function() { + setTimeout(function() { var href = location.href.split('#') if (href.length == 2) { @@ -155,7 +156,6 @@ $(document).ready(function() { } }, 100); - $("#btn_font_menos").click(function() { $(".dpt").css("font-size", "-=1"); }); @@ -169,4 +169,10 @@ $(document).ready(function() { $(this).css('z-index', 15-nivel) }); + + + /*$(".indent").each(function() { + $(this).removeClass('indent'); + $(this.parentElement).addClass('indent'); + });*/ }); diff --git a/sapl/static/styles/compilacao.scss b/sapl/static/styles/compilacao.scss index ac397b0a8..946852703 100644 --- a/sapl/static/styles/compilacao.scss +++ b/sapl/static/styles/compilacao.scss @@ -165,7 +165,6 @@ a:link:after, a:visited:after { .cp { .desativado { - .dtxt, .dtxt *, .dpt-link, .dpt-link * { text-decoration: line-through; color: #999 !important; @@ -180,6 +179,7 @@ a:link:after, a:visited:after { text-decoration: none; cursor: pointer; } + .diff { .desativado, .desativado * { text-decoration: line-through; @@ -194,7 +194,9 @@ a:link:after, a:visited:after { .dpt { font-size:1em; position: relative; - + &.indent { + padding-left: 1em; + } .ementa { padding: 2em 0em 2em 35%; font-weight: bold; @@ -259,26 +261,22 @@ a:link:after, a:visited:after { } .paragrafo { - padding-left: 1.5em; font-size: 1.1em; margin-top: 0.2222em; } .inciso { font-size: 1.1em; - padding-left: 2.5em; margin-top: 0.1667em; } .alinea { font-size: 1.0em; - padding-left: 3.5em; margin-top: 2px; } .item { font-size: 1.0em; - padding-left: 4.5em; margin-top: 2px; } @@ -294,6 +292,7 @@ a:link:after, a:visited:after { a, table, table td { color: #018 !important; } + } .dn { /* Notas de Dispositivo*/ @@ -376,7 +375,6 @@ a:link:after, a:visited:after { &:hover { display: block; - * { display: block; } @@ -524,6 +522,7 @@ a:link:after, a:visited:after { color: #02baf2 !important; } } + .dpt { display: block; & > .dpt-actions-fixed { @@ -592,6 +591,7 @@ a:link:after, a:visited:after { } } } /* fim .dpt */ + .dpt-alts { margin: 0; margin-bottom: 1em; @@ -768,7 +768,7 @@ a:link:after, a:visited:after { } } -.cp.cpe1 { +.cp.cpe1_old_apagar { margin-bottom: 15em; diff --git a/sapl/templates/base.html b/sapl/templates/base.html index d8c1d2c0c..c1ab49e09 100644 --- a/sapl/templates/base.html +++ b/sapl/templates/base.html @@ -94,7 +94,9 @@ - {% block sections_nav %} {% subnav %} {% endblock sections_nav %} +
    + {% block sections_nav %} {% subnav %} {% endblock sections_nav %} +
    {% endblock main_header %} @@ -148,7 +150,7 @@ {% block footer_container %} -