Criando novas views para o admin

Um bom tempo sem postar nada novo… porém estou de volta com uma dica simples que já usei bastante.

Quem utiliza o Django com certeza adora a app admin que o acompanha, nós desenvolvedores ganhamos de presente uma administração de sites útil para praticamente qualquer site, e diga-se de passagem as administrações são sempre as partes mais chatas quando se produz um site.

Como nem tudo é perfeito, quando é necessário customizar o admin temos diversas opções, porém essa não é a intenção desse post. Quem quiser saber mais detalhes sobre customização do admin pode acessar a documentação do django e essa página da IBM.

O nosso caso é diferente, existem situações em que precisamos criar novas funcionalidades específicas para a administracao (não estão relacionadas a nenhum model), mantendo o layout e as restrições de acesso.

O primeiro passo é criar a view:

from django.shortcuts import render_to_response
from django.template import RequestContext

def minha_view(request):
    #executa alguma funcionalidade
    return render_to_response('app/minha_view.html',locals(),request_instance=RequestContext(request))

Note que a view acima é igual a qualquer outra que se faz normalmente.
Em seguida definimos uma url para nossa view:

urlpatterns = patterns('',
    (r'^admin/admin_view','app.views.minha_view'),
    (r'^admin/', include(admin.site.urls)),
)

Eu preciso definir a minha url antes da url do admin, se ela for colocada após o admin a view não é encontrada. Cuidado para não sobrepor nenhuma view já existente do admin.

Agora criamos o template minha_view.html:

{% extends 'admin/base_site.html' %}
{% block content %}
Aqui ficarão os dados da sua view
{% endblock %}

O template acima é muito simples, caso queira fazer algo mais sofisticado (como css, breadcrumbs e forms) sugiro olhar os templates padrões do admin. Eles podem ser encontrados em: %DJANGO_DIR%/contrib/admin/templates/admin.

Pronto, sua view já possui a ‘cara’ do admin. Porém ela pode ser acessada por qualquer usuário, até mesmo os logados.

O passo natural seria usar o decorator login_required para exigir o login, so que por algum motivo desconhecido os redirecionamentos do admin não combinam muito bem com views externas. Nesso caso a solução é transformar a nossa view numa view do admin, para isso editamos o urls.py:

from django.contrib import admin
from app.views import minha_view

urlpatterns = patterns('',
    (r'^admin/admin_view',admin.site.admin_view(minha_view),
    (r'^admin/', include(admin.site.urls)),
)

Pronto sua view está protegida de acessos anônimos e todos os redirecionamentos funcionam corretamente.
Agora é só criar suas novas funcionalidades embutidas diretamente no admin.

Espero ter ajudado. Até a próxima.

Um pouco de metaprogramação

Quem assina a lista django-brasil recebeu essa mensagem do Luciano Ramalho sobre metaprogramação. Achei muito interressante e demonstra perfeitamente o quão poderosa é a linguagem Python.

Estou reproduzindo o texto sem alterações:

Em geral não é tedioso programar com o Django, mas às vezes é, veja só quanta repetição:

# http://pastebin.com/f2fc7b238

class DescriptorInline(admin.TabularInline):
   model = Descriptor

class RecruitmentCountryInline(admin.TabularInline):
   model = RecruitmentCountry

class OutcomeInline(admin.StackedInline):

   model = Outcome

class TrialInterventionCodeInline(admin.TabularInline):

   model = TrialInterventionCode

class SecondaryNumberInline(admin.TabularInline):

   model = TrialNumber

class TrialContactInline(admin.TabularInline):

   model = TrialContact

class TrialInstitutionInline(admin.TabularInline):

   model = TrialInstitution

class ClinicalTrialAdmin(admin.ModelAdmin):

   inlines = [SecondaryNumberInline, RecruitmentCountryInline,

              OutcomeInline, TrialContactInline, TrialInstitutionInline,

              DescriptorInline, TrialInterventionCodeInline]

No código acima, cada nome de modelo aparece três vezes… (uma vez ao
declarar a classe XXXInline, no atributo model do inline, e mais uma
vez no atributo inlines).

Além disso, quase todas as declarações de inlines são idênticas… E o
princípio DRY, como fica?

Felizmente Django é Python. Então aquilo pode ser reescrito de uma
maneira mais legal assim:

# http://pastebin.com/f5af2bd28

tabular_inline_models = [Descriptor, RecruitmentCountry, TrialInterventionCode,

                        TrialNumber, TrialContact, TrialInstitution]

tabular_inlines = []

for model in tabular_inline_models:

   cls_name = model.__name__+'Inline'

   cls = type(cls_name, (admin.TabularInline,), {'model':model})

   tabular_inlines.append(cls)

class OutcomeInline(admin.StackedInline):

   model = Outcome

class ClinicalTrialAdmin(admin.ModelAdmin):

   inlines = tabular_inlines + [OutcomeInline]

O truque aqui foi usar a função type(), que pode ser usada para criar
classes dinamicamente. Para isso a gente passa três argumentos para a
função type():

- uma string que será o __name__ da classe
- uma tupla de super-classes (importante: a vírgula dentro do
parêntesis indica que é uma tupla de um elemento)
- um dicionário com os atributos da classe (métodos e variáveis)

Não tente fazer isso em Java ;-) .

