Skip to content

Curso Practico de Django desde Sistemas Operativos GNU Linux

Notifications You must be signed in to change notification settings

RETBOT/Django-X-Linux

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

99 Commits
 
 
 
 

Repository files navigation

Curso Practico de Django desde Sistemas Operativos GNU Linux

En este repositorio iremos viendo todo lo necesario para la instalacion de los elementos necesarios
Contacto:
Isaias Gerardo
Roberto Esquivel
img-foro

Instalacion del Sistema Operativo

Para este taller se usara el Sistema operativo Fedora 36 Fedora-Logo

Por lo que deberemos de ir al siguiente link para obtener la ISO.

Ir a la pagina de descarga

Virtualizacion del sistema operativo

Una vez teniendo la iso, vamos a requerir un virtualizador. Puede ser cualquiera, pero para este caso reduciremos las alternativas a solo 2 VMware Player/Pro(Workstation) o VirtualBox

Links de descarga:
VMware : https://www.vmware.com/mx/products/workstation-player/workstation-player-evaluation.html
Virtual Box: https://www.virtualbox.org/

Instalacion de las herramientas necesarias en el SO

Instalacion del IDE/Editor Visual Studio Code : Descargamos el archivo .rmp del siguiente sitio

Ir a la carpeta en donde se almaceno el .rpm, regularmente por defecto se va a la carpeta de Descargas. Con el siguiente comando podremos ir a esa carpeta.
cd $HOME/Descargas/

Una vez estando ahi ejecutamos el siguiente comando para instalar el editor.

sudo rpm -i code-1.73.1-1667967421.el7.x86_64.rpm    

Debido a que estamos en GNU/Linux no tendremos que realizar pasos como descargar o actualizar la version de python. Debido a que por defecto ya viene integrada la version mas reciente o de las mas recientes de python3.

Entorno virtual de python

No son sumamente necesarios para el funcionamiento del proyecto, mas sin embargo son altamente recomendados usarlos en entornos de produccion. Por lo que en este taller lo manejaremos por entornos virtuales para mejor uso practico.

Comandos necesarios desde la terminal

Deberemos de hacer una serie de pasos en la terminal antes de ir a la codificacion en Visual Studio Code. Creacion de carpetas
mkdir congresoDjango

Cambiar de directorio a la carpeta que creamos

cd congresoDjango/

Instalacion de pip
sudo dnf install python3-pip

Verificamos que se haya instalado pip correctamente.

pip --help

Instalacion del entorno de virtual de python

pip3 install pipenv

Preparativos para el uso de Django

Una vez hemos conseguido pip y creado nuestra carpeta de trabajo, lo que prosigue es instalar Django con pip con el siguiente comando

pipenv install django

El siguiente paso sera entrar al pipenv shell para que todos los comandos que escribamos afecten directamente a ese proyecto

pipenv shell

Hasta este punto si todo sale bien, te debera de aparecer al lado izquierdo en la terminal algo similar a esto (base)

Creacion de proyecto de django

Hemos llegado a nuestro primer momento de interaccion con django, para comenzar nuestro proyecto introducimos el siguiente comando.
django-admin startproject congreso .

Se anade el . al final para que nos genere los archivos en el directorio actual Verificamos si se nos han anadido los archivos con el siguiente comando :

tree

Arrancamos el motor de django por primera vez.

python manage.py runserver

Creacion de aplicaciones

Para creacion de aplicaciones en django lo hacemos con el siguiente comando.
django-admin startapp blog

Migracion de la base de datos


python manage.py migrate

Creacion de super usuario para la administracion del proyecto

Lo cremos con el siguiente comando :

python manage.py createsuperuser

Instalacion de Heroku

Heroku es un servicio de nube en el cual alojaremos nuestra aplicacion web. Para instalarlo primero tendremos que instalar el gestor de paquetes.

sudo dnf install npm

Una vez teniendo el gestor de paquetes de node, procederemos a instalar heroku con el siguiente comando:

sudo npm install heroku

Creando una aplicación web desde Django

Aplicación Blog

Ahora crearemos una aplicación de Blog que permita a los usuarios crear, editar eliminar publicaciones. La pagina de inicio enumerara todas las publicaciones del blog y habrá una pagina de detalle dedicada para cada publicación individual.

Configuración inicial:

Lo primero que haremos será crear un nuevo directorio [RETBOT@RETBOT ~]$ mkdir blog

mkdir blog

[RETBOT@RETBOT ~]$ cd blog

cd blog

[RETBOT@RETBOT blog]$ pipenv install django

pipenv install django

Instalación-Django

Para empezar a configurar nuestro proyecto, iniciamos el entorno virtual:

[RETBOT@RETBOT blog]$ pipenv shell
Launching subshell in virtual environment...
. /home/RETBOT/.local/share/virtualenvs/blog-YSH-3orc/bin/activate

pipenv shell  

(blog) [RETBOT@RETBOT blog]$ django-admin startproject config .

django-admin startproject config .  

(blog) [RETBOT@RETBOT blog]$ tree
.
├── config
│   ├── asgi.py
│   ├── init.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── Pipfile
└── Pipfile.lock

1 directory, 8 files

tree  

Creamos una aplicación llamada blog, que sera la entrada inicial de nuestra pagina
(blog) [RETBOT@RETBOT blog]$ python manage.py startapp blog

python manage.py startapp blog    

(blog) [RETBOT@RETBOT blog]$ tree
.
├── blog
│   ├── admin.py
│   ├── apps.py
│   ├── init.py
│   ├── migrations
│   │   └── init.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── config
│   ├── asgi.py
│   ├── init.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── Pipfile
└── Pipfile.lock

tree    

Ahora aplicaremos los cambios en nuestra aplicacion
(blog) [RETBOT@RETBOT blog]$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
(blog) [RETBOT@RETBOT blog]$

