Após 1 ano sem postar absolutamente nada, precisei "forçar" um pouco as coisas:

- Tirei uma certificação (ISTQB CTFL);
- Parei um pouco o desenvolvimento e estou me concentrando em criar um processo p/ a equipe de teste na empresa onde trabalho. A idéia é uniformizar o modus operandi e termos ferramentas e índices para não esquentarmos mais a cabeça com artefatos de qualidade questionável, como análise de requisitos.

E estou fazendo um mapa mental do Silmarillion...

Mexendo no Django admin - parte 2

Terminado o cadastro de produtos, resolvi criar um cadastro de requisitos. Não tenho como colar um modelo de banco de dados aqui, então vamos adaptar.

  1. O requisito será pela versão do projeto;
  2. Cada requisito terá um tipo.


Os campos, a princípio são esses:

TIPO_REQUISITO
(id, tipo, descrição)

REQUISITO
(id,id_versão_projeto, id_tipo_requisito, código, título, descrição)

Id_versão_projeto e id_tipo_requisito são chaves estrangeiras.

Tipos de requisitos


O model e modeladmin dos tipos de requisitos é bem simples:

models.py


admin.py


O que é necessário é carregar os dados iniciais no banco. Para isso criei um arquivo requirements.json no diretório fixtures com este conteúdo:


[{"model":"pjmanager.requirementtype",
"pk":1,
"fields": {"requirement_type": "Functional",
"requirement_type_description": "Describes a set of inputs, behavior and outputs. It defines what a system is supposed to accomplish."}
},
{"model":"pjmanager.requirementtype",
"pk":2,
"fields": {"requirement_type": "Non-functional",
"requirement_type_description": "Defines how a system is supposed to be."}
},
{"model":"pjmanager.requirementtype",
"pk":3,
"fields": {"requirement_type": "Performance",
"requirement_type_description": "Describes how well something has to be done."}
}]


Segundo a documentação ([1]), caso eu crie um arquivo initial_data.json, os dados serão carregados automaticamente ao executar o syncdb. Mas como estou usando o South p/ versionar o model, a princípio, vou ter que carregar os dados na mão mesmo.

Ainda, o Django localiza as fixtures no diretório fixtures de cada app. Como não me agrada a idéia de ter esses diretórios espalhados em todas as apps, criei um diretório top-level e incluí a variável  FIXTURE_DIRS  no arquivo settings.py apontando para esse diretório global de fixtures.

Após executar o syncdb e o migrate, a carga dos dados é feita através do comando  manage.py loaddata requirements . Ao acessar o Django admin tem-se o modelo dos tipos de requisitos registrado p/ operação normal e a lista dos tipos já cadastrados.


Requisitos


Essa parte foi um pouco mais complicada, pois precisei alterar diversas coisas que não eram muito óbvias, pelo menos p/ quem está começando e não sabe como as coisas estão amarradas dentro do admin. O arquivo models.py completo:



Caso registremos o model dos requisitos normalmente teremos apenas os campos versão do projeto, tipo do requisito, código do requisito, título do requisito e descrição do requisito, mas não poderemos selecionar o projeto para o qual queremos cadastrar o requisito. Então adicionamos o campo  project , salvamos e recarregamos a tela de cadastro de requisitos para ver a seguinte mensagem:


ImproperlyConfigured at /admin/pjmanager/requirement/add/
'RequirementAdmin.fields' refers to field 'project' that is missing from the form.


O que acontece é que o campo project não faz parte do modelo Requirement que está registrado no admin. Para que possamos ter esse campo na página, precisamos criar um formulário personalizado. Escolhi criar um modelform, pois  project  é o único campo que preciso personalizar. Normalmente vejo tutoriais criando modelforms e forms em um mesmo arquivo, mas optei por separar os dois tipos. O código da primeira versão do modelform é este:



Para que o modeladmin use esse formulário devemos especificá-lo no atributo  form  do modeladmin.

Estamos filtrando apenas os projetos ativos. Embora não dê erro ao recarregar o cadastro de requisitos no admin, podemos ver todas as versões de projeto cadastradas, independente se elas pertencem a projetos ativos ou a um determinado projeto ao qual você queira adicionar um requisito. Isso não está muito certo, então, a princípio o campo de versões de um projeto deve esperar a seleção do projeto para exibir suas versões. Enquanto isso não ocorrer, ele deve ser apresentado vazio.

Eu poderia remover o campo project_version do modelform e adicionar um outro campo para fazer esse papel (tal como foi feito com o campo project). De fato, foi uma das coisas que fiz. Inicialmente ele estaria vazio e a lista de elementos seria construída sob demanda. Mas ao salvar me deparei com uma mensagem de erro dizendo que a opção selecionada não era um valor válido. Provavelmente devido à inicialização do atributo  choices  do campo no form (vazio). Como esse caminho não deu muito certo, fiquemos com o Javascript.

