Primeros pasos con el framework DJANGO

Vamos a construir una aplicación muy sencilla para conocer el framework Django.

Instalación de DJANGO

La forma más sencilla de instalarlo es mediante pip

pip install django

Comprobamos que funciona correctamente…

miguelm$ python
Python 2.7.10 (default, Aug 17 2018, 17:41:52) 
[GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.0.42)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import django
>>> django.VERSION
(1, 11, 18, u'final', 0)

Creación de primer proyecto y aplicación

Entramos en nuestro directorio de trabajo y creamos un nuevo proyecto, al que vamos a llamar Plantillas. Un proyecto es una instancia de un cierto conjunto de aplicaciones de Django y las configuraciones de éstas. Lo entenderemos mejor después.

django-admin startproject plantillas
cd plantillas

A continuación vamos a crear nuestra primera Aplicación dentro del proyecto, a la que llamaremos primera (en un alarde de originalidad). Una aplicación es un conjunto portable de alguna funcionalidad de Django, típicamente incluye modelos y vistas, que conviven en un solo paquete de Python.

django-admin startapp primera

Lo siguiente es indicar a nuestro proyecto “plantillas” que use nuestra aplicación primera. Para eso editamos la variable INSTALLED_APPS en plantillas/settings.py e incluimos primera:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'primera',
]

También nos aseguramos que la variable APP_DIRS dentro de TEMPLATES esté a True en plantillas/settings.py. APP_DIRS indica si el motor debe buscar plantillas dentro de las aplicaciones instaladas. Cada backend define un nombre convencional para el subdirectorio dentro de las aplicaciones donde se deben almacenar sus plantillas (templates), lo veremos más adelante. De momento, comprobamos que APP_DIRS está a True:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Lo siguiente será crear la carpeta de la aplicación donde dejaremos las plantillas html de nuestra aplicación primera:

mkdir primera/templates

El fichero que enruta las peticiones (tal url la resuelve tal función) es urls.py. Lo normal es que haya un fichero urls.py general del proyecto (plantillas/urls.py) y después un fichero urls.py dentro de cada aplicación (será primera/urls.py). Le decimos al fichero de rutas del proyecto (plantillas/urls.py) que todas las rutas que sean del tipo /primera las atienda con el fichero de rutas de nuestra aplicación primera, es decir, derivamos el control hacia primera/urls.py.

El fichero de routing del proyecto, plantillas/urls.py quedará así:

from django.conf.urls import url, include
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^primera/', include('primera.urls')),
]

A continuación voy a crear la primera ruta de mi aplicación primera. Para ello edito primera/urls.py y le digo que para cualquier ruta que reciba este controlador, se ejecute la función current_datetime, que definiremos más adelante en primera/views.py.

from django.conf.urls import include, url
from . import views
	
urlpatterns = [
    url(r'^$', views.current_datetime),
] 

Y definiremos la función current_datetime, lo que haremos editando primera/views.py, que quedará así.

	
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.shortcuts import render # nos permite renderizar plantillas de forma cómoda
from datetime import datetime # Este import lo hacemos para poder usarlo en la func current_datetime
	
def current_datetime(request):
    return render(request, 'hora.html', {'current_date' : str(datetime.now()) })

NOTA: Hay una forma, un poco más “sucia”, pero mucho más rápida, de pasar las variables locales definidas en una función (por ejemplo dentro de current_date) del controlador a la vista, que pasaría por usar la función locals() en la llamada a render. Si lo hacemos así, TODAS las variables definidas en current_datetime pasarán a la vista (es cómodo, pero a veces puedes estar pasando variables que no necesites, y además construyes el diccionario de forma dinámica, lo que no es súper eficiente). Pero bueno, es cómodo, y sería así:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.shortcuts import render # nos permite renderizar plantillas de forma cómoda
from datetime import datetime # Este import lo hacemos para poder usarlo en la func current_datetime
	
def current_datetime(request):
    current_date = str(datetime.now())
    return render(request, 'hora.html', locals())

Ya solo nos falta definir el template hora.html, que quedaría en primera/templates/hora.html y podría ser algo tan sencillo como ésto:

<html><head><title>Mi pagina</title></head><body>La fecha es {{ current_date }}</body></html>

Sería poco eficiente y repetiríamos código si cada página construye su plantilla desde cero. Es mejor tener una plantilla “padre”, que define la estructura común que tendrán todas las páginas, y plantillas “hijas”, que hereden del padre y sobreescriban determinadas zonas. Para eso vamos a crear una plantilla padre a la que llamaremos primera/templates/layout_3col.html y que estará basada en Bootstrap, que tendrá éste contenido:

<html>
<head>
    <title>{% block titlepag %} Killer App {% endblock %}</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>

<body>

    <div class="jumbotron text-center">
        {% block titulo %}
            <h1> titulo principal </h1>
        {%endblock %}
    </div>

    <div class="container">
        <div class="row">
            <div class="col-sm-4">
                {% block columna1 %}
                    <h3>Column 1</h3>
                    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit...</p>
                    <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris...</p>
                {%endblock %}
            </div>
            <div class="col-sm-4">
             {% block columna2 %}
            
              <h3>Column 2</h3>
              <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit...</p>
              <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris...</p>
            
            {%endblock %}
            </div>
            <div class="col-sm-4">
            {% block content %}
            
              <h3>Column 3</h3>
              <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit...</p>
              <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris...</p>
            
            {%endblock %}
   
            </div>
            
        </div>
    </div>
</body>
</html>
</html>

Y ahora vamos a escribir una plantilla “hija”, que heredará desde layout_3col.html y sobreescribirá únicamente el contenido del bloque definido como content. El fichero quedará en primera/templates/hora.html y tendrá éste contenido:

{% extends "layout_3col.html" %}

{% block content %}
La hora es {{ current_date }}
{% endblock %}

Ya solo nos falta decirle a nuestra función controlador (que es la función current_datetime definida en primera/views.py) que emplee esta nueva plantilla. Para ello dejaremos el contenido de así:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.shortcuts import render

from datetime import datetime

def current_datetime(request):
    current_date = str(datetime.now())
    return render(request, 'hora.html', locals())

Arrancando el servidor de pruebas

Django provee de una utilidad que permite levantar un servidor de pruebas de forma muy sencilla. Para ello únicamente hemos de hacer:

python manage.py runserver

Y nos levantará un servidor, así que consultaremos http://127.0.0.1:8000/primera/ y veremos algo así:
primeros pasos con django

Manejando archivos estáticos

Si necesitas servir ficheros estáticos (por ejemplo, imágenes, js, css, etc), necesitarás comprobar que en tu fichero de settings del proyecto (plantillas/settings.py), en la parte de INSTALLED_APPS, se encuentra la aplicación django.contrib.staticfiles. Debería ser algo así:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'primera',
]

El siguiente paso será definir, en este mismo archivo plantillas/settings.py, la ruta desde donde se servirán los recursos estáticos. Para ello se emplea la variable STATIC_URL, a la que damos valor:

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/'

A continuación creamos esta carpeta, que quedará en primera/static y metemos una imagen de pruebas (que quedará en primera/static/img_snowtops.jpg)

mkdir primera/static
cd primera/static
wget https://www.w3schools.com/w3css/img_snowtops.jpg
cd ../..

Y modificamos nuestra plantilla primera/templates/layout_3col.html para incluir esa imagen. La vamos a meter, por defecto, en la columna1. Para eso modificamos la parte del bloque columna1 del template, que quedará así:

{% block columna1 %}
        <<h3>Column 1<</h3>
        <<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit...<</p>
        <<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris...<</p>
        {% load static %}
        <<img src="{% static "img_snowtops.jpg" %}" alt="My image"/>
{%endblock %}

Es probable que el proyecto plantillas también tenga activos estáticos que no estén vinculados a una aplicación en particular, como podría ser primera. Además de usar un directorio /static dentro de sus aplicaciones, puede definir una lista de directorios (STATICFILES_DIRS) en su archivo de configuración del proyecto (plantillas/settings.py) donde Django también buscará archivos estáticos. Por ejemplo vamos a crear el directorio plantillas/static y /another-path/to/static-files/:

mkdir static #quedará al mismo nivel que la raíz del proyecto, es decir, al mismo nivel que manage.py
mkdir /another-path/to/static-files

Y metemos en el primer directorio una imagen de pruebas:

cd static
wget https://www.unizar.es/sites/default/files/identidadCorporativa/imagen/logoUZ.png
mv logoUZ.png logo.png

