jul 12, 2015
Hoy he estado programando en Django una vista para la descarga de informes, en
la que los usuarios registrados de una página pueden pedir un informe de la actividad
para un determinado año. La particularidad es que esos informes se generan de forma
dinámica en el momento de la petición, nada complicado pero antes de empezar a
programar he buscado que paquetes había disponibles, y me he encontrado
django-downloadview.
Con eso y un poco de código extra para comprobar que el usuario está autenticado
he solucionado el problema. Algunas veces CBV parecen magia.
#views.py
from django.shortcuts import render
from django.core.files.base import ContentFile
from django_downloadview import VirtualDownloadView
from django.http import HttpResponseForbidden, HttpResponseServerError
class LoginRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated():
return HttpResponseForbidden()
else:
return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)
class DownloadReportView(LoginRequiredMixin, VirtualDownloadView):
def generar_report(self, user, year):
# Generar contenido del informe para el usuario y fecha
return "Report content for {} {}".format(user.username, year)
def get_file(self):
# Metodo de VirtualDownloadView que devuelve el archivo virtual
report_name = 'report.txt'
return ContentFile(self.report_content, name=report_name)
def get(self, request, *args, **kwargs):
report_year = kwargs.get('year', None)
# El contenido del informe se genera aquí en lugar de en get_file, para
# simplificar la
try:
self.report_content=self.generate_report(request.user, report_year)
except Exception:
return HttpResponseServerError("There was an error while generating the report")
return super(DownloadReportView, self).get(request, *args, **kwargs)
Y el archivo urls.py para quien le pueda interesar:
#urls.py
from django.conf.urls import url
from .views import DownloadReportView
urlpatterns = [
url( # Handle report downloads
regex = r'^download_report/(?P<year>[0-9]{4})$',
view = DownloadReportView.as_view(),
name = 'download_user_report'),
]
Click to read and post comments
abr 17, 2015
He leído infinidad de tutoriales sobre PayPal REST API, desde los más técnicos a los más prácticos,
y en todos ellos lo que he echado de menos es que profundizaran un poco más en como integrar paypal
en una aplicación real.
Este tutorial es la primera parte de una serie, que pretende llenar ese hueco, y esta pensado como
un complemento a esos otros tutoriales, mostrando distintos ejemplos de uso de PayPal.
Antes de empezar recomiendo que te familiarizes con la biblioteca
Paypal Python SDK, porque el propósito de éste
tutorial no es explicar su funcionamiento.
La tienda
Para éste primer tutorial la aplicación que voy a usar como ejemplo es una tienda de libros, en
concreto una tienda de ebooks. Su funcionamiento es muy sencillo, cada libro tiene un botón de
compra que al ser pulsado redirige al cliente a la página de pago de paypal, cuando la transacción
se finaliza, el libro es enviado a su dirección de correo.
El proyecto esta compuesto por dos "apps" principales, libros y pagos. La primera
gestiona el contenido de la tienda, con cuatro modelos Autor, Genero, Editorial,
y Libro. Siendo este último el modelo más importante.
# libros.models.py
class Libro(PublicadoMixin, ImagenMixin):
"""Novels are never displayed, and are used to link together
all the editions, with the common information among them
authors, genre..."""
# Informacion basica del libro
titulo = models.CharField(max_length=200, db_index=True)
titulo_completo = models.CharField(max_length=300, blank=True)
numero_paginas = models.PositiveIntegerField(default=0)
resumen = models.TextField(max_length=2000, blank=True)
fecha_publicacion = models.DateField(blank=True, null=True)
idioma = LanguageField(default='es')
# Informacion sobre edicion y editorial
numero_edicion = models.PositiveSmallIntegerField(default=1)
editorial = models.ForeignKey(Editorial, blank=True, null=True)
# Algunos libros pueden tener multiples autores
autores = models.ManyToManyField('Autor')
# Generos del libro
generos = models.ManyToManyField('Genero')
# Ruta del ebook en los distintos formatos
epub = models.FileField(upload_to='ebooks/epub/', blank=True)
mobi = models.FileField(upload_to='ebooks/mobi/', blank=True)
# ISBN del libro.
isbn = ISBNField(blank=True)
# Precio del libro
precio = models.DecimalField(max_digits=6, decimal_places=2)
class META:
ordering = ['-fecha_publicacion']
def get_absolute_url(self):
return reverse('libro-detail', args=[str(self.id),])
def __str__(self):
return self.titulo
La segunda "app" pagos se encarga de generar y aceptar los pagos por PayPal, y tiene
un único modelo que se usa para registrar la información de la transacción, en concepto
de que libro se realizaron, y la información del cliente proporcionada por PayPal.
# pagos.models.py
from django.db import models
from decimal import Decimal
from libros.models import Libro
class PagoPaypalManager(models.Manager):
def crear_pago(self, payment_id, libro):
pago=self.create(libro=libro,
payment_id=payment_id,
precio=libro.precio)
return pago
class PagoPaypal(models.Model):
# Foreign Key hacia el libro de este pago
libro = models.ForeignKey(Libro)
# Identificador de paypal para este pago
payment_id = models.CharField(max_length=64, db_index=True)
# Id unico asignado por paypal a cada usuario no cambia aunque
# la dirección de email lo haga.
payer_id = models.CharField(max_length=128, blank=True, db_index=True)
# Dirección de email del cliente proporcionada por paypal.
payer_email = models.EmailField(blank=True)
# Guardamos una copia del precio de libro, porque puede variar en el tiempo
precio = models.DecimalField(max_digits=8, decimal_places=2,
default = Decimal('0.00'))
pagado = models.BooleanField(default=False)
objects = PagoPaypalManager()
Por último para introducir los datos de libros, editoriales, y demás información
se usa el interfaz de administración de Django.
Credenciales de Paypal
El primer paso para desarrollar una aplicación con PayPal es crear una cuenta,
habilitar REST Api en Paypal Developer, y obtener
las credenciales y cuentas de desarrollo. Después solo hay que añadirlas en el
archivo settings.py
PAYPAL_MODE= "sandbox"
PAYPAL_CLIENT_ID = "EBWKjlELKMYqRNQ6sYvFo64FtaRLRR5BdHEESmha49TM",
PAYPAL_CLIENT_SECRET = "EO422dn3gQLgDbuwqTjzrFgFtaRLRR5BdHEESmha49TM"
Paypal
En las páginas con la información de cada uno de los libros, se encuentran los
respectivos botones de compra, con los cuales un cliente puede iniciar el proceso
de pago a traves de PayPal.
Veamos código del botón de compra en el template:
<a href="{% url 'pago-paypal' libro.pk %}" class="btn btn-info btn-lg pull-right">
Comprar {{libro.precio}}€
</a>
El botón redirige al usuario a 'pago-paypal' con el identificador del libro como
parámetro, ese alias corresponde con la vista pagos.views.PaypalView, donde
se encuentra la lógica para iniciar el proceso de pago del libro.
# pagos.views.py
class PaypalView(RedirectView):
permanent = False
def _generar_lista_items(self, libro):
""" """
items = []
items.append({
"name": str(libro),
"sku": str(libro.id),
"price": ('%.2f' % libro.precio),
"currency": "EUR",
"quantity": 1,
})
return items
def _generar_peticion_pago_paypal(self, libro):
"""Crea el diccionario para genrar el pago paypal de libro"""
peticion_pago = {
"intent": "sale",
"payer": {"payment_method": "paypal"},
"redirect_urls": {
"return_url": settings.SITE_URL+reverse('aceptar-pago-paypal'),
"cancel_url": settings.SITE_URL},
# Transaction -
"transactions": [ {
# ItemList
"item_list":{
"items": self._generar_lista_items(libro)},
# Amount
"amount": {
"total": ('%.2f' % libro.precio),
"currency": 'EUR'},
#Description
"description": str(libro),}
]}
return peticion_pago
def _generar_pago_paypal(self, libro):
"""Genera un pago de paypal para libro"""
paypalrestsdk.configure({
"mode": settings.PAYPAL_MODE,
"client_id": settings.PAYPAL_CLIENT_ID,
"client_secret":settings.PAYPAL_CLIENT_SECRET,})
pago_paypal = paypalrestsdk.Payment(self._generar_peticion_pago_paypal(libro))
if pago_paypal.create():
for link in pago_paypal.links:
if link.method == "REDIRECT":
url_pago = link.href
else:
raise Exception(pago.error)
return url_pago, pago_paypal
def get_redirect_url(self, *args, **kwargs):
"""Extrae el libro que el usuario quiere comprar, genera un pago de
paypal por el precio del libro, y devuelve la direccion de pago que
paypal generó"""
libro = get_object_or_404(Libro, pk=int(kwargs['libro_pk']))
url_pago, pago_paypal = self._generar_pago_paypal(libro)
# Se añade el identificador del pago a la sesion para que PaypalExecuteView
# pueda identificar al ususuario posteriorment
self.request.session['payment_id'] = pago_paypal.id
# Por ultimo salvar la informacion del pago para poder determinar que
# libro le corresponde, al terminar la transaccion.
PagoPaypal.objects.crear_pago(pago_paypal.id, libro)
return url_pago
Esta vista se encarga de crear un pago para el libro con el identificador proporcionado,
y redirigir al usuario a la dirección proporcionada por PayPal para completar la transacción.
Para implementar esta vista he usado una Class Based View que hereda de
RedirectView,
y he redefinido el método get_redirect_url, para que obtenga el modelo del libro seleccionado,
genere un pago de Paypal con los datos de ese libro, y por último devuelva la dirección a la
que el usuario debe ser redirigido para finalizar el pago.
En la página de PayPal aparecerá el título del libro, su número de artículo, y el precio.
Una vez el usuario termina el pago es redirigido a la url de retorno, en la que se
debe "ejecutar" el pago para finalizar la transacción, esta dirección de retorno
se especificó en el método _generar_peticion_pago_paypal(), de la clase
PaypalView.
"return_url": settings.SITE_URL+reverse('aceptar-pago-paypal'),
En esta url se encuentra la vista PaypalExecuteView que acepta el pago, guarda la
información del cliente, marca el registro como pagado, y envía el email que contiene
el libro a la dirección de email del usuario proporcionada por PayPal.
# pagos.views.py
class PaypalExecuteView(TemplateView):
template_name = 'pagos/paypal_exito.html'
def _enviar_ebook_email(self, registro_pago):
"""Enviar Email con el libro al cliente """
#TODO: adjuntar los archivos del modelo libro
libro = registro_pago.libro
mensaje = "Gracias por su compra de %s" % libro.titulo
send_mail('TiendaEbook', mensaje,
from_email = settings.DEFAULT_FROM_EMAIL,
recipient_list=[registro_pago.payer_email,])
def _aceptar_pago_paypal(self, payment_id, payer_id):
"""Aceptar el pago del cliente, actualiza el registro con los datos
del cliente proporcionados por paypal"""
registro_pago = get_object_or_404(PagoPaypal, payment_id=payment_id)
pago_paypal = paypalrestsdk.Payment.find(payment_id)
if pago_paypal.execute({'payer_id': payer_id}):
registro_pago.pagado = True
registro_pago.payer_id = payer_id
registro_pago.payer_email = pago_paypal.payer['payer_info']['email']
registro_pago.save()else:
else:
raise HttpResponseBadRequest
return registro_pago
def get(self, request, *args, **kwargs):
"""Extraer identificacion de paypal del cliente, la id del pago,
aceptar el pago, y enviar el email."""
context = self.get_context_data(**kwargs)
try:
payer_id = request.GET['PayerID']
payment_id = request.session['payment_id']
except Exception:
raise HttpResponseBadRequest
registro_pago = self._aceptar_pago_paypal(payment_id, payer_id)
self._enviar_ebook_email(registro_pago)
return self.render_to_response(context)
Y eso es todo, el código está disponible en Github.
En la próxima entrega de este tutorial mejoraré algunas de las deficiencias de la aplicación,
añadiendo un carro de la compra para simplificar la compra de varios libros, y un sistema de
descarga para no tener que enviar el libro por correo.
Click to read and post comments
ene 01, 2015
Recientemente he tenido que crear un panel de control para una aplicación
django, su función era mostrar estadísticas de los pedidos. Como en
casi todos los casos el primer problema fue crear un sistema de login, esta
es una situación muy común que se presenta en la mayoría de aplicaciones,
la diferencia es que en este caso me decidí por usar Class Based Views,
y me he quedado sorprendido con la simplicidad y limpieza del código:
# views.py
from django.views.generic import FormView, TemplateView, RedirectView
# Authentication imports
from django.contrib.auth import login, logout
from django.contrib.auth.forms import AuthenticationForm
from django.core.urlresolvers import reverse_lazy
from cesta.models import Pedido
class LoginView(FormView):
form_class = AuthenticationForm
template_name = "control_panel/login.html"
success_url = reverse_lazy("panel-dashboard")
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated():
return HttpResponseRedirect(self.get_success_url())
else:
return super(LoginView, self).dispatch(request, *args, **kwargs)
def form_valid(self, form):
login(self.request, form.get_user())
return super(LoginView, self).form_valid(form)
class LogoutView(RedirectView):
pattern_name = 'panel-login'
def get(self, request, *args, **kwargs):
logout(request)
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated():
return HttpResponseRedirect(reverse('panel-login'))
else:
return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)
class ControlPanelView(LoginRequiredMixin, TemplateView):
template_name = 'control_panel/panel.html'
def get_context_data(self, **kwargs):
context = super(ControlPanelView, self).get_context_data(**kwargs)
context['pedidos_pendientes'] = Pedido.objects.pendientes()
# ....
# Recopilar resto de la informacion
# ....
return context
Descargar views.py
# urls.py
from django.conf.urls import patterns, url
from .views import ControlPanelView, LoginView, LogoutView
urlpatterns = patterns('',
url(
regex = r'^$',
view = ControlPanelView.as_view(),
name = "panel-dashboard"),
url(
regex = r'^login/$',
view = LoginView.as_view(),
name = "panel-login"),
url(
regex = r'^logout/$',
view = LogoutView.as_view(),
name = "panel-logout"),
)
Descargar urls.py
Por último un ejemplo para el template login.html:
{% extends 'base.html' %}
{% block content %}
<div class="login-panel">
<form method="post" role="form">{% csrf_token %}
{{ form.username }}
{{ form.password }}
<input type="submit" value="Login"/>
</form>
</div>
{% endblock %}
Click to read and post comments
ene 01, 2015
Recientemente he tenido que crear un panel de control para una aplicación
django, su función era mostrar estadísticas de los pedidos. Como en
casi todos los casos el primer problema fue crear un sistema de login, esta
es una situación muy común que se presenta en la mayoría de aplicaciones,
la diferencia es que en este caso me decidí por usar Class Based Views,
y me he quedado sorprendido con la simplicidad y limpieza del código:
# views.py
from django.views.generic import FormView, TemplateView, RedirectView
# Authentication imports
from django.contrib.auth import login, logout
from django.contrib.auth.forms import AuthenticationForm
from django.core.urlresolvers import reverse_lazy
from cesta.models import Pedido
class LoginView(FormView):
form_class = AuthenticationForm
template_name = "control_panel/login.html"
success_url = reverse_lazy("panel-dashboard")
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated():
return HttpResponseRedirect(self.get_success_url())
else:
return super(LoginView, self).dispatch(request, *args, **kwargs)
def form_valid(self, form):
login(self.request, form.get_user())
return super(LoginView, self).form_valid(form)
class LogoutView(RedirectView):
pattern_name = 'panel-login'
def get(self, request, *args, **kwargs):
logout(request)
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated():
return HttpResponseRedirect(reverse('panel-login'))
else:
return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)
class ControlPanelView(LoginRequiredMixin, TemplateView):
template_name = 'control_panel/panel.html'
def get_context_data(self, **kwargs):
context = super(ControlPanelView, self).get_context_data(**kwargs)
context['pedidos_pendientes'] = Pedido.objects.pendientes()
# ....
# Recopilar resto de la informacion
# ....
return context
Descargar views.py
# urls.py
from django.conf.urls import patterns, url
from .views import ControlPanelView, LoginView, LogoutView
urlpatterns = patterns('',
url(
regex = r'^$',
view = ControlPanelView.as_view(),
name = "panel-dashboard"),
url(
regex = r'^login/$',
view = LoginView.as_view(),
name = "panel-login"),
url(
regex = r'^logout/$',
view = LogoutView.as_view(),
name = "panel-logout"),
)
Descargar urls.py
Por último un ejemplo para el template login.html:
{% extends 'base.html' %}
{% block content %}
<div class="login-panel">
<form method="post" role="form">{% csrf_token %}
{{ form.username }}
{{ form.password }}
<input type="submit" value="Login"/>
</form>
</div>
{% endblock %}
Click to read and post comments