segunda-feira, 26 de setembro de 2016

Manter apenas um usuário logado por credenciais

Neste post vou mostrar como manter apenas um usuário logado por credenciais. Desta forma quando ele logar com as mesmas credenciais em outra máquina, a sessão anterior é deslogada.

No seu models.py da app Cadastro adicione o seguinte atributo:

class Cadastro(models.Model):
 user = models.OneToOneField(User, null=True, blank=True)
 ...
 session_key = models.CharField(max_length=100, null=True)
 ...


E na sua views de login edite conforme a necessidade, incluíndo as linhas abaixo:
...
from django.contrib.auth import authenticate, logout, login as authlogin
from django.contrib.sessions.backends.db import SessionStore
...
u = authenticate(username=email, password=senha)
if u is not None:
 if u.is_active:
  if u.cadastro.session_key: # check if user has session_key. This will be true for users logged in on another device
   try:
    s = SessionStore.objects.get(session_key=u.cadastro.session_key)
   except Session.DoesNotExist:
    pass
  else:
   s.delete() # delete the old session_key from db

  # set new session_key for user instance
  u.cadastro.session_key = request.session.session_key
  u.cadastro.save() # save the user

  u.save()
  authlogin(request, u)
...

hasta!

terça-feira, 20 de setembro de 2016

Como ler feed de redes sociais: Facebook, Twitter, Instagram



A ideia é simples: gostaria de postar algo nas redes sociais e seria fantástico se esses posts fossem incorporados no site.

Bom, tem como.

Primeiro vamos a estrutura:


  1. Crie uma app para armazenar esse conteúdo:
    python manage.py startapp redes_sociais
  2. models.py:
    from django.db import models
    
    # Create your models here.
    
    REDES_SOCIAIS_C = (
    	('FACEBOOK', 'Facebook'),
    	('TWITTER', 'Twitter'),
    	('INSTAGRAM', 'Instagram'),
    )
    
    class Post(models.Model):
    	"""(Post description)"""
    	texto = models.TextField()
    	imagem = models.ImageField(upload_to=U.retira_acento,null=True,blank=True,)
    	imagem_src = models.TextField(null=True,blank=True)
    	redesocial = models.CharField(max_length=255, choices=REDES_SOCIAIS_C)
    	pid = models.CharField(max_length=255)
    	data = models.DateTimeField(null=True, blank=True)
    	link = models.CharField(max_length=255, null=True, blank=True)
    	ativo = models.BooleanField(default=True)
    
    	class Meta:
    		verbose_name, verbose_name_plural = u"Post" , u"Posts"
    		ordering = ('-data',)
    
    	def __unicode__(self):
    		return u"%s" % self.texto
    
    
    	def get_imagem(self):
    		if self.imagem:
    			return self.imagem
    		elif self.imagem_src:
    			return self.imagem_src
    		return None
  3. admin.py:
    # coding: utf-8
    from django.contrib import admin
    from .models import *
    
    class PostAdmin(admin.ModelAdmin):
    	search_fields = ('texto',)
    	list_display = ('texto', 'redesocial', 'pid','link' ,'data','ativo')
    	list_filter = ['redesocial','data','ativo']
    	list_editable = ['ativo',]
    	readonly_fields = ['texto', 'redesocial', 'pid','link' ,'data','imagem','imagem_src']
    	save_on_top = True
    
    	fieldsets = (
    	    (u'Ativo', {'fields': ('ativo',)}),
    	    (u'Infos', {'fields': ('redesocial', 'texto', 'pid', 'link')}),
    	    (u'Data', {'fields': ('data',)}),
    	    (u'Imagem', {'fields': ('imagem','imagem_src')}),
    	)
    
    admin.site.register(Post, PostAdmin)
    
  4. Fiz dois campos para imagens para dar prioridade de edição sobre o campo do tipo file, uma vez que o facebook retorna o caminho da imagem com mais de 255 chars para um varchar normal. O método get_imagem é o responsável por definir qual imagem exibir.