Hay que indicarle a Django que busque estáticos en estos paths, además de dentro de las carpetas static de las aplicaciones. Para ello editamos plantillas/settings.py y añadimos al final:

STATICFILES_DIRS = [
        os.path.join(BASE_DIR, "static"),
        "/another-path/to/static-files/",
]

Volvemos a modificar la plantilla de nuestra aplicación, es decir, primera/templates/layout_3col.html, para incluir la imagen que acabamos de descargar. La incluiremos por ejemplo en el bloque titulo. Quedaría así:

{% block titulo %}
    <h1> titulo principal </h1>
    {% load static %}
        <img src="{% static "logo.png" %}" alt="My LOGO"/>
{%endblock %}

Hay que notar que este método funcionará en entorno de desarrollo, siempre que la aplicación django.contrib.staticfiles esté entre las incluídas en el proyecto, y si DEBUG está a True. Para servir ficheros en producción, consulta éste enlace: https://docs.djangoproject.com/en/1.11/howto/static-files/deployment/

Y, ¿qué sucede si tengo dos aplicaciones, por ejemplo primera y segunda, ambas con sus directorios de static, y dentro de ellos un fichero que se llame igual, por ejemplo, logo.png, pero se trata de un fichero distinto?

Vamos a probarlo. Para ello nos vamos al raíz de nuestro proyecto plantillas y creamos una nueva aplicación:

django-admin startapp segunda # crea la aplicación "segunda" en el directorio "segunda"

La damos de alta entre las INSTALLED_APPS del proyecto. Para eso editamos plantillas/settings.py:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'primera',
    'segunda',
]

Creamos el directorio templates y static dentro de "segunda":

mkdir segunda/templates
mkdir segunda/static
cd segunda/static

#traemos una imagen de logo que sea distinta a la anterior. por ejemplo:
wget https://upload.wikimedia.org/wikipedia/commons/thumb/2/2a/ITunes_12.2_logo.png/600px-ITunes_12.2_logo.png
mv 600px-ITunes_12.2.logo.png logo.png
cd ..

