Altering your media files’ creation date

On my last vacation, I have shot a bunch of videos with two cameras. The problem was that camera #1 time was set to one hour later than camera #2. So in order for the videos to show up correctly in my video editing software, I needed to fix that.

I use OSX and could not find a quick way to do this. If you have XCode installed, this will come with SetFile, a command line utility that can change file creation dates. Note that SetFile can change either the file creation date, or the file modification date, but not both at the same time. Also note that SetFile expects dates in American notation…

Anyways, here’s a small Python script that changes the file creation time of a bunch of video files (which names contain string “P14”):

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

import os
import datetime

def modify_file_creation_date(path='.'):
    """
    Alter the creation and modification time (set it one hour earlier) of all files named *P14* in path
    """

    files = []
    # r=root, d=directories, f = files
    for r, d, f in os.walk(path):
        for file in f:
            if "P14" in file:

            	# get creation date and time
            	date = 	(os.path.getctime(file))

                # create a new date, just one hour earlier
                new_date = datetime.datetime.fromtimestamp(date) - datetime.timedelta(minutes=60)

                print("File %s - ORIGINAL DATETIME: %s - NEW DATETIME: %s\n" %(file, datetime.datetime.fromtimestamp(date), new_date	))
    
                # set the file creation date with the "-d" switch, which presumably stands for "dodification"
                os.system('SetFile -d "{}" {}'.format(new_date.strftime('%m/%d/%Y %H:%M:%S'), file))
    
                # set the file modification date with the "-m" switch
                os.system('SetFile -m "{}" {}'.format(new_date.strftime('%m/%d/%Y %H:%M:%S'), file))
    

modify_file_creation_date()

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

Curso gratis Yii2: Día 0, primeros pasos

Ahora que ya tenemos instalado y correctamente configurado nuestro entorno de desarrollo local con Dockers, vamos a comenzar a escribir una pequeña aplicación Yii2.

Para ello primero vamos a familiarizarnos con el Framework.

Creación de “hello world” en Yii2

Todas las peticiones que se reciben en Yii son recibidas por web/index.php.

En la carpeta controllers vemos los distintos controladores. Partiendo del ejemplo del post anterior, vemos que existe el fichero controllers/SiteController.php. Aqui añadiremos nuestra función. Los nombres de las funciones deben comenzar con action. Por ejemplo vamos a definir una ruta nueva en nuestra aplicación, que sea /di-algo y muestre un mensaje.

Nuestro primer controlador

Para ello editamos controllers/SiteController.php y añadimos nuestra nueva ruta:

    /**
     *
     * @return string
     */
    public function actionDiAlgo($message = 'Hola')
    {
        return $this->render('diAlgo', ['message' => $message]);
    }

Le hemos dicho que renderice usando la plantilla diAlgo y que le pase el mensaje indicado en $message (o, si no le digo nada, por defecto pondrá “Hola”).

Nuestra primera plantilla

Vamos a crear la plantilla a la que estamos llamando. Para ello creamos un nuevo fichero diAlgo.php en views/site/diAlgo.php con este contenido:

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

<?= Html::encode($message) ?>

Para evitar un posible ataque XSS no nos limitamos a mostrar el valor de $message, sino que lo pasamos por el helper Html::encode primero (que está definido en yii\helpers\Html).

Ahora podemos acceder a http://unizaryii.docker.localhost:8000/index.php?r=site%2Fdi-algo&message=Hola+mundo y veremos nuestro Hello World en acción.

Hola Mundo en Yii2

Añadiendo un nuevo enlace en la barra de menús

Vamos a añadir un nuevo enlace en nuestra barra de menú superior.

El layout del menú se controla en views/layout/main.php. Modificamos el código para añadir al array el nuevo link, que quedará así…