Criei um diretório static\js, adicionando-o à variável  STATICFILES_DIRS  em settings.py. Lendo um pouco descobri que o Django admin usa JQuery (yay!) e isso facilita muito a minha vida, já que meu primeiro contato com Javascript foi através do JQuery (dificilmente precisei usar Javascript "puro") e não foi há muito tempo. Das minhas experiências anteriores, lembrei que havia um jeito de indicar arquivos CSS e Javascript ao se criar um form, através da classe Media. O modeladmin ficou assim:



E este é o arquivo requirement_form.js:



Aqui foi a primeira pedra no caminho. Estou acostumada a usar o JQuery fora do Django admin (frameworks em geral), então o óbvio  $(document).ready  simples não funcionou: a reclamação é que $ não está definido. Tive que rodar a internet p/ saber como usar o JQuery dentro do admin. Novamente o StackOverflow [2] salvou meu dia.

Funcionamento: amarramos ao evento change do dropdown do projeto o envio de uma requisição ajax ao URL /get_project_versions/(\d+). Aqui escrevemos o primeiro código de controlador necessário para o projeto. Seguindo a estrutura de projeto montada pelo Django, o código é escrito em views.py, mas optei por criar um arquivo ajax_views.py e importá-lo no views.py. Segue o código do controlador (server side):



O que o controlador retorna é um objeto JSON contendo a chave primária e o nome da versão. Voltando ao código Javascript: após o retorno dos dados, precisamos exibi-los na dropdown correta. Antes disso devemos remover quaisquer versões que possam estar presentes como opções.

Como já temos o cadastro pronto, o ponto seguinte é alterar a listagem dos requisitos p/ exibir informações relevantes. Por enquanto gostaria de exibir o nome do projeto, a versão associada ao requisito, seu tipo, código e título. Para isso usamos o atributo  list_display  do modeladmin novamente:



Agora, vamos à edição de um registro. O primeiro problema que aparece é o campo  project  não selecionado. Como ele não está ligado ao model (declaramos separadamente p/ poder usar no formulário de cadastro), não tem como vir preenchido. Novamente precisamos adaptar as coisas e foi aqui que empaquei por uns 3 ou 4 dias e tive que rodar a Internet atrás de uma solução. Poderia usar Javascript novamente, fazendo uma requisição p/ obter o projeto e selecioná-lo no template, mas fiquei com a opção de indicar um valor padrão pelo formulário pelo lado do Django. Após esse tempo, encontrei a solução dentro do código do framework: sobrescrever o método que exibe o template p/ edição, o  change_view . Essa sobrecarga vai no modeladmin:



Agora o projeto está selecionado, mas ainda temos problemas: o dropdown da versão do projeto ainda vem com todas as versões de projeto cadastradas. Temos que escolher entre permitir a alteração do projeto (e mexer no dropdown da versão usando Javascript) ou fixar o projeto e exibir somente as versões relacionadas ao projeto. Escolhi a primeira opção e alterei o requirement_form.js:



Para obter os dados relativos ao requisito, enviamos uma requisição para /get_requirement_data/(\d+), cujo controlador é este:



Não é bonito. Voltei atrás e tentei usar o modelform p/ restringir as versões do projeto ao exibir a página de edição [3]. Para isso tive que alterar o método  __init__ :



Assim, removemos a sobrecarga do método  change_view , já que não consegui alterar o queryset por lá.

Ainda temos problemas: caso o usuário acesse a página de edição de um requisito e depois acesse a de cadastro, o projeto aparece pré-selecionado "magicamente". A alteração que fiz foi atribuir None como valor inicial caso 'instance' não seja uma chave no  __init__  do modelform.

Nesse meio tempo decidi que não quero que o usuário altere o projeto ao editar o requisito. Logo, o campo deve estar bloqueado p/ edição. Quando tentei isso no método  change_views  alterando incluindo o campo  project no atributo  readonly_fields , recebi uma mensagem de erro. Então optei pelo Javascript:



No entanto, ao salvar uma edição, aparece uma mensagem informando que o campo  project  é obrigatório. Ele está preenchido, mas o que acontece é que, segundo o W3C, controles desabilitados não são válidos para envio [4]. Então, o que eu devo fazer é criar um hidden input com o mesmo nome do controle desabilitado. Tentei fazer isso no server side, mas o Django admin não suporta hidden fields ainda [5]. Lá fui eu p/ o Javascript de novo:



Tendo arrumado o envio do formulário, resta agora agruparmos a lista de requisitos por projeto. A ordenação será feita por código do projeto e versão do mesmo. Para isso atribuímos uma tupla ao atributo  ordering  do modeladmin.

Depois de todo esse trabalho, eis os códigos finais:







Referências


[1] https://docs.djangoproject.com/en/dev/howto/initial-data/
[2] http://stackoverflow.com/questions/4709298/difficulty-with-django-and-jquery-why-is-undefined-in-the-admin-app
[3] http://stackoverflow.com/questions/949268/django-accessing-the-model-instance-from-within-modeladmin
[4] http://www.w3.org/TR/html401/interact/forms.html#h-17.12
[5] http://stackoverflow.com/questions/4999005/create-a-hidden-field-in-django-admin