Instagram:

  1. Instale o app em seu celular e crie uma conta.
  2. Acesse: https://www.instagram.com/developer/ e registre uma aplicação
    1. Na aba Details preencha as informações obrigagtórias
    2. Na aba Security coloque uma url válida em Valid redirect URIs. Esta urla lhe dará o access token depois da autorização
    3. Desabilite a opção: Disable implicit OAuth
    4. Acesse https://www.instagram.com/oauth/authorize/?client_id=[CLIENT_ID]&redirect_uri=[URI_DO_PASSO 2.2]&response_type=code
    5. Você será redirecionado para a URI informada com o seu access token no get:
      1. EX: [URI_DO_PASSO 2.2]/?code=rx1p450j0gz2ahgq8ufn0mvw50mvv47e
  3. Configure seu settings.py com as informações abaixo:
    # INSTAGRAM
    INSTAGRAM_USER_ID = '[USER_ID DO FEED]'
    INSTAGRAM_CLIENT_ID = '[CLIENT_ID DO PASSO 2]'
    INSTAGRAM_CLIENT_SECRET = '[CLIENT_SECRET DO PASSO 2]'
    INSTAGRAM_ACCESS_TOKEN = '[TOKEN OBTIDO NO PASSO 5.1]'
    
    
  4. Para o [USER_ID DO FEED] utilizei um site que fornece de maneira fácil: https://smashballoon.com/instagram-feed/find-instagram-user-id/
  5. Instale o python-instagrampy no seu ENV:
    pip install python-instagram==1.3.2
  6. Crie um arquivo chamado feed_instagram.py na mesma pasta do settings.py com o seguinte conteúdo:
    #!/usr/bin/env python
    # coding: utf-8
    
    from os.path import abspath, dirname
    from datetime import datetime, date, timedelta
    import sys, os, commands, time
    
    SETTINGS_DIRECTORY = dirname(dirname(abspath(__file__)))
    
    sys.path.insert(0, SETTINGS_DIRECTORY)
    os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
    
    from django.conf import settings
    from django.db.models import Count, Min, Sum, Avg
    
    from django.template.defaultfilters import slugify
    
    import re, json
    
    def log(texto):
    	print "===================================="
    	print texto
    	print "===================================="
    
    
    def carga():
    	log("INICIO DA ROTINA")
    	from redes_sociais.models import Post
    	from instagram.client import InstagramAPI
    
    	# AUTH REQUIRED
    	access_token = settings.INSTAGRAM_ACCESS_TOKEN
    	client_secret = settings.INSTAGRAM_CLIENT_SECRET
    	api = InstagramAPI(access_token=access_token, client_secret=client_secret)
    	recent_media, next_ = api.user_recent_media(user_id=settings.INSTAGRAM_USER_ID, count=10)
    	for media in recent_media:
    		#print dir(media)
    		#print media.caption.text
    		post = Post.objects.filter(pid=media.id)
    
    		texto = media.caption.text if media.caption else None
    
    		try:
    			if post:
    				post = post[0]
    				post.texto = u'{0}'.format(texto)
    				post.data = media.created_time
    				post.imagem = media.get_standard_resolution_url()
    			else:
    				post = Post(
    					redesocial = 'INSTAGRAM',
    					pid = media.id,
    					texto = u'{0}'.format(texto),
    					data = media.created_time,
    					link = media.link,
    					imagem = media.get_standard_resolution_url(),
    				)
    			post.save()
    		except Exception, e:
    			log(u"Erro ao inserir [{0}]: {1}".format(media.id, media.get_standard_resolution_url()))
    			log(e)
    
    	# AUTH NON REQUIRED
    	# api = InstagramAPI(client_id=settings.INSTAGRAM_CLIENT_ID, client_secret=settings.INSTAGRAM_CLIENT_SECRET)
    	# popular_media = api.media_popular(count=20)
    	# for media in popular_media:
    	# 	print media.images['standard_resolution'].url
    
    	log("FIM DA ROTINA")
    
    if __name__ == '__main__':
    	os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
    	from django.core.wsgi import get_wsgi_application
    	application = get_wsgi_application()
    
    	carga()
    
    
    