python manage.py migrate  

Para asegurarnos de que Django conozca nuestra nueva aplicación, abra su editor de texto y agregue la nueva aplicación a INSTALLED_APPS en nuestro archivo settings.py ubicado en la carpeta config:

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

(blog) [RETBOT@RETBOT blog]$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
November 13, 2022 - 16:46:17
Django version 4.1.3, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Si navega a http://127.0.0.1:8000/

python manage.py runserver   

Run-Django

Crearemos una nueva aplicación para administrar las publicaciones de nuestro blog
(blog) [RETBOT@RETBOT blog]python manage.py startapp publicaciones

python manage.py startapp publicaciones 

(blog) [RETBOT@RETBOT blog]$ tree
.
├── blog
│   ├── admin.py
│   ├── apps.py
│   ├── init.py
│   ├── migrations
│   │   └── init.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── config
│   ├── asgi.py
│   ├── init.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3
├── manage.py
├── Pipfile
├── Pipfile.lock
└── publicaciones
├── admin.py
├── apps.py
├── init.py
├── migrations
│   └── init.py
├── models.py
├── tests.py
└── views.py

5 directories, 23 files
(blog) [RETBOT@RETBOT blog]$

tree   

Para asegurarnos de que Django conozca nuestra nueva aplicación, agregue la nueva aplicación a INSTALLED_APPS en nuestro archivo settings.py ubicado en la carpeta config:

INSTALLED_APPS = [ 
    'django.contrib.admin', 
    'django.contrib.auth', 
    'django.contrib.contenttypes', 
    'django.contrib.sessions', 
    'django.contrib.messages', 
    'django.contrib.staticfiles', 
    'blog', 
    'publicaciones',  # agg 
] 

Esto significa que la instalación esa completada. A continuación crearemos nuestro modelo de base de datos para publicaciones de blog.

Modelos de base de datos

#publicaciones/models.py
from django.db import models 
from django.contrib.auth import get_user_model

class Publicacion(models.Model):
  titulo = models.CharField(max_length=200)
  autor = models.ForeignKey(
        get_user_model(),
        on_delete=models.CASCADE,
    )
  cuerpo = models.TextField()

  def __str__(self):
    return self.titulo

En la parte superior, estamos importando los modelos de la clase y luego creando una subclase de models.Model llamada Publicación.
Al usar esta funcionalidad de subclase, automáticamente tenemos acceso a todo dentro de django.db.models.Models y podemos agregar campos y métodos adicionales según se desee.
Para el titulo, estamos limitando la longitud a 200 caracteres y para el cuerpo estamos usando un TextField que se expandira automaticamente según sea necesario para adaptarse al texto del usuario. Hay muchos tipos de campos disponibles en Django; puedes ver la lista completa aquí: https://es.acervolima.com/lista-de-campos-y-tipos-de-datos-del-modelo-django/

Para el campo de autor, usamos una ForeignKey que permite una relacion de muchos a uno. Eso significara que un usuario determinado puede ser el autor de muchas publicaciones de blog diferentes, pero no al reves. La referencia es al modelo de usuario integrado que Django proporciona para la autenticacion. Para todas las relaciones de muchos a uno, como ForeignKey, tambien debemos especificar una opcion on_delete.

Ahora que se creó nuestro nuevo modelo de base de datos, debemos crear un nuevo registro de migración para él y migrar el cambio a nuestra base de datos. Detenga el servidor con Control + c. Este proceso de dos pasos se puede completar con el siguiente comando:

(blog) [RETBOT@RETBOT blog]$ python manage.py makemigrations publicacioneses
Migrations for 'publicaciones':
publicaciones/migrations/0001_initial.py
- Create model Publicacion

python manage.py makemigrations publicacioneses 

(blog) [RETBOT@RETBOT blog]$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, publicaciones, sessions
Running migrations:
Applying publicaciones.0001_initial... OK
(blog) [RETBOT@RETBOT blog]$

python manage.py migrate

Admin

Necesitamos una forma de acceder a nuestros datos. ¡Ingrese el administrador de Django! Primero cree una cuenta de superusuario escribiendo el comando a continuación y siguiendo las instrucciones para configurar un correo electrónico y una contraseña. Tenga en cuenta que al escribir su contraseña, no aparecerá en la pantalla por razones de seguridad.

(blog) [RETBOT@RETBOT blog]$ python manage.py createsuperuser
Username (leave blank to use 'retbot'): admin
Email address: [email protected]
Password:
Password (again):
Superuser created successfully.
(blog) [RETBOT@RETBOT blog]$

python manage.py createsuperuser

Ahora comience a ejecutar el servidor Django nuevamente con el comando python manage.py runserver y abra el administrador de Django en http://127.0.0.1:8000/admin/. Inicie sesión con su nueva cuenta de superusuario.

Admin1-Django

Ahora agregaremos las publicaciones al administrador, entraremos a publicaciones/admin.py

from django.contrib import admin
from .models import Publicacion
admin.site.register(Publicacion)

Si actualiza la página, verá la actualización.
Admin2-Django
Agreguemos dos publicaciones de blog para que tengamos algunos datos de muestra con los que trabajar. Haga clic en el botón + Agregar junto a Publicaciones para crear una nueva entrada. Asegúrate de agregar un "autor" a cada publicación también, ya que por defecto todos los campos del modelo son obligatorios. Si intenta ingresar una publicación sin un autor, verá un error. Si quisiéramos cambiar esto, podríamos agregar opciones de campo a nuestro modelo para hacer que un campo dado sea opcional o llenarlo con un valor predeterminado.
Pub1-Django Pub2-Django