[ ]s
Luciano

IV Encontro do GruPy-AL

Para quem não sabe faço parte do GruPy-Al. Um grupo de desenvolvedores python do estado de Alagoas. Tudo bem que ainda não é uma participação muito ativa, mas pretendo mudar isso ai.

Quinta-feira aconteceu o IV Encontro, no auditório da IFAL (antigo CEFET), com a presença de 84 participantes, até agora o encontro com o maior número de participantes. Tudo bem que boa parte do pessoal eram alunos do IFAL, mas isso não vem ao caso.

Foram 3 palestras, uma sobre o wiki Moin Moin, uma sobre BRisa e outra sobre BRiGas.

Quem quiser saber mais detalhes sobre o grupo pode acessar a lista de discussão:

http://groups.google.com/group/grupy-al

Ou então acessar o blog:

http://grupyal.blogspot.com/

CMS com o django parte 2

Bom mais de um mês depois da primeira parte, finalmente tive tempo para voltar com a segunda parte desse artigo. Como prometido nessa parte vou mostrar mais duas aplicações que facilitam a nossa vida na construção de um CMS com o django.

django-tinymce

Para quem não conhece o TinyMCE é um poderoso editor WYSIWYG para navegadores web que permite ao usuário editar conteúdo HTML de uma maneira mais amigável. O que o django-tinymce faz é dar opções que facilitam o seu carregamento.

Instalação

Coloque o módulo tinymce no path do python. Vocꬆ pode coloca-lo no diretorio do projeto django ou executar o comando python setup.py install.

Copie o diretorio jscripts/tiny_mce da distribuição do TinyMCE num diretorio chamado js no seu diretório de media. Você pode alterar essa localização em settings.py.

Se você quiser usar algumas das views adicione tinymce na sua lista de aplicações e no URLconf:

settings.py:

INSTALLED_APPS = (
    ...
    'tinymce',
    ...
)

urls.py:

urlpatterns = patterns('',
    ...
    (r'^tinymce/', include('tinymce.urls')),
    ...
)

Utilizando como widget:
Se você usar o widget (recomendado) você precisa adicionar algum código python e possivelmente modificar seu template.
O widget do TinyMCE pode ser habilitado alterando o widget num formfield. Por exemplo, para usar widget para campo content numa flatpage você pode usar o seguinte código:

from django import forms
from django.contrib.flatpages.models import FlatPage
from tinymce.widgets import TinyMCE

class FlatPageForm(ModelForm):
    ...
    content = forms.CharField(widget=TinyMCE(attrs={'cols': 80, 'rows': 30}))
    ...

    class Meta:
        model = FlatPage

O widget requer um link para o código javascript do TinyMCE. Os templates em django.contrib.admin fazem isso automaticamente, então se você só estiver usando tinymce em formulários do admin já está tudo certo. Para seus prórios templates contendo o widget você deve adicionar o seguinte código na tag head do HTML (assumindo que você nomeu seu formulario como form):

<head>
    ...
    {{ form.media }}
</head>

O campo HTMLField

