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

Mexendo no Django admin

Minha preferência por Python e Django não é segredo p/ ninguém que me conheça. Como não me sinto muito à vontade fazendo o trabalho pesado de um designer, resolvi aproveitar os recursos do Django admin e mexer nele p/ ver até onde uma pessoa minimamente curiosa que não usa o framework profissionalmente consegue ir.

A aplicação escolhida é um gerenciador de projetos, quase como aqueles trabalhos de software de locadora que se costuma fazer p/ conclusão de curso. Resolvi que queria entender melhor o que o Django admin pode oferecer (mesmo sem ter nenhuma perspectiva de uso profissional disso por enquanto).

Ambiente: Django 1.5.1, dentro do virtualenv. Precisei incluir o diretório Python27\DLLs no PYTHONPATH do Windows p/ criar o ambiente virtual (ele reclama de uma lib de sockets). Estou usando também o South p/ as migrações do banco e o Django-grappelli.

Convenções (a parte chata):

  • Mensagens de erro serão exibidas na cor vermelha;
  • Códigos extensos serão “colados” do Gist;
  • Nomes de arquivos serão exibidos em negrito;
  • Nomes de diretórios (ex: diretório de apps) serão exibidos em itálico;
  • Classes serão exibidas em itálico sublinhado
  • Variáveis, funções e métodos (tratarei genericamente como “atributos”, incluindo chamadas a métodos) serão exibidos em fundo azul com texto branco em itálico


Projetos e versões


Este é o models.py:



Para que eles sejam visíveis no Django admin é preciso registrá-los. Para isso, criei um arquivo admin.py dentro do diretório da minha app (pjmanager) com este conteúdo (primeira versão):



O problema é que quando o servidor Django deu refresh, me apresentou a seguinte mensagem:

'ProjectAdmin.fieldsets[1][1]['fields']' refers to field 'creation_date' that is missing from the form.

E lá fui eu tentar entender o motivo... Olhando parte da documentação disponível no site do Django [1], notei o seguinte parágrafo:

The fields option, unlike list_display, may only contain names of fields on the model or the form specified by form. It may contain callables only if they are listed in readonly_fields.


O campo  creation_date  faz parte do model, então o erro que o framework exibiu não fez muito sentido. O que me restou foi a segunda parte da restrição: o DateField é um callable? Abri o shell e o comando  hasattr(models.DateField, ‘__call__’)  retornou True. Adicionei a seguinte linha ao ProjectAdmin e o site carregou normalmente:

 readonly_fields = ('creation_date',) 

Primeiro problema resolvido, mas não sem uma dose de "desespero" de quem nunca mexeu no admin. Apesar de ter feito as aulas do Henrique Bastos (www.welcometothedjango.com.br — recomendadíssimo), o dia-a-dia é o melhor professor que existe. Parti p/ a criação das versões de um projeto.

Para o cadastro das versões de um projeto, não quis utilizar uma tela separada, pois já que estamos cadastrando um projeto, por que não colocar o cadastro de versões na própria tela? Assim fica disponível até durante a edição. Lembrei de já ter usado inlines, mas como eu tinha uma idéia meio diferente, resolvi perguntar no Google Groups ("Welcome to the Django"). Uma alma boa me direcionou p/ a documentação dos inlines [2] e optei por usar o TabularInline, mas noob que sou, não consegui fazer o que queria de primeira e perguntei se teria que alterar os templates. A resposta foi "sim". Como estou começando, decidi ficar longe dessas alterações de template por enquanto e fui fuçar no TabularInline:



A propósito, o campo  creation_date  do ProjectVersionInline funciona da mesma maneira que na classe ProjectAdmin. Agora tenho um cadastro de projeto com cadastro (e uma listagem simples) das versões.

Em seguida, fui mexer na listagem de projetos. Para que ela seja alterada é necessário informar os campos no atributo  list_display .O que eu gostaria de exibir, a princípio, é o nome do projeto sendo a concatenação entre o código (ou sigla) e o nome (real) do projeto, além da verão mais atual cadastrada. Para isso criei dois métodos e os listei no atributo  list_display :



Mas o bloco do try (método  current_version )está muito feio. Acabei substituindo por uma única linha graças ao ORM:



No shell esse comando lança uma exceção quando o projeto não possui versões cadastrada, mas no  list_display  é exibido "(None)".


Referências

[1] https://docs.djangoproject.com/en/dev/ref/contrib/admin/#modeladmin-objects
[2] https://docs.djangoproject.com/en/dev/ref/contrib/admin/#inlinemodeladmin-objects