Ahora que nuestro modelo de base de datos está completo, necesitamos crear las vistas, URL y plantillas necesarias para que podamos mostrar la información en nuestra aplicación web.

URLs

Queremos mostrar las publicaciones de nuestro blog en la página de inicio, así que, como en clases anteriores, primero configuraremos nuestras URLConfs a nivel de proyecto y luego nuestras URLConfs a nivel de aplicación para lograr esto. Tenga en cuenta que "nivel de proyecto" significa en la misma carpeta principal que las carpetas config, blog app y publicaciones app.

Cree un nuevo archivo urls.py dentro de nuestro blog y en las publicaciones:
Urls1-Django Urls2-Django

Ahora agregaremos el siguiente código:

# blog/urls.py 
from django.urls import path
from .views import VistaPaginaInicio 
   
urlpatterns = [ 
  path('', VistaPaginaInicio.as_view(), name='inicio'),
] 
 # publicaciones/urls.py 
from django.urls import path
from .views import VistaListaPublicaciones 

urlpatterns = [ 
    path('',VistaListaPublicaciones.as_view(), name='lista_publicaciones'), 
]

Estamos importando nuestras vistas que se crearán próximamente en la parte superior. La cadena vacía '' le dice a Python que coincida con todos los valores y la convertimos en una URL nombrada, inicio, a la que podemos referirnos en nuestras vistas más adelante. Si bien es opcional agregar una URL con nombre, es una práctica recomendada que debe adoptar, ya que ayuda a mantener las cosas organizadas a medida que aumenta la cantidad de URL.
También deberíamos actualizar nuestro archivo urls.py a nivel de proyecto para que sepa reenviar todas las solicitudes directamente a la aplicación del blog.

from django.contrib import admin
from django.urls import path, include 

urlpatterns = [ 
   path('admin/', admin.site.urls),
   path('',include('blog.urls')),
   path('publicaciones/', include('publicaciones.urls')),
]

Hemos agregado include en la segunda y tercer línea un patrón de URL con una expresión regular de cadena vacía "" que indica que las solicitudes de URL deben redirigirse tal cual a las URL del blog y a las URL de publicaciones para obtener más instrucciones.

Vistas

Vamos a utilizar vistas basadas en clases, pero si quieres ver una forma basada en funciones para crear una aplicación de blog, te recomiendo el Tutorial de Django Girls.

# blog/views.py 
from django.views.generic import TemplateView

class VistaPaginaInicio(TemplateView): 
  template_name = 'inicio.html'
 
#publicaciones/views.py
from django.views.generic import ListView 
from .models import Publicacion 

class VistaListaPublicaciones(ListView):
    model = Publicacion
    template_name = 'lista_publicaciones.html' 

En las dos líneas superiores importamos ListView y nuestro modelo de base de datos Publicacion. Luego subclasificamos ListView y agregamos enlaces a nuestro modelo y plantilla. Esto nos ahorra mucho código en comparación con implementarlo todo desde cero.

Templates

Con nuestras URLConfs y vistas ahora completas, solo nos falta la tercera pieza del rompecabezas: las plantillas. Por lo tanto, comenzaremos con un archivo base.html, un archivo inicio.html y un archivo lista_publicaciones.html que heredan de él. Luego, más tarde, cuando agreguemos plantillas para crear y editar publicaciones de blog, también pueden heredar de base.html.
Comience creando nuestro directorio de plantillas a nivel de proyecto con los tres archivos de plantilla.

Django-plantillas

Luego actualice settings.py para que Django identifique como buscar las plantillas.

   TEMPLATES = [
    {
      ... 
        'DIRS': [str(BASE_DIR.joinpath('plantillas'))], 
      ...
            ],
        }, 
    }, 
]  

Luego actualice la plantilla base.html de la siguiente manera.
<!-- plantillas/base.html -->
<html>
  <head>
    <title>Blog Django {% block title%}{% endblock title%}</title>
  </head>
  <body>
      {% block content %}
     
      {% endblock content %}
  </body>
</html>   

Tenga en cuenta que el código entre {% block content%} y {% endblock content%} se puede completar con otras plantillas. Hablando de eso, aquí está el código para inicio.html.

<!-- plantillas/inicio.html -->
{% extends 'base.html' %}

{% block title%}- Inicio{% endblock title%}

{% block content %}
<a href="{% url 'inicio' %}"><h1>Blog en Django</h1></a>
<a href="{% url 'lista_publicaciones' %}">Publicaciones Blog</a>
{% endblock content %}   
 <!-- plantillas/lista_publicaciones.html -->
{% extends 'base.html' %}

{% block content %}
  {% for pub in object_list %}
  <div>
    <h2>
      <a href="#"> {{ pub.titulo }}</a>
      <p>{{ pub.cuerpo }}</p>
    </h2>
  </div>
  {% endfor %}
{% endblock content %}  
   

En la parte superior notamos que esta plantilla extiende base.html y luego envuelve nuestro código deseado con bloques de contenido. Usamos el lenguaje de plantillas Django para configurar un bucle for simple para cada publicación de blog. Tenga en cuenta que config proviene de ListView y contiene todos los objetos en nuestra vista.

Si vuelve a iniciar el servidor Django: python manage.py runserver. Y actualice http://127.0.0.1:8000/, podemos ver que está funcionando.

Django-inicio Django-publicaciones

Imágenes al Blog

Como queremos usar imágenes, necesitamos instalar la librería Pillow, que es necesaria para usar el modelo de datos de Django en imagenes
(blog) [RETBOT@RETBOT blog]$ pip install pillow

pip install pillow

Después debemos entrar en blog/models.py y agregaremos lo siguiente:

   ...
     imagen = models.ImageField(upload_to="img_pub", null=True)
   ...

ImageFiel es un FileFiel con cargas restringidas a formato de imágenes.