#copiamos las mismas plantillas de primera a segunda
cp primera/templates/* segunda/templates/

Generamos una nueva ruta en el fichero controlador principal plantillas/urls.py:

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^primera/', include('primera.urls')),
    url(r'^segunda/', include('segunda.urls')),
]

Creamos el fichero segunda/urls.py:

from django.conf.urls import include, url
from . import views

urlpatterns = [
    url(r'^$', views.current_date),
]

Y también el fichero segunda/views.py:

from __future__ import unicode_literals

from django.shortcuts import render

def current_date(request):
    current_date = "No tengo reloj"
    return render(request, 'hora.html', locals())

Tenemos dos aplicaciones "iguales", pero dos logos distintos. ¿Cuál se cargará?

PRIMERA:

SEGUNDA:

Oh, oh. Problemas. Ambas me cargan la misma imagen, ¡pero deberían ser imágenes distintas!

¿Y si también tuviéramos un "logo.png" en nuestro directorio de estáticos del proyecto, que también fuera distinto? ¿Cuál cargaría?

Este comportamiento se indica en la documentación oficial de Django:

Now we might be able to get away with putting our static files directly in my_app/static/ (rather than creating another my_app subdirectory), but it would actually be a bad idea. Django will use the first static file it finds whose name matches, and if you had a static file with the same name in a different application, Django would be unable to distinguish between them. We need to be able to point Django at the right one, and the easiest way to ensure this is by namespacing them. That is, by putting those static files inside another directory named for the application itself.

Si quieres aprender más en detalle cómo Django busca estáticos, consulta éste enlace: https://docs.djangoproject.com/en/1.11/ref/settings/#std:setting-STATICFILES_FINDERS

Para desambiguar estos nombres iguales, lo mejor es darles un namespace.

Para ello:

mkdir -p primera/static/primera
mkdir -p segunda/static/segunda

mv primera/static/logo.png primera/static/primera/
mv segunda/static/logo.png segunda/static/segunda/

Y editamos el bloque titulo de las plantillas de ambas aplicaciones:

primera/templates/layout_3col.html quedaría así:

{% block titulo %}
    <h1> titulo principal </h1>
    {% load static %}
        <img src="{% static "primera/logo.png" %}" alt="My LOGO"/>
{%endblock %}

segunda/templates/layout_3col.html quedaría así:

{% block titulo %}
    <h1> titulo principal </h1>
    {% load static %}
        <img src="{% static "segunda/logo.png" %}" alt="My LOGO"/>
{%endblock %}

Modelo de datos

Empezaremos configurando la base de datos. Por defecto viene con sqlite3, como se puede comprobar en settings.py:

# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

Vamos a configurar un MySQL. Para ello es necesario tener instalado tanto el servidor como el cliente.

En OS X se puede instalar fácilmente con brew:

brew install mysql

Para arrancar el servidor, también lo haremos con brew:

brew services start mysql

==> Tapping homebrew/services
Cloning into '/usr/local/Homebrew/Library/Taps/homebrew/homebrew-services'...
remote: Enumerating objects: 17, done.
remote: Counting objects: 100% (17/17), done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 17 (delta 0), reused 12 (delta 0), pack-reused 0
Unpacking objects: 100% (17/17), done.
Tapped 1 command (50 files, 62.2KB).
==> Successfully started `mysql` (label: homebrew.mxcl.mysql)

Nos conectaremos como root (de momento lo dejamos sin password) y creamos una base de datos (la llamaremos ejemplo) y un usuario (django) que tenga permisos sobre ella:

mysql -u root

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.13 Homebrew

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> CREATE DATABASE ejemplo;
Query OK, 1 row affected (0,11 sec)

mysql> CREATE USER django IDENTIFIED BY 'mypass';
Query OK, 0 rows affected (0,16 sec)

mysql> GRANT ALL PRIVILEGES ON ejemplo.* TO 'django';
Query OK, 0 rows affected (0,04 sec)

Además instalamos pymysql, que es como Django se conectará a MySQL. Y ya de paso instalamos el paquete cryptography que nos hará falta más adelante:

pip install pymysql
pip install cryptography

Y a continuación editaremos nuestro fichero __init__.py del proyecto (que seguramente estará vacío) y le pondremos el siguiente contenido:

import pymysql

pymysql.install_as_MySQLdb()

Ahora sí, podemos modificar settings.py para añadir nuestra BD. Podemos poner los datos en ese mismo fichero o, lo que sería más elegante, poner los datos de conexión en un fichero aparte. Tal que así:

Fichero settings.py:

DATABASES = {
    'default':{
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
            'read_default_file': '/path/to/mysql_data.cnf',
        }
    }
}

Fichero /path/to/mysql_data.cnf:

[client]
database = ejemplo
user = django
password = mypass
default-character-set = utf8

En Django, en general:
- Cada modelo es una clase python que hereda de django.db.models.Model
- Cada atributo del modelo representa un campo de la base de datos

Vamos a crear nuestro primer modelo. Se llamará Alumno y lo crearemos dentro del fichero nombredeapp/models.py

from django.db import models

class Alumno(models.Model):
    nombre = models.CharField(max_length=50)
    apellido1 = models.CharField(max_length=50)
    apellido2 = models.CharField(max_length=50)
    nip = models.PositiveIntegerField(max_length=6)

Una vez creado el modelo, Django se encargará de crear la correspondiente tabla en la Base de datos. Para ello:

python manage.py makemigrations 
# indica a django que hay cambios en el modelo y que deseamos 
# guardar esos cambios en el archivo de migración. 
# Generará por tanto en el directorio "migrations" de la aplicación 
# un fichero llamado 0001_initial.py

Y una vez se ha generado el fichero en el directorio migrations de la aplicación, utilizamos:

python manage.py migrate
# cogerá todas las migraciones que no han sido aplicadas aún
# (que django controla en una tabla especial de su bd llamada django_migrations)
# y las ejecuta contra la base de datos.

¿Y si ya tengo las tablas creadas? ¿Puede Django generar el modelo? Sí.

Vamos a verlo.

Creamos la tabla:

CREATE TABLE IF NOT EXISTS `clientes` (
`nif` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Clave primaria',
`nombre` varchar(50) NOT NULL COMMENT 'nombre cliente',
`apellidos` varchar(100) NOT NULL COMMENT 'Apellidos cliente',
`telefono` int(9) NOT NULL COMMENT 'móvil',
`codigo_postal` int(5) DEFAULT NULL,
`edad` int(3) DEFAULT NULL,
`sexo` char(1) NOT NULL,
`profesion` text NOT NULL,
PRIMARY KEY (`nif`),
UNIQUE KEY `telefono` (`telefono`),
KEY `nombre` (`nombre`),
FULLTEXT KEY `apellidos` (`apellidos`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='tabla de clientes';

Para generar el modelo:

python manage.py inspectdb > primera/models.py

Y si miramos primera/models.py... 🙂

more models.py
# This is an auto-generated Django model module.
# You'll have to do the following manually to clean this up:
#   * Rearrange models' order
#   * Make sure each model has one field with primary_key=True
#   * Make sure each ForeignKey has `on_delete` set to the desired behavior.
#   * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table
# Feel free to rename the models, but don't rename db_table values or field names.
from __future__ import unicode_literals

from django.db import models


class Clientes(models.Model):
    nif = models.AutoField(primary_key=True)
    nombre = models.CharField(max_length=50)
    apellidos = models.CharField(max_length=100)
    telefono = models.IntegerField(unique=True)
    codigo_postal = models.IntegerField(blank=True, null=True)
    edad = models.IntegerField(blank=True, null=True)
    sexo = models.CharField(max_length=1)
    profesion = models.TextField()

    class Meta:
        managed = False
        db_table = 'clientes'

Vamos a hacer primero un pequeño cambio en primera/models.py, cambiaremos Managed = True y así indicamos a Django que pueda gestionar la tabla.

A continuación, vamos a probar a generar algún Cliente. Para eso podemos usar el shell que nos provee Django:

python manage.py shell
>>> from primera.models import Clientes
>>> micli = Clientes(nif=1234, nombre="Maikel", apellidos="Night", telefono='600000000', profesion='tunante')
>>> help(Clientes)
>>> micli.save()
>>> help(Clientes.objects)
>>> todos = Clientes.objects.all()
>>> for cli in todos:
...     print("Cliente con NIF {}".format(cli.nif))
... 
Cliente con NIF 1234

Veamos ahora cómo gestionar las relaciones entre modelos.

Empezamos por el caso más sencillo: relación 1-N. Un modelo de coche tiene un fabricante, pero un fabricante fabrica más de un modelo de coche. Se sugiere que el campo de la FK se llame como el nombre del modelo con el que se relaciona, en minúsculas. Por ejemplo:

from django.db import models

class Manufacturer(models.Model):
    # ...
    pass

class Car(models.Model):
    manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
    # ...

El siguiente caso: relación N-N. Por ejemplo, una pizza tiene N ingredientes y un ingrediente puede estar presente en varias pizzas. Se sugiere que el nombre de un ManyToManyField sea el plural en minúscula de cuyos objetos depende. El ManyToManyField solo se colocará en uno de los modelos, no en ambos. En general, las instancias ManyToManyField se colocaran en el objeto que se editará vía formulario. Django creará tres tablas, pizza, topping y pizza_toppings.

from django.db import models

class Topping(models.Model):
        name=models.CharField(max_length=50)
        def __unicode__(self):
                return self.name

class Pizza(models.Model):
    name=models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)
    def __unicode__(self):
                return self.name

¿Y si hay que guardar atributos de la relación? Por ejemplo, si queremos saber si un ingrediente de una pizza en concreto está cortado o no. Haríamos algo así:

from django.db import models

class Topping(models.Model):
        name=models.CharField(max_length=50)
        def __unicode__(self):
                return self.name

class Pizza(models.Model):
    name=models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping, through='Estado')
    def __unicode__(self):
                return self.name

class Estado(models.Model):
    pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE)
    topping = models.ForeignKey(Topping, on_delete=models.CASCADE)
    esta_cortado = models.BooleanField()

Veamos ahora la relación 1-1. Por ejemplo, si estuviera creando una base de datos de 'lugares', crearías elementos bastante estándar como la dirección, el número de teléfono, etc. en la base de datos. Luego, si deseas construir una base de datos de 'restaurantes' asociados a los lugares, en lugar de repetirlos y replicar esos campos en el modelo de 'restaurante', podrías hacer que el 'restaurante' tenga un OneToOneField a Lugar (porque un restaurante es un lugar; en de hecho, para manejar esto, normalmente se usa la herencia, que implica una relación uno a uno implícita).:

from django.db import models

class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

    def __str__(self):
        return "%s the place" % self.name

class Restaurant(models.Model):
    place = models.OneToOneField(
    Place,
    on_delete=models.CASCADE,
    primary_key=True,
    )
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)

    def __str__(self):
        return "%s the restaurant" % self.place.name

class Waiter(models.Model):
    restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
    name = models.CharField(max_length=50)

    def __str__(self):
        return "%s the waiter at %s" % (self.name, self.restaurant)

Y juguemos:

>>> p1 = Place(name='Demon Dogs', address='944 W. Fullerton')
>>> p1.save()
>>> p2 = Place(name='Ace Hardware', address='1013 N. Ashland')
>>> p2.save()
Create a Restaurant. Pass the ID of the “parent” object as this object’s ID:

>>> r = Restaurant(place=p1, serves_hot_dogs=True, serves_pizza=False)
>>> r.save()
A Restaurant can access its place:

>>> r.place

A Place can access its restaurant, if available:

>>> p1.restaurant

Veamos ahora los validadores que ofrece Django. Un validador es una función que genera un error de validación ValidationError si no cumple con algunos criterios.

Por ejemplo definimos un validador de números pares:

from django.core.exceptions import ValidationError

def validate_even(value):
    if value % 2 != 0:
        raise ValidationError("no es par")

Y lo podemos añadir a un modelo:

from django.db import models

class MyModel(models.Model):
    even_field = models.IntegerField(validators=[validate_even])

Más adelante veremos cómo integrar validaciones en los formularios.

Curso gratis Yii2: Día 1 – Formularios

Seguimos aprendiendo Yii2. Hoy aprenderemos a realizar un formulario completo, recoger datos y mostrarlos por pantalla. Para el ejemplo vamos a suponer que el usuario va a rellenar una ficha que contiene los datos de su Trabajo Fin de Grado.

Si no has leído los capítulos anteriores de esta serie sobre Yii2, te recomiendo que te pongas al día:

 

Creando el modelo.

Representaremos los datos que pediremos al usuario con una clase modelo que llamaremos DatosTrabajo.

Utilizaremos las convenciones de Yii para la autocarga de clases (aka Class Autoloading) que, básicamente, son dos:

  • Cada clase debe tener un namespace único (p ej foo\bar\DatosTrabajo)
  • Cada clase se debe guardar en un fichero separado cuyo path se determina por el siguiente algoritmo.
    // $className is a fully qualified class name without the leading backslash
    $classFile = Yii::getAlias('@' . str_replace('\\', '/', $className) . '.php');

Por ejemplo, si una Class y Namespace es foo\bar\MyClass, el alias para el fichero de la clase correspondiente sería @foo/bar/MyClass.php.

Vamos a crear por tanto nuestro fichero models/DatosTrabajo.php con el siguiente contenido. El código está documentado para que se entienda

<?php

namespace app\models;

use Yii;
use yii\base\Model;         // utilizada habitualmente para representar modelos no asociados a BD's
// use yii\db\ActiveRecord  // utilizada habitualmente para representar modelos que se corresponden con tablas

class DatosTrabajo extends Model
{
    public $titulo1;        // contendrá el título principal (normalmente, en castellano)
    public $titulo2;        // contendrá el título traducido (normalmente, en inglés)
    public $autor;          // contendrá el nombre y apellidos del autor
    public $director;       // contendrá el nombre y apellidos del director/tutor
    public $resumen1;       // contendrá el resumen en el idioma principal (normalmente, en castellano)
    public $resumen2;       // contendrá el resumen traducido (normalmente, en inglés)
    public $idioma;         // contendrá el idioma principal en que está redactado el trabajo
    public $anyo;           // contendrá el año de la defensa del Trabajo 
    public $notas;          // contendrá notas adicionales que el autor desee hacer constar
    public $palClave;       // contendrá una palabra clave
    public $estudio;        // contendrá el estudio
    public $centro;         // contendrá el centro responsable del estudio
    public $departamento;   // contendrá el departamento asociado al estudio
    public $esEspecial;     // contendrá un booleano indicando si es un trabajo "especial" (acompañado de materiales físicos)
    public $licencia;       // contendrá el valor de la licencia que ha indicado el autor


    /**
     * Método que devuelve un conjunto de reglas para validar los datos. 
     * Esto permitirá que podamos llamar al método "validate" del modelo tal que así:
     *      $model = new DatosTrabajo();
     *      $model->autor = "Miguel"
     *      $model->email = "email@unizar.es"
     *      if ($model->validate()){
     *      {
     *        // Bien!!
     *      } else {
     *        // Fallo! 
     *        use $model->getErrors()
     *      }
     *
     */
    public function rules()
    {
        return [
            [['titulo1', 'autor', 'email'], 'required'],    // titulo, autor e email son obligatorios
            ['email', 'email'],                             // email debe ser una dirección de correo sintáticamente correcta
        ];
    }
}