Outra opção seria utilizar o model field HTMLField, ele utiliza o TinyMCE como widget para renderizar o campo. No exemplo abaixo, a admin irá renderizar o campo my_field usando o widget TinyMCE:

from django.db import models
from tinymce import models as tinymce_models

class MyModel(models.Model):
    my_field = tinymce_models.HTMLField()

Em todos os outros aspectos o HTMLField se comporta exatamente como o TextField padrão do Django.

Utilizando numa View

Se você não pode ou não irá modificar o widget num formulário você pode utilizar a view tinymce-js¬† para converter alguns ou todos campos de texto numa página para editores TinyMCE. No template da página, adicione as seguintes linhas na tag head:

<script type="text/javascript" src="{{ MEDIA_URL }}js/tiny_mce/tiny_mce.js"></script>
<script type="text/javascript" src="{% url tinymce-js "NAME" %}"></script>

O argumento NAME permite você criar múltiplas configurações do TinyMCE. Agora crie um template contendo o código javascript de inicialização. Ele deve ser colocado no path de templates como NAME/tinymce_textareas.js ou tinymce/NAME_textareas.js.
Exemplo:

tinyMCE.init({
    mode: "textareas",
    theme: "advanced",
    plugins: "spellchecker,directionality,paste,searchreplace",
    language: "{{ language }}",
    directionality: "{{ directionality }}",
    spellchecker_languages : "{{ spellchecker_languages }}",
    spellchecker_rpc_url : "{{ spellchecker_rpc_url }}"
});

Essas são apenas as opções básicas do django-tinymce, para informações mais detalhadas acesse: http://django-tinymce.googlecode.com/svn/tags/release-1.5/docs/.build/html/index.html

django-compress

Javascript e CSS minificação/compressão pode diminuir bastante o tempo de carga de sites. É importante reduzir o número de requests que são feitos. Menos arquivos significa melhor desempenho. No entanto, quando você desenvolver CSS, e especialmente se você está construindo um grande site, pode ser bom separar as diferentes partes do CSS do site em arquivos diferentes. Também queremos manter o CSS e Javascript em arquivos diferentes ao mesmo tempo que você desenvolva, para fazer depuração mais fácil.

Também é agradável para comentar CSS e Javascript, mas você não quer que o overhead do envio de comentários e espaços em branco para os clientes.

Você também quer ter a certeza de que, muito do conteúdo estático está em cache, tanto quanto possível, no cliente. O ideal é mesmo evitar a “304 Not Modified”, que são retornados quando um cliente solicitou uma coisa que já tenha sido colocada em cache, e não precisa de ser atualizado. Isto pode ser conseguido setando o HTTP Expire header para uma data distante no futuro. Isso irá forçar o navegador para não pedir atualizações para esta URL. No entanto, quando você precisar atualizar os seus arquivos, você precisa lhes dar uma nova URL, e enviar os arquivos atualizados para os clientes.

Estas técnicas são descritas mais em profundidade no Yahoo:

  • http://developer.yahoo.com/performance/rules.html#num_http
  • http://developer.yahoo.com/performance/rules.html#minify
  • http://developer.yahoo.com/performance/rules.html#expires

O django-compress é uma aplicação que irá proporcionar uma maneira fácil e automatizada para a saída de CSS e Javascript comprimidos / minificados. Ela segue o princípio DRY, você configura o seus arquivos CSS e Javascript nas configurações do seu projeto, e esta pronto. Não há necessidade de introduzir os nomes no seu modelo novamente (algumas templatetags cuidam cuidado disso).

Instalação

  1. Pegue o código do svn: svn co http://django-compress.googlecode.com/svn/trunk/ django-compress
    1. Use o script setup.py para instalar: python setup.py install
    2. OU manualmente coloque o diretório compress em algum lugar do seu¬† PYTHONPATH
  2. Adicione ‘compress’ na suas INSTALLED_APPS

Configuração

Em settings você especifica grupos de arquivos para serem comprimidos. A sintaxe básica para especificar grupos de CSS/JavaScript é:

COMPRESS_CSS = {
    'group_one': {
        'source_filenames': ('css/style.css', 'css/foo.css', 'css/bar.css'),
        'output_filename': 'css/one_compressed.css',
        'extra_context': {
            'media': 'screen,projection',
        },
    },
    # other CSS groups goes here
}