Después crearemos una ruta donde cargar las imágenes:
Ruta-Imgs

Dentro del archivo config/settings.py modificaremos lo siguiente:

import os

STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

y en config/urls.py agregaremos lo siguiente:

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('',include('blog.urls')),
]

urlpatterns += static(settings.MEDIA_URL, document_root= settings.MEDIA_ROOT)

Ahora entramos en la terminal:
(blog) [RETBOT@RETBOT blog]$ python manage.py makemigrations publicacion
Migrations for 'blog':
blog/migrations/0002_publicacion_img.py
- Add field img to publicacion

python manage.py makemigrations publicaciones   

(blog) [RETBOT@RETBOT blog]$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
Applying blog.0002_publicacion_img... OK
(blog) [RETBOT@RETBOT blog]$

python manage.py migrate   

Iniciamos el servidor para verificar si las imágenes se pueden agregar a las publicaciones.
(blog) [RETBOT@RETBOT blog]$ python manage.py runserver

python manage.py runserver

Entramos en http://127.0.0.1:8000/admin y creamos una nueva publicación
Img-Publicacion

Bootstrap

Entramos al archivo base.html ubicado en las plantillas

<head>
  <meta charset="UTF-8">
  <title>Blog Django {% block title %}{% endblock title %}</title>
  <meta name="viewport" content="with=device.width, initial-scale=1, 
      shrink-to-fit=no">
  <!-- CSS BootStrap -->
  <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" crossorigin="anonymous"
    rel="stylesheet">
</head>

y al final del archivo

<!-- JavaScript Opcional -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js/1.14.3/umd/popper.min.js" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
      crossorigin="anonymous"></script>
  </body>
</html>

Ahora personalizaremos nuestro blog

base.html

<!-- plantillas/base.html -->
<html>
<head>
  <meta charset="UTF-8">
  <title>Blog Django {% block title %}{% endblock title %}</title>
  <meta name="viewport" content="with=device.width, initial-scale=1, 
      shrink-to-fit=no">
  <!-- CSS BootStrap -->
  <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" crossorigin="anonymous"
    rel="stylesheet">
</head>

<body>
  <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
    <a class="navbar-brand" href="{% url 'inicio' %}">
      Blog Django
    </a>
    <div class="navbar-collapse" id="navbarSupportedContent">
      {% if user.is_authenticated %}
      <ul class="navbar-nav ml-auto">
        <li class="nav-item">
          <a class="nav-link dropdown-toggle" href="#" role="button" id="navbarDropdown" data-toggle="dropdown"
            aria-haspopup="true" aria-expanded="false">
            Blogs
          </a>
          <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
            <a class="dropdown-item" href="#">
              Blogs
            </a>
            <a class="dropdown-item" href="#">
              Nuevo
            </a>
          </div>
        </li>
        <li class="nav-item">
          <a class="nav-link dropdown-toggle" href="#" role="button" id="navbarDropdown" data-toggle="dropdown"
            aria-haspopup="true" aria-expanded="false">
            {{ user.username }}
          </a>
          <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
            <a class="dropdown-item" href="#">
              Cambiar Contraseña
            </a>
            <a class="dropdown-item" href="#">
              Salir
            </a>
          </div>
        </li>
      </ul>
      {% else %}
      <form class="form-inline ml-auto">
        <a href="#" class="btn btn-outline-secondary">
          Acceder
        </a>
        <a href="#" class="btn btn-primary ml-2">
          Registrarse
        </a>
      </form>
      {% endif %}
    </div>
  </nav>
    {% block content %}
    
    {% endblock content %}
  <!-- JavaScript Opcional -->
  <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/popper.js/1.14.3/umd/popper.min.js" crossorigin="anonymous"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" crossorigin="anonymous"></script>
</body>
</html>  

inicio.html

<!-- plantillas/inicio.html -->
{% extends 'base.html' %}

{% block title%}- Inicio{% endblock title%}

{% block content %}
<main role="main" class="container">
  <h1 class="display-4">Blog Django</h1>
  <div class="jumbotron">
    <p class="lead">Un blog hecho con Django.</p>
    <p class="lead">
      <a class="btn btn-lg btn-primary" 
      href="{% url 'lista_publicaciones' %}" 
      role="button">Entrar</a>
    </p>
  </div>
</main>
{% endblock content %}    

lista_publicaciones.html

<!-- plantillas/lista_publicaciones.html -->
{% extends 'base.html' %}

{% block title %} - Publicaciones {% endblock title %}

{% block content %}
<br>
<div class="jumbotron p-md-2 text-white rounded bg-dark">
  <h1 class="display-4 font-italic text-center" >Publicaciones</h1>
</div>

  {% for pub in object_list %}

  <div class="col-md-12">
    <div class="card flex-md-row mb-3 box-shadow h-md-270">
      <div class="card-body d-flex flex-column align-items-center">
        <strong class="d-inline-block mb-2 text-primary">Autor: {{ pub.autor }}</strong>
        <h3 class="mb-0">
          <a class="text-dark" href="#">{{ pub.titulo }} </a>
        </h3>
        <p class="card-text mb-auto">{{ pub.cuerpo }} </a>
      </div>
    </div>
  </div>
  {% endfor %}
{% endblock content %}    

Teniendo nuestra pagina personalizada ahora queda darle mas funcionalidades, por lo cual agregaremos la forma en la que podremos crear una nueva publicación, ver detalladamente, editar la publicación y eliminarla:

Crearemos los url para poder acceder a las paginas, desde publicaciones/urls.py

publicaciones/urls.py

# publicaciones/urls.py
from django.urls import path
from .views import (
    VistaListaPublicaciones,
    VistaCrearPublicacion,
    VistaEditarPublicacion,
    VistaDetallePublicacion,
    VistaEliminarPublicacion,
    )