Creando la acción

A continuación vamos a crear la nueva action en nuestro controlador site que usará el modelo que acabamos de definir. Para ello volvemos a editar el fichero controllers/SiteController.php y tendremos que añadir dos cosas.

Al principio del fichero, decirle que use nuestro recién creado modelo app\models\DatosTrabajo

// use anteriores ...
use app\models\DatosTrabajo;

Y el código de la propia acción:

/**
     * @return string
     */
    public function actionNuevoTrabajo()
    {
        $model = new DatosTrabajo();

        if ($model->load(Yii::$app->request->post()) && $model->validate()) {
            // tenemos datos válidos en $model
            // aquí haríamos algo con ellos
            return $this->render('entry-confirm', ['model' => $model]);
        } else {
            // dos posibles situaciones, 
            //    - es el primer acceso y aún no tenemos los datos
            //    - hay algń error de validación.
            return $this->render('entry', ['model' => $model]);
        }
    }

La acción instancia un objeto DatosTrabajo. Después intenta completar el modelo con los datos de $_POST, proporcionados por Yii mediante yii\web\Request::post().

Si el modelo ha sido completado con éxito (p.e. el usuario ha enviado el formulario HTML), la acción llama al método validate() para asegurarse de que los datos introducidos son válidos.

La expresión Yii::$app representa la instancia de aplicación, que es un objeto singleton globalmente accesible. También es un service locator que proporciona componentes como request, response, db, etc. para facilitar funconalidades específicas. En el código anterior, el componente request se utiliza para acceder a los datos de $_POST.

Si todo es correcto, la acción renderizará la vista llamada entry-confirm para confirmar al usuario el envío con éxito de los datos. Si no se han enviado datos o los datos contienen errores, la vista entry será llamada, mostrando el formulario junto con cualquier mensaje de error.

Por simplicidad mostramos la página de confirmación cuando el envío es válido. En la práctica se debería considerar el uso de refresh() o redirect() para evitar problemas de reenvío de formularios. Lo veremos más adelante.

Creación de las vistas

Hemos hecho alusión a dos ficheros de vistas: entry y entry-confirm.

Vamos a crear el fichero views/site/entry-confirm.php que mostrará los datos recibidos:

<?php
use yii\helpers\Html;
?>
<p>Has introducido la siguiente información:</p>

<ul>
    <li><label>Título (idioma principal)</label>: <?= Html::encode($model->titulo1) ?></li>
    <li><label>Título (idioma secundario)</label>: <?= Html::encode($model->titulo2) ?></li>
    <li><label>Autor</label>: <?= Html::encode($model->autor) ?></li>
    <li><label>Director</label>: <?= Html::encode($model->director) ?></li>
    <li><label>Resumen (idioma principal)</label>: <?= Html::encode($model->resumen1) ?></li>
    <li><label>Resumen (idioma secundario)</label>: <?= Html::encode($model->resumen2) ?></li>
    <li><label>Idioma</label>: <?= Html::encode($model->idioma) ?></li>
    <li><label>Año</label>: <?= Html::encode($model->anyo) ?></li>
    <li><label>Notas</label>: <?= Html::encode($model->notas) ?></li>
    <li><label>Palabra clave</label>: <?= Html::encode($model->palClave) ?></li>
    <li><label>Estudio</label>: <?= Html::encode($model->estudio) ?></li>
    <li><label>Centro</label>: <?= Html::encode($model->centro) ?></li>
    <li><label>Departamento</label>: <?= Html::encode($model->departamento) ?></li>
    <li><label>¿Trabajo especial?</label>: <?= Html::encode($model->esEspecial) ?></li>
    <li><label>Licencia</label>: <?= Html::encode($model->licencia) ?></li>
</ul>

 

 

Y también el fichero views/site/entry.php que renderizará el formulario para captar los datos con el siguiente contenido:

<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
?>

<?php $form = ActiveForm::begin(); ?>

<?= $form->field($model, 'titulo1') ?>
<?= $form->field($model, 'titulo2') ?>
<?= $form->field($model, 'autor') ?>
<?= $form->field($model, 'director') ?>
<?= $form->field($model, 'resumen1') ?>
<?= $form->field($model, 'resumen2') ?>
<?= $form->field($model, 'idioma') ?>
<?= $form->field($model, 'anyo') ?>
<?= $form->field($model, 'notas') ?>
<?= $form->field($model, 'palClave') ?>
<?= $form->field($model, 'estudio') ?>
<?= $form->field($model, 'centro') ?>
<?= $form->field($model, 'departamento') ?>
<?= $form->field($model, 'esEspecial') ?>
<?= $form->field($model, 'licencia') ?>

<div class="form-group">
        <?= Html::submitButton('Submit', ['class' => 'btn btn-primary']) ?>
</div>


<?php $form = ActiveForm::end(); ?>

 

Esta vista utiliza el widget ActiveForm para generar el formulario HTML. Los métodos begin() y end() del widget generan las etiquetas de apertura y cierre del formulario respectivamente. Entre las llamadas a los dos métodos se crean campos con la llamada al método field(). Después de los dos campos, llamamos al método yii\helpers\Html::submitButton() para generar el botón de envío.

Ahora ya podemos ir a http://unizaryii.docker.localhost:8000/nuevo-trabajo y vemos el formulario para rellenar:
Ejemplo de formulario Yii2

Como vemos, las etiquetas (labels) del formulario tienen el valor con el que hemos llamado el campo en el modelo. ¿Y si deseamos cambiarlas a algo más legible?

Una forma de conseguirlo sería editar la vista views/site/entry.php tal que así:

...
<!-- ANTES  -->
<!-- 
     <?= $form->field($model, 'titulo1') ?>
-->
<!-- DESPUES -->
     <?= $form->field($model, 'titulo1')->textInput()->label('Título idioma principal') ?-->
...

Y ahora el formulario se renderiza con este nuevo valor de la etiqueta y se vería así:
cambio label de formulario en la vista en Yii2

De esta forma únicamente cambiaremos el label en esa vista. Podemos hacerlo de otra forma, que sería más elegante, modificando el modelo. De esta forma en todas las vistas que usen ese modelo tendríamos Para ello tendríamos que editar models/DatosTrabajo.php y añadir una nueva función attributeLabels() tal que así:

    public function attributeLabels()
    {
        return[
            'titulo1' => 'Título (idioma principal)',
            'titulo2' => 'Título (idioma secundario)',
            'autor' => 'Autor',
            'director' => 'Director',
            'resumen1' => 'Resumen (idioma principal)',
            'resumen2' => 'Resumen (idioma secundario)',
            'idioma' => 'Idioma',
            'anyo' => 'Año',
            'notas' => 'Notas',
            'palClave' => 'Palabra clave',
            'estudio' => 'Grado / Máster',
            'centro' => 'Centro',
            'departamento' => 'Departamento',
            'esEspecial' => '¿Es un trabajo especial? (Incluye materiales físicos adicionales)',
            'licencia' => 'Licencia',
        ];
    }

Más adelante veremos cómo preparar la aplicación para multi-idioma y tendremos que realizar algunos cambios adicionales.

De momento el formulario quedaría así:

Curso Yii2- modificando form labels en el modelo de datos

A continuación vamos a introducir un pequeño cambio más. Vamos a validar el modelo para que los campos titulo1, titulo2 y autor deban tener una longitud mínima.

Modificaremos la función rules() en models/DatosTrabajo.php con una nueva línea:

    public function rules()
    {
        return [
            [['titulo1', 'autor', 'email'], 'required'],    // titulo, autor e email son obligatorios
            ['email', 'email'],                             // email debe ser una dirección de correo sintáticamente correcta
            // comprobar que son strings de longitud entre 10 y 100
            [['titulo1', 'titulo2','autor'], 'string', 'length' => [10, 100]],
        ];
    }