...
echo Nav::widget([
        'options' => ['class' => 'navbar-nav navbar-right'],
        'items' => [
            ['label' => 'Home', 'url' => ['/site/index']],
            ['label' => 'About', 'url' => ['/site/about']],
            ['label' => 'Contact', 'url' => ['/site/contact']],
            ['label' => 'Hola Mundo', 'url' => ['/site/di-algo']],
            Yii::$app->user->isGuest ? (
                ['label' => 'Login', 'url' => ['/site/login']]
            ) : (
                '

Y ya se muestra y funciona correctamente.

añadir item de menú yii2

Haciendo URLs pretty en Yii2

En la ruta del ejemplo que hemos construido (http://unizaryii.docker.localhost:8000/index.php?r=site%2Fdi-algo&message=Hola+mundo o, lo que es lo mismo, http://unizaryii.docker.localhost:8000/index.php?r=site/di-algo&message=Hola+mundo) vemos que la URL tiene el sufijo /index.php?r=site/di-algo

Esto se corresponde con el fichero que atiende las peticiones (index.php) seguido del nombre del controlador (site) y después la ruta definida en ese controlador (di-algo) y sus parámetros (&message=Hola+Mundo).

Veamos cómo hacerla más bonita. Para ello tu servidor Apache deberá tener habilitado mod_rewrite. Esto ya viene así por defecto en el entorno que creamos con Docker.

Creamos un archivo .htaccess en el webroot (osea, dentro de la carpeta web) con el siguiente contenido:

RewriteEngine on
# If a directory or a file exists, use it directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Otherwise forward it to index.php
RewriteRule . index.php

Y además modificamos config/web.php. La parte del urlManager quedará así:

'urlManager' => [
            'enablePrettyUrl' => true, // usar url's bonitas
            'showScriptName' => false, // ocultar index.php
            'rules' => [
                '<action:(.*)>' => 'site/',,
            ],
        ],

Y ahora ya podemos visitar la URL http://unizaryii.docker.localhost:8000/di-algo que es mucho más bonita:

pretty urls en yii2

Docker para desarrollo Yii2 en MacOS

En el post anterior vimos cómo crear un entorno de desarrollo para Drupal8 usando Docker.

Pero, ¿y si quiero usar otro framework de PHP, por ejemplo Yii2, en mi OSX? Es muy sencillo.

Instalación del DOCKER stack (docker4php)

docker4php miguelm$ cd Sites/testyii2
docker4php miguelm$ git clone https://github.com/wodby/docker4php
docker4php miguelm$ cd docker4php

Editar .env y dejarlo como sigue:

### Documentation available at https://docs.wodby.com/stacks/php/local
### Changelog can be found at https://github.com/wodby/docker4php/releases
### Images tags format explained at https://github.com/wodby/docker4php#images-tags

### PROJECT SETTINGS

PROJECT_NAME=unizaryii
PROJECT_BASE_URL=unizaryii.docker.localhost

DB_NAME=php
DB_USER=php
DB_PASSWORD=php
DB_ROOT_PASSWORD=password
DB_HOST=mariadb
DB_DRIVER=mysql

### --- PHP ----

#PHP_TAG=7.2-dev-4.8.2
#PHP_TAG=7.1-dev-4.8.2
#PHP_TAG=5.6-dev-4.8.2
PHP_TAG=7.2-dev-macos-4.8.2
#PHP_TAG=7.1-dev-macos-4.8.2
#PHP_TAG=5.6-dev-macos-4.8.2

### --- NGINX ----

NGINX_TAG=1.15-5.0.17
#NGINX_TAG=1.14-5.0.17

### --- NODE ---

NODE_TAG=10-0.9.0
#NODE_TAG=8-0.9.0
#NODE_TAG=6-0.9.0

### --- MARIADB ----

MARIADB_TAG=10.1-3.3.11
#MARIADB_TAG=10.2-3.3.11
#MARIADB_TAG=10.3-3.3.11

### --- POSTGRESQL ----

POSTGRES_TAG=11-1.5.0
#POSTGRES_TAG=10-1.5.0
#POSTGRES_TAG=9.6-1.5.0
#POSTGRES_TAG=9.5-1.5.0
#POSTGRES_TAG=9.4-1.5.0
#POSTGRES_TAG=9.3-1.5.0

### --- REDIS ---

REDIS_TAG=4-3.0.2
#REDIS_TAG=5-3.0.2

### --- ELASTICSEARCH ---

ELASTICSEARCH_TAG=6.3-3.0.2
#ELASTICSEARCH_TAG=6.2-3.0.2
#ELASTICSEARCH_TAG=6.1-3.0.2
#ELASTICSEARCH_TAG=6.0-3.0.2
#ELASTICSEARCH_TAG=5.6-3.0.2
#ELASTICSEARCH_TAG=5.5-3.0.2
#ELASTICSEARCH_TAG=5.4-3.0.2

### --- KIBANA ---

KIBANA_TAG=6.3-3.0.2
#KIBANA_TAG=6.2-3.0.2
#KIBANA_TAG=6.1-3.0.2
#KIBANA_TAG=6.0-3.0.2
#KIBANA_TAG=5.6-3.0.2
#KIBANA_TAG=5.5-3.0.2
#KIBANA_TAG=5.4-3.0.2

### --- SOLR ---

SOLR_TAG=7.4-3.0.6
#SOLR_TAG=7.3-3.0.6
#SOLR_TAG=7.2-3.0.6
#SOLR_TAG=7.1-3.0.6
#SOLR_TAG=6.6-3.0.6
#SOLR_TAG=5.5-3.0.6

### OTHERS

ADMINER_TAG=4.6-3.2.0
APACHE_TAG=2.4-4.0.2
ATHENAPDF_TAG=2.10.0
MEMCACHED_TAG=1-2.2.1
RSYSLOG_TAG=latest
VARNISH_TAG=4.1-3.0.10
WEBGRIND_TAG=1.5-1.6.2
OPENSMTPD_TAG=6.0-1.4.0
XHPROF_TAG=1.0.2

Compilamos y levantamos…

docker4php miguelm$ docker-compose up -d

En el fichero docker-compose.yml la variable NGINX_SERVER_ROOT tiene el valor /var/www/html/public
Así que creamos el directorio public y un fichero index.php para probar si todo ha ido bien…

docker4php miguelm$ mkdir public
docker4php miguelm$ echo "" > public/index.php

Lo siguiente será editar (como root) el fichero /etc/hosts de la máquina anfitrión y añadir algunas entradas que nos serán útiles:

127.0.0.1 unizaryii.docker.localhost
127.0.0.1 pma.unizaryii.docker.localhost

Y ya podemos ir a nuestro navegador en la máquina anfitrión: http://unizaryii.docker.localhost:8000/ y comprobar que todo funciona.

Instalación de Yii2 y creación de proyecto

Ahora que ya hemos comprobado que el setup funciona, vamos a instalar Yii2.

docker4php miguelm$ git clone https://github.com/yiisoft/yii2 _host-volumes/yii2
docker4php miguelm$ mv _host-volumes/yii2 .
docker4php miguelm$ rm -Rf _host-volumes

Y vamos a crear nuestro primer proyecto Yii2:

# eliminamos el directorio "public" completo
docker4php miguelm$ rm -Rf public
# creamos un nuevo proyecto en la carpeta "demo" 
docker4php miguelm$ composer create-project yiisoft/yii2-app-basic demo

Lo siguiente será editar el fichero docker-compose.yml para indicar el nuevo valor de NGINX_SERVER_ROOT, que deberá ser:

NGINX_SERVER_ROOT: /var/www/html/demo/web

Paramos y arrancamos los dockers con la nueva configuración:

docker4php miguelm$ docker-compose stop
docker4php miguelm$ docker-compose up -d

Ya tenemos nuestro proyecto Yii2 listo…

¿Y si en lugar de NGINX quiero utilizar Apache como servidor web?

Muy sencillo. Editamos el archivo docker-compose.yml y comentamos la sección de NGINX y descomentamos la sección de Apache. El fichero docker-compose.yml quedaría así:

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
#    volumes:
#      - ./mariadb-init:/docker-entrypoint-initdb.d # Place init .sql file(s) here.
#      - /path/to/mariadb/data/on/host:/var/lib/mysql # I want to manage volumes manually.

#  postgres:
#    image: wodby/postgres:$POSTGRES_TAG
#    container_name: "${PROJECT_NAME}_postgres"
#    stop_grace_period: 30s
#    environment:
#      POSTGRES_PASSWORD: $DB_PASSWORD
#      POSTGRES_DB: $DB_NAME
#      POSTGRES_USER: $DB_USER
#    volumes:
#      - ./postgres-init:/docker-entrypoint-initdb.d # Place init file(s) here.
#      - /path/to/postgres/data/on/host:/var/lib/postgresql/data # I want to manage volumes manually.

  php:
    image: wodby/php:$PHP_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
## Read instructions at https://wodby.com/stacks/php/docs/local/xdebug/
#      PHP_XDEBUG: 1
#      PHP_XDEBUG_DEFAULT_ENABLE: 1
#      PHP_XDEBUG_REMOTE_CONNECT_BACK: 0
#      PHP_IDE_CONFIG: serverName=my-ide
#      PHP_XDEBUG_REMOTE_HOST: 172.17.0.1 # Linux
#      PHP_XDEBUG_REMOTE_HOST: 10.254.254.254 # macOS
#      PHP_XDEBUG_REMOTE_HOST: 10.0.75.1 # Windows
    volumes:
      - ./:/var/www/html
## For macOS users (https://wodby.com/stacks/php/docs/local/docker-for-mac/)
#      - ./:/var/www/html:cached # User-guided caching
#      - docker-sync:/var/www/html # Docker-sync
## For XHProf and Xdebug profiler traces
#      - files:/mnt/files

#  nginx:
#    image: wodby/nginx:$NGINX_TAG
#    container_name: "${PROJECT_NAME}_nginx"
#    depends_on:
#      - php
#    environment:
#      NGINX_STATIC_OPEN_FILE_CACHE: "off"
#      NGINX_ERROR_LOG_LEVEL: debug
#      NGINX_BACKEND_HOST: php
#      NGINX_VHOST_PRESET: php
#      NGINX_SERVER_ROOT: /var/www/html/demo/web
#    volumes:
#      - ./:/var/www/html
# Options for macOS users (https://wodby.com/stacks/php/docs/local/docker-for-mac/)
#      - ./:/var/www/html:cached # User-guided caching
#      - docker-sync:/var/www/html # Docker-sync
#    labels:
#      - 'traefik.backend=${PROJECT_NAME}_nginx'
#      - 'traefik.port=80'
#      - 'traefik.frontend.rule=Host:${PROJECT_BASE_URL}'

  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/demo/web
    volumes:
      - ./:/var/www/html
## For macOS users (https://wodby.com/stacks/php/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}'

  mailhog:
    image: mailhog/mailhog
    container_name: "${PROJECT_NAME}_mailhog"
    labels:
      - 'traefik.backend=${PROJECT_NAME}_mailhog'
      - 'traefik.port=8025'
      - 'traefik.frontend.rule=Host:mailhog.${PROJECT_BASE_URL}'

#  varnish:
#    image: wodby/varnish:$VARNISH_TAG
#    container_name: "${PROJECT_NAME}_varnish"
#    depends_on:
#      - nginx
#    environment:
#      VARNISH_SECRET: secret
#      VARNISH_BACKEND_HOST: nginx
#      VARNISH_BACKEND_PORT: 80
#      VARNISH_PURGE_EXTERNAL_REQUEST_HEADER: X-Real-IP
#    labels:
#      - 'traefik.backend=${PROJECT_NAME}_varnish'
#      - 'traefik.port=6081'
#      - 'traefik.frontend.rule=Host:varnish.${PROJECT_BASE_URL}'

#  redis:
#    container_name: "${PROJECT_NAME}_redis"
#    image: wodby/redis:$REDIS_TAG

#  adminer:
#    container_name: "${PROJECT_NAME}_adminer"
#    image: wodby/adminer:$ADMINER_TAG
#    environment:
## For PostgreSQL:
##      ADMINER_DEFAULT_DB_DRIVER: pgsql
#      ADMINER_DEFAULT_DB_HOST: $DB_HOST
#      ADMINER_DEFAULT_DB_NAME: $DB_NAME
#    labels:
#      - 'traefik.backend=${PROJECT_NAME}_adminer'
#      - 'traefik.port=9000'
#      - 'traefik.frontend.rule=Host:adminer.${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}'

#  solr:
#    image: wodby/solr:$SOLR_TAG
#    container_name: "${PROJECT_NAME}_solr"
#    environment:
#      SOLR_HEAP: 1024m
#    labels:
#      - 'traefik.backend=${PROJECT_NAME}_solr'
#      - 'traefik.port=8983'
#      - 'traefik.frontend.rule=Host:solr.${PROJECT_BASE_URL}'

#  elasticsearch:
#    image: wodby/elasticsearch:$ELASTICSEARCH_TAG
#    environment:
#      ES_JAVA_OPTS: "-Xms500m -Xmx500m"
#    ulimits:
#      memlock:
#        soft: -1
#        hard: -1

#  kibana:
#    image: wodby/kibana:$KIBANA_TAG
#    depends_on:
#      - elasticsearch
#    labels:
#      - 'traefik.backend=${PROJECT_NAME}_kibana'
#      - 'traefik.port=5601'
#      - 'traefik.frontend.rule=Host:kibana.php.docker.localhost'

#  memcached:
#    container_name: "${PROJECT_NAME}_memcached"
#    image: wodby/memcached:$MEMCACHED_TAG

#  rsyslog:
#    container_name: "${PROJECT_NAME}_rsyslog"
#    image: wodby/rsyslog:$RSYSLOG_TAG

#  athenapdf:
#    image: arachnysdocker/athenapdf-service:$ATHENAPDF_TAG
#    container_name: "${PROJECT_NAME}_athenapdf"
#    environment:
#      WEAVER_AUTH_KEY: weaver-auth-key
#      WEAVER_ATHENA_CMD: "athenapdf -S"
#      WEAVER_MAX_WORKERS: 10
#      WEAVER_MAX_CONVERSION_QUEUE: 50
#      WEAVER_WORKER_TIMEOUT: 90
#      WEAVER_CONVERSION_FALLBACK: "false"

#  node:
#    image: wodby/node:$NODE_TAG
#    container_name: "${PROJECT_NAME}_node"
#    working_dir: /app
#    labels:
#      - 'traefik.backend=${PROJECT_NAME}_node'
#      - 'traefik.port=3000'
#      - 'traefik.frontend.rule=Host:front.${PROJECT_BASE_URL}'
#    expose:
#      - "3000"
#    volumes:
#      - ./path/to/your/single-page-app:/app
#    command: sh -c 'npm install && npm run start'

#  blackfire:
#    image: blackfire/blackfire
#    environment:
#      BLACKFIRE_SERVER_ID: XXXXX
#      BLACKFIRE_SERVER_TOKEN: YYYYY

#  webgrind:
#    image: wodby/webgrind:$WEBGRIND_TAG
#    environment:
#      WEBGRIND_PROFILER_DIR: /mnt/files/xdebug/profiler
#    labels:
#      - 'traefik.backend=${PROJECT_NAME}_webgrind'
#      - 'traefik.port=8080'
#      - 'traefik.frontend.rule=Host:webgrind.php.docker.localhost'
#    volumes:
#      - files:/mnt/files

#  opensmtpd:
#    container_name: "${PROJECT_NAME}_opensmtpd"
#    image: wodby/opensmtpd:$OPENSMTPD_TAG

#  xhprof:
#    image: wodby/xhprof:$XHPROF_TAG
#    restart: always
#    volumes:
#      - files:/mnt/files
#    labels:
#      - 'traefik.backend=${PROJECT_NAME}_xhprof'
#      - 'traefik.port=8080'
#      - 'traefik.frontend.rule=Host:xhprof.${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

#volumes:
## Docker-sync for macOS users
#  docker-sync:
#    external: true
## For Xdebug profiler
#  files:

Después simplemente:

docker4php miguelm$ docker-compose up -d --remove-orphans

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"

How to add a “out of stock” badge in Woocommerce using AVADA (SOLVED)

You can easily add an “out of stock” badge in Woocommerce by just using CSS:

.outofstock .images a, .products .outofstock a {position:relative;display:block;}
.products .outofstock .fusion-image-wrapper a:before {
    height: 100%;
    position: absolute;
    width: 100%;
    display: inherit !important;
    content: "Agotado \A Disponible por encargo"; // customize this message
    white-space: pre;
    font-size: 0.8em;
    letter-spacing:2px;
    font-weight:bold;
    background: rgba(0, 0, 0, 0.5);
    text-transform: uppercase;
    position: absolute;
    width: 100%;
    text-align: center;
    display: block;
    opacity: 0.7;
    line-height: normal;
    top: 0%;
    padding-top:40%;
    color:white;
}

This is how it looks like in my WordPress install (using Avada theme):

WOOCOMMERCE out of stock avada theme

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 );

Actualizar navegador Porsche PCM2.1 (Boxster, Cayenne, Cayman): La guía definitiva

Os dejo el manual para actualizar el navegador PCM2.1 de Porsche (Boxster / Cayenne / Cayman) a los últimos mapas disponibles a día de hoy (versión 2015). Probado en PCM2.1 equipado en Porsche BoxsterS MY05 con amplificador BOSE.

DISCLAIMER: No me hago responsable de cualquier daño ocasionado en vuestros PCM.

El software completo de actualización de compone de 3 CD’s y un DVD.

CD1: 000.043.205.46.002 – update LevelA to LevelB. http://dl.dropbox.com/u/14202177/Update-PCM%202.1-disco%201.nrg

CD2: 000.043.205.46.003 – updates Motorola Phone Module, if installed. http://dl.dropbox.com/u/14202177/Update-PCM%202.1-disco%202.nrg

CD3: 000.044.901.40 – Update for PCM21_NavDVD – Updates Navigation after Level B is installed or if Level B or Level C/C1 was previously installed) http://uploaded.net/file/nl4gapqx

DVD Mapas. En mi caso PORSCHE PCM 2.1 DVD EUROPE 2015 (mapas 2015)

http://ul.to/nu9hrsvw
http://ul.to/3yxpp8pl
http://ul.to/6awukige
http://ul.to/2xuzbgka
http://ul.to/144f68w1
http://ul.to/hpsmtt56

 

Actualizar navegador PCM2.1: PASO 0

Descargar todos los CD’s y grabarlos. Os recomiendo seguir el método indicado por el forero Grimosa (aqui: [url]http://soloporsche.com/showpost.php?p=1288917&postcount=1213[/url]). Es decir, grabarlos a mínima velocidad (1x) con ImgBurn, sobre CD-R de marca reconocida, tipo Verbatim.

Una vez tengamos todo grabado, nos vamos al coche, sacamos el CD del PCM 2.1 y también sacamos la SIM, si hubiera una insertada.

Después pasamos al PASO 1.

Actualizar navegador PCM2.1: PASO 1

Vamos a averiguar la versión de firmware de nuestro PCM2.1. Para ello, se enciende el PCM y se aprietan MAIN+NAVI simultáneamente.

En mi caso, los datos de inicio eran estos:

Aparato: Software de sist.
Teórico: 04113AD1 / 1104
Real: 04113AD1 / 1104
Introducidos: Sí
Encontrados: Sí

Aparato: PCM
Teórico:04202JD1 / 3004 
Real:04202JD1 / 3004
Introducidos: Sí
Encontrados: Sí

Aparato: Amplificador
Teórico: 00017500 / 3504
Real: 00017500 / 3504 
Introducidos: Sí
Encontrados: Sí

Aparato: Sistema navegac...
Teórico: /
Real: 00174156 / 2704
Introducidos:Sí
Encontrados:Sí

PCM2.1 firmware version mode level A

PCM2.1 bose amplifier firmware version mode level A

Si tenéis equipo BOSE, lo más importante es mirar la versión del firmware que tiene. Si tiene la versión 16400 lo más probable es que el equipo quede inservible al actualizar el navegador. Por tanto, NO ACTUALICES si tienes la 16400.

Otro dato importante es la versión del Aparato: PCM. Los valores posibles serán:

04035xxx --> indica Standard Version.
04202xxx --> indica Model Level A.
05024xxx --> indica Model Level B.
05395xxx --> indica Model Level C.

Si estás en la 04035xxx te recomiendo que NO HAGAS NADA, podrías dejar el BOSE inservible. La propia documentación de PORSCHE lo indica:
[quote]Vehicles equipped with Standard Version (04035xxx) first need to be updated to Model Level A (04202xxx) using update disc 000.043.205.46.004 which is not included in this kit. Refer to bulletin Group 9, #1/04, titled “PCM2.1 & BOSE Amplifier Spare Parts Requirements, Part ID 9110.
Read the information in bulletin #1/04 carefully. An update of the BOSE Amplifier (if equipped) may be necessary.[/quote]

Si estás en Model Level A, debes actualizar a Model Level B para poder usar el DVD de mapas de 2015. Ve al PASO 2.

Si estás en Model Level B, únicamente debes actualizar el firmware del navegador y los mapas. Ve al PASO 4.

Actualizar navegador PCM2.1: PASO 2

El propósito de este paso es actualizar del model level A al model level B. Para ello vamos a utilizar el CD1: 000.043.205.46.002.

Lo primero, ENCIENDE EL MOTOR. Si por un casual el PCM se apaga durante la actualización de firmware, el equipo puede quedar inservible. Asi pues, MANTEN ENCENDIDO EL MOTOR durante esta actualización. Haz todo el proceso en un sitio bien ventilado.

Enciende el PCM.

Inserta el CD1 en la ranura del PCM. Pondrá algo asi como “reconociendo CD de datos” y al poco, se iniciará automáticamente. Nos preguntará si queremos actualizar a Model Level B. Le decimos que Sí. Tarda un poco, pero al poco sale una barra de proceso. Ahora podemos tener dos casos, en función de si tienes instalado módulo de teléfono y la versión del mismo.

En el caso de vehículos sin módulo de teléfono o con el módulo de teléfono Motorola, la actualización te irá indicando 2 pasos (R51 y R02).

pcm2.1a update pcm2.1b

pcm2.1a update pcm2.1b

En el caso de vehículos con módulo de teléfono Harman/Becker, la actualización se hará en tres pasos (V5.03.4, R51 y R02).

Tarda un rato. No seas impaciente, no toques nada hasta que termine. Cuando termine el PCM se apagará y volverá a encenderse solo. Si no se encendiera solo, enciéndelo tú a mano y saca el CD.

Si tienes módulo de teléfono Motorola, pasa al PASO 3. Sino, pasa al PASO 4.

Actualizar navegador PCM2.1: PASO 3

El propósito de este paso es actualizar el firmware del módulo de teléfono Motorola.
Asegúrate de que no haya SIM en la ranura de SIM y de tener el motor encendido.

Inserta el CD2 (000.043.205.46.003). Te pondrá algo asi como “reconociendo CD de datos” y al poco te preguntará si quieres actualizar MicPegel Motorola.

update micpegel motorola porsche pcm2.1

Le dices que sí y esperas. Saldrá una barra de proceso. Te indicará que está actualizando a versión R36.

update micpegel motorola porsche pcm2.1

Al terminar aparecerá un mensaje indicando que el proceso ha concluido de forma exitosa.

update micpegel motorola porsche pcm2.1

Saca el CD del PCM y ve al PASO 4.

Actualizar navegador PCM2.1: PASO 4

Este paso debe acometerse cuando nuestro firmware está en Mode Level B, C o C1. Aprieta MAIN+NAVI y asegúrate de que la versión del PCM es 05024xxx (Model Level B) o 05395xxx (Mode Level C).

Saca el DVD de navegación anterior de su sitio. En mi Boxster está en el maletero delantero. En los Cayenne creo que está bajo el asiento del copiloto. Guarda ese DVD.

Inserta ahora en el DVD de navegación (NO en el PCM!) el CD3 (000.044.901.40). Apaga el PCM. Apaga el motor y déjalo apagado 2 minutos o más.

Enciende el motor y DÉJALO ENCENDIDO.

El navegador se encenderá automáticamente. Si no se encendiera, pulsa NAVI en el PCM.

Aparecerá un mensaje preguntándote si quieres actualizar el software de navegación. Le dices que sí.

update pcm2.1 navigation

Aparecerá una barra de proceso, que ira subiendo hasta llegar al 10%.

actualizar navegador pcm porsche boxster 987

Pasará del 10% al 0 y te pondrá: “error durante arranque navegación”.

actualizar navegador pcm porsche boxster 987

En ese momento, ve al cargador de DVD de navegación (maletero o asiento), saca el CD3 y mete el DVD con los mapas de 2015. Pondrá “Sistema navegación arrancando” y al poco, el proceso termina y ya tenemos los nuevos mapas.

actualizar navegador pcm porsche boxster 987

El firmware actualizado:
actualizar navegador pcm porsche boxster 987

Espero que os resulte útil.