urlpatterns = [
    path('',VistaListaPublicaciones.as_view(), name='lista_publicaciones'),
    path('nuevo/',VistaCrearPublicacion.as_view(), name='nueva_publicacion'),
    path('<int:pk>/editar/',VistaEditarPublicacion.as_view(), name='editar_publicacion'),
    path('<int:pk>/detalle/',VistaDetallePublicacion.as_view(), name='detalle_publicacion'),
    path('<int:pk>/eliminar/',VistaEliminarPublicacion.as_view(), name='eliminar_publicacion'),
]    

publicaciones/views.py

#publicaciones/views.py
from django.views.generic import ListView, DetailView
from django.views.generic.edit import DeleteView, UpdateView, CreateView
from django.urls import reverse_lazy
from .models import Publicaciones

# Create your views here.
class VistaListaPublicaciones(ListView):
  model = Publicaciones
  template_name = 'lista_publicaciones.html'

class VistaCrearPublicacion(CreateView):
  model = Publicaciones
  template_name = 'nueva_publicacion.html'
  fields = ['titulo', 'cuerpo', 'imagen',]

class VistaDetallePublicacion(DetailView):
  model = Publicaciones
  template_name = 'detalle_publicacion.html'
  context_object_name = 'pub'
  
class VistaEditarPublicacion(UpdateView):
  model = Publicaciones
  template_name = 'editar_publicacion.html'
  fields = ['titulo', 'cuerpo', 'imagen',]
  context_object_name = 'pub'

class VistaEliminarPublicacion(DeleteView):
  model = Publicaciones
  template_name = 'eliminar_publicacion.html'
  context_object_name = 'pub'
  success_url = reverse_lazy('lista_publicaciones') 

Crearemos 4 nuevos archivos para crear una nueva publicación, ver detalladamente, editar la publicación y eliminarla
nueva_publicacion.html
detalle_publicacion.html
editar_publicacion.html
eliminar_publicacion.html
Django-Templates

Iniciamos modificando nueva_publicacion.html

<!--planillas/nueva_publicacion.html -->
{% extends 'base.html' %}
{% block title %} - Nueva Publicacion {% endblock title %}

{% block content %}
<div class="form-group">
  <h1>Nueva Publicacion</h1>
  <form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
      {{ form.as_p }}
      <input class="btn btn-success" type="submit" value="Guardar cambios" style="color: black;">
  </form>
</div>
{% endblock content %}

Cargamos la direccion http://127.0.0.1:8000/publicaciones/nueva/ para validar que nuestra pagina esta funcionado

Django-Nueva-Publicacion

y redireccionamos al usuario a la pagina de detalle

# publicaciones/models.py
...
from django.urls import reverse

# Create your models here.
class Publicacion(models.Model):
  titulo = models.CharField(max_length=200)
  cuerpo = models.TextField()
  imagen = models.ImageField(upload_to="img_pub", null=True)
  autor = models.ForeignKey(
        get_user_model(),
        on_delete=models.CASCADE,
    )

  def __str__(self):
    return self.titulo

  def get_absolute_url(self):
        return reverse('detalle_publicacion',args=[str(self.id)])

Ahora modificaremos detalle publicaciones

<!-- plantillas/detalle_publicacion.html -->
{% extends 'base.html' %}

{% block title %} - Detalle Publicacion{% endblock title %}

{% block content %}
<br>

        <div class="card">
            <div class="card-header">
              <div class="jumbotron p-md-2 text-white rounded bg-dark">
                <h1 class="display-4 font-italic text-center" >{{ pub.titulo }}</h1>
              </div>
                <span class="text-muted">Creado por {{ pub.autor }}</span>
            </div>
            <div class="card-body">
                {{ pub.cuerpo }}
            </div>
            {% if pub.imagen.url != null %}
            <center>
              <img class="img-fluid" src="{{ pub.imagen.url }}" alt="Imagen blog">
            </center>
            {% endif %}
            </div>
            
            <div class="card-footer text-center text-muted">
                <a href="#">Editar</a> | 
                <a href="#">Eliminar</a> |
                <a href="#">Volver a Publicaciones</a>
            </div>
        </div>
{% endblock content %}

y configuramos la ruta en lista publicación, en plantillas/lista_publicaciones.html

<a class="text-dark" href="{% url 'detalle_publicacion' pub.pk %}">{{ pub.titulo }} </a>

También configuramos la pagina principal en plantillas/base.html

<a class="dropdown-item" href="{% url 'lista_publicaciones' %}">
   Blogs
</a>
<a class="dropdown-item" href="{% url 'nueva_publicacion' %}">
   Nuevo
</a>  

Ahora configuraremos editar_publicaciones.html de plantillas

<!-- plantillas/editar_publicacion.html -->
{% extends 'base.html' %}

{% block title %} - Editar Publicación{% endblock title %}

{% block content %}
<h1>Editar publicacion</h1>
<form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
    {{ form.as_p }}
    <input class="btn btn-success ml-2" type="submit" value="Guardar cambios" style="color: black;">
</form>
{% endblock content %} 

y eliminar_publicaciones.html de plantillas

<!-- plantillas/eliminar_publicaciones.html -->
{% extends 'base.html' %}

{% block title %} - Eliminar Publicación{% endblock title %}

{% block content %}
    <h1>Eliminar publicacion</h1>
    <form action="" method="post">{% csrf_token %}
        <p>¿Está completamente seguro de que desea eliminar "{{ object.titulo }}" ? </p>
        <input class="btn btn-danger ml-2" type="submit" value="Confirmar eliminarción" style="color: black;">        
    </form>
{% endblock content %} 

en detalle_publicacion.html de plantillas agregaremos las urls