Ahora si intentamos introducir una cadena de longitud inadecuada, se quejará:
errores de validación en formulario yii2

En el próximo post veremos cómo añadir una Base de datos que nos permitirá almacenar y mostrar los trabajos enviados.

Almacenando nuestros avances en un sistema de control de versiones Git

Emplearemos github para almacenar nuestro proyecto.

Para ello creamos un nuevo proyecto en github.com/new. Lo inicializaremos sin ficheros .gitignore ni README (los añadiremos más tarde). En mi caso creo https://github.com/miguel-martin/yii2deposito

Después en nuestro ordenador, nos iremos a la raiz de nuestro proyecto (ojo, NO a la carpeta web, sino al raíz (en mi caso /Users/miguelm/Sites/testyii2/docker4php/demo) y allí añadimos el remoto:

# ve al raiz de tu proyecto Yii2
cd /Users/miguelm/Sites/testyii2/docker4php/demo 

# incializar git
git init

# añadir el origen remoto 
git remote add origin https://github.com/miguel-martin/yii2deposito.git

Antes de realizar nuestro primer commit, nos aseguramos de que tengamos los ficheros README.md y .gitignore en local.

Mi fichero .gitignore es así:

# phpstorm project files
.idea

# netbeans project files
nbproject

# zend studio for eclipse project files
.buildpath
.project
.settings

# windows thumbnail cache
Thumbs.db

# composer vendor dir
/vendor

# composer itself is not needed
composer.phar

# Mac DS_Store Files
.DS_Store

# phpunit itself is not needed
phpunit.phar
# local phpunit config
/phpunit.xml

tests/_output/*
tests/_support/_generated

#vagrant folder
/.vagrant

Y como fichero README.md tengo:

Yii2 - Deposito
===============

Este proyecto es un ejemplo sencillo de aplicación escrita en Yii2 para conocer las posibilidades de este Framework.
Para poder seguir bien el tutorial deberías tener unos mínimos conocimientos de PHP.


Descripción
-----------

Puedes ir siguiendo el ejemplo en [leccionespracticas.com](https://www.leccionespracticas.com)
+ [Preparación del entorno de desarrollo con Dockers](http://www.leccionespracticas.com/?p=2594)
+ [Primeros pasos con Yii2](http://www.leccionespracticas.com/?p=2604) 
+ [Formulario y control de versiones](http://www.leccionespracticas.com/?p=2634)

Ahora ya lo tenemos preparado y podemos hacer nuestro primer commit:

# añadir todos los ficheros
git add .

# creamos nuestro primer commit
git commit -m "Creación de la base. +info en http://www.leccionespracticas.com/?p=2634"

# hacer un push de la rama "master" al remoto "origin" 
# y "set up" tracking (así en el futuro solo tendremos que hacer git push y git pull 
# +info aqui https://mislav.net/2010/07/git-tips/)
git push -u origin master

Entorno para desarrollo Drupal 8 en Mac: Docker con Apache + MariaDB + PHP + …

Vamos a ver cómo crear un entorno de desarrollo para Drupal 8 en OSX utilizando docker-compose. Con unos sencillos pasos conseguiremos tener montado de forma muy rápida un entorno completo que cuente con todo lo necesario para empezar a desarrollar en Drupal 8. Lo que vamos a tener será:
– MariaDB
– PHP-FPM
– Apache
– PhpMyAdmin
– Traefik
– Portainer

INSTALACIÓN DE DOCKER.

La instalación es muy sencilla. En linux ya viene preinstalado. En Mac se instala de forma muy simple, basta con seguir las instrucciones de la documentación oficial: https://docs.docker.com/docker-for-mac/install/#install-and-run-docker-for-mac

INSTALACIÓN DE dockerfordrupal

Lo primero será crear una cuenta en Docker Hub (repositorio de imgs Docker). Después abrimos Terminal y nos loggeamos:

docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: AQUITULOGIN
Password: AQUITUPASSWORD
Login Succeeded

Vamos a aprovechar el trabajo realiazdo por Wodby y clonamos su repo dockerfordrupal.

Para ello:

git clone https://github.com/wodby/docker4drupal
cd dockerfordrupal

En el directorio veremos varios archivos:

docker-compose.override.yml  docker.mk        LICENSE.md  README.md  traefik.yml
docker-compose.yml           docker-sync.yml  Makefile    tests

Vamos a modificar el fichero .env para darle nombre a nuestro proyecto (en mi caso unizar) e indicarle la base url. Para eso editaremos estas líneas.

PROJECT_NAME=unizar
PROJECT_BASE_URL=unizar.docker.localhost

El archivo docker-compose.override.yml de momento no vamos a usarlo. Es útil cuando se tienen varios entornos (desarrollo/producción) con distintos settings. En este caso lo vamos a renombrar porque editaremos directamente docker-compose.yml:

mv docker-compose.override.yml docker-compose.override.yml.back

Lo siguiente será eliminar todo el contenido de docker-compose.yml y reemplazarlo por el siguiente:

version: "3"
  
services:
  mariadb:
    image: wodby/mariadb:$MARIADB_TAG
    container_name: "${PROJECT_NAME}_mariadb"
    stop_grace_period: 30s
    environment:
      MYSQL_ROOT_PASSWORD: $DB_ROOT_PASSWORD
      MYSQL_DATABASE: $DB_NAME
      MYSQL_USER: $DB_USER
      MYSQL_PASSWORD: $DB_PASSWORD

  php:
    image: wodby/drupal:$DRUPAL_TAG
    container_name: "${PROJECT_NAME}_php"
    environment:
      PHP_SENDMAIL_PATH: /usr/sbin/sendmail -t -i -S mailhog:1025
      DB_HOST: $DB_HOST
      DB_USER: $DB_USER
      DB_PASSWORD: $DB_PASSWORD
      DB_NAME: $DB_NAME
      DB_DRIVER: $DB_DRIVER
      COLUMNS: 80 # Set 80 columns for docker exec -it.
## Read instructions at https://wodby.com/stacks/drupal/docs/local/xdebug/
      PHP_XDEBUG: 1
      PHP_XDEBUG_DEFAULT_ENABLE: 1
      PHP_XDEBUG_REMOTE_CONNECT_BACK: 1
      PHP_FPM_CLEAR_ENV: "no"
      #PHP_IDE_CONFIG: serverName=my-ide
      #PHP_XDEBUG_REMOTE_HOST: host.docker.internal # Docker 18.03+ & Linux/Mac/Win
      PHP_XDEBUG_REMOTE_HOST: 172.17.0.1 # Linux, Docker < 18.03
    volumes:
      - ./code:/var/www/html


  apache:
    image: wodby/apache:$APACHE_TAG
    container_name: "${PROJECT_NAME}_apache"
    depends_on:
      - php
    environment:
      APACHE_LOG_LEVEL: debug
      APACHE_BACKEND_HOST: php
      APACHE_VHOST_PRESET: php
      APACHE_DOCUMENT_ROOT: /var/www/html/web
    volumes:
      - ./code/:/var/www/html
# For macOS users (https://wodby.com/stacks/drupal/docs/local/docker-for-mac/)
#      - ./:/var/www/html:cached # User-guided caching
#      - docker-sync:/var/www/html # Docker-sync
    labels:
      - 'traefik.backend=${PROJECT_NAME}_apache'
      - 'traefik.port=80'
      - 'traefik.frontend.rule=Host:${PROJECT_BASE_URL}'


  pma:
    image: phpmyadmin/phpmyadmin
    container_name: "${PROJECT_NAME}_pma"
    environment:
      PMA_HOST: $DB_HOST
      PMA_USER: $DB_USER
      PMA_PASSWORD: $DB_PASSWORD
      PHP_UPLOAD_MAX_FILESIZE: 1G
      PHP_MAX_INPUT_VARS: 1G
    labels:
      - 'traefik.backend=${PROJECT_NAME}_pma'
      - 'traefik.port=80'
      - 'traefik.frontend.rule=Host:pma.${PROJECT_BASE_URL}'

  portainer:
    image: portainer/portainer
    container_name: "${PROJECT_NAME}_portainer"
    command: --no-auth -H unix:///var/run/docker.sock
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    labels:
      - 'traefik.backend=${PROJECT_NAME}_portainer'
      - 'traefik.port=9000'
      - 'traefik.frontend.rule=Host:portainer.${PROJECT_BASE_URL}'

  traefik:
    image: traefik
    container_name: "${PROJECT_NAME}_traefik"
    command: -c /dev/null --web --docker --logLevel=INFO
    ports:
      - '8000:80'
      - '8080:8080' # Dashboard
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Compilamos y construimos:

docker-compose up -d

En este momento ya podemos ver los contenedores, para ello empleamos el comando "docker-compose ps"

docker-compose ps
      Name                    Command               State                      Ports
--------------------------------------------------------------------------------------------------------
unizar_apache      /docker-entrypoint.sh sudo ...   Up      80/tcp
unizar_mariadb     /docker-entrypoint.sh mysqld     Up      3306/tcp
unizar_php         /docker-entrypoint.sh sudo ...   Up      9000/tcp
unizar_pma         /run.sh supervisord -n           Up      80/tcp, 9000/tcp
unizar_portainer   /portainer --no-auth -H un ...   Up      9000/tcp
unizar_traefik     /traefik -c /dev/null --we ...   Up      0.0.0.0:8000->80/tcp, 0.0.0.0:8080->8080/tcp

Si queremos pararlos:

docker-compose stop

Y para arrancarlos:

docker-compose start

Para ver logs:

docker-compose logs

Lo siguiente será editar (como root) el fichero /etc/hosts para indicarle algunos nombres que nos irán bien. Para ello debemos tener presente lo que escribimos anteriormente en el fichero .env. En mi caso sería así:

# /etc/hosts
127.0.0.1    unizar.docker.localhost
127.0.0.1    pma.unizar.docker.localhost
127.0.0.1    portainer.unizar.docker.localhost 

Llegados a este punto ya podremos abrir un navegador y acceder a varias direcciones:
http://unizar.docker.localhost:8000 -> para comenzar la instalación de Drupal, una vez traido el source
http://pma.unizar.docker.localhost:8000 -> para acceder a PhpMyAdmin
http://portainer.unizar.docker.localhost:8000 -> para tener una visión web de los contenedores Dockers.

Si lo que queremos es ENTRAR a la máquina host, podemos hacerlo asi:

docker-compose ps # nos da la lista de dockers que están corriendo

docker exec -it unizar_php bash # ejecutar "bash" dentro del docker llamado "unizar_php" # así mantiene la máquina "viva"

Woocommerce: cómo añadir NIF a pedidos y facturas [2017] (RESUELTO)

Añadir NIF a pedidos y facturas WooCommerce

En España es necesario incluir el NIF o DNI en la factura realizada a un particular para que dicha factura sea válida. WooCommerce, el plugin que amplía la funcionalidad de WordPress para incluir una tienda online no contempla este dato por defecto.

Woocommerce también tiene otra deficiencia, y es que no dispone de la funcionalidad de generación de facturas automáticas en su núcleo.

Vamos a solucionar ambos aspectos. Para ello instala y activa estos tres plugins

1. Woocommerce Checkout manager
2. Woocommerce PDF Invoices & Packing Slips
3. WooCommerce Store Toolkit

Configurar Woocommerce Checkout Manager para añadir el NIF/CIF

Vamos a usar este plugin para añadir el campo CIF/NIF a nuestros datos de facturación (billing). Para ello vamos a las opciones del plugin (/wp-admin/admin.php?page=woocommerce-checkout-manager) y en la pestaña Billing, añadimos el nuevo campo. Quedará algo asi:

añadir nif a facturas woocommerce

A partir de este momento, el sistema pedirá al cliente que inserte su CIF/NIF en el proceso de compra.

Configurar Woocommerce PDF para añadir el NIF/CIF

El siguiente paso es configurar este plugin para que nos permita emitir facturas en pdf, enviárselas al cliente y además refleje el nuevo campo NIF/CIF que hemos creado en el paso anterior.

El primer paso consiste en copiar las plantillas del plugin a nuestro theme para crear plantillas custom. De esta forma podremos crear unas plantillas para facturas a medida y no perderlas al actualizar el plugin. Para ello copiamos /wp-content/plugins/woocommerce-pdf-invoices-packing-slips/templates/pdf/Simple a nuestro theme /wp-content/themes/MY_THEME/woocommerce/pdf/FacturaConCIF

woocommerce factura pdf con cif

Después personalizaremos nuestro archivo invoice.php (el que genera la factura en PDF) para mostrar el nuevo campo CIF/NIF.

Para averiguar cómo se llama el nuevo campo, podemos instalar y activar WooCommerce Store Toolkit y de esta forma, en la pantalla del pedido, veremos al final todas las variables personalizadas y sus valores. Algo asi:

En mi caso el campo (cuya abreviatura era myfield12) se puede recuperar con el id _billing_myfield12

Por tanto vamos a personalizar la plantilla haciendo que salga este campo también, añadiendo la siguiente línea:

 <br /><?php $wpo_wcpdf->custom_field('_billing_myfield12', 'NIF/CIF:'); ?>

El archivo /wp-content/themes/MY_THEME/woocommerce/pdf/FacturaConCIF/invoice.php, que quedará asi:

El último paso es indicarle al plugin que use nuestra plantilla custom. Para ello vamos a Woocommerce > Facturas PDF o ruta /wp-admin/admin.php?page=wpo_wcpdf_options_page&tab=template y en el desplegable, elegimos nuestra plantilla FacturaConCIF:
cif en facturas woocommerce

¡Listo!

WooCommerce: Mostrar solo envío gratuito si está disponible

En Woocommerce 2.6 ha habido cambios importantes en la parte de los Envíos. Si tenías una versión anterior, verás que al acceder a la configuración de envíos, aparecen “Tarifa plana (heredado)” y “Envío gratuito (heredado)”. Hay que definir los nuevos métodos de envío mediante las “Shipping zones”. Lo explican muy bien aqui: http://www.tiendaonlinemurcia.es/woocommerce-versiones-novedades/

Imagina que configuras un envío gratuito para pedidos de > 120€ y una tarifa plana de 5€ para el resto de envíos. Entonces, verás que el cliente puede elegir el método de envío gratuito y el envío de 5€. Realmente solo debería aparecer el envío gratuito, porque el cliente tiene derecho a él, y no el envío por tarifa estándar, que puede dar lugar a confusión.

¿Cómo se consigue ésto? Muy fácil,

Primero borra las cachés de WooCommerce: WooCommerce > System Status > Tools > WC Transients > Clear transients.

Y después mete estas líneas al final del archivo functions.php de tu theme:

/**
 * Hide shipping rates when free shipping is available.
 * Updated to support WooCommerce 2.6 Shipping Zones.
 *
 * @param array $rates Array of rates found for the package.
 * @return array
 */
function my_hide_shipping_when_free_is_available( $rates ) {
	$free = array();
	foreach ( $rates as $rate_id => $rate ) {
		if ( 'free_shipping' === $rate->method_id ) {
			$free[ $rate_id ] = $rate;
			break;
		}
	}
	return ! empty( $free ) ? $free : $rates;
}
add_filter( 'woocommerce_package_rates', 'my_hide_shipping_when_free_is_available', 100 );

Magento 1.9: Extensión Wishlist admin email, traducir a español y arreglar precios

Puede que nos interese conocer qué productos han añadido nuestros clientes a su wishlist. Para ello, existe una extensión (gratuita) que funciona con Magento 1.9, llamada Wishlist Admin Email.

Esta extensión funciona muy bien, pero tiene dos pegas:
1) Cuando envía el email de la wishlist, el precio que muestra en los productos es el precio SIN IMPUESTOS. Para corregirlo y que muestre el precio con impuestos, debemos editar el fichero app/design/frontend/base/default/template/me/wlae/email/items.phtml y modificar el cálculo del precio.