Twitter

  1. Crie um aplicativo em: https://apps.twitter.com/
  2. Após criado, na aba settings tem as informações necessárias para configuração:
    1. Access Token [TOKEN]
    2. Access Token Secret [TOKEN_SECRET]
    3. Access Level Read and write
    4. Owner [SEU USUARIO]
    5. Owner ID [SEU ID]
  3. Configure seu settings.py com as informações abaixo:
    1. # TWITTER
      TWITTER_USERNAME = 'zejuniortdr'
      TWITTER_CONSUMER_KEY = '0aYPbXNNNYvhGyPg9CYufUncN'
      TWITTER_CONSUMER_SECRET = 'Lf6MfaSRp3qQaAGlbrVyt0hyo3659obj5bL2W6tiO8PWEAE0vU'
      TWITTER_ACCESS_TOKEN = '30026596-WKqQtcC3AmboRBOqp9uE1cEPfJkZIfeNB8mdR1nVK'
      TWITTER_ACCESS_SECRET = '9j7D0yzgvIybg6vnB9iqac1354Shv21YE2OwSO1oUarTw'
      
  4. Instale o python-instagrampy no seu ENV:
    pip install tweepy==3.3.0
  5. Crie um arquivo chamado feed_twitter.py na mesma pasta do settings.py com o seguinte conteúdo:
    #!/usr/bin/env python
    # coding: utf-8
    
    from os.path import abspath, dirname
    from datetime import datetime, date, timedelta
    import sys, os, commands, time
    
    SETTINGS_DIRECTORY = dirname(dirname(abspath(__file__)))
    
    sys.path.insert(0, SETTINGS_DIRECTORY)
    os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
    
    from django.conf import settings
    
    import re
    
    def log(texto):
    	print "===================================="
    	print texto
    	print "===================================="
    
    
    def carga():
    	log("INICIO DA ROTINA")
    	from redes_sociais.models import Post
    	import tweepy
    	from tweepy import OAuthHandler
    
    	consumer_key = settings.TWITTER_CONSUMER_KEY
    	consumer_secret = settings.TWITTER_CONSUMER_SECRET
    	access_token = settings.TWITTER_ACCESS_TOKEN
    	access_secret = settings.TWITTER_ACCESS_SECRET
    
    	auth = OAuthHandler(consumer_key, consumer_secret)
    	auth.set_access_token(access_token, access_secret)
    
    	api = tweepy.API(auth)
    
    	username = settings.TWITTER_USERNAME
    	user = tweepy.API(auth).get_user(username)
    	# log(dir(user))
    
    
    	status = api.user_timeline(screen_name=username, count=200, include_entities=True)
    
    
    	for s in status:
    		imagem = None
    		if 'media' in s.entities.keys():
    			imagem = s.entities['media'][0]['media_url']
    
    		post = Post.objects.filter(pid=s.id)
    		try:
    			if post:
    				post = post[0]
    				post.texto = u'{0}'.format(s.text)
    				post.data = s.created_at
    				post.imagem = imagem
    			else:
    				post = Post(
    					redesocial = 'TWITTER',
    					pid = s.id,
    					texto = u'{0}'.format(s.text),
    					data = s.created_at,
    					link = 'https://twitter.com/{0}/status/{1}'.format(username, s.id),
    					imagem = imagem,
    				)
    			post.save()
    		except Exception, e:
    			log(u"Erro ao inserir [{0}]: {1}".format(status.id, status.text))
    
    	log("FIM DA ROTINA")
    
    if __name__ == '__main__':
    	os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
    	from django.core.wsgi import get_wsgi_application
    	application = get_wsgi_application()
    
    	carga()
    
    