COMPRESS_JS = {
    'all': {
        'source_filenames': ('js/jquery-1.2.3.js', 'js/jquery-preload.js', 'js/jquery.pngFix.js',
        'js/my_script.js', 'js/my_other_script.js'),
        'output_filename': 'js/all_compressed.js',
    }
}

Opções de grupos

  • source_filenames é uma tuple com os arquivos a serem comprimidos. Os arquivos são concatenados na ordem que são especificados na tupla. Essa opção é obrigatória.
  • output_filename é o nome do arquivo resultante da compressão. Essa opção é obrigatória.
    extra_context é um dicionário de valores para adicionar no contexto do tempate, quando gerando o HTML para as tags HTML com as templatetags. Essa opção é opcional. Para CSS, se você não especificar extra_context/media, a media padrão exibida será media=”all”.

Outras opções:

  • COMPRESS: Quando COMPRESS é True, CSS e JavaScripts serão concatenado e filtrados. Quando False, os aqruivos fonte serão utilizados. Padrão é not DEBUG (arquivos comprimidos so serão utilizados em modo de produção)
  • COMPRESS_CSS_FILTERS: Uma tuple de filtros para serem aplicados a arquivos CSS. Padrão (‘compress.filters.csstidy.CSSTidyFilter’, ). Note que a configuração padrão neessita do CCSTidy.
  • COMPRESS_JS_FILTERS: Uma tuple de filtros que serão aplicados em arquivos JavaScript. Padrão (‘compress.filters.jsmin.JSMinFilter’,)

Como podemos perceber o django-compress utiliza o CSSTidy para comprimir os arquivos CSS. Porém nem sempre se tem acesso para instalação de pacotes no servidor, e a aplicação não oferece uma alternativa ao CSSTidy. Para resolver isso utilizei o código disponibilizado aqui para criar um filtro simples que somente remove os espaços em branco e os comentários.

Crie um pacote chamado cssmin no __init__.py coloque:

import warnings

warnings.simplefilter('ignore', RuntimeWarning)
from compress.filter_base import FilterBase
from cssmin.cssmin import minimalize 

class CssMinFilter(FilterBase):
    def filter_css(self,css):
        return minimalize(css)

E crie o arquivo cssmin.py e coloque:

import re

# Constants for use in compression level setting.
NONE = 0
SIMPLE = 1
NORMAL = 2
FULL = 3

_REPLACERS = {
    NONE: (None),                           # dummy
    SIMPLE: ((r'\/\*.{4,}?\*\/', ''),       # comment
             (r'\n\s*\n', r"\n"),           # empty new lines
             (r'(^\s*\n)|(\s*\n$)', "")),   # new lines at start or end
    NORMAL: ((r'/\*.{4,}?\*/', ''),         # comments
             (r"\n", ""),                   # delete new lines
             ('[\t ]+', " "),               # change spaces and tabs to one space
             (r'\s?([;:{},+>])\s?', r"\1"), # delete space where it is not needed, change ;} to }
             (r';}', "}"),                  # because semicolon is not needed there
             (r'}', r"}\n")),               # add new line after each rule
    FULL: ((r'\/\*.*?\*\/', ''),            # comments
           (r"\n", ""),                     # delete new lines
           (r'[\t ]+', " "),                # change spaces and tabs to one space
           (r'\s?([;:{},+>])\s?', r"\1"),   # delete space where it is not needed, change ;} to }
           (r';}', "}")),                   # because semicolon is not needed there
}

class CssMin:
    def __init__(self, level=FULL):
        self.level = level

    def compress(self, css):
        """Tries to minimize the length of CSS code passed as parameter. Returns string."""
        css = css.replace("\r\n", "\n") # get rid of Windows line endings, if they exist
        for rule in _REPLACERS[self.level]:
            css = re.compile(rule[0], re.MULTILINE|re.UNICODE|re.DOTALL).sub(rule[1], css)
       return css

def minimalize(css, level=NORMAL):
    """Compress css using level method and return new css as a string."""
    return CssMin(level).compress(css)

