sexta-feira, 25 de setembro de 2015

Plugins Úteis: django-geoposition - Como incorporar o google maps dentro do admin para geolocalização

Precisa incorporar um mapa com base em um endereço no administrativo para exibir no front? Tarefa simples pro django-geoposition (https://github.com/philippbosch/django-geoposition).

A instalação é simples:
pip install django-geoposition

Coloque a linha abaixo no seu arquivo settings.py, em INSTALLED_APPS:
INSTALLED_APPS = (
    ...
    'geoposition',
    ...
)

Outra configuração que vale a pena fazer, é incluir no seu arquivo settings.py as linhas abaixo para definir o zoom:
GEOPOSITION_MAP_OPTIONS = {
    'minZoom': 15,
    'maxZoom': 18,
}

No seu arquivo models.py, deixe como a seguir:
from geoposition.fields import GeopositionField
from localflavor.br.br_states import STATE_CHOICES

class SuaClasse(models.Model):
 """(Unidade description)"""
 endereco = models.CharField(max_length=255, verbose_name=u'Endereço', help_text='Para uma melhor localização no mapa, preencha sem abreviações. Ex: Rua Martinho Estrela,  1229') 
 bairro = models.CharField(max_length=255,)
 cidade = models.CharField(max_length=255,help_text="Para uma melhor localização no mapa, preencha sem abreviações. Ex: Belo Horizonte")
 estado = models.CharField(max_length=2, null=True, blank=True,choices=U.STATE_CHOICES)
 position = GeopositionField(verbose_name=u'Geolocalização', help_text="Não altere os valores calculados automaticamente de latitude e longitude")

 class Meta:
  verbose_name, verbose_name_plural = u"Sua Classe" , u"Suas Classes"
  ordering = ('endereco',)

 def __unicode__(self):
  return u"%s" % self.endereco 

No administrativo será gerado tudo automático, mas existem algumas melhorias que podem ser feitas pra ficar ainda mais bacana. Inclua o arquivo css e js abaixo que em seguida explico como faremos.
# coding: utf-8
from django import forms
from django.contrib import admin
from .models import *


class SuaClasseForm(forms.ModelForm):
 class Media:
  css = {
   'all': ('admin/css/geoposition_override.css',)
  }
  js = ('admin/js/geoposition_override.js',)

class SuaClasseAdmin(admin.ModelAdmin):
 form = SuaClasseForm
 search_fields = ('endereco', 'cidade',)
 list_display = ('endereco', 'cidade','estado','bairro')
 list_filter = ['estado',]
 save_on_top = True



admin.site.register(SuaClasse, SuaClasseAdmin)

No arquivo css adicionado (geoposition_override.css), iremos ocultar o campo de busca do mapa, e utilizaremos nossos próprios atributos da classe para tal, mesmo porquê, o atributo position irá só gravar em banco a latitude e longitude e não o endereço pesquisado.

Crie um arquivo dentro de <STATIC_DIR>/admin/css, chamado geoposition_override.cssm, e nele coloque simplemente a linha abaixo:

.geoposition-search input{display:none;}

Agora, crie um js chamado geoposition_override.js na pasta <STATIC_DIR>/admin/js com as linhas abaixo:
django.jQuery(document).ready(function($) {
 $('#id_position_0, #id_position_1').attr('readonly', 'readonly');

 $('#id_endereco, #id_cidade, #id_estado').blur(function(event) {
  /* Act on the event */
  if ($('#id_endereco').val()!='' && $('#id_cidade').val()!='' && $('#id_estado').val()!='') {
   $('.geoposition-search input').val($('#id_endereco').val()+' ' +$('#id_cidade').val()+' '+$('#id_estado').val());
   
   // TRIGGER DO ENTER PARA EXECUTAR A BUSCA
   var e = $.Event("keydown");
   e.which = 50; // # Some key code value
   $(".geoposition-search input").trigger(e);
  };
 });
});

E o resultado:

hasta!

quarta-feira, 2 de setembro de 2015

Desmistificando Forms: CreateView e UpdateView com o trabalho pesado


O básico do CreateView

Começando do básico, o exemplo abaixo já quebra um galho enorme quando de trata de formulários simples, como por exemplo o Contato de algum site.

forms.py
# coding: utf-8
from django import forms

from .models import Contato

class ContatoForm(forms.ModelForm):
 nome = forms.CharField(widget=forms.TextInput(attrs={'class' : 'required',}), label="Nome")
 email = forms.EmailField(widget=forms.TextInput(attrs={'class' : 'required',}), label="E-mail")
 texto = forms.CharField(widget=forms.Textarea(attrs={'class' : 'required',}), label="Texto")


 class Meta:
  model = Contato
  fields = '__all__'

 def dados(self):
  return {'form':self.cleaned_data, 'data':datetime.now()}

Até aqui sem muitas dificuldades né? Vemos a definição da classe para gerenciamento do form, com alguns atributos. Com o fields = '__all__' não era necessário definir os atributos, mas isso é obrigatório caso queira adicionar uma classe, como por exemplo o required ou outro atributo qualquer, por exemplo um placeholder, etc.

views.py
# Create your views here.
# coding: utf-8
from django.views.generic.edit import CreateView

from .forms import ContatoForm
from .models import Contato


class ContatoView(CreateView):
 form_class = ContatoForm
 success_url = '/contato/sucesso/'
 template_name = 'contato/contato.html'
 model = Contato

 def get_context_data(self, **kwargs):
  kwargs.update({
   'menu':'contato',
   'title': 'Contato',
  })
  return kwargs

A view também é bem tranquila. Herdando as características da CreateView, exige apenas algumas informações para resolver o problema de inserção e validação dos campos de forma elegante:

  • form_class: Classe do Form definida no forms.py
  • success_url: URL para qual será redirecionado após o sucesso da inserção
  • template_name: Html do form.
  • model: Classe definida no models.py

contato.html
<form method="post" action="">
    {% csrf_token %}
    {{ form.errors}}
    {{ form.as_p }}
    <input class="btn" type="submit" value="Enviar">
</form>

Acima temos um exemplo bem sucinto de como montar um form de forma bem automágica e podemos observar algumas coisas interessantes com o exemplo acima:

  • A CreateView trabalha com o post para própria página (mesma url) por isso no action do form não tem nenhuma informação. 
  • A validação do CSRF já é nativa da CreateView, a unica coisa que precisa e incluir o token no html dentro da tag form.
  • {{ form.errors }} vai gerar uma <ul> com a tag errorlist onde cada <li> será responsável por lista os erros de um campo específico e terá uma <ul> também com a classe errorlist uma lista de <li> para todos os erros deste respectivo campo.
Outra coisa legal de fazer é definir no html os campos separamente para um tratamento de erro e layout melhor apresentados. Isso pode ser feito assim:

<p>{{ form.email }}</p>
<div class="error">{{ form.email.errors }}</div>


E o UpdatView?

Um formulário de contato não sofrerá update bem possivelmente. Mas apenas para ilustar usaremos o e mesmo exemplo.


views.py
class ContatoUpdateView(UpdateView):
 form_class = ContatoForm
 model = Contato
 success_url = '/contato/sucesso/'
 template_name =  'contato/contato.html'

 def get_context_data(self, **kwargs):
  kwargs.update({
   'menu':'contato',
   'title': 'Contato',
   'update':True,
  })
  return kwargs

Como assim só isso? Pois é. Só isso. E sim o HTML é exatemente o mesmo. Se usar algum recurso para gerar o form como o {{ form.as_p }}, {{ form.as_table }} é só isso e o mesmo html acima. Caso defina os campos individualmente, lembre-se de colocar {{ form.id }} para validar a instancia que está sendo editada.


Eu já mostrei como fazer para utilizar os inlines junto com a CreateView neste post: http://djangoweb.blogspot.com.br/2013/08/como-utilizar-os-inlines-no-front-para.html. Agora vamos a algo mais divertido. 

Que tal criarmos um exemplo mais complexo, onde não terá apenas uma, mas sim várias classes inline, com o CreateView e UpdateView?


Multiplos inlines


forms.py
# coding: utf-8
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms.models import inlineformset_factory

from django import forms
from .models import *

from util import util as U

class ModeloForm(forms.ModelForm):
 
 class Meta:
  model = Modelo
  fields = '__all__'

 def dados(self):
  return {'form':self.cleaned_data, 'data':datetime.now()}


class ModeloInline1Form(forms.ModelForm):

 class Meta:
  model = ModeloInline1
  fields = '__all__'

ModeloInline1FormSet = inlineformset_factory(Modelo, ModeloInline1, extra=0, min_num=1, form=ModeloInline1Form, fields='__all__')

class ModeloInline2Form(forms.ModelForm):

 class Meta:
  model = ModeloInline1
  fields = '__all__'

ModeloInline2FormSet = inlineformset_factory(Modelo, ModeloInline2, extra=0, min_num=1, form=ModeloInline1Form, fields='__all__')


views.py
class ModeloCreateView(CreateView):
 form_class = ModeloForm
 success_url = '/home/'
 template_name = 'modelo/form.html'
 model = Modelo

 def get(self, request, *args, **kwargs):
  self.object = None
  form_class = self.get_form_class()
  form = self.get_form(form_class)
  inline1_form = ModeloInline1FormSet()
  inline2_form = ModeloInline2FormSet()
  return self.render_to_response(self.get_context_data(form=form, inline1_form = inline1_form , inline2_form = inline2_form ))
 def post(self, request, *args, **kwargs):
  self.object = None
  form_class = self.get_form_class()
  form = self.get_form(form_class)
  inline1_form = ModeloInline1FormSet(self.request.POST)
  inline2_form = ModeloInline1FormSet(self.request.POST)
  if (form.is_valid() and inline1_form .is_valid() and
   inline2_form .is_valid()):
   return self.form_valid(form, inline1_form , inline2_form )
  else:
   return self.form_invalid(form, inline1_form , inline2_form )

 def form_valid(self, form, inline1_form, inline2_form ):
  self.object = form.save()
  inline1_form.instance = self.object
  inline1_form.save()
  inline2_form.instance = self.object
  inline2_form.save()
  return HttpResponseRedirect(self.get_success_url())

 def form_invalid(self, form, inline1_form, inline2_form ):
  return self.render_to_response(
   self.get_context_data(
    form=form,
    inline1_form=inline1_form,
    inline2_form=inline2_form)
   )


 def get_context_data(self, **kwargs):
  kwargs.update({})
  return kwargs


class ModeloUpdateView(UpdateView):
 form_class = ModeloForm
 model = Modelo
 success_url = '/home/'
 template_name =  'modelo/form.html'

 def get(self, request, *args, **kwargs):
  self.object = self.get_object()
  form_class = self.get_form_class()
  form = self.get_form(form_class)

  # Render form
  inline1_form = ModeloInline1FormSet(instance=self.object)
  inline2_form = ModeloInline2FormSet(instance=self.object)
  return self.render_to_response(self.get_context_data(form=form,inline1_form=inline1_form,inline2_form=inline2_form))

 def post(self, request, *args, **kwargs):
  self.object = self.get_object()
  form_class = self.get_form_class()
  form = self.get_form(form_class)
  inline1_form = ModeloInline1FormSet(self.request.POST, instance=self.object)
  inline2_form = ModeloInline2FormSet(self.request.POST, instance=self.object)
  if (form.is_valid() and inline1_form.is_valid() and inline2_form.is_valid()):
   return self.form_valid(form, inline1_form, inline2_form)
  else:
   return self.form_invalid(form, inline1_form, inline2_form)

 def form_valid(self, form, inline1_form , inline2_form ):
  self.object = form.save()
  inline1_form.instance = self.object
  inline1_form.save()
  inline2_form.instance = self.object
  inline2_form.save()
  return HttpResponseRedirect(self.get_success_url())

 def form_invalid(self, form, inline1_form , inline2_form ):

  return self.render_to_response(
   self.get_context_data(
    form=form,
    inline1_form=inline1_form,
    inline2_form=inline2_form)
   )

 def get_context_data(self, **kwargs):
  kwargs.update({
   'update':True,
  })
  return kwargs

Nos exemplos acima, o formulário html pode ser o mesmo como no exemplo do Contato. Podemos também utilizar o recurso do inline visto no post cima citado para inserção de quantos inlines forem necessários com o mesmo recurso do admin para sempre adicionar mais um conforme necessidade.

Um jeito mais elegante de usar isso é usar um template vinculado ao js, com o plugin underscore disponível aqui: http://underscorejs.org/

<script type="text/javascript" src="/static/site/js/plugins/underscore/underscore-min.js"></script>
<script type="text/html" id="modeloinline1-template">
  <div class="bloco-modeloinline1 clearfix">
   <div class="col-lg-12">
    <div class="form-group">
     <label for="">Exemplo de Textarea</label>
     <textarea rows="5" name="modeloinline1_set-<%= id %>-descricao" id="id_modeloinline1_set-<%= id %>-descricao" cols="40" class="form-control"></textarea>
    </div>
   </div>
  </div>
 </script>

django.jQuery('.container-modeloinline1').on('click', '.btn-adicionar-modeloinline1', function(ev) {
    ev.preventDefault();
    django.jQuery(this).parent().find('button').hide();
    var count = django.jQuery('.container-modeloinline1').children().length;
    var tmplMarkup = django.jQuery('#modeloinline1-template').html();
    var compiledTmpl = _.template(tmplMarkup, { id : count });
    django.jQuery('.modeloinline1-form"').append(compiledTmpl);
    // update form count
    django.jQuery('#id_modeloinline1_set-TOTAL_FORMS').attr('value', count+1);
   });
</script>
<form role="form" action="" method="post" class=''>

 {{ modeloinline1.management_form }}
 <div class="container-modeloinline1-form">
 {% for form in inline1_form %}
  {% if update %}
   {{ form.id }}
   {{ form.avaliacao }}
  {% endif %}
  {% if form.errors %}
       <div class="alert alert-danger">
                     Por favor corriga os erros abaixo para prosseguir:
                     {{ form.errors}}
               </div>
         {% endif %}
 </div>
</form>
<button class="btn btn-primary btn-sm btn-adicionar-modeloinline1" type="button">Adicionar outro Modeloinline1</button>

quinta-feira, 20 de agosto de 2015

Como usar o filter_horizontal/filter_vertical fora do admin do Django


Tem um ManyToManyField em sua classe e deseja usar o fantástico recurso do filter_horizontal/filter_vertical em um formulário no front? Vou lhe dizer como.

No html, deixe como a seguir:

{% load static from staticfiles %}
<!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge">
 <link rel="stylesheet" href="/static/admin/css/widgets.css">
</head>
<body>
 <form action="" method="post">
  <!-- Não esqueca de colcoar o atributo multiple="multiple" -->
  <select name="seu_campo" id="id_seu_campo"  multiple="multiple">
   <option value="1">1</option>
   <option value="2">2</option>
   <option value="3">3</option>
   <option value="4">4</option>
   <option value="5">5</option>
   <option value="6">6</option>
  </select>
   
  <input type="submit">
 </form>

 <script src="{% static "site/js/jquery.js" %}"></script>
 <script type="text/javascript" src="{% static "admin/js/jquery.init.js" %}"></script>
    <script type="text/javascript" src="/admin/jsi18n/"></script>
    <script type="text/javascript" src="{% static "admin/js/core.js" %}"></script>
    <script type="text/javascript" src="{% static "admin/js/SelectBox.js" %}"></script>
    <script type="text/javascript" src="{% static "admin/js/SelectFilter2.js" %}"></script>
    <script type="text/javascript">
     // PARAMETROS DO SELECT FILTER:
     // ID do seu elemento select
     // NAME do seu elemento select
     // 0 - para filter_horizontal / 1 para filter_vertical
     // Caminho do admin para source de imagens utilizadas

        addEvent(window, "load", function(e) {SelectFilter.init("id_seu_campo", "seu_campo", 0, "/static/admin/"); });
    </script>

</body>
</html>
hasta!

sexta-feira, 14 de agosto de 2015

Plugins Úteis: django-auditlog - Ótima solução para registro de logs



Precisa de um registro de logs para todas as alterações realizadas em um sistema? Isso normalmente é necessário para sistemas que sofrerão auditoria.

O caminho das pedras é duro, alterar todas as views, fazer os registros manualmente, coonsiderar se quer apenas registrar qual atributo foi alterado ou pior, registrar qual era a informação antiga e qual é a informação nova de cada atributo. Trabalhoso? Muito.

Eis que surge o django-auditlog (https://github.com/jjkester/django-auditlog). Com ele o log fica bem completo e é muito fácil de configurar.

Você vai precisar de:
  • Python 2.7 ou 3.4 
  • Django 1.7 ou 1.8 

Vamos a um passo a passo bem simples para colocar o plugin pra funcionar:


1. Instale ele no seu virtualenv:
pip install django-auditlog

2. Adicione ele na setting INSTALLED_APPS:
...
'auditlog'
...

3. Rode o comando migrate para criar as tabelas necessárias no banco:
manage.py migrate

4. Adicione a linha abaixo na setting MIDDLEWARE_CLASSES:
...
'auditlog.middleware.AuditlogMiddleware'
...

5. Pronto, agora é só registrar os modelos que quer manter o histórico de mudanças como no examplo abaixo:
from auditlog.registry import auditlog
from django.db import models

class MyModel(models.Model):
    pass
    # Model definition goes here

auditlog.register(MyModel)


Depois de tudo configurado, terá sido criado uma tabela no seu banco de dados com o nome de auditlog_logentry,  com os seguintes campos:

  • id
  • object_pk
  • object_id
  • object_repr
  • action
  • changes
  • timestamp
  • actor_id
  • content_type_id
  • remote_addr
  • additional_data


Algumas considerações importantes:

  • object_pk e object_id armezenam a chave primária e o id do registro em questão. Caso sua classe não possua uma chave primária definida, estes dois atributos terão o mesmo valor: a chave primária ID criada automaticamente pelo Django na ausencia de uma definida.
  • object_repr será o valor definido no método __unicode__ da classe do objeto em questão.
  • action será um valor númerico (0,1 ou 2), onde o mais valor é considerado a atividade mais intrusiva no banco, logo em ordem temos: Inserção (0), Alteração (1)  e Remoção (2)
  • changes armazena as mudanças feitas na forma de um dicionário, onde cada chave  será o atributo alterado e terá uma lista com dois valores associado, o antigo e o novo. Ex:
    • {"ultima_troca_senha": ["2015-01-13", "2015-08-13"], "senha": ["1234567", "123456"]}
  • timestamp registrará o momento da mudança no formato datetime, yyyy-mm-dd h:i:s
  • actor_id armazena o id do usuário logado que fez a alteração. Caso haja em algum momento alteração do model registrado e o usuário não estiver logado, este atributo será nulo. Este campo faz referência ao model User utilizado pelo django, na tabela auth_user.
  • content_type_id armazena o id do content_type, fazendo referência ao model ContentType utilizado pelo django, na tabela django_content_type.
  • remote_addr registra o ip do usuário que fez a mudança
  • additional_data armazenará informações adicionais, sendo nulo no registro automático exemplificado acima.



Para mais infrmações, consulte a documentação: http://django-auditlog.readthedocs.org/en/latest/

hasta!

quarta-feira, 12 de agosto de 2015

Criando um middleware para controlar acesso ao site via IP


Para tal, criarei uma classe para gerenciar IPs permitidos via administrativo. Crie uma nova app no seu projeto e dê o nome de permissoes.

Deixe os arquivos como a seguir:

models.py

# coding: utf-8
from django.db import models

# Create your models here.
class IP(models.Model):
 """(IP description)"""
 ip = models.GenericIPAddressField()
 descricao = models.CharField(max_length=255, null=True, blank=True,verbose_name=u'Descrição')
 data = models.DateTimeField(auto_now=True)

 class Meta:
  verbose_name, verbose_name_plural = u"IP" , u"IPs"
  ordering = ('-data',)

 def __unicode__(self):
  return u"%s" % self.ip


admin.py

# coding: utf-8
from django.contrib import admin
from .models import *

class IPAdmin(admin.ModelAdmin):
 search_fields = ('ip', 'descricao',)
 list_display = ('ip', 'descricao','data',)
 list_filter = ['data',]
 save_on_top = True

admin.site.register(IP, IPAdmin)


Adicione sua app no INSTALLED_APPS:

...
'permissoes',
...


E por fim, não menos importante, o middleware. Crie um arquivo chamado middleware.py e coloque no mesmo diretorio do settings.py com o conteúdo abaixo:

# coding: utf-8

from django.conf import settings
from django.http import HttpResponseRedirect

from permissoes.models import IP

class NeedToLoginMiddleware(object):
 def process_request(self, request):
                # pega o IP do usuário
  ip = request.META['REMOTE_ADDR']

                # verifica se o ip consta na base de ips permitidos
  ip_permitido = IP.objects.filter(ip=ip).count()

                # lista de urls que não serão validadas
  ALLOWED_URLS = [
   '%s' % settings.LOGIN_URL,
   '/admin/',
  ]

  for a in ALLOWED_URLS:
   try:
    if request.META['PATH_INFO'].find(a) >= 0:
     return None
   except:
    pass

                # verifica se o ip está permitido
  if ip_permitido:
   return None

  else:
                        # se o ip não estiver permitido, verifica se o usuário está autenticado 
                        # e redireciona para url de login em caso negativo.
   if not request.user.is_authenticated(): #if ip check failed, make authentication check
    return HttpResponseRedirect(settings.LOGIN_URL)


  return None

Adicone ele na sua setting MIDDLEWARE_CLASSES:

MIDDLEWARE_CLASSES = (
    ...
    'app.middleware.NeedToLoginMiddleware',
)

hasta!

sexta-feira, 7 de agosto de 2015

Autenticação sem o request.user.get_profile


Desde do Django 1.5, o framework possibilita a criação de um modelo de Usuário personalizado. Faço parte do time de pessoas que não concorda em adicionar campos não relacionados com a autenticação na classe de Usuário.

Para resolver esse problema, já que a partir da versão 1.7 do django este método não é mais recomendado, faremos algumas modificações nos arquivos.

Mudanças que tratrei aqui são fáceis de implementar e não irão gerar problemas com autenticação já existente e/ou migrações de banco.

Cenário: Imagine que temos uma app cadastros que possui uma classe Cadastro e esta tem um vínculo com a classe User do Django, para fazer uso do sistema de autenticação.

Remova a setting AUTH_PROFILE_MODULE:


Este passo é fácil. Encontre o AUTH_PROFILE_MODULE no seu arquivo settings.py e remova  linha.

# No seu settings.py:
AUTH_PROFILE_MODULE = 'cadastros.cadastro'     # remova esta linha


Mude seu modelo que possui o vínculo com a classe User do Django:


Antes:

from django.db import models
from django.contrib.auth.models import User

class Cadastro(models.Model):
   """modelo que representa informações adicionais sobre o usuário"""
   user = models.ForeignKey(User, unique=True)  # mude esta linha
   # ... outros campos aqui

Depois:

from django.db import models
from django.contrib.auth.models import User

class Cadastro(models.Model):
   """modelo que representa informações adicionais sobre o usuário"""
   user = models.OneToOneField(User) # nova linha
   # ... outros campos aqui


Aplique as migrações para concluir esta etapa:

python manage.py makemigrations cadastros
python manage.py migrate cadastros


Atualize seu código:


Antes:
cadastro = request.user.get_profile()

Depois:
cadastro = request.user.cadastro


hasta!

quarta-feira, 29 de julho de 2015

Creating a ModelForm without either the 'fields' attribute or the 'exclude' attribute is prohibited;

Para todos que pegarem o erro Creating a ModelForm without either the 'fields' attribute or the 'exclude' attribute is prohibited, basta fazer uma pequena alteração no ModelForm
para resolver.

Altere seu forms.py de:

class BlocoForm(forms.ModelForm):

 class Meta:
  model = Bloco

 def dados(self):
  return {'form':self.cleaned_data, 'data':datetime.now()}


Para:

class BlocoForm(forms.ModelForm):

 class Meta:
  model = Bloco
  fields = '__all__'

 def dados(self):
  return {'form':self.cleaned_data, 'data':datetime.now()}


hasta!

Calling modelformset_factory without defining 'fields' or 'exclude' explicitly is prohibited.

Para todos que pegarem o erro Calling modelformset_factory without defining 'fields' or 'exclude' explicitly is prohibited, basta fazer uma pequena alteração no inlineformset_factory
para resolver.

Altere seu forms.py de:

FotoFormSet = inlineformset_factory(Bloco, Foto, extra=1)


Para:

FotoFormSet = inlineformset_factory(Bloco, Foto, fields='__all__', extra=1)


hasta!

terça-feira, 21 de julho de 2015

django.core.exceptions.AppRegistryNotReady: Models aren't loaded yet.

Precisa escrever um script em python e fazer uso do Django para automatizar algo pela cron?

Na versão atual do django, quando fazemos isso igual nas versões mais antigas, tipo 1.4 até 1.6, recebmos o erro:

django.core.exceptions.AppRegistryNotReady: Models aren't loaded yet.


Para resolver isso, deixe seu script python como a seguir:

#!/usr/bin/env python
# coding: utf-8

from os.path import abspath, dirname

SETTINGS_DIRECTORY = dirname(dirname(abspath(__file__)))

sys.path.insert(0, SETTINGS_DIRECTORY)
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'

import django
django.setup()


def sincorniza_busca():
 from django.template.defaultfilters import striptags

 # importe seus models aqui
 from sua_app.models import SeuModel

 # faça o que tiver que fazer aqui



if __name__ == '__main__':
 sua_funcao()



hasta!

sexta-feira, 12 de junho de 2015

AppRegistryNotReady: The translation infrastructure cannot be initialized before the apps registry is ready


Dê uma olhada em seu arquivo wsgi que carrega os módulos e apps. O erro acima é muito comum a partir do Django 1.7

Troque:
module = django.core.handlers.wsgi:WSGIHandler()



Por:
module = get_wsgi_application()



Exemplo:

import sys, os

sys.path.append(os.getcwd())

# CONFIGURACAO PARA ENV
sys.path.insert(0,'<CAMINHO PARA SEU ENV>/ENV/bin')
sys.path.insert(0,'/<CAMINHO PARA SEU ENV>/ENV/lib/python2.7/site-packages')
# -------------------------

sys.path.insert(1, "/<CAMINHO PARA SEU PROJETO>/app")
os.environ['DJANGO_SETTINGS_MODULE'] = 'app.settings'
import django.core.handlers.wsgi

#application = django.core.handlers.wsgi.WSGIHandler()

from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

quinta-feira, 22 de janeiro de 2015

Validações customizadas no forms.ModelForm com método clean para um atributo


As vezes alguns campos requerem validações além do tipo de dados neles inserido. Abaixo tem um exemplo que fiz para um caso desse.

A necessidade era validar um charfield de acordo com uma série de valores e por ser campo aberto não poderia fazer um choices.


models.py
class SuaClasse(models.Model):
 ...
 seu_atributo = models.CharField(max_length=20)
 ...

admin.py
from django.core.exceptions import ValidationError
import re

class SuaClasseAdminForm(forms.ModelForm):
 class Meta:
  model = SuaClasse

 def clean_seu_atributo(self):
  cleaned_data = super(SuaClasseAdminForm, self).clean()
  seu_atributo = cleaned_data.get("seu_atributo")
  if not seu_atributo in ['TL','TR','BR','BL','C','R']:
   m = re.search('[0-9]*(%)*x[0-9]*(%)*', seu_atributo)
   if not m:
    raise ValidationError("Preencha o atributo com uma das opções disponíveis.")

class SuaClasseAdmin(admin.ModelAdmin):
 form = SuaClasseAdminForm

 ...

No exemplo acima, preciso que o valor de seu_atributo seja um dos valores contidos na lista ['TL','TR','BR','BL','C','R'] ou respeite a expressão regular que testa para os seguintes exemplos: 300x200, 50%x300, 40x90% ou 150x400.

O que podemos notar é que o método clean_seu_atributo fará a validação no campo especificado e trará o erro logo acima do campo em caso de falha. Também é possivel fazer somente o método clean, contudo o erro será mostrado no início do form, sem vínculo a campo algum.


hasta!

quarta-feira, 21 de janeiro de 2015

Como criar um PNG com marca d'água e texto dinâmico

A necessidade: Criar uma ferramenta para upload de uma imagem e gerar uma saída com uma marca d'água e um texto dinâmico. Saída era esperada em PNG para melhor renderização no Facebook.

Pra isso, você vai precisar de:
  • PIL==1.1.7
  • django-easy-thumbnails-watermark==0.6.7

Crie uma classe no models.py para tal:
class Visita(models.Model):
 """(Visita description)"""
 data = models.DateField(auto_now_add=True)
 linha1 = models.CharField(max_length=255)
 linha2 = models.CharField(max_length=255, null=True, blank=True,)
 imagem = models.ImageField(upload_to="uploads/visitas/imagem/%Y",help_text="800x671")

 class Meta:
  verbose_name, verbose_name_plural = u"Visita à Fábrica" , u"Visitas à Fábrica"
  ordering = ('-data',)

 def __unicode__(self):
  return u"%s" % self.data


 def gerar(self):
  return 'Gerar' % self.id
 gerar.allow_tags = True
 gerar.is_safe = True

Aqui criei um modelo que tem uma data, por questão de organização, duas linhas para texto que serão inseridas na imagem e a imagem em si.

No urls.py, crie uma entrada para chamada da view:
url(r'^visitas/(?P\d+)/$', 'imagens_facebook.views.visitas', name="visitas"),

E da view abaixo:
def visitas(request, vid):
 img_obj = get_object_or_404(Visita, id=vid)
 img = '{0}/{1}'.format(settings.MEDIA_ROOT, img_obj.imagem)
 IMAGE = Image.open('{0}/{1}'.format(settings.MEDIA_ROOT, img_obj.imagem))
 WM = {
  'image':'{0}/site/img/visita.png'.format(settings.STATIC_PATH,),
  'position': 'BL',
  'opacity': 1,
  'scale': 1,
  'tile': False,
  'greyscale': False,
  'rotation': 0,
 }
 WK_DONE = watermark_processor(IMAGE, WM )
 path = '{0}/watermarks/visitas/{1}'.format(settings.MEDIA_ROOT, 'imagem.png')
 WK_DONE.save(path, "PNG")
 img = Image.open('{0}/watermarks/visitas/{1}'.format(settings.MEDIA_ROOT, 'imagem.png'))
 draw = ImageDraw.Draw(img)
 font = ImageFont.truetype("{0}/fonts/trebuc.ttf".format(settings.STATIC_PATH),20)
 draw.text((60, 465), img_obj.linha1.upper() ,(55,185,115),font=font)
 if img_obj.linha2:
  draw.text((60, 490), img_obj.linha2.upper() ,(55,185,115),font=font)
 img.save('{0}/watermarks/visitas/{1}'.format(settings.MEDIA_ROOT, 'imagem.png'))
 return force_download(request, path)

Linha a linha temos:

  1. Definição da View "visitas" que recebe o request e o id da visita, identificado por "vid"
  2. Obtenção do objeto visita com o id informado
  3. Definição do caminho de onde está a imagem que foi feita upload
  4. Abertura da imagem como objeto da Classe Image
  5. Definição da WaterMark com o parâmetros necessários:
    1. caminho da marca d'agua
    2. Posição que será inserida, BL para bottom left
    3. Opacidade =1 para não ter transparencia, do contrario coloque valores entre 0 e 1.
    4. Escala 1 para não alterar o tamanho
    5. Tile é falso para não esticar imagem utilizada como marca d'agua em toda a imagem definida no upload.
    6. Escala de cinza falso para sair colorido
    7. Sem rotação para sair em orientação normal.
  6. Criação da imagem com marca d'agua utilizando o watermark_processor
  7. Definição de onde será gerado o arquivo
  8. Ação de salvar para gerar o arquivo em png
  9. A partir desta linha é onde será escrito o texto dinamico. Aqui abrimos a imagem gerada no passo anterior.
  10. Criação de um objeto draw da classe ImageDraw.
  11. Definição da fonte a ser utilizada. Aqui será necessário copiar o arquivo da fonte para seu projeto.
  12. A escrita do texto proprimente dita com os respectivos parâmetros: posição(left, top), texto a ser inserido, cor em RGB e caminho da font definida no passo anterior
  13. Apenas um if para testar se há uma segunda linha no texto.
  14. Se houver repete a mesma ação do passo 12, passando o texto diferente.
  15. Ação de salvar a imagem para gerar o arquivo físico.
  16. View para forçar o download do arquivo gerado. Esta view pode ser vista aqui: http://djangoweb.blogspot.com.br/2011/10/forcando-download-de-arquivos-no-django.html

hasta!


quinta-feira, 15 de janeiro de 2015

Tradução de maneira descomplicada com o Model Translation


Todo mundo já deve ter passado por um projeto multi-idioma. Se ainda não, recomendo mesmo assim a leitura deste post. Por participar de algo assim, pude aprender mais algumas coisas interessantes.

Aqui seguem alguns passos que podem auxiliar e muito a confecção de um projeto multi-idioma

1. Primeiro de tudo, defina quais idiomas o projeto terá. 

Isso se faz com algumas configurações no settings.py:

LANGUAGE_CODE = 'pt-br'
TIME_ZONE = 'America/Sao_Paulo'
USE_I18N = True
USE_L10N = True
ugettext = lambda s: s
LANGUAGES = (
    ('pt-BR', ugettext(u'Português')),
    ('en-US', ugettext(u'Inglês')),
    ('es-ES', ugettext(u'Espanhol')),
)
USE_TZ = False
LOCALE_PATHS = (
   os.path.join( PROJECT_PATH, '../locale' ),
)
  • LANGUAGE_CODE: Aqui é definido a linguagem inicial do projeto. Valor padrão: 'en-us'. Veja a lista de outras línguas aqui: http://www.i18nguy.com/unicode/language-identifiers.html
  • TIME_ZONE: Define o fuso horário do projeto. Valor padrão: 'America/Chicago' 
  • USE_I18N: Flag que determina se o sistema de tradução do Django deve ser habilitado ou não. Deve ser True para multi-idiomas e False, por questões de otimização, quando tiver apenas um idioma. Valor padrão: False 
  • USE_L10N: Um booleano que especifica se a formatação localizada de dados será ativado por padrão ou não. Se esta é definida como True , por exemplo Django irá exibir números e datas usando o formato da localidade atual. Valor padrão: True 
  • LANGUAGES: Tupla com as linguagens que serão utilizadas no site. 
  • USE_TZ: Determina se o django usará o datetime com base no fuso ou não.Valor padrão: False 
  • LOCALE_PATH: Tupla com os diretórios onde serão gerados os arquivos para tradução.

2. Instale o django-modeltranslation

$ pip install django-modeltranslation

No settings, certifique-se que o modeltranslation está acima do admin do Django, pra não ter problemas, coloque ele como a primeira linha do INSTALLED_APPS:
INSTALLED_APPS = (
    # TRANSLATION
    'modeltranslation',
    ...
)

Cerfitique-se que o Locale Middleware está instalado:
MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)

E inclua a url de tradução no urls.py:
...
(r'^i18n/', include('django.conf.urls.i18n')),
...

Na aplicação que terá conteúdos dinâmicos para cada idioma deixe os arquivos como abaixo:

models.py
# coding: utf-8
from django.db import models

# Create your models here.

class Conteudo(models.Model):
	"""(Conteudo description)"""
	titulo = models.CharField(u'Título', max_length=255)
	texto = models.TextField()
	slug = models.SlugField(verbose_name=u'Slug / URL')

	class Meta:
		verbose_name, verbose_name_plural = u"Conteúdo" , u"Conteúdos"
		ordering = ('titulo',)

	def __unicode__(self):
		return u"%s" % self.titulo

admin.py
# Register your models here.

from django.contrib import admin
from .models import *
from modeltranslation.admin import TranslationAdmin

class ConteudoAdmin(TranslationAdmin):
	search_fields = ('titulo', 'texto',)
	list_display = ('titulo', 'texto')
	prepopulated_fields = {"slug": ("titulo",)}
	save_on_top = True

admin.site.register(Conteudo, ConteudoAdmin)

translation.py
# translation.py

from modeltranslation.translator import translator, TranslationOptions
from .models import *

class ConteudoOptions(TranslationOptions):
    fields = ('titulo', 'texto',)
translator.register(Conteudo, ConteudoOptions)


Quando usar a migração (South para Django<1.7, ou nativo para Django>=1.7), serão gerados no banco os campos definidos no translation.py para cada idioma, isto é, o resultado será uma tabela com:
  • titulo_pt_br
  • texto_pt_br
  • titulo_en_us
  • texto_en_us
  • titulo_es_es
  • texto_es_es
  • slug

E o admin já trará todos eles com uma marcação de idioma através de [<idioma>] nas labels de cada campo.


Observação Importante: 

Caso não haja necessidade de traduzir a url que ficará no navegador, não inclua o slug no translation.py (recomendo que não inclua! Insista, convença seu chefe!). Isso facilita e muito quando o usuario troca de idioma para redirecioná-lo para a mesma página ao invés de mandá-lo de volta pra home do site e/ou fazer qualquer gambiarra para redirecioná-lo corretamente.

3. Nos templates, fica muito simples:

{{ obj.titulo }}

Aqui o próprio Model Translation irá verificar qual o é o idioma atual e exibir o valor correto. Sem IFs e sem duplicação de templates.

4. Para conteúdos estáticos, o processo continua o mesmo com a geração dos .po

Carrege o módulo i18n nos templates que for traduzir:
{% load i18n %}

Marque nos templates estáticos onde sofrerá tradução com as tags trans e blocktrans
{% trans "Exemplo de texto a ser traduzido nos seus htmls" %}

Execute o comando makemessages para gerar as mensagens:
$ python manage.py makemessages --locale=en --ignore=templates/admin --ignore=project/settings.py

Serão gerados arquivos .po para tradução no LOCALE_PATH definido nas settings com a seguinte estrutura:
  • locale
    • en
      • LC_MESSAGES
        • django.po
    • es
      • LC_MESSAGES
        • django.po


Nestes arquivos, serão armazenados os termos a serem traduzidos para envio para a pessoa responsável para tradução, e eles se parecem com isso:
#: templates/atendimento/atendimento.html:21
#: templates/atendimento/sucesso.html:21 templates/curriculos/sucesso.html:21
#: templates/curriculos/trabalhe-conosco.html:25
msgid "Dados enviados com sucesso"
msgstr ""

No exemplo acima, as linhas iniciadas com # indicam o caminho dos arquivos em tem o termo contido no msgid em comum, e no msgstr é onde deve ser preenchido com a tradução para o idioma correspondente.

Caso a pessoa responsável pela tradução não conseguir/quiser editar direto os .po, existem alguns apps que podem ser utilizados para facilitar esse processo, como o Rosetta (mais informações em: https://github.com/mbi/django-rosetta) por exemplo. Este cria uma interface com base no bom e velho admin provido pelo Django.

Depois de tudo preenchido, basta compilar as mensagens com o comando compilemessages:
$ python manage.py compilemessages --locale=en
$ python manage.py compilemessages --locale=es


5. Para trocar de idioma no site, basta um form:


{% get_available_languages as LANGUAGES %}
{% get_current_language as LANGUAGE_CODE %}

<form action="{% url 'set_language' %}" method="post">
{% csrf_token %}
<input name="next" type="hidden" value="{{ redirect_to }}" />
<select name="language">
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
    {{ language.name_local }} ({{ language.code }})
</option>
{% endfor %}
</select>
<input type="submit" value="Go" />
</form>


Em {{ redirect_to }}, se o slug não tiver sido traduzido e cada conteúdo tiver uma única url, aqui vc pode por a url atual, {{ request.META.PATH_INFO }},  e quando o usuário trocar de idioma, já vai ser redirecionado para a mesma página que estava visualizando.



Referências:


hasta!