- O requisito será pela versão do projeto;
- 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