if __name__ == '__main__':
    import sys

    if len(sys.argv) <> 3:
        print "Usage: %s  " % sys.argv[0]
        sys.exit(1)

    f = open(sys.argv[1])
    inputcss = f.read()
    f.close()
    open(sys.argv[2], 'w').write(minimalize(inputcss))

Agora é só configurar a COMPRESS_CSS_FILTERS para:

COMPRESS_CSS_FILTERS = ['seu.projeto.cssmin.CssMinFilter']

Para maiores informações sobre o django-compress acesse: http://code.google.com/p/django-compress/

Bom, chegamos ao fim dessa parte do artigo, em breve a última parte com mais aplicações que facilitam a sua vida na criação de um CMS com o django.

Até a próxima.

CMS com django Parte 1

Olá pessoas, nesse mês de janeiro comecei a desenvolver um CMS utilizando o ótimo framework Django. Durante o processo abracei a causa DRY (don’t repeat yourself) e acabei encontrado ótimas aplicações disponibilizadas pela comunidade. Nos próximos posts vou mostrar algumas aplicações que diminuem ainda mais o tempo necessário para construir uma aplicação de grande porte.

django-tagging

O django-tagging é uma aplicação de tags genérica, com ela possível criar tags específicas para cada tipo de objeto. Sendo assim, pode-se adicionar tags a qualquer modelo do seu projeto.

Digamos que tenha o seguinte modelo:

from django.db import models
class BlogPost(models.Model):
    title = models.CharField(max_length=30)
    body = models.TextField()
    date_posted = models.DateField()

É um simples modelo de post, mas para adicionar a funcionalidade de tags fazemos:

from django.db import models
from tagging.fields import TagField

class BlogPost(models.Model):
    title = models.CharField(max_length=30)
    body = models.TextField()
    date_posted = models.DateField()
    tags = TagField()

O django-tagging adiciona um novo campo chamado TagField onde as tags serão cadastradas. Para facilitar ainda mais pode adicionar os seguintes métodos:

def _get_tags(self):
    return Tag.objects.get_for_object(self)
def _set_tags(self, tag_list):
    Tag.objects.update_tags(self, tag_list)
tags = property(_get_tags, _set_tags)

Agora para obter as tags relacionadas ao BlogPost fazemos assim:

e = BlogPost.objects.get(pk=12)
e.tags # retorna a lista de tags
e.tags = 'foo bar' # agora o BlogPost possui as tags foo e bar

Então, imagine uma situação em que vários modelos da sua aplicação possuem um campo de tag, seria interessante descobrir quais são as tags usadas num determinado tipo de modelo, para isso podemos fazer:

from myapp.models import MyModel
from tagging.models import Tag
m1 = MyModel.objects.get(pk=1)
Tag.objects.update_tags(m1, 'house thing')
m2 = MyModel.objects.get(pk=2)
Tag.objects.update_tags(m2, 'cheese toast house')
Tag.objects.usage_for_model(MyModel)
[<Tag: cheese>, <Tag: house>, <Tag: thing>, <Tag: toast>]

O código retorna uma lista com todas as tags utilizadas pelo modelo MyModel. Ainda é possível saber quantas vezes cada tag foi usada pelo modelo.

tags = Tag.objects.usage_for_model(MyModel, counts=True)
[(tag.name, tag.count) for tag in tags]
[('cheese', 1), ('house', 2), ('thing', 1), ('toast', 1)]

Para ter o total de tags e limitar o mínimo para um determinado número usamos o argumento min_count.

tags = Tag.objects.usage_for_model(MyModel, min_count=2)
[(tag.name, tag.count) for tag in tags]
[('house', 2)]

Ainda é possível selecionar objetos baseados nas suas tags.

from tagging.models import TaggedItem
house_tag = Tag.objects.get(name='house')
TaggedItem.objects.get_by_model(MyModel, house_tag)
[<Widget: pk=1>, <Widget: pk=2>]

O django-tagging é muito poderoso para mais detalhes leia a documentação disponível no arquivo de download.
No próximo post estarei mostrando mais sobre o django-tinymce e do django-compress. Até a próxima!

Utilizando iw.fss no Plone 2.5

