├── .gitignore ├── README.md ├── manage.py ├── media └── post │ └── photos │ └── create_user_custom.png ├── photogram ├── __init__.py ├── asgi.py ├── middleware.py ├── settings.py ├── urls.py ├── views.py └── wsgi.py ├── posts ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── readme_img ├── app_posts.png ├── create_user_custom.png ├── csrf_token.png ├── dashboard_login.png ├── dashboard_menu.png ├── dashboard_profile_list_custom.png ├── dashboard_users_add.png ├── dashboard_users_list.png ├── detalle_personalizado.png ├── django_2.png ├── estilado.gif ├── hello_world.png ├── middleware.png ├── numbers.png ├── post_hola_mundo.png ├── posts.png ├── posts_diccionario.png ├── posts_diccionario_titulos.png ├── posts_navbar.png ├── profile.png ├── profile_error.png ├── profile_error_styled.png ├── registed_protected.gif ├── static_folder.png ├── template_feed.png ├── templates.png ├── unregisted_protected.gif ├── url_params_1.png ├── url_params_2.png ├── user_dashboard_custom.png └── views.png ├── static ├── css │ ├── bootstrap.min.css │ └── main.css └── img │ ├── default-profile.png │ └── instagram.png ├── templates ├── base.html ├── nav.html ├── posts │ ├── feed.html │ └── new.html └── users │ ├── base.html │ ├── login.html │ ├── signup.html │ └── update_profile.html └── users ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations ├── 0001_initial.py └── __init__.py ├── models.py ├── pictures ├── dashboard_menu.png └── dashboard_users_list.png ├── tests.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | db.sqlite3 3 | __pycache__/ 4 | media/ 5 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Guía para proyectos en Django