Reemplazar:

<?php echo $coreHelper->formatPrice($_product->getFinalPrice()); ?>

Por

<!-- modifico para que salga el precio con impuestos -->
<!--<?php echo $coreHelper->formatPrice($_product->getFinalPrice()); ?>-->
<?php 
$_priceIncludingTax = Mage::helper('tax')->getPrice($_product, $_product->getFinalPrice());
echo $_priceIncludingTax;
?>

2) El email se envía en inglés. Aunque no es muy trascendente, puesto que solo el administrador recibe estos emails (y NO los reciben clientes) es interesante saber cómo traducir la plantilla.

Para añadir el fichero de traducciones, vamos a app/locale/es_ES y subimos el fichero con las traducciones (Me_Wlae.csv) con el siguiente contenido:

"Wishlist Admin Email","Wishlist Admin Email"
"Basic","Basico"
"Enable Extension","Habilitar extension"
"Enable Extension.","Deshabilitar extension."
"Yes","Si"
"No","No"
"Email Options","Opciones de email"
"Email Wishlist Items","Enviar items de wishlist"
"Send Only Newly Added","Enviar solo los nuevos"
"Send Entire Wishlist","Enviar wishlist completa"
"Please select to send only the recently added item, or the whole wishlist. In the second option the newly added item will be marked.","Por favor seleccione si enviar solo los items nuevos o la wishlist completa. En la segunda opcion se marcaran los productos nuevos."
"Send Emails To","Enviar emails a"
"Please enter the email address where notification will be sent.","Introduzca la direccion de email a la que llegaran las notificaciones."
"Send BCC Emails To","Enviar BCC a"
"Please enter the bcc copy email address where notification will be also sent.","Por favor introduce la direccion de email a la que se enviara copia de la notificacion."
"Email Sender","Email Sender"
"Please select the email sender from store email addresses.","Por favor, seleccione un sender de la lista de emails."
"Email Template","Plantilla Email"
"Please select the custom email template or use the default.","Por favor seleccione la plantilla custom de email o utilice la defecto."
"Email Items Setting","Ajustes basicos de email"
"Email Basic Setting","Ajuste basico email"