<div class="card-footer text-center text-muted">
     <a href="{% url 'editar_publicacion' pub.pk %}">Editar</a> | 
     <a href="{% url 'eliminar_publicacion' pub.pk %}">Eliminar</a> |
     <a href="{% url 'lista_publicaciones' %}">Volver a Publicaciones</a>
</div>

Administrar cuentas de usuarios

Iniciaremos creando una aplicación llamada cuentas (blog) [RETBOT@RETBOT blog]$ python manage.py startapp cuentas

python manage.py startapp cuentas

(blog) [RETBOT@RETBOT blog]$ tree

tree  

.
├── blog
│   ├── admin.py
│   ├── apps.py
│   ├── init.py
│   ├── migrations
│   │   └── init.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── config
│   ├── asgi.py
│   ├── init.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── cuentas
│   ├── admin.py
│   ├── apps.py
│   ├── init.py
│   ├── migrations
│   │   └── init.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── db.sqlite3
├── manage.py
├── media
│   └── img_pub
│   ├── logo2.png
│   └── logo.jpg
├── Pipfile
├── Pipfile.lock
├── plantillas
│   ├── base.html
│   ├── detalle_publicacion.html
│   ├── editar_publicacion.html
│   ├── eliminar_publicacion.html
│   ├── inicio.html
│   ├── lista_publicaciones.html
│   └── nueva_publicacion.html
├── publicaciones
│   ├── admin.py
│   ├── apps.py
│   ├── init.py
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   └── init.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
└── static

11 directories, 45 files


Agregamos la nueva aplicación a config/settings.py de config para que pueda ser reconocido

# Application definition
INSTALLED_APPS = [
...

     # locales
    'blog',
    'publicaciones',
    'cuentas', 
]

y también la configuracion de cuentas de usuario personalizada debro de config/settings.py

AUTH_USER_MODEL = 'cuentas.UsuarioPers'

despues entramos a urls.py de config:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('cuentas/', include('django.contrib.auth.urls')), #
    path('cuentas/', include('cuentas.urls')), #
    path('',include('blog.urls')),
    path('publicaciones/', include('publicaciones.urls')),
]

Para la configuración de las urls de las cuentas, primero crearemos un archivo llamado urls.py dentro de la aplicación cuentas:
Django-Cuentas-URLs
Dentro de urls.py

# cuentas/urls.py
from urllib.parse import urlparse
from django.urls import path
from .views import VistaRegistro

urlpatterns = [
    path('registro/', VistaRegistro.as_view(), name='signup'),
]

y configuramos las views.py de cuentas, para tener la vista de usuarios

from audioop import reverse
from django.urls import reverse_lazy
from django.views.generic import CreateView
from .forms import FormularioCreacionUsuarioPers

# Create your views here.
class VistaRegistro(CreateView):
    form_class = FormularioCreacionUsuarioPers
    success_url = reverse_lazy('login')
    template_name = 'registration/signup.html'

Después creamos un archivo llamado forms.py dentro de cuentas:
Django-FormsCuentas

#cuentas/froms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import UsuarioPers

class FormularioCreacionUsuarioPers(UserCreationForm):
    class Meta(UserCreationForm):
        model = UsuarioPers
        fields = ('username','email','edad',)

class FormularioCambioUsuarioPers(UserChangeForm):
    class Meta:
        model = UsuarioPers
        fields = ('username','email','edad',)

Y después lo agregaremos a admin.py para que pueda ser desplegado en la pagina del administrador

#cuentas/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import FormularioCambioUsuarioPers, FormularioCreacionUsuarioPers
from .models import UsuarioPers

# Register your models here.
class UsuarioPersAdmin(UserAdmin):
    add_form = FormularioCreacionUsuarioPers
    form = FormularioCambioUsuarioPers
    model = UsuarioPers
    list_display = ['email', 'username', 'edad', 'is_staff',]
    fieldsets = UserAdmin.fieldsets + (
        (None, {'fields': ('edad',)}),
    )
    add_fieldsets = UserAdmin.add_fieldsets + (
        (None, {'fields': ('edad',)}),
    )

admin.site.register(UsuarioPers, UsuarioPersAdmin)

Lo que falta es migrar la información a la base de datos, pero tenemos que tener en cuenta, que estamos utilizando un modelo de usuarios personalizados, primero tenemos que comentar

INSTALLED_APPS = [
    #'django.contrib.admin',
…
]

y también en urls.py de config

urlpatterns = [
    #path('admin/', admin.site.urls),
  ...
] 

Después eliminaremos las migraciones de publicaciones ya que estamos utilizando los modelos de usuarios y causa conflicto en la migración de usuario
Django-Elim-Migracion
Iniciamos las migraciones de la aplicación cuentas

(blog) [RETBOT@RETBOT blog]$ python manage.py makemigrations cuentas
Migrations for 'cuentas':
cuentas/migrations/0001_initial.py
- Create model UsuarioPers

python manage.py makemigrations cuentas

(blog) [RETBOT@RETBOT blog]$ python manage.py migrate
Operations to perform:
Apply all migrations: auth, contenttypes, cuentas, sessions
Running migrations:
Applying cuentas.0001_initial... OK

python manage.py migrate 

y después des-comentamos el código comentado anteriormente y hacemos la migración en publicaciones
(blog) [RETBOT@RETBOT blog]$ python manage.py makemigrations publicaciones
Migrations for 'publicaciones':
publicaciones/migrations/0001_initial.py
- Create model Publicacion

python manage.py makemigrations publicaciones

(blog) [RETBOT@RETBOT blog]$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, cuentas, publicaciones, sessions
Running migrations:
No migrations to apply.
(blog) [RETBOT@RETBOT blog]$

python manage.py migrate