Facebook

  1. Crie um app com uma conta de desenvolvedor onde esta pessoa seja um dos administradores da Fan Page que quer extrair o feed: https://developers.facebook.com/apps
  2. Configure seu settings.py com as informações abaixo:
    # FACEBOOK
    FACEBOOK_PAGE_ID = [ID DA PAGINA QUE QUER O FEED DE POSTAGENS] # must be integer
    FACEBOOK_APP_ID = [ID DA APLICACAO CRIADA] # must be integer
    FACEBOOK_APP_SECRET = "[APP SECRET DA APLICACAO CRIADA]"
    
  3. Instale as dependencias do Facebook no seu ENV:
    facebook-sdk==2.0.0
    facepy==1.0.8
  4. Crie um arquivo chamado feed_facebook.py na mesma pasta do settings.py com o seguinte conteúdo:
    #!/usr/bin/env python
    # coding: utf-8
    
    from os.path import abspath, dirname
    from datetime import datetime, date, timedelta
    import sys, os, commands, time
    
    SETTINGS_DIRECTORY = dirname(dirname(abspath(__file__)))
    
    sys.path.insert(0, SETTINGS_DIRECTORY)
    os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
    
    from django.conf import settings
    from django.db.models import Count, Min, Sum, Avg
    
    from django.template.defaultfilters import slugify
    
    import re, json
    
    def log(texto):
    	print "===================================="
    	print texto
    	print "===================================="
    
    
    def carga():
    	log("INICIO DA ROTINA")
    	from redes_sociais.models import Post
    	from facepy import utils, GraphAPI
    	import facebook
    
    	app_id = settings.FACEBOOK_APP_ID
    	page_id = settings.FACEBOOK_PAGE_ID
    	app_secret = settings.FACEBOOK_APP_SECRET
    	oath_access_token = utils.get_application_access_token(app_id, app_secret)
    
    
    	a = GraphAPI(oath_access_token)
    	response = a.get('{0}/posts?fields=id,picture,message,link,full_picture,created_time,object_id'.format(page_id))
    
    	for r in response['data']:
    		post_id = None
    		post_message = None
    		post_link = None
    		post_full_picture = None
    		post_created_time = None
    		for k, v in r.items():
    			if k == 'id':
    				post_id = v
    			elif k == 'message':
    				post_message = v
    			elif k == 'link':
    				post_link = v
    			elif k == 'full_picture':
    				post_full_picture = v
    			elif k == 'created_time':
    				post_created_time = v
    
    		post = Post.objects.filter(pid=post_id)
    
    
    		try:
    			if post:
    				post = post[0]
    				post.texto = u'{0}'.format(post_message)
    				post.pid = u'{0}'.format(post_id)
    				post.data = post_created_time
    				post.imagem_src = post_full_picture
    				post.link = u'{0}'.format(post_link)
    			else:
    				post = Post(
    					texto = u'{0}'.format(post_message),
    					redesocial = 'FACEBOOK',
    					pid = u'{0}'.format(post_id),
    					imagem_src = post_full_picture,
    					data = post_created_time,
    					link = u'{0}'.format(post_link),
    				)
    
    			post.save()
    		except Exception, e:
    			log(u"Erro ao inserir [{0}]: {1} - {2}".format(post_id, post_message, e))
    
    
    	log("FIM DA ROTINA")
    
    if __name__ == '__main__':
    	os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
    	from django.core.wsgi import get_wsgi_application
    	application = get_wsgi_application()
    
    	carga()
    
    
    

Agora para executar as rotinas, basta estar com seu ENV ativado e rodar os comandos:
python app/feed_instagram.py 
python app/feed_twitter.py 
python app/feed_facebook.py

Após rodar as rotinas vai ter em sua base de dados os post das três redes sociais. Na view que for fazer o render, basta utilizar o ORM  para obter os registros.

Espero ter ajudado mais gente.

Nota: do Google Plus está em desenvolvimento, mas é a documentação mais enjoada de conseguir informação. Se alguém tiver algum script pronto que faça o que estes acima se propõe a fazer seria de grande ajuda para complementar este post, com os devidos créditos, lógico.


hasta!


terça-feira, 6 de setembro de 2016

Validação customizada usando o ModelForm

Sempre existe alguma regra de validação particular de algum form. Seja um campo obrigatório caso outro seja preenchido, etc.

Vejamos como fazer isso de um jeito bem fácil:


class SeuModelForm(forms.ModelForm):
 campo1 = forms.CharField(required=False, widget=forms.TextInput(),)
 campo1a = forms.BooleanField(required=False, widget=forms.CheckboxInput(),)
 campo2 = forms.CharField(required=False, widget=forms.TextInput(),)
 campo2a = forms.BooleanField(required=False, widget=forms.CheckboxInput(),)
 campo3 = forms.BooleanField(required=False, widget=forms.CheckboxInput(),)

 class Meta:
  model = SeuModel
  fields = '__all__'



 #def clean_aceito(self):
 # campo3 = self.cleaned_data['campo3']
 #  if not campo3:
 #   raise forms.ValidationError("Mensagem de erro do campo3")
 #  return campo3




 def clean(self):
  cleaned_data = super(SeuModelForm, self).clean()

  campo1 = cleaned_data.get("campo1")
  campo1a = cleaned_data.get("campo1a")

  campo2 = cleaned_data.get("campo2")
  campo2a = cleaned_data.get("campo2a")

  campo3 = cleaned_data['campo3']

  if not campo1 and not campo1a:
   self.add_error('campo1', u'Mensagem de erro do campo1')


  if not campo2 and not campo2a:
   self.add_error('campo2', u'Mensagem de erro do campo2')

  if not campo3:
   self.add_error('campo3', u'Mensagem de erro do campo3')

Acima vemos duas possibilidades:

  • Validar um campo específico, através do método clean_<campo>
  • Validar o form todo, testando todos campos juntos com o método clean

hasta!

sexta-feira, 19 de agosto de 2016