Asi nos quedaría, ya traducido, el panel de Admin del módulo (System > Config > Admin Wishlist Email)

magevolve-wishlist admin email traducido

Además hay que modificar el fichero app/locale/es_ES/template/email/me_wlae_notification.html y ponerle éste contenido:

<!--@subject {{var store.getFrontendName()}}: Wishlist Admin Notification @-->
<!--@vars
{"store url=\"\"":"Store Url",
"var logo_url":"Email Logo Image Url",
"var logo_alt":"Email Logo Image Alt",
"var customer_name":"Customer Name",
"var customer_email":"Customer Email",
"var items":"Items",
"var store.getFrontendName()":"Store Name"}
@-->
<!--@styles
body,td { color:#2f2f2f; font:11px/1.35em Verdana, Arial, Helvetica, sans-serif; }
@-->
 
<body style="background:#F6F6F6; font-family:Verdana, Arial, Helvetica, sans-serif; font-size:12px; margin:0; padding:0;">
<div style="background:#F6F6F6; font-family:Verdana, Arial, Helvetica, sans-serif; font-size:12px; margin:0; padding:0;">
    <table cellspacing="0" cellpadding="0" border="0" height="100%" width="100%">
        <tr>
            <td align="center" valign="top" style="padding:20px 0 20px 0">
                <!-- [ header starts here] -->
                <table bgcolor="#FFFFFF" cellspacing="0" cellpadding="10" border="0" width="650" style="border:1px solid #E0E0E0;">
                    <tr>
                        <td valign="top"><a href="{{store url=""}}"><img src="{{var logo_url}}" alt="{{var logo_alt}}" style="margin-bottom:10px;" border="0"/></a></td>
                    </tr>
                    <!-- [ middle starts here] -->
                    <tr>
                        <td valign="top">
                            <h1 style="font-size:22px; font-weight:normal; line-height:22px; margin:0 0 11px 0;">Notificación de wishlist</h1>
                            <p style="font-size:12px; line-height:16px; margin:0 0 16px 0;">Nuevo producto en wishlist...</p>
                            <p style="font-size:12px; line-height:16px; margin:0 0 16px 0;">
                                <strong>Detalles:</strong><br /><br />
                                Nombre cliente: {{var customer_name}}<br />
                                Email cliente: {{var customer_email}}
                            </p>
                            {{var items}}
                        </td>
                    </tr>
                    <tr>
                        <td bgcolor="#EAEAEA" align="center" style="background:#EAEAEA; text-align:center;"><center><p style="font-size:12px; margin:0;"><strong>{{var store.getFrontendName()}}</strong></p></center></td>
                    </tr>
                </table>
            </td>
        </tr>
    </table>
</div>
</body>

Después vaciamos la cache y ¡listo!

Asi quedaría el email que recibe el admin…
magevolve admin wishlist email including taxes

[SOLVED] Magento 1.9 BUG FIX Cron email alerts sent in wrong language

I found a Magento bug which makes cron (price, stock) alerts emails being in the wrong language.

Issue

I have configured price alerts for a client (which originally registered into my shop with spanish language).

The user receives the email, but it is in english.

For instance, the text: “You are receiving this notification because you subscribed to receive alerts when the prices for the following products changed:” (in ./app/design/frontend/base/default/template/email/productalert/price.phtml) is not being translated to spanish.

I have checked and the translation is present in ./app/locale/es_ES/Mage_ProductAlert.csv: “You are receiving this notification because you subscribed to receive alerts when the prices for the following products changed:”,”Usted está recibiendo esta notificación porque está suscrito para recibir alertas cuando los precios de los siguientes productos cambien:”

I am using a custom theme.

I have also tried to add that translation to my theme: app/design/frontend/mytheme/default/locale/es_ES/translate.csv

I have flushed translations cache and deleted Magento cache. Still not being translated.

Fix

What I did to fix the issue:

Edit app/code/core/Mage/ProductAlert/Model/Email.php

Replace this (line 255):

       $appEmulation = Mage::getSingleton('core/app_emulation');
       $initialEnvironmentInfo = $appEmulation->startEnvironmentEmulation($storeId);

With this:

        $appEmulation = Mage::getSingleton('core/app_emulation');
        $initialEnvironmentInfo = $appEmulation->startEnvironmentEmulation($storeId);
        //Dealing with uninitialized translator!
        Mage::app()->getTranslator()->init('frontend', true);

Flushed the cache and voila! Now magento is sending the email alerts in the right language 🙂

Magento 1.9: Alertas de reposición de stock y alertas de precio

Una de las funcionalidades que ofrece Magento es que el usuario pueda suscribirse a alertas en productos. Bien sean alertas de modificación de precio (“productalert/price”) o de reposición de stocks (“productalert/stock”).

Para activar dicha funcionalidad, hay que ir a Sistema > Configuración > Catálogo > Catálogo:

alertas magento precio y stock

Y los productos (de los productos configurables hablaremos más adelante) pasan a mostrar dicha alerta:
alertas_magento_stock_price-23

Para ello, el theme debe llamar a:

<?php echo $this->getChildHtml('alert_urls') ?>

Y la plantilla por defecto que se ejecuta es productalert/product/view.phtml:

<?php /* @var $this Mage_ProductAlert_Block_Product_View */?>
<h1>Hello</h1>
<p class="<?php echo $this->getHtmlClass() ?>">
    <a href="<?php echo $this->escapeHtml($this->getSignupUrl()) ?>" title="<?php echo $this->escapeHtml($this->__($this->getSignupLabel())); ?>"><?php echo $this->escapeHtml($this->__($this->getSignupLabel())); ?></a>
</p>

En layout/productalert.xml encontramos la función a la que se llama (preparePriceAlertData o prepareStockAlertData):

<layout version="0.1.0">
    <catalog_product_view>
        <reference name="content">
            <reference name="product.info">
                <reference name="alert.urls">
                    <block type="productalert/product_view" name="productalert.price" as="productalert_price" template="productalert/product/view.phtml">
                        <action method="preparePriceAlertData"/>
                        <action method="setHtmlClass"><value>alert-price link-price-alert</value></action>
                        <action method="setSignupLabel" translate="value"><value>Sign up for price alert</value></action>
                    </block>
                    <block type="productalert/product_view" name="productalert.stock" as="productalert_stock" template="productalert/product/view.phtml">
                        <action method="prepareStockAlertData"/>
                        <action method="setHtmlClass"><value>alert-stock link-stock-alert</value></action>
                        <action method="setSignupLabel" translate="value"><value>Sign up to get notified when this product is back in stock</value></action>
                    </block>
                </reference>
            </reference>
        </reference>
    </catalog_product_view>
</layout>

Además, desde cada producto (Catálogo > Administrar producto), se puede ver quién se ha suscrito a cada alerta, en la pestaña “Alertas de producto”:

magento alerts

Estas alertas se almacenan internamente en la tabla product_alert_* (una para precio, otra para stock).

Si queremos obtener un informe con sumario, podemos hacer una query a las tablas anteriores o, si queremos verlo de forma gráfica, podemos instalar la extensión Amasty – Product Alert Reports (gratuita) https://amasty.com/product-alerts-reports.html y en Informes > Alerts dispondremos de la información de alertas a modo de sumario.

Para que los correos de alerta salgan, es necesario tener configurado CRON (+info aqui y aqui).

Hasta aqui, todo bien. Ahora, algunas dudas habituales:

¿Qué sucede si el cliente no está registrado y por tanto no se dispone de su email?
Si el cliente no está registrado, por defecto Magento le pide que se identifique para poder usar las alertas. No muestra un input donde el cliente no registrado pueda introducir su email para suscribirse a la alerta…

¿Qué sucede con los productos configurables?
Imaginemos que vendemos camisetas en tallas S,M,L. El producto “camiseta roja” será un prod configurable y los productos simples tendrán la variante de la talla. Sin embargo, Magento por defecto NO muestra el aviso de “Sign up to get notified when product is back in stock” en los productos configurables, aunque alguno de los productos simples asociados no esté en stock.

¿Puede el cliente registrado acceder desde su panel a la lista de suscripciones de stock y precio?
No, por defecto no puede. El código para mostrar las alertas de cada cliente sería algo asi:

/* LIST STOCK ALERTS */
$customer_id = Mage::getSingleton('customer/session')->getCustomer()->getId();
$customer_stock_alerts = Mage::getModel('productalert/stock')
         ->getCollection()
         ->addFieldToFilter('customer_id', $customer_id);
<p>foreach ( $customer_stock_alerts as $alert )
{
 echo $alert->getProductId();
}
 
/* LIST PRICE ALERTS */
$customer_id = Mage::getSingleton('customer/session')->getCustomer()->getId();
$customer_stock_alerts = Mage::getModel('productalert/price')
         ->getCollection()
         ->addFieldToFilter('customer_id', $customer_id);
<p>foreach ( $customer_stock_alerts as $alert )
{
 echo $alert->getProductId();
}

¿Cómo solucionar estas cuestiones, si no queremos ponernos a programar? Podemos comprar e instalar el módulo Amasty – Out of stock notification, que da solución a estas cuestiones: https://amasty.com/out-of-stock-notification.html

+info:
https://stackoverflow.com/questions/11648920/magento-show-sign-up-to-get-notified-when-this-product-is-back-in-stock-link-f
http://magento.stackexchange.com/questions/10989/cant-see-alert-urls-block-template
http://stackoverflow.com/questions/10810960/show-out-of-stock-for-configurable-products-in-magento
http://magento.stackexchange.com/questions/470/configurable-product-inventory-stock-availability-status-doesnt-change-when
http://stackoverflow.com/questions/5290868/how-would-test-cron-with-magento
http://merch.docs.magento.com/ee/user_guide/content/catalog/inventory-product-alert-run-settings.html
http://www.megamagento.com/2013/11/magento-product-stock-back-alerts-out.html

Magento 1.9 – mostrar productos sin existencias

Imaginemos un producto configurable (“Camiseta”) con varios productos simples asociados, de diversas tallas (“Camiseta-M”, “Camiseta-XL”). Imaginemos que tenemos 5 unidades de Camiseta-M pero ninguna de Camiseta-L.

Por defecto, si tenemos marcada en Magento la opción de configuración de no mostrar productos sin stock, Magento solo mostrará en el desplegable de tallas las tallas para las que haya stock.

¿Cómo mostrar las tallas para las que no hay stock?

Hay dos posibles soluciones:

1) Por código