Como cambiamos autenticación de usuario tenemos que crear nuevamente un usuario de administración
(blog) [RETBOT@RETBOT blog]$ python manage.py createsuperuser
Username: admin
Email: [email protected]
Password:
Password (again):
Superuser created successfully.
(blog) [RETBOT@RETBOT blog]$

python manage.py createsuperuser

Login

Creamos una carpeta en plantillas llamada registration y dentro de la carpeta creamos un nuevo archivo llamado login.html
plantillas_login

<!-- plantillas/registration/login.html -->
{% extends 'base.html' %}
{% block title %}- Acceso{% endblock title %}

{% block content %}
<h1>Bienvenido</h1>
    <h2>Iniciar Sesión</h2><br>
    <form method="post">{% csrf_token %}
        {{ form.as_p }}
        <input class="btn btn-success ml-2" type="submit" value="Acceder (Iniciar Sesión)" style="color: black;">
    </form>
    <br>
    <a href="{% url 'password_reset' %}" type="button" class="formulario__submit" style="color: black;">¿Olvido su contraseña?</a>
{% endblock content %}

En la misma carpeta creamos un archivo llamado signup.html, el cual tendrá el siguiente código:

<!-- plantillas/registration/signup.html -->
{% extends 'base.html' %}
{% block title %}-Registro{% endblock title %} 

{% block content %}
<br><h2>Registro</h2><br>
<form method="post"> {% csrf_token %} 
    {{ form.as_p }}
    <input class="btn btn-success ml-2" type="submit" value="Registro" style="color: black;">
</form>
{% endblock content %} 

Agregamos la url para cambiar contraseña, salir, acceder e iniciar sesión a base.html

<a class="dropdown-item" href="{% url 'password_change' %}">
     Cambiar Contraseña
</a>
<a class="dropdown-item" href="{% url 'logout' %}">
     Salir
</a>
<a href="{% url 'login' %}" class="btn btn-outline-secondary">
  Acceder
</a>
<a href="{% url 'signup' %}" class="btn btn-primary ml-2">
  Registrarse
</a>

Ahora crearemos un archivo llamado password_change_done y password_change_form para cambiar la contraseña

<!-- plantillas/registration/password_change_form.htnl -->
{% extends 'base.html' %}

{% block title %}- Cambio de contrasela{% endblock title %}

{% block content %}

    <h2>Cambio de contraseña</h2>
    <p>Por favor introduzca su contrasela anterior.  por razones de seguridad,
        y luego introduzca su nueva contraseña dos veces para poder verificar que la escribió 
        correctamente.</p>
        <div class="registro">
            <div class="registro_contenido">
        <form  method="post">{% csrf_token %}{{ form.as_p }}
            <input class="btn btn-success ml-2" type="submit" value="Cambiar contraseña" style="color: black;">
        </form>
    </div>
</div>
{% endblock content %}
<!-- plantillas/registration/password_change_done.html -->
{% extends 'base.html' %}

{% block title %}- Contraseña modificada satisfactoriamente {% endblock title %}  

{% block content %}
    <h1>Contraseña modificada satisfactoriamente</h1>
    <p style="text-align: center;">Su contraseña fue modificada</p>
{% endblock content %}

Ahora para restaurar la contraseña debemos configurar el envió de correos electrónicos en django para eso necesitamos entrar a la carpeta config y en el archivo settings.py agregar los siguiente:
La primera linea sirve para hacer pruebas desde la consola

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Lo segundo sirve para configurar el envio de correos electronicos desde nuestra cuenta de google

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = '587'
EMAIL_HOST_USER = '[email protected]'
EMAIL_HOST_PASSWORD = 'contraseña'
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = '[email protected]' 

Para agregar la contraseña debemos entrar al perfil y en administrar tu cuenta>Seguridad
AdmCuenta
Seguridad

Debemos tener activada la verificación en 2 pasos
ver2pasos
y seleccionaremos la contraseña de aplicaciones
contrasenha
Seleccionamos generar y se mostrara la contraseña que ingresaremos EMAIL_HOST_PASSWORD en config/settings.py

Ya teniendo la configuración, crearemos los siguientes archivos
password_change_done.html
password_change_form.html
password_reset_complete.html
password_reset_confirm.html
password_reset_done.html
password_reset_form.html
plantillas

Los cuales tendran los siguientes codigos:

<!-- plantillas/registration/password_change_done.html -->
{% extends 'base.html' %}

{% block title %}- Contraseña modificada satisfactoriamente {% endblock title %}  

{% block content %}
    <h1>Contraseña modificada satisfactoriamente</h1>
    <p style="text-align: center;">Su contraseña fue modificada</p>
{% endblock content %}  
<!-- plantillas/registration/password_change_form.htnl -->
{% extends 'base.html' %}

{% block title %}- Cambio de contrasela{% endblock title %}

{% block content %}

    <h2>Cambio de contraseña</h2>
    <p>Por favor introduzca su contrasela anterior.  por razones de seguridad,
        y luego introduzca su nueva contraseña dos veces para poder verificar que la escribió 
        correctamente.</p>
        <div class="registro">
            <div class="registro_contenido">
        <form  method="post">{% csrf_token %}{{ form.as_p }}
            <input class="btn btn-success ml-2" type="submit" value="Cambiar contraseña" style="color: black;">
        </form>
    </div>
</div>
{% endblock content %}
<!-- plantillas/registration/password_reset_complete.htnl -->
{% extends 'base.html' %}

{% block title %}- Restablecimiento de contraseña completo {% endblock title %} 

{% block content %}
    <h1>Restablecimiento de contraseña completo</h1>
    <p>Su nueva contrasela ha sido reestablecida. <br>Puede iniciar sesión ahora en  la 
    <a href="{% url 'login' %}">Página de acceso</a>
    </p>
{% endblock content %}
<!-- plantillas/registration/password_reset_confirm.htnl -->
{% extends 'base.html' %}