Erros Frequentes: Django==1.9.8 + django-ckeditor-updated==4.4.4 No module named util

Peguei o seguinte erro:


Traceback (most recent call last):
  File " in="" line="" manage.py="" module="">    execute_from_command_line(sys.argv)
  File "/var/www/SEU_SITE/ENV/local/lib/python2.7/site-packages/django/core/management/__init__.py", 
line 353, in execute_from_command_line
    utility.execute()
  File "/var/www/SEU_SITE/ENV/local/lib/python2.7/site-packages/django/core/management/__init__.py", 
line 327, in execute
    django.setup()
  File "/var/www/SEU_SITE/ENV/local/lib/python2.7/site-packages/django/__init__.py", line 18, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/var/www/SEU_SITE/ENV/local/lib/python2.7/site-packages/django/apps/registry.py", line 108, in populate
    app_config.import_models(all_models)
  File "/var/www/SEU_SITE/ENV/local/lib/python2.7/site-packages/django/apps/config.py", 
line 202, in import_models
    self.models_module = import_module(models_module_name)
  File "/usr/lib/python2.7/importlib/__init__.py", line 37, in import_module
    __import__(name)
  File "/var/www/SEU_SITE/app/blog/models.py", line 11, in 
    from ckeditor.fields import RichTextField
  File "/var/www/SEU_SITE/ENV/local/lib/python2.7/site-packages/ckeditor/fields.py", line 4, in 
    from ckeditor.widgets import CKEditorWidget
  File "/var/www/SEU_SITE/ENV/local/lib/python2.7/site-packages/ckeditor/widgets.py", line 10, in 
    from django.forms.util import flatatt
ImportError: No module named util

Para resolver, basta editar o arquivo /var/www/SEU_SITE/ENV/local/lib/python2.7/site-packages/ckeditor/widgets.py como abaixo:



Troque a linha:
django.forms.util import flatatt

Por:
django.forms.utils import flatatt

sexta-feira, 12 de agosto de 2016

Plugins Úteis: django-image-cropping - Como incoporar uma ferramenta de recorte para fotos no admin


Imagine que temos um projeto onde o usuário, responsável por alimentar os conteúdos, não tem a mínima noção do tamanho da imagem necessária para não destruir o layout, e nenhum pouco de boa vontade para ler a especificação do tamanho no help_text?

Claro que sempre dá pra fazer o recorte automático com o sorl-thumbnail, mas e se o usuário quiser escolher como recortar a foto?

Bom, pra isso, apresento-lhes o impressionante : django-image-cropping.


Vejamos como utilizar:

1. Primeiramente, instale ele no seu virtualenv:

pip install django-image-cropping

2. Se não tiver o easy_thumbnails instalado, instale:

pip install easy_thumbnails 

3. Adicione os dois no INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    'easy_thumbnails',
    'image_cropping',
    ...
]

4. Adicone também no settings.py:

from easy_thumbnails.conf import Settings as thumbnail_settings
THUMBNAIL_PROCESSORS = (
    'image_cropping.thumbnail_processors.crop_corners',
) + thumbnail_settings.THUMBNAIL_PROCESSORS

5. No seu model que quiser a função de recorte no admin deixe como a seguir:

from image_cropping import ImageRatioField

class SeuModel(models.Model):
    ...
    imagem = models.ImageField(upload_to="/caminho/para/upload/",)
    cropping = ImageRatioField('imagem', '900x900')
    ...


Mude a proporção do atributo cropping de acordo com sua necessidade.

6. No admin.py:

from django.contrib import admin
from image_cropping import ImageCroppingMixin

class SeuModelAdmin(ImageCroppingMixin, admin.ModelAdmin):
    pass

admin.site.register(SeuModel, SeuModelAdmin)

7. No template que for exibir a foto recortada utilize a tag provida pelo plugin:

{% cropped_thumbnail instancia_do_seu_model "cropping" [scale=INT|width=INT|height=INT|max_size="INTxINT"] %}

EX:
<img cropping="" scale="0.5" src="{% cropped_thumbnail instancia_do_seu_model " />


8. Dá inclusive para utilizar esse plugins para Inlines:


from image_cropping import ImageCroppingMixin
class ImagemInline(ImageCroppingMixin, admin.TabularInline):
 model = Imagem
 extra = 0



Desta maneira, ao fazer o upload, o usuário poderá selecionar a área de recorte diretamente no admin, respeitando a proporção imposta no models.py e sem destruir o layout.