Uma das grandes vantagens do Zope/Plone é o seu banco de dados ZODB, porém existem situações onde é necessário armazenar grandes quantidades de arquivos, ou arquivos muito grandes. Isso pode acarretar no crescimento desnecessário do Data.fs. Uma das soluções existentes é a utilização do produto FileSystemStorage. O que ele faz é, disponibilizar um novo tipo storage para Archetypes, onde os valores dos campos como arquivos e imagens são armazenados no sistema de arquivos.

Primeiro precisamos baixar uma versão compatível com o Plone 2.5, segundo a página do produto a versão mais atual (2.7.1) só é suportada pela versão 3 do Plone. Sendo assim vamos precisar da versão 2.7.0, disponível aqui.

Instalação:

Existem 3 maneiras de instalar o FSS.

1ª) EGG

Baixe este egg, e utilize o easy_install para instalar.

# easy_install iw.fss-2.7.0-py2.4.egg

2ª)Instalando do SVN

Não encontrei o tar.gz da versão 2.7.0, sendo assim, baixei do repositório.

# svn export http://svn.plone.org/svn/collective/iw.fss/tags/2.7.0/ iw.fss
# cd iw.fss
# python setup.py install

3ª) Copiando para lib/pyton

Segue os mesmos passos acima, porém, ao invés de excutar o último comando, copiamos a pasta iw para a lib/python da sua instância.

# svn export http://svn.plone.org/svn/collective/iw.fss/tags/2.7.0/ iw.fss
# cd iw.fss
# cp iw $INSTANCE_HOME/lib/python

Configuração

Podemos notar que o FSS já adota os padrões do zope 3, ou seja, não é simplesmente extrair seu conteúdo na pasta Products e pronto. Para que ele seja carregado necessitamos criar algums arquivos zcml.

$INSTANCE_HOME/etc/package-includes/iw.fss-meta.zcml, com a linha:

<include package="iw.fss" file="meta.zcml"/>

$INSTANCE_HOME/etc/package-includes/iw.fss-configure.zcml, com a linha:

<include package="iw.fss" />

OBS.: No Plone 2.5 pode acontecer de não existir a pasta package-include em $INSTANCE_HOME/etc, se for este o caso será necessário copiar o conteúdo de $ZOPE_HOME/lib/python/Products/Five/skel para $INSTANCE_HOME/etc.

Agora abra o arquivo $INSTANCE_HOME/lib/python/iw/fss/etc/plone-filesystemstorage.conf e defina as variáveis abaixo:

storage-path $$INSTANCE_HOME/var/fss_files
backup-path $$INSTANCE_HOME/var/fss_backup
storage-strategy flat

storage-path e backup-path são, respectivamente, os caminhos de onde ficarão os arquivos e os backups. Já storage-strategy é tipo de estratégia utilizada para armezar os arquivos, por enquanto vamos deixar a estratégia padrão ‘flat’.

Verifique se o FSS está diponível para instalação. Caso sim, você fez tudo corretamente. Seguindo…

Alterando o storage para arquivos e imagens

Até agora o que fizemos foi instalar e configurar o FSS, mas só isso não basta. Para que os arquivos e as imagens dos content-types padrão do Plone (News Item, File e Image) passem a utilizá-lo precisamos de mais um passo. Adcione a seguinte linha em algum dos arquivos em $INSTANCE_HOME/etc/package-includes:

<include package="iw.fss" file="atct.zcml" />

Agora reinicie o zope e adicione uma nova imagem ou arquivo e verifique na pasta que você definiu como storage e note que agora existe um arquivo lá.

Definindo tipos em produtos personalizados

Muitas vezes somente os tipos padrão do plone não suficientes, e temos que criar nossos próprios tipos. Para utilizar o FSS em nossos produtos personalizados fazemos o seguinte:


from iw.fss.FileSystemStorage import FileSystemStorage
...
my_schema = Schema((
    FileField('file',
              ...
              storage=FileSystemStorage(),
              widget=FileWidget(...)
              ),
    ...
    ))
 ...

Bem simples, faça o mesmo na definição do seu tipo e o arquivo será salvo no sistema de arquivos. Para quem usa o Plone 3, os passos são parecidos, porém pode-se utilizar o buildout para facilitar ainda mais o processo.

Esse foi o meu primeiro(de muitos) tutorial, espero que gostem. t+