{% block title %}- Introduzca su nueva contraseña {% endblock title %} 

{% block content %}
    <h1>Establezca su nueva contraseña</h1>
    <div class="registro">
        <div class="registro_contenido">
    <form method="post"> {% csrf_token %}
        {{ form.as_p }}
        <input class="btn btn-success ml-2" type="submit" value="Cambiar mi contraseña" style="color: black;">
    </form>
</div>
</div>
{% endblock content %}
<!-- plantillas/registration/password_reset_done.htnl -->
{% extends 'base.html' %}

{% block title %}- Email enviado {% endblock title %}

{% block content %}
    <h1>Verifique su buzón de entrada</h1>
    <p>Le hemos enviado instrucciónes para restablecer su contraseña, por una nueva.
        Deberoa de recibir el correo en unos minutos! </p>
{% endblock content %}
<!-- plantillas/registration/password_reset_form.htnl -->
{% extends 'base.html' %}

{% block content %}
    <h1>¿Olvido su contraseña?</h1>
    <p>Introdusca su email abajo, y le enviaremos instrucciones para establecer una nueva contraseña </p>
    <div class="registro">
        <div class="registro_contenido">
    <form method="post"> {% csrf_token %}
        {{ form.as_p }}
        <input class="btn btn-success ml-2" type="submit" value="Enviarme instrucciones" style="color: black;">
    </form>
    </div>
</div>
{% endblock content %}
    

Configuración de privacidad

Entramos a views.py de publicaciones e importamos lo siguiente

    from django.contrib.auth.mixins import (
    LoginRequiredMixin,
    UserPassesTestMixin
    )

Agregamos LoginRequiredMixin a la vista crear publicacion:

class VistaCrearPublicacion(LoginRequiredMixin, CreateView):
    model = Publicacion
    template_name = 'nueva_publicacion.html'
    fields = ['titulo', 'cuerpo','imagen']

    login_url = 'login'
    
    def form_valid(self, form):
        form.instance.autor = self.request.user
        return super().form_valid(form)

y LoginRequiredMixin, UserPassesTestMixin a editar y eliminar

class VistaEditarPublicacion(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Publicacion
    template_name = 'editar_publicacion.html'
    fields = ['titulo', 'cuerpo','imagen',]
    login_url = 'login'

    def test_func(self):
        obj = self.get_object()
        return obj.autor == self.request.user

class VistaEliminarPublicacion(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Publicacion
    template_name = 'eliminar_publicacion.html'
    success_url = reverse_lazy('lista_publicaciones')
    login_url = 'login'

    def test_func(self):
        obj = self.get_object()
        return obj.autor == self.request.user

y en settings.py de config agregamos, para redireccionar al usuario a la página de inicio:

LOGIN_REDIRECT_URL = 'inicio'
LOGOUT_REDIRECT_URL = 'inicio'  

Esto para que solo el creador de la publicación pueda eliminar la publicación

INSTALACION Y CONFIGURACION DE HEROKU

Heroku por así decirlo es como una nube, en la cual podremos subir nuestra aplicación web.
Existen diversas formas de instalar heroku en gnu/Linux, mas sin embargo utilizaremos una forma global de instalar en cualquier distribución Linux.
Para ello se hara con el siguiente comando : curl https://cli-assets.heroku.com/install.sh | sh

curl https://cli-assets.heroku.com/install.sh | sh

Una vez hecho esto, podremos continuar con los siguientes pasos.

CONFIGURACION DE HEROKU

Ejecutar pipenv lock para generar el Pipfile.lock apropiado

pipenv lock

Luego creamos un Procfile que le diga a Heroku como ejecutar el servidor remoto vivirá nuestro código.

touch Procfile

Por ahora, le estamos diciendo a Heroku en el archivo Procfile que use gunicorn como nuestro servidor de producción y busque en nuestro archivo config.wsgi para obtener mas instrucciones.

web: gunicorn config.wsgi –log-file – 

A continuación, instale gunicorn que usaremos en producción mientras seguimos usando el servidor interno de Django para uso de desarrollo local.

pipenv install gunicorn==19.9.0

Nos regresamos al visual studio a modificar nuestro proyecto de django, nos vamos a la carpeta de config/settings.py . Y modificaremos lo siguiente :

ALLOWED_HOST  = [‘*’] 

Esto lo hacemos para que la aplicación no marque error al intentar subir nuestro sitio a heroku
Con esto hemos terminado los primeros pasos, ahora lo que resta es mandarlo a git.

Iniciamos el proyecto de git

git init 

Vemos los archivos si hay archivos ya agregados

git status

En caso de que no agregamos con el parámetro –A todos los elementos del directorio actual.

git add –A 

Hacemos el commit mas un comentario para dar a conocer que hicimos en esta version

git commit –m “Nuevas actualizaciones para el despliegue en Heroku”

Lo mandamos a nuestro git hub para juntarlo con el proyecto

git push –u origin master 

Despliegue de Heroku

Iniciamos sesión en nuestra cuenta de Heroku.

heroku login

Luego, ejecute el comando de creación y Heroku creara aleatoriamente un nombre de aplicación para usted.

heroku create 

Configure git para usar el nombre de su nueva aplicación.

heroku git:remote –a nombre-proyecto-generado-1234

Le decimos a heroku que ignore los archivos estáticos.

heroku config:set DISABLE_COLLECTSTATIC=1

Hacemos el push al master de heroku

git push heroku master

Cambiamos el proyecto a un modelo gratuito.

heroku ps:scale web=1  

Proyecto

Pagina web: https://djangoxblog.herokuapp.com/

About

Curso Practico de Django desde Sistemas Operativos GNU Linux

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published