Fonte e mais informações: https://pypi.python.org/pypi/django-image-cropping


hasta!

quarta-feira, 20 de abril de 2016

Como esconder alguns registros do changelist do admin do Django



A pergunta é simples: Pra que eu um dia iria precisar esconder alguns registros do change list, se já estou no admin como superuser?

Bom, imagine que seu cliente quer o acesso ao admin para gerenciar os conteúdos e também acesso a alguma seção do projeto que o login é necessário.

Quão surpreso você ficaria se descobrisse, que aquele seu cadastro de teste onde vc conseguia entrar para ajudar, testar e acompanhar o andamento do projeto fosse removido?

Isso, apesar de ridículo, não é de todo o impossível, e foi por passar por algo semelhante que escrevo este post.


Para ocultar os registros de uma determinada classe, basta  sobrescrever o método get_queryset do admin daquela classe no admin.py:

class CadastroAdmin(admin.ModelAdmin):
 form = CadastroForm
 search_fields = ('nome',)
 list_display = ('nome', 'email', 'unidade','ativo',)
 list_filter = ['ativo','unidade']
 list_editable = ['ativo',]
 exclude = ('user',)
 save_on_top = True


 def get_queryset(self, request):
  qs = super(CadastroAdmin, self).get_queryset(request)
  if request.user.is_superuser:
   return qs
  return qs.exclude(id__in=[13,14,15])

admin.site.register(Cadastro, CadastroAdmin)

No exemplo acima, quando o usuário logado for superuser do admin, este verá todos os registros, do contrário, a queryset excluirá dos resultados os cadastros com ids 13, 14 e 15.

Com pequenos ajustes, este método tornar bastante útil para evitar dedos inquietos clicando onde não devem.


hasta!

sexta-feira, 8 de abril de 2016

SyntaxError: expected expression, got '<' /admin/jsi18n/ (line 1) interpolate is not defined

SyntaxError: expected expression, got '<'  <!DOCTYPE html> /admin/jsi18n/ (line 1) 
interpolate is not defined

Esse erro apareceu quando fiz a utilização do SelectFilter2 para um form no front.

Ele é responsável pela ferramenta de filter_horizontal / filter_vertical do admin, e deveria ser exibido assim:




E com o erro era exibido assim:





Para resolver esse problema:


Adicione no seu urls.py:
...
url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog'),
...


E no seu template que for utilzar o widget:

<script type="text/javascript" src="/jsi18n/" > </script>

segunda-feira, 1 de fevereiro de 2016

Como utilizar o "Salvar e adicionar outro" do Admin no Front com Class-Based Views

O botão de Salvar e Adicionar outro é muito útil no administrativo quando se faz necessário inserir uma lista de dados em algum modelo.

No front, quando o usuário tem permissão de inserir algo para persistir o banco, também é possível utilizar este recurso. Vejamos como:

views.py

class SuaClasseCreateView(CreateView):
	form_class = SuaClasseForm
	success_url = '/url-de-sucesso/'
	template_name = 'form.html'
	model = SuaClasse

	def post(self, request, *args, **kwargs):

		form_class = self.get_form_class()
		form = self.get_form(form_class)

		if form.is_valid():

			if request.POST.get('_addanother'):
				self.success_url = '/url-de-nova-insercao/'

			return self.form_valid(form)
		else:
			return self.form_invalid(form)


	def form_valid(self, form):
		form.save()
		return HttpResponseRedirect(self.success_url)

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



class SuaClasseUpdateView(UpdateView):
	form_class = SuaClasseForm
	model = SuaClasse
	success_url = '/url-de-sucesso/'
	template_name = 'form.html'


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

		if form.is_valid():

			if request.POST.get('_addanother'):
				self.success_url = '/url-de-nova-insercao/'

			return self.form_valid(form)
		else:
			return self.form_invalid(form)


	def form_valid(self, form):
		form.save()
		return HttpResponseRedirect(self.success_url)

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

As views acima funcionam para qualquer modelo, sem nenhuma alteração nos forms.py. No html, apenas é necessário incluir o botão de Salvar e adicionar outro como exemplo abaixo:

form.html
<input type="submit" name="_addanother" value="Salvar e adicionar outro(a)">


Quando o clique é feito neste botão, o value dele é enviado via post, e a tratativa disso está no método post da view. Quando não é clicado, o valor não é enviado, não redirecionando assim para uma nova tela com o formulário aberto.

hasta!