: crear un nuevo módulo y en el config.xml del módulo añadir 2 eventos dentro del tag :

    <events>
        <controller_action_layout_render_before_catalog_product_view>
            <observers>
                <[namespace]_[module]>
                    <class>[module]/observer</class>
                    <method>showOutOfStock</method>
                </[namespace]_[module]>
            </observers>
        </controller_action_layout_render_before_catalog_product_view>
        <controller_action_layout_render_before_checkout_cart_configure>
            <observers>
                <[namespace]_[module]>
                    <class>[module]/observer</class>
                    <method>showOutOfStock</method>
                </[namespace]_[module]>
            </observers>
        </controller_action_layout_render_before_checkout_cart_configure>
    </events>

A continuación crear un observer en app/code/local/[Namespace]/[Module]/Model/Observer.php

class [Namespace]_[Module]_Model_Observer {
    public function showOutOfStock($observer){
        Mage::helper('catalog/product')->setSkipSaleableCheck(true);
    }
}

2) Solución por extensión (la que he aplicado yo)

– Descargar e instalar la extensión EasyLifeSwitcher para Magento: https://github.com/tzyganu/Switcher (mirror)
– Configurarla (más info aqui) para que permita mostrar y seleccionar prods simples sin stock

easylife_switcher

– Editar js/easylife_switcher/product.js y reemplazar:

/**
     * rewrite fillSelect to transform elements to labels
     * @param $super
     * @param element
     */
    fillSelect: function($super, element){
        $super(element);
        //if (this.config.transform_dropdowns){
        var transformed = this.transformDropdown(element);
        if (!transformed && !this.getConfigValue(this.config, 'allow_no_stock_select', false)) {
            var attributeId = element.id.replace(/[a-z]*/, '');
            var options = this.getAttributeOptions(attributeId);
            for (var i in options) {
                if (options.hasOwnProperty(i)){
                    var optVal = options[i].id;
                    var inStock = this.isInStock(attributeId, optVal);
                    $(element).select('option').each (function(elem){
                        if ($(elem).value == optVal && !inStock) {
                            $(elem).disabled="disabled";
                        }
                    });
                }
            }
        }
    },

Por:

/**
     * rewrite fillSelect to transform elements to labels
     * @param $super
     * @param element
     */
    fillSelect: function($super, element){
        $super(element);
        //if (this.config.transform_dropdowns){
        var transformed = this.transformDropdown(element);
        if (!transformed && !this.getConfigValue(this.config, 'allow_no_stock_select', false)) {
            var attributeId = element.id.replace(/[a-z]*/, '');
            var options = this.getAttributeOptions(attributeId);
            for (var i in options) {
                if (options.hasOwnProperty(i)){
                    var optVal = options[i].id;
                    var inStock = this.isInStock(attributeId, optVal);
                    $(element).select('option').each (function(elem){
                        if ($(elem).value == optVal && !inStock) {
                            $(elem).disabled="disabled";
                            // este evento solo se dispara si allow_no_stock_select=0 (NO Permitir seleccionar productos sin existencias)
                            //alert("debug product.js");
                        }
                    });
                }
            }
        }
        // para añadir etiqueta "sin stock" - esto se ejecuta solo si allow_no_stock_select=1 (Permitir seleccionar productos sin existencias)
        if (!transformed && this.getConfigValue(this.config, 'allow_no_stock_select', false)) {
            var attributeId = element.id.replace(/[a-z]*/, '');
            var options = this.getAttributeOptions(attributeId);
            for (var i in options) {
                if (options.hasOwnProperty(i)){
                    var optVal = options[i].id;
                    var inStock = this.isInStock(attributeId, optVal);
                    $(element).select('option').each (function(elem){
                        if ($(elem).value == optVal && !inStock) {
                            //$(elem).label = "Sin stock";
                            $(elem).classList.add("sin-stock-actual");
                        }
                    });
                }
            }
        } // HASTA AQUI ACERTIUS NUEVO
    },

– Editar nuestro archivo de estilos y añadir las reglas:

 /* para mostrar en rojo los productos sin stock */
 .sin-stock-actual{ color: red; }
 .sin-stock-actual::after{content: " - ¡Existencias agotadas!"; }

De esta forma nuestros desplegables de tallas mostrarán todas las tallas:

easylife_switcher_custom_css

[RESUELTO] Cómo cargar varios ficheros Javascript desde uno único

¿Cómo cargar varios archivos JS desde un JS único y ejecutar funciones de dentro?

Se puede hacer usando la llamada jQuery getScript, asi:

$.when(
    $.getScript( "/mypath/myscript1.js" ),
    $.getScript( "/mypath/myscript2.js" ),
    $.getScript( "/mypath/myscript3.js" ),
    $.Deferred(function( deferred ){    // esperar a que el DOM esté listo
        $( deferred.resolve );
    })
    ).done(function(){
          //poner aqui tu código. todos los scripts anteriores estarán cargados y listos para usarse
    });

Si no te funciona, asegúrate de que no hay errores de parsing en tus ficheros JS (prueba primero que se carguen bien metiéndolos en un tag <script>

jQuery.getScript() considerará errores de parsing un load fallido y se llamará fail() en vez de done()