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>