3 |
4 | 5 |
6 | hello-world-capture 9 |
10 | 11 | # Tabla de contenido 12 | - [Preparando entorno](#Preparando-entorno) 13 | - [Configuración de entorno de trabajo](#configuración-de-entorno-de-trabajo) 14 | - [Creacion de entorno virtual con Python](#Creacion-de-entorno-virtual-con-Python) 15 | - [Comandos del entorno](#Comandos-del-entorno) 16 | - [Instalación de Django](#Instalación-de-Django) 17 | - [Django Admin](#Django-Admin) 18 | - [Creación de proyecto](#Creación-de-proyecto) 19 | - [Exploración de los archivos](#Exploración-de-los-archivos) 20 | - [settings.py](#Archivo-settings.py) 21 | - [manage.py](#Archivo-manage.py) 22 | - [Levantar servicio](#Levantar-servicio) 23 | - [Vistas](#Vistas) 24 | - [Crear la primera vista](#Crear-la-primera-vista) 25 | - [Como Django procesa un request](#Como-Django-procesa-un-request) 26 | - [Separando las vistas](#Separando-las-vistas) 27 | - [El objeto Request](#El-objeto-Request) 28 | - [Pasando argumentos por URL](#Pasando-argumentos-por-URL) 29 | - [Crear una app](#Crear-una-app) 30 | - [Template system](#Template-system) 31 | - [Pasando datos a nuestro template](#Pasando-datos-a-nuestro-template) 32 | - [Modelos](#Modelos) 33 | - [Dashboard de Administración](#Dashboard-de-Administración) 34 | - [Implementación de modelos](#Implementación-de-modelos) 35 | - [Implementar modelos en base de datos](#Implementar-modelos-en-base-de-datos) 36 | - [Reflejar modelos en dashboard de administración](#Reflejar-modelos-en-dashboard-de-administración) 37 | - [Dashboard administrativo personalizado](#Dashboard-administrativo-personalizado) 38 | - [Personalizando detalle de registro de modelo](#Personalizando-detalle-de-registro-de-modelo) 39 | - [Personalizando Dashboards Nativos](#Personalizando-Dashboards-Nativos) 40 | - [Relacionando modelos](#Relacionando-modelos) 41 | - [Hacer funcionar los links](#Hacer-funcionar-los-links) 42 | - [Templates, auth y middlewares](#Templates,-auth-y-middlewares) 43 | - [Archivos estáticos](##Archivos-estáticos) 44 | - [Templates](#Templates) 45 | - [Login y protegiendo vistas](#Login-y-protegiendo-vistas) 46 | - [Logout](#Logout) 47 | - [Signup](#Signup) 48 | - [Middlewares](#Middlewares) 49 | - [Forms](#Forms) 50 | - [Formularios en Django](#Formularios-en-Django) 51 | - [Mostrando el form en el template](#Mostrando-el-form-en-el-template) 52 | - [Models forms y validación](#Models-forms-y-validación) 53 | 54 | # Preparando entorno 55 | 56 | ## Configuración de entorno de trabajo 57 | Primero debemos tener instalado Python. Luego de la instalacion abrimos la terminal y nos posicionamos en la ruta que deseamos establecer nuestro proyecto. 58 | 59 | ## Creacion de entorno virtual con Python 60 | Vamos a crear un entorno virtual para nuestro proyecto, el cual contendra todas las dependencias. Es muy importante que este entorno este fuera de nuestro proyecto. Para crearlo ejecutamos: 61 | 62 | ``` 63 | python -m venv .env 64 | ``` 65 | 66 | Nota: .env sera el nombre de nuestro entorno. 67 | 68 | ## Comandos del entorno 69 | 70 | Para **activar** nuestro entorno ejecutamos 71 | 72 | ``` 73 | source .env/bin/activate 74 | ``` 75 | 76 | Y para **desactivarlo** 77 | 78 | ``` 79 | deactivate 80 | ``` 81 | 82 | Si queremos **listar las librerias instaladas** usamos 83 | 84 | ``` 85 | pip freeze 86 | ``` 87 | 88 | ## Instalación de Django 89 | Para **instalar** la ultima version de Django ejecutamos 90 | 91 | ``` 92 | pip install django -U 93 | ``` 94 | 95 | ## Django Admin 96 | Es una interfaz instalada junto con Django que contiene subcomandos que utiles. Para listar los subcomandos utilizamos 97 | 98 | ``` 99 | django-admin 100 | ``` 101 | 102 | ## Creación de proyecto 103 | Para **crear** un proyecto ejecutamos 104 | 105 | ``` 106 | django-admin startproject name 107 | ``` 108 | 109 | ## Exploración de los archivos 110 | Lo primero que veremos es un folder con el nombre de nuestro proyecto, el cual contiene los archivos: 111 | 112 | - **\_\_init_\_.py:** la unica finalidad de este archivo es declarar nuestra carpeta como un modulo de python. 113 | - **settings.py:** es el mas importante, define todas las configuraciones de nuestro proyecto. 114 | - **urls.py:** es el archivo de punto de entrada para todas las peticiones a nuestro proyecto. 115 | - **wsgi.py:** es usado para el deployment a produccion y es la interfaz WSGI cuando el servidor corre en producción. 116 | - **manage.py:** es un archivo que no tocamos, pero interactuamos con el durante todo el desarrollo. 117 | 118 | ### Archivo settings.py 119 | Dentro del archivo setting podemos encontrar variables relevantes para nuestro proyecto, las cuales son: 120 | - **BASE_DIR:** Declara el lugar donde esta corriendo el proyecto. Se considera la linea mas importante. 121 | - **SECRET_KEY:** Es utilizado para el hashing de las contraseñas y las sesiones que se almacenan en las bases de datos. 122 | - **DEBUG:** Determina si nuestro proyecto se encuentra en desarrollo. 123 | - **ALLOWED_HOSTS:** Lista los host que estan permitidos para interactuar con nuestro proyecto. 124 | - **INSTALLED_APPS:** Lista las aplicaciónes instaladas y ligadas a nuestro proyecto. 125 | - **MIDDLEWARE:** Lista los middleware instalados y ligados a nuestro proyecto. 126 | - **ROOT_URLCONF:** Define el archivo principal de urls. 127 | - **TEMPLATES:** Los templates de nuestras aplicaciónes. 128 | - **WSGI_APPLICATION:** Archivo de entrada de nuestro WSGI. 129 | - **DATABASES:** Almacena las configuraciones de las bases de datos. 130 | - **AUTH_PASSWORD_VALIDATORS:** Los validadores de contraseñas. 131 | - **LANGUAGE_CODE:** El idioma en el que se interactua con nuestra aplicación. 132 | - **TIME_ZONE:** Zona horaria en el cual corre nuestra aplicación. 133 | - **STATIC_URL:** En lugar de resolver la url establecidas en el archivo de urls, va a buscar resolver el archivo estático con la url estalecida en esta variable. 134 | 135 | ### Archivo manage.py 136 | Este archivo contiene un gran listado de subcomandos los cuales podemos listar con: 137 | 138 | ``` 139 | python manage.py 140 | ``` 141 | 142 | ## Levantar servicio 143 | Para levantar el servicio ejecutamos: 144 | 145 | ``` 146 | python manage.py runserver 147 | ``` 148 | 149 | # Vistas 150 | 151 | ## Crear la primera vista 152 | Para este ejercicio lo haremos simple. En el archivo **urls.py** importamos **django.http.HttpResponse** y definimos una **funcion** que devuelva una respuesta (en este caso hello_world), y establemos en que path estara esta despuesta: 153 | 154 | ```py 155 | from django.contrib import admin 156 | from django.urls import path 157 | from django.http import HttpResponse 158 | 159 | def hello_world(request): 160 | return HttpResponse('Hello, world!') 161 | 162 | urlpatterns = [ 163 | path('hello-world/', hello_world) 164 | ] 165 | ``` 166 | 167 | Corremos nuestro servidor con 168 | 169 | ``` 170 | python manage.py runserver 171 | ``` 172 | 173 | Luego accedemos al [**http://localhost:8000/hello-world**](http://localhost:8000/hello-world) donde podremos acceder a nuestra vista: 174 | 175 |
176 | hello-world-capture 179 |
180 | 181 | 182 | ## Como Django procesa un request 183 | 1. Primero va a buscar en el archivo **settings.py** en la variable **ROOT_URLCONF** 184 | 2. Luego Django desde el archivo **urls.py** carga los modulos de Python definidos en la variable **urlpatterns** 185 | 3. Dentro de **urlpatterns** se busca el patron coincidente a la peticion 186 | 4. Una vez encontrado la URL que coincide, Django importa y llama la vista en una funcion simple en Python. Se le pasa como argumento: 187 | - Una instancia del HttpRequest 188 | - Si la URL pasa mas argumentos entonces los entregara 189 | - Si definimos argumentos adicionales tambien lo enviara 190 | 5. Si ninguna URL coincide, Django enviara una excepción 191 | 192 | ## Separando las vistas 193 | Es buena practica tener las vistas separadas del archivo url.py, por lo que crearemos un archivo **views.py** dentro de nuestra aplicación que contendra las vistas: 194 | 195 |
196 | hello-world-capture 199 |
200 | 201 | Dentro de nuestro archivo **views.py** importamos **HttpResponse** y traemos nuestra funcion **hello_world()** creado en urls.py 202 | 203 | ```py 204 | from django.http import HttpResponse 205 | 206 | def hello_world(request): 207 | return HttpResponse('Hello, world!') 208 | ``` 209 | 210 | Ahora debemos importar nuestra funcion al archivo **urls.py**. 211 | No olvidemos **borrar** la importacion de HttpResponse y la funcion hello_world() en el archivo. 212 | 213 | ```py 214 | from django.contrib import admin 215 | from django.urls import path 216 | from photogram import views 217 | 218 | urlpatterns = [ 219 | path('hello-world/', views.hello_world) 220 | ] 221 | ``` 222 | 223 | Si revisamos la url [**http://localhost:8000/hello-world**](http://localhost:8000/hello-world) nuestro proyecto seguira funcionando. 224 | 225 | ## El objeto Request 226 | A traves del objeto request podemos acceder a varios atributos los cuales se encuentran detallados en la [documentación](https://docs.djangoproject.com/en/3.0/ref/request-response/) de Django. Algunos atributos utiles son: 227 | 228 | - **request.method:** nos muestra el metodo HTTP ("GET", "POST", etc.) usado por el request en formato de string en UPPERCASE. Un ejemplo de uso seria: 229 | 230 | ```py 231 | if request.method == 'GET': 232 | do_something() 233 | elif request.method == 'POST': 234 | do_something_else() 235 | ``` 236 | 237 | - **request.GET:** Un diccionario que contiene todos los parametros entregados por HTTP GET. Por ejemplo: 238 | 239 | Pasamos una lista de numeros en la variable numbers **(?numbers)** 240 | 241 | ```http 242 | http://localhost:8000/numbers/?numbers=10,2,6,7 243 | ``` 244 | 245 | Para acceder a la lista usamos 246 | 247 | ```py 248 | request.GET['numbers'] 249 | ``` 250 | 251 | *Nota: En el siguiente ejemplo se creo la vista numbers* 252 | 253 | Un ejemplo practico seria: 254 | 255 | ```py 256 | def numbers(request): 257 | numbers = request.GET['numbers'] 258 | return HttpResponse(str(numbers)) 259 | ``` 260 | 261 | De esta forma podemos ver los valores de number a traves de nuetra vista. 262 | 263 |
264 | numbers 267 |
268 | 269 | ## Pasando argumentos por URL 270 | Podemos pasar argumentos a traves de la URL, para esto primero creamos la funcion que hara uso de estos parametros y devolvera la vista en el archivo **views.py** 271 | ```py 272 | from django.http import HttpResponse 273 | 274 | def say_hi(request, name, age): 275 | if age < 12: 276 | message = 'Sorry {}, you are not allowed here'.format(name) 277 | else: 278 | message = 'Hello {}! Welcome to Photogram'.format(name) 279 | 280 | return HttpResponse(message) 281 | ``` 282 | 283 | Luego definimos el path para esta vista en el archivo **urls.py**. Para definir los parametros que pasaran por la url los encerramos con "<>" definiendo el tipo de dato y el nombre del parametro. 284 | 285 | ```py 286 | from django.contrib import admin 287 | from django.urls import path 288 | from photogram import views 289 | 290 | urlpatterns = [ 291 | path('hi///', views.say_hi) 292 | ] 293 | ``` 294 | 295 | En el resultado final si ingresamos **age = 26** y **name = Karl** obtenemos el resultado definido en nuestra funcion **say_hi()**: 296 | 297 |
298 | numbers 301 |
302 | 303 | Pero si cambiamos **age = 10** obtenemos: 304 | 305 |
306 | numbers 309 |
310 | 311 | ## Crear una app 312 | Con Django podemos crear una app de forma rapida y sencilla ejecutando el comando 313 | 314 | ``` 315 | python manage.py startapp name 316 | ``` 317 | 318 | En este ejemplo creamos un app llamada **posts**, el cual genero una carpeta con todos los archivos basicos necesarios 319 | 320 |
321 | numbers 324 |
325 | 326 | Para desplegar una vista de esta aplicacion vamos al archivo *./posts/views.py* donde crearemos una vista a traves de la funcion **list_posts()** 327 | 328 | ```py 329 | from django.shortcuts import render 330 | from django.http import HttpResponse 331 | 332 | def list_posts(request): 333 | posts = [1, 2, 3, 4] 334 | return HttpResponse(str(posts)) 335 | ``` 336 | 337 | Luego vamos al archivo settings de nuestro proyecto, en este caso *./photogram/settings.py* donde incorporaremos en la variable **INSTALLED_APPS** nuestra nueva app 338 | 339 | ```py 340 | INSTALLED_APPS = [ 341 | # Django apps 342 | 'django.contrib.admin', 343 | 'django.contrib.auth', 344 | 'django.contrib.contenttypes', 345 | 'django.contrib.sessions', 346 | 'django.contrib.messages', 347 | 'django.contrib.staticfiles', 348 | 349 | # Local apps 350 | 'posts', 351 | ] 352 | ``` 353 | 354 | Ahora nos toca asignar un path para nuestra vista **list_posts()**. Para eso vamos al archivo **urls.py** de nuestro proyecto, en este caso *./photogram/urls.py* e importamos nuestra nueva app, y le asignamos un path a nuestra vista. 355 | 356 | Para que no existan conflictos al llamar views vamos asignar un **alias** para las views de cada aplicacion. 357 | 358 | ```py 359 | from django.contrib import admin 360 | from django.urls import path 361 | from photogram import views as local_views 362 | 363 | # Importamos las vistas de nuestra aplicacion posts 364 | from posts import views as posts_views 365 | 366 | urlpatterns = [ 367 | 368 | path('hello-world/', local_views.hello_world), 369 | path('numbers/', local_views.numbers), 370 | path('hi///', local_views.say_hi), 371 | 372 | # Asignamos el path para nuestra vista list_posts 373 | path('posts/', posts_views.list_posts), 374 | 375 | ] 376 | ``` 377 | 378 | Ahora vamos a [**http://localhost:8000/posts/**](http://localhost:8000/posts/) para ver nuestro resultado 379 | 380 |
381 | numbers 384 |
385 | 386 | ## Template system 387 | 388 | El template system es una manera de mostrar los datos usando HTML, incluye lógica de programacion lo cual nos facilita un poco el crear nuestros templates. 389 | 390 | Para crear nuestros templates lo que haremos es dentro de nuestra aplicacion **crear una carpeta templates** y un **archivo html** con el nombre de nuestro template, en este caso _feed.html_ 391 | 392 |
393 | numbers 396 |
397 | 398 | Dentro de nuestro archivo **feed.html** solo escribiremos: 399 | 400 | ```html 401 | Hola, mundo! 402 | ``` 403 | 404 | Y dentro de **views.py** de nuestra aplicación ya no es necesario el HttpResponse, por que borramos su importación. A través de la función que devolvemos nuestra vista devolveremos nuestro nuevo template con el metodo **render**, que le pasaremos la request y la vista: 405 | 406 | ```py 407 | from django.shortcuts import render 408 | 409 | def list_posts(request): 410 | return render(request, 'feed.html') 411 | ``` 412 | 413 | Si revisamos el path [**http://localhost:8000/posts/**](http://localhost:8000/posts/) tendremos nuestro "Hola, mundo!" 414 | 415 |
416 | numbers 419 |
420 | 421 | ¿Como logro funcionar si dentro de render jamas definimos la ruta donde buscar nuestro template? (en nuetro caso solo _feed.html_). Si revisamos en el archivo **settings.py** de nuestro proyecto, en la definicion de **TEMPLATES** veremos 422 | 423 | ```py 424 | 425 | ... 426 | 427 | TEMPLATES = [ 428 | { 429 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 430 | 'DIRS': [], 431 | 'APP_DIRS': True, 432 | 'OPTIONS': { 433 | 'context_processors': [ 434 | 'django.template.context_processors.debug', 435 | 'django.template.context_processors.request', 436 | 'django.contrib.auth.context_processors.auth', 437 | 'django.contrib.messages.context_processors.messages', 438 | ], 439 | }, 440 | }, 441 | ] 442 | 443 | ... 444 | 445 | ``` 446 | 447 | En **APP_DIRS** lo tenemos definido como **True**, esto significa que las aplicaciones buscaran los templates dentro de sus directorios, de esta forma funciona sin tener que nombrar la dirección de nuestro template. 448 | 449 | ## Pasando datos a nuestro template 450 | 451 | Primero crearemos un diccionario de datos dentro de nuestra vista (solo a modo de ejemplo) y enviaremos al template estos datos a traves del render. En nuestro caso este diccionario sera posts 452 | 453 | ```py 454 | # Django 455 | from django.shortcuts import render 456 | 457 | # Utilities 458 | from datetime import datetime 459 | 460 | posts = [ 461 | { 462 | 'title': 'Mont Blanc', 463 | 'user': { 464 | 'name': 'Yésica Cortés', 465 | 'picture': 'https://picsum.photos/60/60/?image=1027' 466 | }, 467 | 'timestamp': datetime.now().strftime('%b %dth, %Y - %H:%M hrs'), 468 | 'photo': 'https://picsum.photos/800/600?image=1036', 469 | }, 470 | { 471 | 'title': 'Via Láctea', 472 | 'user': { 473 | 'name': 'Christian Van der Henst', 474 | 'picture': 'https://picsum.photos/60/60/?image=1005' 475 | }, 476 | 'timestamp': datetime.now().strftime('%b %dth, %Y - %H:%M hrs'), 477 | 'photo': 'https://picsum.photos/800/800/?image=903', 478 | }, 479 | { 480 | 'title': 'Nuevo auditorio', 481 | 'user': { 482 | 'name': 'Uriel (thespianartist)', 483 | 'picture': 'https://picsum.photos/60/60/?image=883' 484 | }, 485 | 'timestamp': datetime.now().strftime('%b %dth, %Y - %H:%M hrs'), 486 | 'photo': 'https://picsum.photos/500/700/?image=1076', 487 | } 488 | ] 489 | 490 | def list_posts(request): 491 | """List existing posts.""" 492 | return render(request, 'feed.html', {'posts': posts}) 493 | ``` 494 | 495 | Si logran observar enviamos los datos a traves de **{'posts': posts}**, el cual el **primer parametro sera el nombre de la variable** al momento de enviar al template, y el **segundo es el valor asignado**. 496 | 497 | En nuestro template _feed.html_ ahora imprimiremos nuestro diccionario escribiendo el **nombre de la variable**. 498 | 499 | ```html 500 | {{ posts }} 501 | ``` 502 | 503 | Si revisamos [**http://localhost:8000/posts/**](http://localhost:8000/posts/) veremos nuestro diccionario. 504 | 505 |
506 | numbers 509 |
510 | 511 | Ahora juguemos un poco con la **lógica de programación** y **html**. Vamos a imprimir solo los títulos. Para eso en nuestro **template** _feed.html_ escribiremos: 512 | 513 | ```html 514 | {% for post in posts %} 515 |

{{ post.title }}

516 | {% endfor %} 517 | ``` 518 | 519 | Y el resultado en [**http://localhost:8000/posts/**](http://localhost:8000/posts/) 520 | 521 |
522 | numbers 525 |
526 | 527 | Para ver toda la **lógica de programación** que podemos crear en el template system te recomiendo ir a la [documentación de Django.](https://docs.djangoproject.com/en/3.0/ref/templates/builtins/) 528 | 529 | Ahora despleguemos los datos de nuestro diccionario y estilemos con **Bootstrap** nuestro template _feed.html_. 530 | 531 | ```html 532 | 533 | 534 | 535 | 536 | Platzigram 537 | 538 | 539 | 540 |

541 |
542 |
543 | {% for post in posts %} 544 |
545 |
546 | {{ post.user.name }} 547 |
548 |
{{ post.user.name }}
549 | {{ post.timestamp }} 550 |
551 |
552 | {{ post.title }} 553 |
{{ post.title }}
554 |
555 | {% endfor %} 556 |
557 |
558 | 559 | 560 | 561 | ``` 562 | 563 | Y en [**http://localhost:8000/posts/**](http://localhost:8000/posts/) veremos 564 | : 565 |
566 | numbers 569 |
570 | 571 | ## Creacion de Super Usuario 572 | 573 | Para crear un **super usuario** en Django es bastante facil. En la consola escribimos 574 | 575 | ``` 576 | python3 manage.py createsuperuser 577 | ``` 578 | 579 | Nos preguntara un **username, email (opcional), y contrañesa**, con esto ya tendriamos nuestro super usuario. 580 | 581 | # Modelos 582 | 583 | ## Dashboard de Administración 584 | 585 | Django cuenta con un dashboard de administración. Para acceder a el debemos darle un path dentro del archivo **urls.py** de nuestro proyecto. Para esto importamos **django.contrib.admin** y le asignamos la dirección que deseamos 586 | 587 | ```py 588 | from django.contrib import admin 589 | from django.urls import path 590 | 591 | urlpatterns = [ 592 | path('admin/', admin.site.urls), 593 | ``` 594 | 595 | En este caso le dimos el path **/admin/** para acceder a el. Entonces vamos a la dirección [**http://localhost:8000/admin/**](http://localhost:8000/admin/) para ingresar. 596 | 597 |
598 | numbers 601 |
602 | 603 | Para ingresar utilizaremos el **super usuario** que creamos en la [**sección de creación de super usuario.**](#creacion-de-super-usuario) 604 | 605 | ## Implementación de modelos 606 | 607 | Con Django podemos crear modelos de clases de nuestra aplicación. 608 | 609 | Para estos ejemplos crearemos una nueva aplicacion de usuarios en nuestro proyecto. 610 | 611 | ``` 612 | python manage.py startapp users 613 | ``` 614 | 615 | En el archivo **models.py** de la nueva aplicación crearemos el modelo de nuestros usuarios, el cual sera una clase _Profile_, y los tipos de valores para la clase _models_ estan definidos en la [documentación.](https://docs.djangoproject.com/en/3.0/ref/models/fields/) 616 | 617 | Para poder cargar las referencias de imagenes en neustro modelo instalaremos en nuestro ambiente Pillow, esto nos servira para la siguiente sección. 618 | 619 | ``` 620 | pip install pillow 621 | ``` 622 | 623 | Ahora creamos el modelo de usuarios. 624 | 625 | ```py 626 | from django.contrib.auth.models import User 627 | from django.db import models 628 | 629 | class Profile(models.Model): 630 | 631 | user = models.OneToOneField(User, on_delete=models.CASCADE) 632 | 633 | website = models.URLField(max_length=200, blank=True) 634 | biography = models.TextField(blank=True) 635 | phone_number = models.CharField(max_length=20, blank=True) 636 | 637 | picture = models.ImageField( 638 | upload_to='users/pictures', 639 | blank=True, 640 | null=True 641 | ) 642 | 643 | created = models.DateTimeField(auto_now_add=True) 644 | modified = models.DateTimeField(auto_now=True) 645 | 646 | def __str__(self): 647 | return self.user.username 648 | ``` 649 | 650 | ## Implementar modelos en base de datos 651 | 652 | Los modelos creados en nuestras aplicaciones podemos aplicarlos en el esquema de nuestra base de datos. Primero debemos auditar los cambios en los modelos con: 653 | 654 | ``` 655 | python manage.py makemigration 656 | ``` 657 | 658 | Ahora aplicaremos los cambios auditados en nuestra base de datos. 659 | 660 | ``` 661 | python manage.py migrate 662 | ``` 663 | 664 | ## Reflejar modelos en dashboard de administración 665 | 666 | En primera instancia no podremos ver los _modelos_ que creamos en el dashboard de administración. La clase **ModelAdmin** es la representacion del modelo en la interfaz de administración. Para reflejarlo debemos almacenar el _modelo_ en el archivo **admin.py** de nuestra aplicación. 667 | 668 | ```py 669 | # Django 670 | from django.contrib import admin 671 | 672 | # Modelos 673 | from users.models import Profile 674 | 675 | # Registramos nuestros modelos aquí. 676 | admin.site.register(Profile) 677 | ``` 678 | 679 | De esta forma tendremos la interfaz de administración predeterminada, en nuestro caso incluimos el modelo **Profile**. 680 | 681 |
682 | 686 |
687 |
688 | 692 |
693 |
694 | 698 |
699 | 700 | ## Dashboard administrativo personalizado 701 | 702 | Si queremos mostrar nuestra lista de modelos de una forma personalizada, con Django podemos realizarlo. Para esto debemos crear una clase **ModelAdmin** 703 | 704 | ```py 705 | # Django 706 | from django.contrib import admin 707 | 708 | # Modelo 709 | from users.models import Profile 710 | 711 | # Decoramos la clase con el modelo. 712 | @admin.register(Profile) 713 | class ProfileAdmin(admin.ModelAdmin): #Por convencion la clase que creemos debe terminar en Admin. 714 | 715 | # Con list_display nombramos los campos que queremos visualizar. 716 | list_display = ('pk', 'user', 'phone_number', 'website', 'picture') 717 | 718 | # list_display_links establece como links los campos nombrados. 719 | list_display_links = ('pk', 'user') 720 | 721 | # list_editable nos permite editar el campo desde 722 | # la lista del modelo en vez de ingresar al detalle del registro. 723 | list_editable = ('phone_number',) 724 | 725 | # Para crear un buscador hacemos uso de search_fields. 726 | # Los campos que se ingresan seran los que el buscador recorrera para realizar las busquedas. 727 | search_fields = ( 728 | 'user__email', 729 | 'user__username', 730 | 'user__first_name', 731 | 'user__last_name', 732 | 'phone_number' 733 | ) 734 | 735 | # Podemos crear un filtro para nuestro dashboard del modelo, 736 | # para ello usamos list_filter, y definimos los campos con los que trabajara. 737 | list_filter = ( 738 | 'user__is_active', 739 | 'user__is_staff', 740 | 'created', 741 | 'modified' 742 | ) 743 | ``` 744 | 745 |
746 | 750 |
751 | 752 | Para mas opciones de personalización siempre puedes revisar la [documentación.](https://docs.djangoproject.com/en/3.0/ref/contrib/admin/#modeladmin-options) 753 | 754 | ## Personalizando detalle de registro de modelo 755 | 756 | Podemos perzonalizar nuestros dashbord del registro. En nuestro caso lo haremos para el modelo de usuarios _Profile_. 757 | 758 | ```py 759 | # Django 760 | from django.contrib import admin 761 | 762 | # Models 763 | from users.models import Profile 764 | 765 | @admin.register(Profile) 766 | class ProfileAdmin(admin.ModelAdmin): 767 | list_display = ('pk', 'user', 'phone_number', 'website', 'picture') 768 | list_display_links = ('pk', 'user',) 769 | list_editable = ('phone_number', 'website', 'picture') 770 | search_fields = ( 771 | 'user__email', 772 | 'user__username', 773 | 'user__first_name', 774 | 'user__last_name', 775 | 'phone_number' 776 | ) 777 | list_filter = ( 778 | 'user__is_active', 779 | 'user__is_staff', 780 | 'created', 781 | 'modified' 782 | ) 783 | 784 | # Nos desplagara los datos que deseamos. Es importante que la información este en tuplas. 785 | fieldsets= ( 786 | ('Profile', { # Nombre de la sección. 787 | 'fields': ( # Los campos que visualizaremos. 788 | # Cuando ponemos varios campos en la misma posición dentro de 789 | # la tupla de field vamos a desplegar los datos en la misma fila. 790 | ('user', 'picture'), 791 | ), 792 | }), 793 | ('Extra info', { 794 | 'fields': ( 795 | # En este caso la información se desplegara 796 | # en 2 filas ya que la tupla de fields tiene 2 posiciones. 797 | ('website', 'phone_number'), 798 | ('biography',), 799 | ), 800 | }), 801 | ('Metadata', { 802 | 'fields': ( 803 | # Estos datos no se pueden modificar, 804 | # por lo que haremos uso de readonly_fields. 805 | ('created', 'modified'), 806 | ), 807 | }), 808 | ) 809 | 810 | # Aqui declararemos los campos que solo pueden ser leidos pero no modificados. 811 | readonly_fields = ('created', 'modified',) 812 | ``` 813 |
814 | 818 |
819 | 820 | En la [documentación](https://docs.djangoproject.com/en/3.0/ref/contrib/admin/#modeladmin-options) tenemos muchas mas formas de personalización. 821 | 822 | ## Personalizando Dashboards Nativos 823 | 824 | Existe la posibilidad de personalizar los dashboard nativos de Django, para ello vamos a trabajar sobre el modelo de **Users** para el cual vamos a visualizar los datos que definamos y tambien al momento de crear un usuario tambien podremos crear dentro del proceso una instancia de nuestro modelo _Profile_ 825 | 826 | ```py 827 | # Django 828 | # Importamos UserAdmin 829 | from django.contrib.auth.admin import UserAdmin as BaseUserAdmin 830 | from django.contrib import admin 831 | 832 | # Models 833 | # Tambien haremos uso de User 834 | from django.contrib.auth.models import User 835 | from users.models import Profile 836 | 837 | @admin.register(Profile) 838 | class ProfileAdmin(admin.ModelAdmin): 839 | list_display = ('pk', 'user', 'phone_number', 'website', 'picture') 840 | list_display_links = ('pk', 'user',) 841 | list_editable = ('phone_number', 'website', 'picture') 842 | search_fields = ( 843 | 'user__email', 844 | 'user__username', 845 | 'user__first_name', 846 | 'user__last_name', 847 | 'phone_number' 848 | ) 849 | list_filter = ( 850 | 'user__is_active', 851 | 'user__is_staff', 852 | 'created', 853 | 'modified' 854 | ) 855 | 856 | fieldsets= ( 857 | ('Profile', { 858 | 'fields': ( 859 | ('user', 'picture'), 860 | ), 861 | }), 862 | ('Extra info', { 863 | 'fields': ( 864 | ('website', 'phone_number'), 865 | ('biography',), 866 | ), 867 | }), 868 | ('Metadata', { 869 | 'fields': ( 870 | ('created', 'modified'), 871 | ), 872 | }), 873 | ) 874 | 875 | readonly_fields = ('created', 'modified',) 876 | 877 | # Aqui definiremos el modelo que deseamos asociar a User, en nuestro caso Profile. 878 | class ProfileInline(admin.StackedInline): 879 | model = Profile 880 | can_delete = False 881 | verbose_name_plural = 'profiles' 882 | 883 | # Luego para asociar los modelos e insertarlo en el Dashboard usaremos 884 | # el UserAdmin de Django el cual le dimos el alias de BaseUserAdmin. 885 | class UserAdmin(BaseUserAdmin): 886 | inlines = (ProfileInline,) # Con inlines desplegaremos los campos que hay que llenar asociados a Profile. 887 | list_display = ( # En list_display 888 | 'username', 889 | 'email', 890 | 'first_name', 891 | 'last_name', 892 | 'is_active', 893 | 'is_staff' 894 | ) 895 | 896 | admin.site.unregister(User) 897 | admin.site.register(User, UserAdmin) 898 | ``` 899 | 900 | Si vamos a crear un nuevo **User** podremos encontrar los campos asociados a nuestro modelo _Profile_ que definimos en la variable _inlines_ 901 | 902 |
903 | 907 |
908 | 909 | Y si revisamos la lista de registro **User** veremos los cambios realizados en la variable _list_display_. 910 | 911 |
912 | 916 |
917 | 918 | En la [documentación](https://docs.djangoproject.com/en/3.0/ref/contrib/admin/#modeladmin-options) tenemos muchas mas formas de personalización. 919 | 920 | ## Relacionando modelos 921 | 922 | ¿Que pasa si en nuestro proyecto un modelo depende de otro? Un ejemplo de esto puede ser un **_post_** que solo es posible que exista si esta relacionado con un **_usuario_**. Afortunadamente en Django podemos relacionar los modelos, en nuestro caso lo haremos con el modelo de **posts**, por lo iremos al archivo _posts/models.py_. 923 | 924 | ```py 925 | # Django 926 | from django.db import models 927 | from django.contrib.auth.models import User 928 | 929 | class Post(models.Model): 930 | 931 | user = models.ForeignKey(User, on_delete=models.CASCADE) 932 | # Con ForeignKey podemos relacionar el modelo de posts con profile, 933 | # y para hacer referencia a la clase relacionada lo hacemos con 934 | # el formato de 'aplicacion.NombreClaseDelModelo'. 935 | profile = models.ForeignKey('users.Profile', on_delete=models.CASCADE) 936 | 937 | title = models.CharField(max_length=255) 938 | photo = models.ImageField(upload_to='post/photos') 939 | 940 | created = models.DateTimeField(auto_now_add=True) 941 | modified = models.DateTimeField(auto_now=True) 942 | 943 | def __str__(self): 944 | return '{} by @{}'.format(self.title, self.user.username) 945 | ``` 946 | 947 | ## Hacer funcionar los links 948 | 949 | ¿Te fijaste que los links de los campos de nuestros registros nos llevaba al detalle de estos? Para que estos links nos lleven realmente a sus referencias debemos realizar algunos cambios en el archivo **urls.py** y **settings.py**. 950 | 951 | En nuestro archivo **settings.py** declararemos 2 variables en el fondo del archivo. 952 | 953 | ```py 954 | ... 955 | 956 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 957 | MEDIA_URL = '/media/' 958 | ``` 959 | 960 | Luego iremos al archivo **urls.py** y a _urlpatterns_ donde tenemos definidos los path de nuestras aplicaciones vamos a concatenar un valor static 961 | 962 | ```py 963 | from django.contrib import admin 964 | from django.conf import settings 965 | from django.conf.urls.static import static 966 | from django.urls import path 967 | 968 | from photogram import views as local_views 969 | from posts import views as posts_views 970 | 971 | urlpatterns = [ 972 | 973 | path('admin/', admin.site.urls), 974 | 975 | path('hello-world/', local_views.hello_world), 976 | path('numbers/', local_views.numbers), 977 | path('hi///', local_views.say_hi), 978 | 979 | path('posts/', posts_views.list_posts), 980 | 981 | # concatenamos static con los valores definidos en settings.py 982 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 983 | ``` 984 | 985 | Con esto estaría todo listo para que los valores definidos como links en los dashboard funcionen correctamente. 986 | 987 | # Templates, auth y middlewares 988 | 989 | ## Archivos estáticos 990 | 991 | Los archivos estáticos son elementos de nuestro proyecto que podremos usar de forma transversal. Técnicamente podemos usar tipo de elemento como estético pero por lo general se hacen uso de **css** e **imágenes.** 992 | 993 | En la raíz de nuestro proyecto crearemos una carpeta llamada _static_, y en ella contendra otras 2 carpetas llamadas _css_ y _img_. Estas van a contener nuestros archivos **css** e **imagenes** respectivamente. 994 | 995 |
996 | 1000 |
1001 | 1002 | Ahora vamos al archivo _settings.py_ de nuestro proyecto. Justo debajo de la variable **STATIC_URL** vamos a pegar las variables de **STATICFILES_DIRS** y **STATICFILES_FINDERS**. 1003 | 1004 | ```py 1005 | ... 1006 | 1007 | STATICFILES_DIRS = ( 1008 | os.path.join(BASE_DIR, 'static'), 1009 | ) 1010 | STATICFILES_FINDERS = [ 1011 | 'django.contrib.staticfiles.finders.FileSystemFinder', 1012 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 1013 | ] 1014 | 1015 | ... 1016 | ``` 1017 | 1018 | Con esto tus archivos estáticos ya pueden ser referenciados. 1019 | 1020 | ## Templates 1021 | 1022 | Los templates de nuestro proyecto tienen la capacidad de **extenderse** desde otros templates, asi podremos reutilizar los elementos que deseamos, como por ejemplo un _navbar_. 1023 | 1024 | Para preparar todo iremos al archivo _settings.py_, y en la variable **TEMPLATES** vamos a definir donde buscar los **templates** para nuestro proyecto. 1025 | 1026 | ```py 1027 | TEMPLATES = [ 1028 | { 1029 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 1030 | 'DIRS': [ 1031 | # Aqui definimos la carpeta donde ira a buscar el template nuestras aplicaciones. 1032 | os.path.join(BASE_DIR, 'templates'), 1033 | ], 1034 | 'APP_DIRS': True, 1035 | 'OPTIONS': { 1036 | 'context_processors': [ 1037 | 'django.template.context_processors.debug', 1038 | 'django.template.context_processors.request', 1039 | 'django.contrib.auth.context_processors.auth', 1040 | 'django.contrib.messages.context_processors.messages', 1041 | ], 1042 | }, 1043 | }, 1044 | ] 1045 | ``` 1046 | 1047 | Ln la **raíz** de nuestro proyecto crearemos la carpeta _templates_ definido anteriormente y dentro de este crearemos todos los elementos compartidos, como por ejemplo un **navbar, base, etc.** Para los templates **no compartidos** que deseamos agregar vamos a crear **carpetas** de estos elementos. Para nuestro ejemplo vamos a crear los templates compartidos de **base** y **navbar**, y para los elementos particulares crearemos las carpetas **posts** y **users**, con los archivos _feed.html_ y _base.html_ respectivamente. 1048 | 1049 |
1050 | 1054 |
1055 | 1056 | Primero vamos a crear nuestro navbar en el archivo _templates/**nav.html**_ y haremos referencias a nuestros **archivos estáticos** creados en la [sección anterior.](#Archivos-estáticos) 1057 | 1058 | ```html 1059 | 1060 | {% load static %} 1061 | 1095 | ``` 1096 | 1097 | Sin embargo nuestro **navbar** aun no aparecera en nuestra aplicación. Para esto crearemos el archivo _templates/**base.html**_ y como lo hicimos en el archivo anterior vamos a cargar los **archivos estáticos.** Pero no nos detengamos ahí, tambien definamos el **bloque del head** y el **container que desplegara los templates** que se extenderan. 1098 | 1099 | ```html 1100 | 1101 | 1102 | 1103 | 1104 | 1105 | {% block head_content %}{% endblock %} 1106 | 1107 | 1108 | {% load static %} 1109 | 1110 | 1111 | 1112 | 1113 | 1114 | 1115 | 1116 | {% include "nav.html" %} 1117 | 1118 |
1119 | 1120 | {% block container %} 1121 | {% endblock%} 1122 |
1123 | 1124 | 1125 | 1126 | ``` 1127 | 1128 | Con esto ya desplegamos nuestro template **navbar** dentro de **base**, pero no nos detengamos ahí. Vamos a crear el template para **posts** que se **extendera** del archivo _templates/base.html_, el cual sera _templates/posts/**feed.html**_ 1129 | 1130 | ```html 1131 | 1132 | {% extends "base.html" %} 1133 | 1134 | 1135 | {% block head_content %} 1136 | Platzigram feed 1137 | {% endblock %} 1138 | 1139 | 1140 | {% block container %} 1141 |
1142 | {% for post in posts %} 1143 |
1144 |
1145 | {{ post.user.name }} 1146 |
1147 |
{{ post.user.name }}
1148 | {{ post.timestamp }} 1149 |
1150 |
1151 | {{ post.title }} 1152 |
{{ post.title }}
1153 |
1154 | {% endfor %} 1155 |
1156 | {% endblock %} 1157 | ``` 1158 | 1159 | Como ahora este template esta fuera de la aplicación debemos referenciarla en el render de la vista, por lo que iremos a _posts/views.py_ a realizar los cambios. Lo referenciaremos como _**posts/feed.html**_ que hace referencia al path de _template/posts/feed.html_. **No es necesario definir la carpeta _templates_** ya que en el archivo _settings.py_ definimos que **los templates seran buscados en esta carpeta**. 1160 | 1161 | ```py 1162 | ... 1163 | 1164 | def list_posts(request): 1165 | # En la función que nos devuelve el render, debemos referenciar correctamente el template, 1166 | # en este caso a posts/feed.html 1167 | return render(request, 'posts/feed.html', {'posts': posts}) 1168 | ``` 1169 | 1170 | Ahora si revisamos el path de la aplicación [http://localhost:8000/posts/](http://localhost:8000/posts/) veremos el template de **base**, **navbar** y **posts** desplegados correctamente, ademas del head definido en _templates/posts/feed.html_ 1171 | 1172 |
1173 | 1177 |
1178 | 1179 | ## Login y protegiendo vistas 1180 | 1181 | Vamos a crear el login de nuestra aplicación, y este estara alojado en la aplicaciónde **users**. Tambien protegeremos las vistas de **posts** para solo poder acceder a ellas cuando estemos iniciados. 1182 | 1183 | Primero que todo llego la hora de poner **alias a las rutas** de nuestro proyecto, de esta forma podemos referenciar al alias en cualquier parte de nuestra aplicación sin preocuparnos si cambian el path, para esto iremos a _urls.py_ 1184 | 1185 | ```py 1186 | from django.contrib import admin 1187 | from django.conf import settings 1188 | from django.conf.urls.static import static 1189 | from django.urls import path 1190 | 1191 | from photogram import views as local_views 1192 | from posts import views as posts_views 1193 | from users import views as users_views 1194 | 1195 | urlpatterns = [ 1196 | 1197 | path('admin/', admin.site.urls), 1198 | 1199 | # A los path podemos asignarles valores a la variable name indicando un alias a la ruta 1200 | path('hello-world/', local_views.hello_world, name='hello_world'), 1201 | path('numbers/', local_views.numbers, name='sort'), 1202 | path('hi///', local_views.say_hi, name='hi'), 1203 | 1204 | path('posts/', posts_views.list_posts, name='feed'), 1205 | 1206 | path('users/login/', users_views.login_view, name='login') 1207 | 1208 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 1209 | ``` 1210 | 1211 | En el archivo _users/**views.py**_ vamos a renderizar el **login** 1212 | 1213 | ```py 1214 | # Django 1215 | # Importamos authenticate y login 1216 | from django.contrib.auth import authenticate, login 1217 | # Redirect nos ayudara a redireccionarnos a otro path 1218 | from django.shortcuts import render, redirect 1219 | 1220 | def login_view(request): 1221 | if request.method == 'POST': 1222 | username = request.POST['username'] 1223 | password = request.POST['password'] 1224 | # El metodo authenticate tratara de contrastar el usuario 1225 | # con una instancia del modelo users que creamos. 1226 | user = authenticate(request, username=username, password=password) 1227 | if user: 1228 | # En caso de ser exitoso la autenticación creara 1229 | # un token de nuestro usuario para almacenarlo en memoria. 1230 | login(request, user) 1231 | # Y nos redireccionaremos al path con alias 'feed' que es 'posts/' 1232 | return redirect('feed') 1233 | else: 1234 | # En caso de dar false la autenticacion volveremos a renderizar el login, 1235 | # pero enviando la variable 'error' 1236 | return render(request, 'users/login.html', {'error': 'Invalid username and password'}) 1237 | return render(request, 'users/login.html') 1238 | ``` 1239 | 1240 | En _templates/users_ crearemos 2 archivos, **base.html** y **login.html**. La razón del porque ocuparemos un base distinto al anterior es por que a nivel de contenido son distintos, sin embargo _template/users/**login.html**_ extendera de _template/users/**base.html**_ 1241 | 1242 | ```html 1243 | 1244 | 1245 | 1246 | 1247 | 1248 | {% block head_content %}{% endblock %} 1249 | 1250 | {% load static %} 1251 | 1252 | 1253 | 1254 | 1255 | 1256 | 1257 | 1258 |
1259 |
1260 |
1261 | 1262 | 1263 | 1264 | {% block container %}{% endblock%} 1265 | 1266 |
1267 |
1268 |
1269 | 1270 | 1271 | 1272 | ``` 1273 | 1274 | ```html 1275 | 1276 | {% extends "users/base.html" %} 1277 | 1278 | {% block head_content %} 1279 | Platzigram sign in 1280 | {% endblock %} 1281 | 1282 | {% block container %} 1283 | 1284 | 1285 |
1286 | 1287 | 1288 | {% if error %} 1289 |

{{ error }}

1290 | {% endif %} 1291 | 1292 | 1293 | {% csrf_token %} 1294 | 1295 | 1296 | 1297 | 1298 | 1299 | 1300 |
1301 | 1302 | {% endblock %} 1303 | ``` 1304 | 1305 | Si observarte bien en el archivo _login.html_ hacemos uso del metodo _csrf_token_ de Django. Este método evita un tipo de exploit malicioso llamado "Cross-site request forgery", el cual consiste en llenados de formularios desde fuera del sitio. La forma en la que trabaja _csrf_token_ es que cuando se realiza una peticion 'GET' se te envia un token único, y cuando realizas el submit del formulario con un metodo 'POST' se va a revisar el token que conseguiste antes, de esta forma se evita el exploit. 1306 | 1307 |
1308 | 1312 |
1313 | 1314 | Ahora para **proteger** las vistas de _posts_ y solo podamos acceder a ellas si hemos **iniciado sesión** vamos al archivo _settings.py_ de nuestro proyecto y al fondo del código creamos la variable **LOGIN_URL** con el path de nuestro **login**, de esta forma nos redigira al path definido si tratamos de renderizar una vista protegida. 1315 | 1316 | ```py 1317 | ... 1318 | # Usamos el alias del path de login 1319 | LOGIN_URL = 'login' 1320 | ``` 1321 | 1322 | Ahora para proteger las vistas debemos ir al archivo views.py de nuestra aplicación. 1323 | 1324 | ```py 1325 | # Django 1326 | # Importamos login_required 1327 | from django.contrib.auth.decorators import login_required 1328 | from django.shortcuts import render 1329 | 1330 | ... 1331 | 1332 | # Decoramos con login_required la función que renderiza nuestra vista, 1333 | # el cual ahora necesitara una sesión iniciada para poder renderizarse. 1334 | # En caso de no estarlo nos redirigira al path de login. 1335 | @login_required 1336 | def list_posts(request): 1337 | return render(request, 'posts/feed.html', {'posts': posts}) 1338 | ``` 1339 | 1340 | Ahora veamos las vistas protegidas en acción. Primero con un usuario **sin registrar.** 1341 | 1342 |
1343 | 1347 |
1348 | 1349 | Y segundo con un usario **registrado**. 1350 | 1351 |
1352 | 1356 |
1357 | 1358 | # Logout 1359 | 1360 | El proceso de **logout** es bastante sencillo en Django. Primero iremos a las vistas de nuestro aplicativo y crearemos una función para ello. 1361 | 1362 | ```py 1363 | # Archivo users/views.py 1364 | # Django 1365 | # Importamos logout. 1366 | from django.contrib.auth import authenticate, login, logout 1367 | from django.contrib.auth.decorators import login_required 1368 | from django.shortcuts import render, redirect 1369 | 1370 | def login_view(request): 1371 | if request.method == 'POST': 1372 | username = request.POST['username'] 1373 | password = request.POST['password'] 1374 | user = authenticate(request, username=username, password=password) 1375 | if user: 1376 | login(request, user) 1377 | return redirect('feed') 1378 | else: 1379 | return render(request, 'users/login.html', {'error': 'Invalid username and password'}) 1380 | return render(request, 'users/login.html') 1381 | 1382 | # Creamos la funcion logout_view, y lo decoramos con 1383 | # login_required, asi solo se ejecutara si existe una sesión. 1384 | @login_required 1385 | def logout_view(request): 1386 | logout(request) # Ejecutamos logout, el cual borrara los tokens del navegador. 1387 | return redirect('login') # Redirigimos a path de login. 1388 | ``` 1389 | 1390 | Luego de ello iremos a las _urls.py_ de nuestro proyecto. 1391 | 1392 | ```py 1393 | 1394 | ... 1395 | 1396 | urlpatterns = [ 1397 | ... 1398 | # Creamos el path de logout. 1399 | path('users/logout', users_views.logout_view, name='logout'), 1400 | 1401 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 1402 | ``` 1403 | 1404 | Y por terminar, en el **html** haremos referencia al path de 'logout'. 1405 | 1406 | ```html 1407 | 1408 | {% load static %} 1409 | ... 1410 | 1416 | ... 1417 | ``` 1418 | 1419 | Listo, ahora tenemos un logout funcionando perfectamente de forma sencilla. 1420 | 1421 | ## Signup 1422 | 1423 | Ahora aprenderemos a registrar un usuario y guardar un instacia de nuestro modelo **Profile.** 1424 | 1425 | Primero crearemos un **template** para el registro, asi que creamos el archivo _template/users/**signup.html**_ 1426 | 1427 | ```html 1428 | 1429 | 1430 | {% extends 'users/base.html' %} 1431 | 1432 | {% block head_content %} 1433 | Platzigram sign up 1434 | {% endblock %} 1435 | 1436 | {% block container %} 1437 | 1438 | 1439 | {% if error %} 1440 |

{{ error }}

1441 | {% endif %} 1442 | 1443 | 1444 |
1445 | 1446 | {% csrf_token %} 1447 | 1448 |
1449 |
1450 |
1451 |
1452 |
1453 |
1454 | 1455 | 1456 | 1457 |
1458 | 1459 | {% endblock %} 1460 | ``` 1461 | 1462 | Teniendo listo nuestro **template** ahora crearemos la función que renderizara nuestra vista. Para ello iremos a _users/**views.py**_ 1463 | 1464 | ```py 1465 | # Django 1466 | ... 1467 | # Vamos hacer uso de render y redirect 1468 | from django.shortcuts import render, redirect 1469 | 1470 | # Exceptions 1471 | # Importamos posible error al tratar de crear una instancia con valor único que ya existe 1472 | from django.db.utils import IntegrityError 1473 | 1474 | # Models 1475 | # Importamos los modelos de las instancias que crearemos 1476 | from django.contrib.auth.models import User 1477 | from users.models import Profile 1478 | 1479 | ... 1480 | 1481 | def signup(request): 1482 | # Al recibir el metodo POST. 1483 | if request.method == 'POST': 1484 | username = request.POST['username'] 1485 | password = request.POST['password'] 1486 | password_confirmation = request.POST['password_confirmation'] 1487 | 1488 | # Confirmamos que las constraseñas sean iguales. 1489 | if password != password_confirmation: 1490 | # En caso de error volvemos a renderizar signup, pero enviamos el error. 1491 | return render(request, 'users/signup.html', {'error': 'Passwords does not match'}) 1492 | 1493 | try: 1494 | # Creamos una instancia de User 1495 | user = User.objects.create_user(username=username, password=password) 1496 | except IntegrityError: 1497 | # En caso que username (nuestro valor unico) ya exista renderizara 1498 | # nuevamente signup pero enviando el error. 1499 | return render(request, 'users/signup.html', {'error': 'Username is already exist'}) 1500 | # Ya creada la instancia le pasamos los siguientes valores. 1501 | user.first_name = request.POST['first_name'] 1502 | user.last_name = request.POST['last_name'] 1503 | user.email = request.POST['email'] 1504 | # Lo guardamos en nuestra base de datos. 1505 | user.save() 1506 | 1507 | # Creamos nuestra instacia de Profile a traves de user. 1508 | profile = Profile(user=user) 1509 | # Lo guardamos en la base de datos. 1510 | profile.save() 1511 | 1512 | # Nos redirigimos a login para iniciar sesion con el nuevo usuario. 1513 | return redirect('login') 1514 | 1515 | return render(request, 'users/signup.html') 1516 | 1517 | ... 1518 | ``` 1519 | 1520 | Ahora nos faltaría solo asignar un path a nuestro signup, lo configuraremos en _urls.py_ 1521 | 1522 | ```py 1523 | ... 1524 | 1525 | urlpatterns = [ 1526 | ... 1527 | 1528 | path('users/signup', users_views.signup, name='signup'), 1529 | 1530 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 1531 | 1532 | ... 1533 | ``` 1534 | 1535 | ## Middlewares 1536 | 1537 | El término **middleware** se refiere a un sistema de software que ofrece servicios y funciones comunes para las aplicaciones. En general, el middleware se encarga de las tareas de gestión de datos, servicios de aplicaciones, mensajería, autenticación y gestión de API. 1538 | 1539 | En este apartado aprenderemos como crear nuestro propio **middleware**, este no permitira la navegación en la aplicación si es que el usuario **no tiene fotografia o biografía.** 1540 | 1541 | Primero crearemos un template del perfil donde el usuario podra modificar su información. Este template sera simple por el momento y estara en _templates/users/**update_profile.html**_ 1542 | 1543 | ```html 1544 | 1545 | {% extends "base.html" %} 1546 | 1547 | {% block head_content %} 1548 | @{{ request.user.username }} | Update profile 1549 | {% endblock %} 1550 | 1551 | {% block container %} 1552 |

@{{ request.user.username }}

1553 | {% endblock %} 1554 | ``` 1555 | 1556 | Ahora en nuestro archivo _users/views.py_ vamos a crear la funcion que renderizara el template recien creado. 1557 | 1558 | ```py 1559 | ... 1560 | 1561 | def update_profile(request): 1562 | return render(request, 'users/update_profile.html') 1563 | 1564 | ... 1565 | ``` 1566 | 1567 | Luego de definir nuestra vista vamos asignarle un path dentro del archivo _urls.py_ 1568 | 1569 | ```py 1570 | ... 1571 | 1572 | urlpatterns = [ 1573 | ... 1574 | 1575 | path('users/me/profile', users_views.update_profile, name='update_profile'), 1576 | 1577 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 1578 | ``` 1579 | 1580 | Llego el momento esperado, es hora de crear nuestro **middleware**. Por lo general los middleware se crean en la aplicación relacionada, pero **solo para efectos practicos** crearemos el nuestro en la carpeta principal de nuestro proyecto como **middleware.py** 1581 | 1582 |
1583 | 1587 |
1588 | 1589 | El **objetivo** de nuestro middleware es evitar que se pueda navegar por la aplicación si es que el usuario no tiene foto de perfil o no ha escrito su biografía, por lo que nuestro middleware contendra una clase que realizara todas estas validaciones. 1590 | 1591 | ```py 1592 | # Django 1593 | # Para nuestro objetivo ocuparemos redirect para que el usuario 1594 | # se dirija a la configuración en caso de no cumplir con los requisitos. 1595 | from django.shortcuts import redirect 1596 | # Usaremos reverse para hacer referencia al alias 1597 | # de los path de nuestro proyecto. 1598 | from django.urls import reverse 1599 | 1600 | class ProfileCompletionMiddleware: 1601 | # Este __init__ siempre ira, asi que es fundamental al crear tu clase 1602 | def __init__(self, get_response): 1603 | self.get_response = get_response 1604 | 1605 | # Dentro de __call__ es donde realizaremos nuestras validaciones. 1606 | def __call__(self, request): 1607 | # En caso de que el usuario no sea anonimo. 1608 | if not request.user.is_anonymous: 1609 | profile = request.user.profile 1610 | 1611 | # Verificamos que la instacia de user tenga 1612 | # una foto o biografía. 1613 | if not profile.picture or not profile.biography: 1614 | # En caso de que no trate de navegar al path de 1615 | # 'update_profile' o 'logout' 1616 | if request.path not in [reverse('update_profile'), reverse('logout')]: 1617 | # Vamos a redireccionarlo al path de 'update_profile' 1618 | return redirect('update_profile') 1619 | 1620 | # En caso de que cumple todos los requisitos devolvemos la solicitud original. 1621 | response = self.get_response(request) 1622 | return response 1623 | ``` 1624 | 1625 | Nos falta un paso mas, tenemos que decirle a nuestro proyecto que ahora también debe usar este middleware para las peticiones. Para ello iremos al archivo _settings.py_ y lo incluiremos en la variable de **MIDDLEWARE.** 1626 | 1627 | ```py 1628 | MIDDLEWARE = [ 1629 | # Django 1630 | 'django.middleware.security.SecurityMiddleware', 1631 | 'django.contrib.sessions.middleware.SessionMiddleware', 1632 | 'django.middleware.common.CommonMiddleware', 1633 | 'django.middleware.csrf.CsrfViewMiddleware', 1634 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 1635 | 'django.contrib.messages.middleware.MessageMiddleware', 1636 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 1637 | # Propios 1638 | # Referenciamos al middleware por el nombre de la clase 1639 | 'photogram.middleware.ProfileCompletionMiddleware', 1640 | ] 1641 | ``` 1642 | 1643 | Y con esto ya creamos nuestro primer middleware. 1644 | 1645 | # Forms 1646 | 1647 | ## Formularios en Django 1648 | 1649 | En esta sección veremos en acción los **forms** en Django. Primero que todo crearemos nuestro form en _templates/users/**update_profile.html**_ creado en la sección de [middlewares](#Middlewares). 1650 | 1651 | ```html 1652 | 1653 | {% extends "base.html" %} 1654 | {% load static %} 1655 | 1656 | {% block head_content %} 1657 | @{{ request.user.username }} | Update profile 1658 | {% endblock %} 1659 | 1660 | {% block container %} 1661 | 1662 |
1663 | 1664 |
1665 |
1666 | 1667 | 1668 |
1669 | {% csrf_token %} 1670 | 1671 | {% if form.errors %} 1672 |

{{ form.errors }}

1673 | {% endif %} 1674 | 1675 |
1676 | {% if profile.picture %} 1677 | 1678 | {% else%} 1679 | 1680 | {% endif %} 1681 | 1682 |
1683 |
@{{ user.username }} | {{ user.get_full_name }}
1684 |

1685 |
1686 |
1687 | 1688 |

1689 | 1690 |
1691 | 1692 | 1699 |
1700 | 1701 |
1702 | 1703 | 1704 |
1705 | 1706 |
1707 | 1708 | 1715 |
1716 | 1717 | 1718 |
1719 |
1720 |
1721 |
1722 | 1723 | {% endblock %} 1724 | ``` 1725 | 1726 | Django ya incorpora una **clase forms** del cual podemos hacer uso, asi que crearemos nuestra clase forms para crear un formulario de usuario. 1727 | 1728 | ```py 1729 | # Django 1730 | from django import forms 1731 | 1732 | class ProfileForm(forms.Form): 1733 | 1734 | website = forms.URLField(max_length=200, required=True) 1735 | biography = forms.CharField(max_length=500, required=False) 1736 | phone_number = forms.CharField(max_length=20, required=False) 1737 | picture = forms.ImageField() 1738 | 1739 | ``` 1740 | 1741 | En la documentación podras encontrar como trabajar con [formularios](https://docs.djangoproject.com/en/3.0/topics/forms/) y los [campos](https://docs.djangoproject.com/en/3.0/ref/forms/fields/) que puedes usar 1742 | 1743 | Para poder recibir los datos y guardarlos en nuestra base de datos vamos a ir a nuestra vista de la aplicación _users/**views.py**_ en donde crearemos la función que se encargara de ello. 1744 | 1745 | ```py 1746 | # Archivo users/views.py 1747 | ... 1748 | 1749 | # Forms 1750 | # Importamos el ProfileForm que creamos anteriormente 1751 | from users.forms import ProfileForm 1752 | 1753 | # En la vista de update_profile vamos a recibir el request. 1754 | def update_profile(request): 1755 | 1756 | # Crearemos una variable que guardara el profile 1757 | # que esta realizando el request. 1758 | profile = request.user.profile 1759 | 1760 | # Si el request es de tipo 'POST' 1761 | if request.method == 'POST': 1762 | 1763 | # Crearemos una instancia de ProfileForm 1764 | # con los datos que recibimos a traves de request 1765 | form = ProfileForm(request.POST, request.FILES) 1766 | 1767 | # Si la instacia se crea sin problemas. 1768 | if form.is_valid(): 1769 | 1770 | #Guardaremos los datos recibidos en base de datos. 1771 | data = form.cleaned_data 1772 | 1773 | profile.website = data['website'] 1774 | profile.phone_number = data['phone_number'] 1775 | profile.biography = data['biography'] 1776 | profile.picture = data['picture'] 1777 | profile.save() 1778 | 1779 | # Y redireccionaremos a la pagina update_profile 1780 | # para reflejar los cambios. 1781 | return redirect('update_profile') 1782 | else: 1783 | form = ProfileForm() 1784 | 1785 | return render( 1786 | request = request, 1787 | template_name = 'users/update_profile.html', 1788 | 1789 | # Enviaremos al template los datos del usuario. 1790 | context = { 1791 | 'profile': profile, 1792 | 'user': request.user, 1793 | 'form': form, 1794 | } 1795 | ) 1796 | 1797 | ... 1798 | ``` 1799 | 1800 | Terminados estos pasos podremos ver nuestro profile con los datos de nuestro usuario y actualizarlos, preservando los datos. 1801 | 1802 |
1803 | 1807 |
1808 | 1809 | En caso de que algun dato no cumpla con los requisitos establecidos en la clase form desplegaremos en pantalla los errores que tengamos. 1810 | 1811 |
1812 | 1816 |
1817 | 1818 | ## Mostrando el form en el template 1819 | 1820 | Si te diste cuenta en la sección anterior, si enviamos campos inválidos estos vuelven con el valor anterior que tenian. En esta sección haremos persistentes estos datos. 1821 | 1822 | Para ello solo tendremos que modificar nuestro _template/**update_profile.html**_ 1823 | 1824 | ```html 1825 | {% extends "base.html" %} 1826 | {% load static %} 1827 | 1828 | {% block head_content %} 1829 | @{{ request.user.username }} | Update profile 1830 | {% endblock %} 1831 | 1832 | {% block container %} 1833 | 1834 |
1835 | 1836 |
1837 |
1838 | 1839 |
1840 | {% csrf_token %} 1841 | 1842 |
1843 | {% if profile.picture %} 1844 | 1845 | {% else%} 1846 | 1847 | {% endif %} 1848 | 1849 |
1850 |
@{{ user.username }} | {{ user.get_full_name }}
1851 |

1852 |
1853 |
1854 | 1855 | 1856 | {% for error in form.picture.errors %} 1857 |
1858 | Picture: {{ error }} 1859 |
1860 | {% endfor %} 1861 | 1862 |

1863 | 1864 |
1865 | 1866 | 1869 | 1876 | 1877 |
1878 | {% for error in form.website.errors %} 1879 | {{ error }} 1880 | {% endfor %} 1881 |
1882 |
1883 | 1884 |
1885 | 1886 | 1889 | 1893 | 1894 |
1895 | {% for error in form.biography.errors %} 1896 | {{ error }} 1897 | {% endfor %} 1898 |
1899 |
1900 | 1901 |
1902 | 1903 | 1906 | 1913 | 1914 |
1915 | {% for error in form.phone_number.errors %} 1916 | {{ error }} 1917 | {% endfor %} 1918 |
1919 |
1920 | 1921 | 1922 |
1923 |
1924 |
1925 |
1926 | 1927 | {% endblock %} 1928 | ``` 1929 | 1930 | Con esto estos cambios ahora los valores ingresados **persistiran** en nuestro formulario sin importar si existe un error, ademas de mostrarlos de forma estilizadas. 1931 | 1932 |
1933 | 1937 |
1938 | 1939 | ## Models forms y validación 1940 | 1941 | Hasta ahora hemos estado validando los datos a traves de la renderizacion de las vistas, sin embargo podemos validar directamente sobre los formularios los datos, simplificando la sintaxis de nuestro codigo y haciendolo mas legible. 1942 | 1943 | Para ello primero crearemos un archivo **forms.py** en nuestra aplicación _users_. 1944 | 1945 | ```py 1946 | # Archivo users/forms.py 1947 | # Django 1948 | # Django tiene una clase forms del cual podemos crear nuestros formularios. 1949 | from django import forms 1950 | 1951 | # Models 1952 | # Importaremos los modelos que crearemos a traves 1953 | # de nuestro formulario. 1954 | from django.contrib.auth.models import User 1955 | from users.models import Profile 1956 | 1957 | class SignupForm(forms.Form): 1958 | 1959 | # Definimos los campos. 1960 | username = forms.CharField(min_length=4, max_length=50) 1961 | 1962 | password = forms.CharField(max_length=70, widget=forms.PasswordInput()) 1963 | password_confirmation = forms.CharField(max_length=70, widget=forms.PasswordInput()) 1964 | 1965 | first_name = forms.CharField(min_length=2, max_length=50) 1966 | last_name = forms.CharField(min_length=2, max_length=50) 1967 | 1968 | email = forms.CharField(min_length=6, max_length=70, widget=forms.EmailInput()) 1969 | 1970 | # verificamos que el username no exista. 1971 | def clean_username(self): 1972 | username = self.cleaned_data['username'] 1973 | username_taken = User.objects.filter(username=username).exists() 1974 | if username_taken: 1975 | # En caso de existir devolvemos un mensaje de error. 1976 | raise forms.ValidationError('Username is already in use.') 1977 | 1978 | return username 1979 | 1980 | def clean(self): 1981 | data = super().clean() 1982 | 1983 | password = data['password'] 1984 | password_confirmation = data['password_confirmation'] 1985 | 1986 | # Verificamos que las constraseñas coincidan. 1987 | if password != password_confirmation: 1988 | # En caso de ser distintas devolvemos un mensaje de error. 1989 | raise forms.ValidationError('Passwords do not match.') 1990 | 1991 | return data 1992 | 1993 | # Creamos una instancia de User y Profile. 1994 | def save(self): 1995 | data = self.cleaned_data 1996 | data.pop('password_confirmation') 1997 | 1998 | user = User.objects.create_user(**data) 1999 | profile = Profile(user=user) 2000 | profile.save() 2001 | 2002 | ``` 2003 | 2004 | Ya que tenemos nuestro formulario creado lo aplicaremos en la vista de **signup** de la aplicación _users_. 2005 | 2006 | ```py 2007 | # Django 2008 | ... 2009 | from django.shortcuts import render, redirect 2010 | 2011 | ... 2012 | 2013 | # Forms 2014 | # Importamos nuestro formulario 2015 | from users.forms import SignupForm 2016 | 2017 | ... 2018 | 2019 | def signup(request): 2020 | if request.method == 'POST': 2021 | # Le enviamos los datos de request a nuestro formulario 2022 | form = SignupForm(request.POST) 2023 | 2024 | # En caso de ser valido guarda las instancias 2025 | # y nos redirige al login. 2026 | if form.is_valid(): 2027 | form.save() 2028 | return redirect('login') 2029 | 2030 | else: 2031 | form = SignupForm() 2032 | 2033 | return render( 2034 | request=request, 2035 | template_name='users/signup.html', 2036 | context={ 2037 | 'form': form 2038 | } 2039 | ) 2040 | 2041 | ... 2042 | ``` 2043 | 2044 | Con esta metodología hacemos uso de las herramientas de Django para crear formularios, facilitando el desarrollo y sintaxis de nuestro proyecto. -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'photogram.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /media/post/photos/create_user_custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/media/post/photos/create_user_custom.png -------------------------------------------------------------------------------- /photogram/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/photogram/__init__.py -------------------------------------------------------------------------------- /photogram/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for photogram project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'photogram.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /photogram/middleware.py: -------------------------------------------------------------------------------- 1 | # Django 2 | from django.shortcuts import redirect 3 | from django.urls import reverse 4 | 5 | class ProfileCompletionMiddleware: 6 | def __init__(self, get_response): 7 | self.get_response = get_response 8 | 9 | def __call__(self, request): 10 | if not request.user.is_anonymous: 11 | if not request.user.is_staff: 12 | profile = request.user.profile 13 | 14 | if not profile.picture or not profile.biography: 15 | if request.path not in [reverse('update_profile'), reverse('logout')]: 16 | return redirect('update_profile') 17 | 18 | response = self.get_response(request) 19 | return response -------------------------------------------------------------------------------- /photogram/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for photogram project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'n94!o-3fdc_vy1-au1vqq(dbqn5o##cvqb_sei-kv10m^(6$xi' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | # Django apps 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 42 | # Local apps 43 | 'posts', 44 | 'users', 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | 'photogram.middleware.ProfileCompletionMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'photogram.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [ 64 | os.path.join(BASE_DIR, 'templates'), 65 | ], 66 | 'APP_DIRS': True, 67 | 'OPTIONS': { 68 | 'context_processors': [ 69 | 'django.template.context_processors.debug', 70 | 'django.template.context_processors.request', 71 | 'django.contrib.auth.context_processors.auth', 72 | 'django.contrib.messages.context_processors.messages', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'photogram.wsgi.application' 79 | 80 | 81 | # Database 82 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 83 | 84 | DATABASES = { 85 | 'default': { 86 | 'ENGINE': 'django.db.backends.sqlite3', 87 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 88 | } 89 | } 90 | 91 | 92 | # Password validation 93 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 94 | 95 | AUTH_PASSWORD_VALIDATORS = [ 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 107 | }, 108 | ] 109 | 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 113 | 114 | LANGUAGE_CODE = 'en-us' 115 | 116 | TIME_ZONE = 'UTC' 117 | 118 | USE_I18N = True 119 | 120 | USE_L10N = True 121 | 122 | USE_TZ = True 123 | 124 | 125 | # Static files (CSS, JavaScript, Images) 126 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 127 | 128 | STATIC_URL = '/static/' 129 | STATICFILES_DIRS = ( 130 | os.path.join(BASE_DIR, 'static'), 131 | ) 132 | STATICFILES_FINDERS = [ 133 | 'django.contrib.staticfiles.finders.FileSystemFinder', 134 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 135 | ] 136 | 137 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 138 | MEDIA_URL = '/media/' 139 | 140 | LOGIN_URL = 'login' -------------------------------------------------------------------------------- /photogram/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.conf import settings 3 | from django.conf.urls.static import static 4 | from django.urls import path 5 | 6 | from photogram import views as local_views 7 | from posts import views as posts_views 8 | from users import views as users_views 9 | 10 | urlpatterns = [ 11 | 12 | path('admin/', admin.site.urls), 13 | 14 | path('hello-world/', local_views.hello_world, name='hello_world'), 15 | path('numbers/', local_views.numbers, name='sort'), 16 | path('hi///', local_views.say_hi, name='hi'), 17 | 18 | path('', posts_views.list_posts, name='feed'), 19 | path('posts/new', posts_views.create_post, name='create_post'), 20 | 21 | path('users/login/', users_views.login_view, name='login'), 22 | path('users/logout', users_views.logout_view, name='logout'), 23 | path('users/signup', users_views.signup, name='signup'), 24 | path('users/me/profile', users_views.update_profile, name='update_profile'), 25 | 26 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -------------------------------------------------------------------------------- /photogram/views.py: -------------------------------------------------------------------------------- 1 | # Django 2 | from django.http import HttpResponse 3 | 4 | # Utilities 5 | from datetime import datetime 6 | import json 7 | 8 | def hello_world(request): 9 | return HttpResponse('Oh, hi! Current server time is {now}'.format( 10 | now=datetime.now().strftime('%b %dth, %Y - %H:%M hrs') 11 | )) 12 | 13 | def numbers(request): 14 | numbers = [int(i) for i in request.GET['numbers'].split(',')] 15 | sorted_ints = sorted(numbers) 16 | data = { 17 | 'status': 'ok', 18 | 'numbers': sorted_ints, 19 | 'message': 'Integers sorted successfully' 20 | } 21 | return HttpResponse( 22 | json.dumps(data, indent=4), 23 | content_type='application/json' 24 | ) 25 | 26 | def say_hi(request, name, age): 27 | if age < 12: 28 | message = 'Sorry {}, you are not allowed here'.format(name) 29 | else: 30 | message = 'Hello {}! Welcome to Photogram'.format(name) 31 | 32 | return HttpResponse(message) -------------------------------------------------------------------------------- /photogram/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for photogram project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'photogram.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /posts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/posts/__init__.py -------------------------------------------------------------------------------- /posts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from posts.models import Post 3 | 4 | @admin.register(Post) 5 | class PostAdmin(admin.ModelAdmin): 6 | list_display = ('pk', 'user', 'photo', 'created', 'modified') 7 | list_display_links = ('pk', 'user') 8 | list_filter = ( 9 | 'created', 10 | 'modified' 11 | ) -------------------------------------------------------------------------------- /posts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PostsConfig(AppConfig): 5 | name = 'posts' 6 | varbose_name = 'Posts' 7 | -------------------------------------------------------------------------------- /posts/forms.py: -------------------------------------------------------------------------------- 1 | # Django 2 | from django import forms 3 | 4 | # Models 5 | from posts.models import Post 6 | 7 | class PostForm(forms.ModelForm): 8 | 9 | class Meta: 10 | 11 | model = Post 12 | fields = ('user', 'profile', 'title', 'photo') -------------------------------------------------------------------------------- /posts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-05-16 21:13 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('users', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Post', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('title', models.CharField(max_length=255)), 23 | ('photo', models.ImageField(upload_to='post/photos')), 24 | ('created', models.DateTimeField(auto_now_add=True)), 25 | ('modified', models.DateTimeField(auto_now=True)), 26 | ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Profile')), 27 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 28 | ], 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /posts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/posts/migrations/__init__.py -------------------------------------------------------------------------------- /posts/models.py: -------------------------------------------------------------------------------- 1 | # Django 2 | from django.db import models 3 | from django.contrib.auth.models import User 4 | 5 | class Post(models.Model): 6 | 7 | user = models.ForeignKey(User, on_delete=models.CASCADE) 8 | profile = models.ForeignKey('users.Profile', on_delete=models.CASCADE) 9 | 10 | title = models.CharField(max_length=255) 11 | photo = models.ImageField(upload_to='post/photos') 12 | 13 | created = models.DateTimeField(auto_now_add=True) 14 | modified = models.DateTimeField(auto_now=True) 15 | 16 | def __str__(self): 17 | return '{} by @{}'.format(self.title, self.user.username) -------------------------------------------------------------------------------- /posts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /posts/views.py: -------------------------------------------------------------------------------- 1 | # Django 2 | from django.contrib.auth.decorators import login_required 3 | from django.shortcuts import render, redirect 4 | 5 | # Forms 6 | from posts.forms import PostForm 7 | 8 | # Models 9 | from posts.models import Post 10 | 11 | @login_required 12 | def list_posts(request): 13 | posts = Post.objects.all().order_by('-created') 14 | return render(request, 'posts/feed.html', {'posts': posts}) 15 | 16 | @login_required 17 | def create_post(request): 18 | if request.method == 'POST': 19 | form = PostForm(request.POST, request.FILES) 20 | if form.is_valid(): 21 | form.save() 22 | return redirect('feed') 23 | 24 | else: 25 | form = PostForm() 26 | 27 | return render( 28 | request=request, 29 | template_name='posts/new.html', 30 | context={ 31 | 'form': form, 32 | 'user': request.user, 33 | 'profile': request.user.profile 34 | } 35 | ) 36 | -------------------------------------------------------------------------------- /readme_img/app_posts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/app_posts.png -------------------------------------------------------------------------------- /readme_img/create_user_custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/create_user_custom.png -------------------------------------------------------------------------------- /readme_img/csrf_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/csrf_token.png -------------------------------------------------------------------------------- /readme_img/dashboard_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/dashboard_login.png -------------------------------------------------------------------------------- /readme_img/dashboard_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/dashboard_menu.png -------------------------------------------------------------------------------- /readme_img/dashboard_profile_list_custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/dashboard_profile_list_custom.png -------------------------------------------------------------------------------- /readme_img/dashboard_users_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/dashboard_users_add.png -------------------------------------------------------------------------------- /readme_img/dashboard_users_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/dashboard_users_list.png -------------------------------------------------------------------------------- /readme_img/detalle_personalizado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/detalle_personalizado.png -------------------------------------------------------------------------------- /readme_img/django_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/django_2.png -------------------------------------------------------------------------------- /readme_img/estilado.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/estilado.gif -------------------------------------------------------------------------------- /readme_img/hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/hello_world.png -------------------------------------------------------------------------------- /readme_img/middleware.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/middleware.png -------------------------------------------------------------------------------- /readme_img/numbers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/numbers.png -------------------------------------------------------------------------------- /readme_img/post_hola_mundo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/post_hola_mundo.png -------------------------------------------------------------------------------- /readme_img/posts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/posts.png -------------------------------------------------------------------------------- /readme_img/posts_diccionario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/posts_diccionario.png -------------------------------------------------------------------------------- /readme_img/posts_diccionario_titulos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/posts_diccionario_titulos.png -------------------------------------------------------------------------------- /readme_img/posts_navbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/posts_navbar.png -------------------------------------------------------------------------------- /readme_img/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/profile.png -------------------------------------------------------------------------------- /readme_img/profile_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/profile_error.png -------------------------------------------------------------------------------- /readme_img/profile_error_styled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/profile_error_styled.png -------------------------------------------------------------------------------- /readme_img/registed_protected.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/registed_protected.gif -------------------------------------------------------------------------------- /readme_img/static_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/static_folder.png -------------------------------------------------------------------------------- /readme_img/template_feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/template_feed.png -------------------------------------------------------------------------------- /readme_img/templates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/templates.png -------------------------------------------------------------------------------- /readme_img/unregisted_protected.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/unregisted_protected.gif -------------------------------------------------------------------------------- /readme_img/url_params_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/url_params_1.png -------------------------------------------------------------------------------- /readme_img/url_params_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/url_params_2.png -------------------------------------------------------------------------------- /readme_img/user_dashboard_custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/user_dashboard_custom.png -------------------------------------------------------------------------------- /readme_img/views.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/readme_img/views.png -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background: #fafafa; 3 | height: 100%; 4 | width: 100%; 5 | font-family: 'Roboto', sans-serif; 6 | } 7 | 8 | #auth-container { 9 | background: #fff; 10 | border: 1px solid #e6e6e6; 11 | border-radius: 1px; 12 | } 13 | 14 | #main-navbar { 15 | background: #fff; 16 | border-bottom: 1px solid rgba(0,0,0,.0975); 17 | } 18 | 19 | .post-container { 20 | background: #fff; 21 | border: 1px solid #e6e6e6; 22 | border-radius: 3px; 23 | } 24 | 25 | .nav-icon a { 26 | color: #262626; 27 | font-size: 1.3em; 28 | display: block; 29 | margin-top: .2em; 30 | margin-left: 1.2em; 31 | font-weight: 100; 32 | } 33 | .nav-icon a:hover { 34 | color: #1d5e72;; 35 | } 36 | #profile-box { 37 | background: #fff; 38 | border: 1px solid rgba(0,0,0,.0975); 39 | border-radius: 1px; 40 | margin-top: 6em; 41 | } 42 | 43 | #user-posts #post-thumb { 44 | background: #fff; 45 | } 46 | 47 | -------------------------------------------------------------------------------- /static/img/default-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/static/img/default-profile.png -------------------------------------------------------------------------------- /static/img/instagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/static/img/instagram.png -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block head_content %}{% endblock %} 6 | 7 | 8 | {% load static %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% include "nav.html" %} 17 | 18 |
19 | {% block container %} 20 | {% endblock%} 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /templates/nav.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | -------------------------------------------------------------------------------- /templates/posts/feed.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head_content %} 4 | Platzigram 5 | {% endblock%} 6 | 7 | {% block container %} 8 |
9 |
10 | 11 | {% for post in posts %} 12 |
13 |
14 | {{ post.user.get_full_name }} 15 |
16 |

{{ post.user.get_full_name }}

17 |
18 |
19 | 20 | {{ post.title }} 21 | 22 |

23 | 24 | 25 | 30 likes 26 |

27 |

28 | {{ post.title }} - {{ post.created }} 29 |

30 |
31 | {% endfor %} 32 |
33 |
34 | {% endblock %} -------------------------------------------------------------------------------- /templates/posts/new.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head_content %} 4 | Create new post 5 | {% endblock %} 6 | 7 | {% block container %} 8 | 9 |
10 |
11 |
12 |

Post a new photo!

13 | 14 |
15 | {% csrf_token %} 16 | 17 | 18 | 19 | 20 | {# Website field #} 21 |
22 | 29 |
30 | {% for error in form.title.errors %}{{ error }}{% endfor %} 31 |
32 |
33 | 34 | {# Photo field #} 35 |
36 | 37 | 44 |
45 | {% for error in form.photo.errors %}{{ error }}{% endfor %} 46 |
47 |
48 | 49 | 50 |
51 |
52 |
53 |
54 | 55 | {% endblock %} -------------------------------------------------------------------------------- /templates/users/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block head_content %}{% endblock %} 6 | 7 | {% load static %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 | 19 | 20 | 21 | {% block container %}{% endblock%} 22 | 23 |
24 |
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /templates/users/login.html: -------------------------------------------------------------------------------- 1 | {% extends "users/base.html" %} 2 | 3 | {% block head_content %} 4 | Platzigram sign in 5 | {% endblock %} 6 | 7 | {% block container %} 8 | 9 |
10 | 11 | {% if error %} 12 |

{{ error }}

13 | {% endif %} 14 | 15 | {% csrf_token %} 16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 |
24 | 25 | 26 | 27 |
28 | 29 |

Don't have an account yet? Sign up here.

30 | 31 | {% endblock %} -------------------------------------------------------------------------------- /templates/users/signup.html: -------------------------------------------------------------------------------- 1 | {% extends 'users/base.html' %} 2 | 3 | {% block head_content %} 4 | Platzigram sign up 5 | {% endblock %} 6 | 7 | {% block container %} 8 | 9 |
10 | {% csrf_token %} 11 | 12 | {{ form.as_p }} 13 | 14 | 15 | 16 |
17 | 18 | {% endblock %} -------------------------------------------------------------------------------- /templates/users/update_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block head_content %} 5 | @{{ request.user.username }} | Update profile 6 | {% endblock %} 7 | 8 | {% block container %} 9 | 10 |
11 | 12 |
13 |
14 | 15 |
16 | {% csrf_token %} 17 | 18 |
19 | {% if profile.picture %} 20 | 21 | {% else%} 22 | 23 | {% endif %} 24 | 25 |
26 |
@{{ user.username }} | {{ user.get_full_name }}
27 |

28 |
29 |
30 | 31 | {% for error in form.picture.errors %} 32 |
33 | Picture: {{ error }} 34 |
35 | {% endfor %} 36 | 37 |

38 | 39 |
40 | 41 | 48 |
49 | {% for error in form.website.errors %} 50 | {{ error }} 51 | {% endfor %} 52 |
53 |
54 | 55 |
56 | 57 | 61 |
62 | {% for error in form.biography.errors %} 63 | {{ error }} 64 | {% endfor %} 65 |
66 |
67 | 68 |
69 | 70 | 77 |
78 | {% for error in form.phone_number.errors %} 79 | {{ error }} 80 | {% endfor %} 81 |
82 |
83 | 84 | 85 |
86 |
87 |
88 |
89 | 90 | {% endblock %} -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/users/__init__.py -------------------------------------------------------------------------------- /users/admin.py: -------------------------------------------------------------------------------- 1 | # Django 2 | from django.contrib.auth.admin import UserAdmin as BaseUserAdmin 3 | from django.contrib import admin 4 | 5 | # Models 6 | from django.contrib.auth.models import User 7 | from users.models import Profile 8 | 9 | @admin.register(Profile) 10 | class ProfileAdmin(admin.ModelAdmin): 11 | list_display = ('pk', 'user', 'phone_number', 'website', 'picture') 12 | list_display_links = ('pk', 'user',) 13 | list_editable = ('phone_number', 'website', 'picture') 14 | search_fields = ( 15 | 'user__email', 16 | 'user__username', 17 | 'user__first_name', 18 | 'user__last_name', 19 | 'phone_number' 20 | ) 21 | list_filter = ( 22 | 'user__is_active', 23 | 'user__is_staff', 24 | 'created', 25 | 'modified' 26 | ) 27 | 28 | fieldsets= ( 29 | ('Profile', { 30 | 'fields': ( 31 | ('user', 'picture'), 32 | ), 33 | }), 34 | ('Extra info', { 35 | 'fields': ( 36 | ('website', 'phone_number'), 37 | ('biography',), 38 | ), 39 | }), 40 | ('Metadata', { 41 | 'fields': ( 42 | ('created', 'modified'), 43 | ), 44 | }), 45 | ) 46 | 47 | readonly_fields = ('created', 'modified',) 48 | 49 | 50 | class ProfileInline(admin.StackedInline): 51 | model = Profile 52 | can_delete = False 53 | verbose_name_plural = 'profiles' 54 | 55 | 56 | class UserAdmin(BaseUserAdmin): 57 | inlines = (ProfileInline,) 58 | list_display = ( 59 | 'username', 60 | 'email', 61 | 'first_name', 62 | 'last_name', 63 | 'is_active', 64 | 'is_staff' 65 | ) 66 | 67 | admin.site.unregister(User) 68 | admin.site.register(User, UserAdmin) -------------------------------------------------------------------------------- /users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | 6 | name = 'users' 7 | verbose_name = 'Users' 8 | -------------------------------------------------------------------------------- /users/forms.py: -------------------------------------------------------------------------------- 1 | # Django 2 | from django import forms 3 | 4 | # Models 5 | from django.contrib.auth.models import User 6 | from users.models import Profile 7 | 8 | class SignupForm(forms.Form): 9 | 10 | username = forms.CharField(min_length=4, max_length=50) 11 | 12 | password = forms.CharField(max_length=70, widget=forms.PasswordInput()) 13 | password_confirmation = forms.CharField(max_length=70, widget=forms.PasswordInput()) 14 | 15 | first_name = forms.CharField(min_length=2, max_length=50) 16 | last_name = forms.CharField(min_length=2, max_length=50) 17 | 18 | email = forms.CharField(min_length=6, max_length=70, widget=forms.EmailInput()) 19 | 20 | def clean_username(self): 21 | username = self.cleaned_data['username'] 22 | username_taken = User.objects.filter(username=username).exists() 23 | if username_taken: 24 | raise forms.ValidationError('Username is already in use.') 25 | 26 | return username 27 | 28 | def clean(self): 29 | data = super().clean() 30 | 31 | password = data['password'] 32 | password_confirmation = data['password_confirmation'] 33 | 34 | if password != password_confirmation: 35 | raise forms.ValidationError('Passwords do not match.') 36 | 37 | return data 38 | 39 | def save(self): 40 | data = self.cleaned_data 41 | data.pop('password_confirmation') 42 | 43 | user = User.objects.create_user(**data) 44 | profile = Profile(user=user) 45 | profile.save() 46 | 47 | 48 | class ProfileForm(forms.Form): 49 | 50 | website = forms.URLField(max_length=200, required=True) 51 | biography = forms.CharField(max_length=500, required=False) 52 | phone_number = forms.CharField(max_length=20, required=False) 53 | picture = forms.ImageField() 54 | -------------------------------------------------------------------------------- /users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-05-14 20:53 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Profile', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('website', models.URLField(blank=True)), 22 | ('biography', models.TextField(blank=True)), 23 | ('phone_number', models.CharField(blank=True, max_length=20)), 24 | ('picture', models.ImageField(blank=True, null=True, upload_to='users/pictures')), 25 | ('created', models.DateTimeField(auto_now_add=True)), 26 | ('modified', models.DateTimeField(auto_now=True)), 27 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 28 | ], 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/users/migrations/__init__.py -------------------------------------------------------------------------------- /users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | 4 | class Profile(models.Model): 5 | 6 | user = models.OneToOneField(User, on_delete=models.CASCADE) 7 | 8 | website = models.URLField(max_length=200, blank=True) 9 | biography = models.TextField(blank=True) 10 | phone_number = models.CharField(max_length=20, blank=True) 11 | 12 | picture = models.ImageField( 13 | upload_to='users/pictures', 14 | blank=True, 15 | null=True 16 | ) 17 | 18 | created = models.DateTimeField(auto_now_add=True) 19 | modified = models.DateTimeField(auto_now=True) 20 | 21 | def __str__(self): 22 | return self.user.username -------------------------------------------------------------------------------- /users/pictures/dashboard_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/users/pictures/dashboard_menu.png -------------------------------------------------------------------------------- /users/pictures/dashboard_users_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/35bytes/backend-django/af0e9691addd4b73e0ba584cd9a834960119e43b/users/pictures/dashboard_users_list.png -------------------------------------------------------------------------------- /users/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /users/views.py: -------------------------------------------------------------------------------- 1 | # Django 2 | from django.contrib.auth import authenticate, login, logout 3 | from django.contrib.auth.decorators import login_required 4 | from django.shortcuts import render, redirect 5 | 6 | # Models 7 | from django.contrib.auth.models import User 8 | from users.models import Profile 9 | 10 | # Forms 11 | from users.forms import ProfileForm, SignupForm 12 | 13 | @login_required 14 | def update_profile(request): 15 | 16 | profile = request.user.profile 17 | 18 | if request.method == 'POST': 19 | form = ProfileForm(request.POST, request.FILES) 20 | if form.is_valid(): 21 | data = form.cleaned_data 22 | 23 | profile.website = data['website'] 24 | profile.phone_number = data['phone_number'] 25 | profile.biography = data['biography'] 26 | profile.picture = data['picture'] 27 | profile.save() 28 | 29 | return redirect('update_profile') 30 | else: 31 | form = ProfileForm() 32 | 33 | return render( 34 | request = request, 35 | template_name = 'users/update_profile.html', 36 | context = { 37 | 'profile': profile, 38 | 'user': request.user, 39 | 'form': form, 40 | } 41 | ) 42 | 43 | def login_view(request): 44 | if request.method == 'POST': 45 | username = request.POST['username'] 46 | password = request.POST['password'] 47 | user = authenticate(request, username=username, password=password) 48 | if user: 49 | login(request, user) 50 | return redirect('feed') 51 | else: 52 | return render(request, 'users/login.html', {'error': 'Invalid username and password'}) 53 | return render(request, 'users/login.html') 54 | 55 | def signup(request): 56 | if request.method == 'POST': 57 | form = SignupForm(request.POST) 58 | 59 | if form.is_valid(): 60 | form.save() 61 | return redirect('login') 62 | 63 | else: 64 | form = SignupForm() 65 | 66 | return render( 67 | request=request, 68 | template_name='users/signup.html', 69 | context={ 70 | 'form': form 71 | } 72 | ) 73 | 74 | @login_required 75 | def logout_view(request): 76 | logout(request) 77 | return redirect('login') 78 | --------------------------------------------------------------------------------