├── auth ├── urls.py ├── __init__.py ├── config │ └── config.py ├── migrations │ └── __init__.py ├── layers │ ├── dao │ │ └── repositories.py │ ├── services │ │ └── services_y.py │ └── transport │ │ └── transport.py ├── templates │ └── auth │ │ └── index.html ├── models.py ├── tests.py ├── admin.py ├── views.py └── apps.py ├── main ├── __init__.py ├── environment │ └── environment.py ├── __pycache__ │ ├── urls.cpython-312.pyc │ ├── wsgi.cpython-312.pyc │ ├── __init__.cpython-312.pyc │ ├── settings.cpython-312.pyc │ └── context_processors.cpython-312.pyc ├── context_processors.py ├── urls.py ├── asgi.py ├── wsgi.py └── settings.py ├── nasa_image_gallery ├── __init__.py ├── migrations │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-312.pyc │ │ ├── 0001_initial.cpython-312.pyc │ │ └── 0002_alter_favourite_unique_together.cpython-312.pyc │ ├── 0002_alter_favourite_unique_together.py │ └── 0001_initial.py ├── tests.py ├── admin.py ├── __pycache__ │ ├── admin.cpython-312.pyc │ ├── apps.cpython-312.pyc │ ├── urls.cpython-312.pyc │ ├── views.cpython-312.pyc │ ├── __init__.cpython-312.pyc │ └── models.cpython-312.pyc ├── config │ ├── __pycache__ │ │ └── config.cpython-312.pyc │ └── config.py ├── layers │ ├── dao │ │ ├── __pycache__ │ │ │ └── repositories.cpython-312.pyc │ │ └── repositories.py │ ├── generic │ │ ├── __pycache__ │ │ │ ├── mapper.cpython-312.pyc │ │ │ └── nasa_card.cpython-312.pyc │ │ ├── nasa_card.py │ │ └── mapper.py │ ├── transport │ │ ├── __pycache__ │ │ │ └── transport.cpython-312.pyc │ │ └── transport.py │ └── services │ │ ├── __pycache__ │ │ └── services_nasa_image_gallery.cpython-312.pyc │ │ └── services_nasa_image_gallery.py ├── apps.py ├── templates │ ├── index.html │ ├── footer.html │ ├── registration │ │ └── login.html │ ├── favourites.html │ ├── home.html │ └── header.html ├── urls.py ├── models.py └── views.py ├── db.sqlite3 ├── requirements.txt ├── manage.py ├── static └── styles.css └── README.md /auth/urls.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /main/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /auth/config/config.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /auth/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /auth/layers/dao/repositories.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /auth/templates/auth/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nasa_image_gallery/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /auth/layers/services/services_y.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /auth/layers/transport/transport.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nasa_image_gallery/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/db.sqlite3 -------------------------------------------------------------------------------- /auth/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /auth/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /auth/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /auth/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /main/environment/environment.py: -------------------------------------------------------------------------------- 1 | # ACÁ SE COLOCARÁN LAS VARIABLES DE ENTORNO CONFIGURABLES POR EL USUARIO -------------------------------------------------------------------------------- /nasa_image_gallery/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /nasa_image_gallery/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /main/__pycache__/urls.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/main/__pycache__/urls.cpython-312.pyc -------------------------------------------------------------------------------- /main/__pycache__/wsgi.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/main/__pycache__/wsgi.cpython-312.pyc -------------------------------------------------------------------------------- /main/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/main/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /main/__pycache__/settings.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/main/__pycache__/settings.cpython-312.pyc -------------------------------------------------------------------------------- /main/context_processors.py: -------------------------------------------------------------------------------- 1 | from nasa_image_gallery.config import config 2 | 3 | def version(request): 4 | return {'VERSION': config.VERSION} -------------------------------------------------------------------------------- /main/__pycache__/context_processors.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/main/__pycache__/context_processors.cpython-312.pyc -------------------------------------------------------------------------------- /nasa_image_gallery/__pycache__/admin.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/__pycache__/admin.cpython-312.pyc -------------------------------------------------------------------------------- /nasa_image_gallery/__pycache__/apps.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/__pycache__/apps.cpython-312.pyc -------------------------------------------------------------------------------- /nasa_image_gallery/__pycache__/urls.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/__pycache__/urls.cpython-312.pyc -------------------------------------------------------------------------------- /nasa_image_gallery/__pycache__/views.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/__pycache__/views.cpython-312.pyc -------------------------------------------------------------------------------- /nasa_image_gallery/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /nasa_image_gallery/__pycache__/models.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/__pycache__/models.cpython-312.pyc -------------------------------------------------------------------------------- /nasa_image_gallery/config/__pycache__/config.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/config/__pycache__/config.cpython-312.pyc -------------------------------------------------------------------------------- /auth/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AuthConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'auth' 7 | -------------------------------------------------------------------------------- /nasa_image_gallery/migrations/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/migrations/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /nasa_image_gallery/layers/dao/__pycache__/repositories.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/layers/dao/__pycache__/repositories.cpython-312.pyc -------------------------------------------------------------------------------- /nasa_image_gallery/layers/generic/__pycache__/mapper.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/layers/generic/__pycache__/mapper.cpython-312.pyc -------------------------------------------------------------------------------- /nasa_image_gallery/migrations/__pycache__/0001_initial.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/migrations/__pycache__/0001_initial.cpython-312.pyc -------------------------------------------------------------------------------- /nasa_image_gallery/layers/generic/__pycache__/nasa_card.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/layers/generic/__pycache__/nasa_card.cpython-312.pyc -------------------------------------------------------------------------------- /nasa_image_gallery/layers/transport/__pycache__/transport.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/layers/transport/__pycache__/transport.cpython-312.pyc -------------------------------------------------------------------------------- /main/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | path('', include('nasa_image_gallery.urls')) 7 | ] -------------------------------------------------------------------------------- /nasa_image_gallery/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class NasaImageGalleryConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'nasa_image_gallery' 7 | -------------------------------------------------------------------------------- /nasa_image_gallery/layers/services/__pycache__/services_nasa_image_gallery.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/layers/services/__pycache__/services_nasa_image_gallery.cpython-312.pyc -------------------------------------------------------------------------------- /nasa_image_gallery/migrations/__pycache__/0002_alter_favourite_unique_together.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungs-ip/ip-public-repo/HEAD/nasa_image_gallery/migrations/__pycache__/0002_alter_favourite_unique_together.cpython-312.pyc -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.7.2 2 | beautifulsoup4==4.12.3 3 | certifi==2024.2.2 4 | charset-normalizer==3.3.2 5 | Django==4.2.10 6 | django-bootstrap-v5==1.0.11 7 | idna==3.6 8 | MarkupSafe==2.1.5 9 | requests==2.31.0 10 | soupsieve==2.5 11 | sqlparse==0.4.4 12 | tzdata==2023.4 13 | urllib3==2.2.0 -------------------------------------------------------------------------------- /nasa_image_gallery/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'header.html' %} {% block content %} {% if request.user.is_authenticated %} 2 |

Bienvenido {{ user.username | upper }}!

3 | {% else %} 4 |

Bienvenido!

5 | {% endif %} 6 |

7 | Mirá las imágenes desde este link. 8 |

9 | {% endblock %} -------------------------------------------------------------------------------- /nasa_image_gallery/templates/footer.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /nasa_image_gallery/config/config.py: -------------------------------------------------------------------------------- 1 | # archivo de configuración del sistema. 2 | 3 | # versión del TP. 4 | VERSION = 'Trabajo práctico - 1er cuatrimestre del 2024.' 5 | 6 | # REST API de la NASA para capturar imágenes de la galería 7 | NASA_REST_API = 'https://images-api.nasa.gov/search?q=' 8 | 9 | DEFAULT_SEARCH_WORD = 'space' 10 | 11 | NASA_REST_API_DEFAULT_SEARCH = NASA_REST_API + DEFAULT_SEARCH_WORD -------------------------------------------------------------------------------- /main/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for main 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/5.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', 'main.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /main/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for main 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/5.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', 'main.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /nasa_image_gallery/migrations/0002_alter_favourite_unique_together.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-02-14 04:45 2 | 3 | from django.conf import settings 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 11 | ('nasa_image_gallery', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterUniqueTogether( 16 | name='favourite', 17 | unique_together={('user', 'title', 'description', 'image_url', 'date')}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /nasa_image_gallery/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('', views.index_page, name='index-page'), 7 | path('login/', views.index_page, name='login'), 8 | path('home/', views.home, name='home'), 9 | path('buscar/', views.search, name='buscar'), 10 | 11 | path('favourites/', views.getAllFavouritesByUser, name='favoritos'), 12 | path('favourites/add/', views.saveFavourite, name='agregar-favorito'), 13 | path('favourites/delete/', views.deleteFavourite, name='borrar-favorito'), 14 | 15 | path('exit/', views.exit, name='exit'), 16 | ] -------------------------------------------------------------------------------- /nasa_image_gallery/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | 4 | # modelo para un favorito. 5 | # un usuario puede tener 0...n favoritos asociados. Si un usuario es borrado, no nos interesa retener sus favoritos, por lo cual se borran en cascada. 6 | class Favourite(models.Model): 7 | title = models.CharField(max_length=200) 8 | description = models.TextField() 9 | image_url = models.TextField() 10 | date = models.DateField() 11 | 12 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) # asociamos el favorito con el usuario en cuestión. 13 | 14 | class Meta: 15 | unique_together = ('user', 'title', 'description', 'image_url', 'date') -------------------------------------------------------------------------------- /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 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /nasa_image_gallery/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'header.html' %} {% block content %} 2 |
3 |
4 | {% csrf_token %} 5 |

Inicio de sesión

6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | {% endblock %} -------------------------------------------------------------------------------- /nasa_image_gallery/layers/generic/nasa_card.py: -------------------------------------------------------------------------------- 1 | class NASACard: 2 | def __init__(self, title, description, image_url, date, user=None, id=None): 3 | self.title = title 4 | self.description = description 5 | self.image_url = image_url 6 | self.date = date 7 | self.user = user 8 | self.id = id 9 | 10 | def __str__(self): 11 | return f'Título: {self.title}, Descripción: {self.description}, URL de la imagen: {self.image_url}, Fecha: {self.date}, Usuario: {self.user}, Id: {self.id}' 12 | 13 | # 2 NASACards son iguales si comparten el mismo title, description e image_url. 14 | # método equals. 15 | def __eq__(self, other): 16 | if not isinstance(other, NASACard): 17 | return False 18 | return (self.title, self.description, self.image_url) == \ 19 | (other.title, other.description, other.image_url) 20 | 21 | # método hashCode. 22 | def __hash__(self): 23 | return hash((self.title, self.description, self.image_url)) -------------------------------------------------------------------------------- /nasa_image_gallery/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-02-14 00:20 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='Favourite', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=200)), 22 | ('description', models.TextField()), 23 | ('image_url', models.TextField()), 24 | ('date', models.DateField()), 25 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /nasa_image_gallery/layers/dao/repositories.py: -------------------------------------------------------------------------------- 1 | # capa DAO de acceso/persistencia de datos. 2 | 3 | from nasa_image_gallery.models import Favourite 4 | 5 | def saveFavourite(image): 6 | try: 7 | fav = Favourite.objects.create(title=image.title, description=image.description, image_url=image.image_url, date=image.date, user=image.user) 8 | return fav 9 | except Exception as e: 10 | print(f"Error al guardar el favorito: {e}") 11 | return None 12 | 13 | def getAllFavouritesByUser(user): 14 | favouriteList = Favourite.objects.filter(user=user).values('id', 'title', 'description', 'image_url', 'date') 15 | return list(favouriteList) 16 | 17 | def deleteFavourite(id): 18 | try: 19 | favourite = Favourite.objects.get(id=id) 20 | favourite.delete() 21 | return True 22 | except Favourite.DoesNotExist: 23 | print(f"El favorito con ID {id} no existe.") 24 | return False 25 | except Exception as e: 26 | print(f"Error al eliminar el favorito: {e}") 27 | return False -------------------------------------------------------------------------------- /nasa_image_gallery/layers/transport/transport.py: -------------------------------------------------------------------------------- 1 | # capa de transporte/comunicación con otras interfaces o sistemas externos. 2 | 3 | import requests 4 | from ...config import config 5 | 6 | # comunicación con la REST API de la NASA. 7 | def getAllImages(input=None): 8 | if input is None: 9 | json_response = requests.get(config.NASA_REST_API_DEFAULT_SEARCH).json() 10 | else: 11 | json_response = requests.get(config.NASA_REST_API + input).json() 12 | 13 | json_collection = [] 14 | for object in json_response['collection']['items']: 15 | try: 16 | if 'links' in object: # verificar si la clave 'links' está presente en el objeto (sin 'links' NO nos sirve, ya que no mostrará las imágenes). 17 | json_collection.append(object) 18 | else: 19 | print("[Capa de transporte --> transport.py]: se encontró un objeto sin clave 'links', omitiendo...") 20 | 21 | except KeyError: # puede ocurrir que no todos los objetos tenga la info. completa, en ese caso descartamos dicho objeto y seguimos con el siguiente en la próxima iteración. 22 | pass 23 | 24 | return json_collection -------------------------------------------------------------------------------- /nasa_image_gallery/layers/services/services_nasa_image_gallery.py: -------------------------------------------------------------------------------- 1 | # capa de servicio/lógica de negocio 2 | 3 | from ..transport import transport 4 | from ..dao import repositories 5 | from ..generic import mapper 6 | from django.contrib.auth import get_user 7 | 8 | def getAllImages(input=None): 9 | # obtiene un listado de imágenes desde transport.py y lo guarda en un json_collection. 10 | # ¡OJO! el parámetro 'input' indica si se debe buscar por un valor introducido en el buscador. 11 | json_collection = [] 12 | 13 | images = [] 14 | 15 | # recorre el listado de objetos JSON, lo transforma en una NASACard y lo agrega en el listado de images. Ayuda: ver mapper.py. 16 | 17 | return images 18 | 19 | 20 | def getImagesBySearchInputLike(input): 21 | return getAllImages(input) 22 | 23 | 24 | # añadir favoritos (usado desde el template 'home.html') 25 | def saveFavourite(request): 26 | fav = '' # transformamos un request del template en una NASACard. 27 | fav.user = '' # le seteamos el usuario correspondiente. 28 | 29 | return repositories.saveFavourite(fav) # lo guardamos en la base. 30 | 31 | 32 | # usados en el template 'favourites.html' 33 | def getAllFavouritesByUser(request): 34 | if not request.user.is_authenticated: 35 | return [] 36 | else: 37 | user = get_user(request) 38 | 39 | favourite_list = [] # buscamos desde el repositorio TODOS los favoritos del usuario (variable 'user'). 40 | mapped_favourites = [] 41 | 42 | for favourite in favourite_list: 43 | nasa_card = '' # transformamos cada favorito en una NASACard, y lo almacenamos en nasa_card. 44 | mapped_favourites.append(nasa_card) 45 | 46 | return mapped_favourites 47 | 48 | 49 | def deleteFavourite(request): 50 | favId = request.POST.get('id') 51 | return repositories.deleteFavourite(favId) # borramos un favorito por su ID. -------------------------------------------------------------------------------- /nasa_image_gallery/layers/generic/mapper.py: -------------------------------------------------------------------------------- 1 | # mapper: se refiere a un componente o conjunto de funciones que se utiliza para convertir o "mapear" datos de un formato o estructura a otro. Esta conversión se realiza típicamente cuando se trabaja con diferentes capas de una aplicación, como por ejemplo, entre la capa de datos y la capa de presentación, o entre dos modelos de datos diferentes, mejorando la coherencia y eficiencia. 2 | 3 | from nasa_image_gallery.layers.generic.nasa_card import NASACard 4 | 5 | # usado cuando la info. viene de la API de la nasa, para transformarlo en una NASACard. 6 | def fromRequestIntoNASACard(object): 7 | nasa_card = NASACard( 8 | title=object['data'][0]['title'], 9 | description=object['data'][0]['description'], 10 | image_url=object['links'][0]['href'], 11 | date=object['data'][0]['date_created'][:10] 12 | ) 13 | 14 | return nasa_card 15 | 16 | 17 | # usado cuando la info. viene del template, para transformarlo en una NASACard antes de guardarlo en la base de datos. 18 | def fromTemplateIntoNASACard(templ): 19 | nasa_card = NASACard( 20 | title=templ.POST.get("title"), 21 | description=templ.POST.get("description"), 22 | image_url=templ.POST.get("image_url"), 23 | date=templ.POST.get("date") 24 | ) 25 | return nasa_card 26 | 27 | 28 | # cuando la info. viene de la base de datos, para transformarlo en una NASACard antes de mostrarlo. 29 | def fromRepositoryIntoNASACard(repo_dict): 30 | nasa_card = NASACard( 31 | id=repo_dict['id'], 32 | title=repo_dict['title'], 33 | description=repo_dict['description'], 34 | image_url=repo_dict['image_url'], 35 | date=repo_dict['date'], 36 | ) 37 | return nasa_card -------------------------------------------------------------------------------- /nasa_image_gallery/templates/favourites.html: -------------------------------------------------------------------------------- 1 | {% extends 'header.html' %} {% block content %} 2 |
3 |
4 |
5 |
6 |
7 |
8 |

Listado de FAVORITOS

9 |
10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for favorito in favourite_list %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 38 | 39 | {% endfor %} 40 | 41 |
#ImagenTítulo DescripciónFecha Acciones
-{{ favorito.title }}{{ favorito.description }}{{ favorito.date }} 32 |
33 | {% csrf_token %} 34 | 35 | 36 |
37 |
42 |
43 |
44 |
45 | {% endblock %} -------------------------------------------------------------------------------- /nasa_image_gallery/views.py: -------------------------------------------------------------------------------- 1 | # capa de vista/presentación 2 | # si se necesita algún dato (lista, valor, etc), esta capa SIEMPRE se comunica con services_nasa_image_gallery.py 3 | 4 | from django.shortcuts import redirect, render 5 | from .layers.services import services_nasa_image_gallery 6 | from django.contrib.auth.decorators import login_required 7 | from django.contrib.auth import logout 8 | 9 | # función que invoca al template del índice de la aplicación. 10 | def index_page(request): 11 | return render(request, 'index.html') 12 | 13 | # auxiliar: retorna 2 listados -> uno de las imágenes de la API y otro de los favoritos del usuario. 14 | def getAllImagesAndFavouriteList(request): 15 | images = [] 16 | favourite_list = [] 17 | 18 | return images, favourite_list 19 | 20 | # función principal de la galería. 21 | def home(request): 22 | # llama a la función auxiliar getAllImagesAndFavouriteList() y obtiene 2 listados: uno de las imágenes de la API y otro de favoritos por usuario*. 23 | # (*) este último, solo si se desarrolló el opcional de favoritos; caso contrario, será un listado vacío []. 24 | images = [] 25 | favourite_list = [] 26 | return render(request, 'home.html', {'images': images, 'favourite_list': favourite_list} ) 27 | 28 | 29 | # función utilizada en el buscador. 30 | def search(request): 31 | images, favourite_list = getAllImagesAndFavouriteList(request) 32 | search_msg = request.POST.get('query', '') 33 | 34 | # si el usuario no ingresó texto alguno, debe refrescar la página; caso contrario, debe filtrar aquellas imágenes que posean el texto de búsqueda. 35 | pass 36 | 37 | 38 | # las siguientes funciones se utilizan para implementar la sección de favoritos: traer los favoritos de un usuario, guardarlos, eliminarlos y desloguearse de la app. 39 | @login_required 40 | def getAllFavouritesByUser(request): 41 | favourite_list = [] 42 | return render(request, 'favourites.html', {'favourite_list': favourite_list}) 43 | 44 | 45 | @login_required 46 | def saveFavourite(request): 47 | pass 48 | 49 | 50 | @login_required 51 | def deleteFavourite(request): 52 | pass 53 | 54 | 55 | @login_required 56 | def exit(request): 57 | pass -------------------------------------------------------------------------------- /nasa_image_gallery/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'header.html' %} {% block content %} 2 |
3 |

Galería de Imágenes de la NASA

4 |
5 | 6 |
7 | {% csrf_token %} 8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 | {% for imagen in images %} 16 |
17 |
18 | imagen 19 |
20 |
{{ imagen.title}}
21 |

{{ imagen.description}}

22 |
23 | {% if request.user.is_authenticated %} 24 | 36 | {% endif %} 37 |
38 |
39 | {% endfor %} 40 |
41 |
42 | {% endblock %} -------------------------------------------------------------------------------- /nasa_image_gallery/templates/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Mi primera aplicación con Django 8 | {% load bootstrap5 %} {% bootstrap_css %} {% bootstrap_javascript %} {% load static %} 9 | 10 | 11 | 12 | 13 | 14 | 44 | 45 | {% block content %} {% endblock %} {% include "footer.html" %} -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #566787; 3 | background: #f5f5f5; 4 | font-family: 'Roboto', sans-serif; 5 | } 6 | 7 | .table-responsive { 8 | margin: 30px 0; 9 | } 10 | 11 | .table-wrapper { 12 | min-width: 1000px; 13 | background: #fff; 14 | padding: 20px; 15 | box-shadow: 0 1px 1px rgba(0, 0, 0, .05); 16 | } 17 | 18 | .table-title { 19 | padding-bottom: 10px; 20 | margin: 0 0 10px; 21 | min-width: 100%; 22 | } 23 | 24 | .table-title h2 { 25 | margin: 8px 0 0; 26 | font-size: 22px; 27 | } 28 | 29 | .search-box { 30 | position: relative; 31 | float: right; 32 | } 33 | 34 | .search-box input { 35 | height: 34px; 36 | border-radius: 20px; 37 | padding-left: 35px; 38 | border-color: #ddd; 39 | box-shadow: none; 40 | } 41 | 42 | .search-box input:focus { 43 | border-color: #3FBAE4; 44 | } 45 | 46 | .search-box i { 47 | color: #a0a5b1; 48 | position: absolute; 49 | font-size: 19px; 50 | top: 8px; 51 | left: 10px; 52 | } 53 | 54 | table.table tr th, 55 | table.table tr td { 56 | border-color: #e9e9e9; 57 | } 58 | 59 | table.table-striped tbody tr:nth-of-type(odd) { 60 | background-color: #fcfcfc; 61 | } 62 | 63 | table.table-striped.table-hover tbody tr:hover { 64 | background: #f5f5f5; 65 | } 66 | 67 | table.table th i { 68 | font-size: 13px; 69 | margin: 0 5px; 70 | cursor: pointer; 71 | } 72 | 73 | table.table td:last-child { 74 | width: 130px; 75 | } 76 | 77 | table.table td a { 78 | color: #a0a5b1; 79 | display: inline-block; 80 | margin: 0 5px; 81 | } 82 | 83 | table.table td a.view { 84 | color: #03A9F4; 85 | } 86 | 87 | table.table td a.edit { 88 | color: #FFC107; 89 | } 90 | 91 | table.table td a.delete { 92 | color: #E34724; 93 | } 94 | 95 | table.table td i { 96 | font-size: 19px; 97 | } 98 | 99 | .pagination { 100 | float: right; 101 | margin: 0 0 5px; 102 | } 103 | 104 | .pagination li a { 105 | border: none; 106 | font-size: 95%; 107 | width: 30px; 108 | height: 30px; 109 | color: #999; 110 | margin: 0 2px; 111 | line-height: 30px; 112 | border-radius: 30px !important; 113 | text-align: center; 114 | padding: 0; 115 | } 116 | 117 | .pagination li a:hover { 118 | color: #666; 119 | } 120 | 121 | .pagination li.active a { 122 | background: #03A9F4; 123 | } 124 | 125 | .pagination li.active a:hover { 126 | background: #0397d6; 127 | } 128 | 129 | .pagination li.disabled i { 130 | color: #ccc; 131 | } 132 | 133 | .pagination li i { 134 | font-size: 16px; 135 | padding-top: 6px 136 | } 137 | 138 | .hint-text { 139 | float: left; 140 | margin-top: 6px; 141 | font-size: 95%; 142 | } -------------------------------------------------------------------------------- /main/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 5 | BASE_DIR = Path(__file__).resolve().parent.parent 6 | 7 | 8 | # Quick-start development settings - unsuitable for production 9 | # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ 10 | 11 | # SECURITY WARNING: keep the secret key used in production secret! 12 | SECRET_KEY = 'django-insecure-2drt2_^@w@6)3npj24wi%xe^&88icb)dw&e0!^%eao3r6l4fkt' 13 | 14 | # SECURITY WARNING: don't run with debug turned on in production! 15 | DEBUG = True 16 | 17 | ALLOWED_HOSTS = ['localhost', '127.0.0.1', '::1'] 18 | 19 | 20 | # Application definition 21 | 22 | INSTALLED_APPS = [ 23 | 'django.contrib.admin', 24 | 'django.contrib.auth', 25 | 'django.contrib.contenttypes', 26 | 'django.contrib.sessions', 27 | 'django.contrib.messages', 28 | 'django.contrib.staticfiles', 29 | 'nasa_image_gallery', 30 | 'bootstrap5', 31 | ] 32 | 33 | MIDDLEWARE = [ 34 | 'django.middleware.security.SecurityMiddleware', 35 | 'django.contrib.sessions.middleware.SessionMiddleware', 36 | 'django.middleware.common.CommonMiddleware', 37 | 'django.middleware.csrf.CsrfViewMiddleware', 38 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 39 | 'django.contrib.messages.middleware.MessageMiddleware', 40 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 41 | ] 42 | 43 | ROOT_URLCONF = 'main.urls' 44 | 45 | TEMPLATES = [ 46 | { 47 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 48 | 'DIRS': [], 49 | 'APP_DIRS': True, 50 | 'OPTIONS': { 51 | 'context_processors': [ 52 | 'django.template.context_processors.debug', 53 | 'django.template.context_processors.request', 54 | 'django.contrib.auth.context_processors.auth', 55 | 'django.contrib.messages.context_processors.messages', 56 | 'main.context_processors.version', 57 | ], 58 | }, 59 | }, 60 | ] 61 | 62 | WSGI_APPLICATION = 'main.wsgi.application' 63 | 64 | 65 | # Database 66 | # https://docs.djangoproject.com/en/5.0/ref/settings/#databases 67 | 68 | DATABASES = { 69 | 'default': { 70 | 'ENGINE': 'django.db.backends.sqlite3', 71 | 'NAME': BASE_DIR / 'db.sqlite3', 72 | } 73 | } 74 | 75 | 76 | # Password validation 77 | # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators 78 | 79 | AUTH_PASSWORD_VALIDATORS = [ 80 | { 81 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 82 | }, 83 | { 84 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 85 | }, 86 | { 87 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 88 | }, 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 91 | }, 92 | ] 93 | 94 | 95 | # Internationalization 96 | # https://docs.djangoproject.com/en/5.0/topics/i18n/ 97 | 98 | LANGUAGE_CODE = 'en-us' 99 | 100 | TIME_ZONE = 'UTC' 101 | 102 | USE_I18N = True 103 | 104 | USE_TZ = True 105 | 106 | 107 | # Static files (CSS, JavaScript, Images) 108 | # https://docs.djangoproject.com/en/5.0/howto/static-files/ 109 | 110 | STATIC_URL = 'static/' 111 | STATICFILES_DIRS = [ 112 | os.path.join(BASE_DIR, 'static'), 113 | ] 114 | 115 | # Default primary key field type 116 | # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field 117 | 118 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 119 | 120 | # VARIABLES QUE INTEGRAN LOS REDIRECTS DE AUTH 121 | LOGIN_REDIRECT_URL = 'index-page' 122 | LOGOUT_REDIRECT_URL = 'index-page' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introducción a la Programación - primer semestre del 2024. 2 | ## Trabajo práctico: galería de imágenes de la NASA 🚀 3 | 4 | 5 | ![Galería de Imágenes de la NASA](https://api.nasa.gov/assets/img/general/apod.jpg) 6 | 7 | ### Introducción 8 | - El trabajo consiste en implementar una aplicación web *fullstack* usando **[Django Framework](https://docs.djangoproject.com/en/4.2/)** que permita consultar las imágenes de la API pública que proporciona la NASA. La información que provenga de esta API será renderizada por el *framework* en distintas *cards* que mostrarán -como mínimo- la imagen en cuestión, un título y una descripción. Adicionalmente -y para enriquecerla- se prevee que los estudiantes desarrollen la lógica necesaria para hacer funcionar el buscador central y un módulo de autenticación básica (usuario/contraseña) para almacenar uno o más resultados como **favoritos**, que luego podrán ser consultados por el usuario al loguearse. En este último, la app deberá tener la lógica suficiente para verificar cuándo una imagen fue marcada en favoritos. 9 | 10 | - Gran parte de la aplicación ya está resuelta: solo falta implementar las funcionalidades más importantes 😉. 11 | 12 | ### ¿Cómo empiezo? 13 | 14 | 1. Descargá e instalá **Visual Studio Code** desde ```https://code.visualstudio.com/``` 15 | - Adicionalmente, se recomienda la instalación de las siguientes extensiones para facilitar el desarrollo: 16 | - After Dark. 17 | - Prettier - Code formatter. 18 | - Pylance. 19 | - Python. 20 | - Python Debugger. 21 | 22 | Finalizada la instalación, ejecutá el programa. Deberías ver algo como lo siguiente (muestra dentro del mismo TP): 23 | ![imagen](https://i.ibb.co/0BGkYrz/extens.png) 24 | Guía oficial de instalación de extensiones disponible [aquí](https://code.visualstudio.com/docs/editor/extension-marketplace). 25 | 26 | 2. Instalá la última versión de Python desde ```www.python.org```. **Asegúrate de agregarlo al PATH durante la instalación:** 27 | 28 | ![imagen](https://i.postimg.cc/JnY2cVWq/python-image.png) 29 | 30 | 3. Creá una cuenta en GitHub [desde acá](https://github.com/signup?ref_cta=Sign+up&ref_loc=header+logged+out&ref_page=%2F&source=header-home). Luego, debés efectuar un *fork* (copia) del proyecto a tu repositorio (o al del grupo): [tutorial para hacer forks - thx. MitoCode](https://www.youtube.com/watch?v=9YUaf-uxuRM). 31 | 32 | 4. Cloná el repositorio copiado en tu máquina local (*git clone*). A continuación, dentro de la carpeta del repositorio local, abrí una terminal de *VS Code* e instalá Django ejecutando el siguiente comando: 33 | ```pip install django==4.2.10``` 34 | 35 | 5. Instalá las dependencias necesarias: 36 | ```pip install -r requirements.txt``` 37 | 38 | 6. Ejecutá el servidor Django (3000 representa el puerto donde se ejecutará la app): 39 | ```python manage.py runserver 3000``` 40 | 41 | 7. Abrí tu navegador y dirigíte a ```http://localhost:3000``` para ver la aplicación. Deberá mostrar una pantalla como la siguiente: 42 | ![imagen](https://i.ibb.co/GFJdgHr/galeria-default.png) 43 | 44 | 8. Por último, para ver el contenido de la base integrada (SQLite), recomendamos el uso de **DB Browser for SQLite**. Link de descarga: https://sqlitebrowser.org/dl/ 45 | - El archivo que se debe abrir es **db.sqlite3**. 46 | 47 | ### Lo que ya está implementado 48 | - A nivel **template**, se cuenta con 4 HTMLs: **header (cabecera de la página)**, **footer (pie de página)**, **home (sección donde se mostrarán las imágenes y el buscador)** e **index (contener principal que incluye a los 3 HTMLs anteriores).** Para el caso del *header*, se implementó cierta lógica para determinar si un usuario está logueado (o no) y obtener así su nombre; para el caso del *home*, éste tiene un algoritmo que permite recorrer cada objeto de la API y dibujar su información en pantalla. El *footer* no posee acciones a nivel código relevantes para el desarrollo. 49 | 50 | - A nivel **views**, en el archivo **views.py** encontrarán algunas funciones semidesarrolladas: *index_page(request)* que renderiza el contenido de 'index.html'; *home(request)* que obtiene todas las imágenes mapeadas de la API -a través de la capa de servicio- y los favoritos del usuario, y muestra el contenido de 'home.html' pasándole dicha información. Esta última hace uso de la función auxiliar *getAllImagesAndFavouriteList(request)* que devuelve 2 listas: una de las imágenes de la API y otra de las imágenes marcadas como favoritos del usuario. 51 | 52 | - A nivel **lógica**, se incluye el archivo **transport.py** completo con todo el código necesario para consumir la API. Además, se anexa un **mapper.py** con la lógica necesaria para convertir/mapear los resultados en una **NASACard** (objeto que finalmente se utilizará en el template para dibujar los resultados). 53 | 54 | - El proyecto está construido sobre una **[arquitectura multicapas](https://medium.com/@e0324913/multilayered-software-architecture-1eaa97b8f49e)**, donde cada capa posee una única responsabilidad y se encuentra desacoplada del resto. Son las siguientes: 55 | - **DAO** (empleada para el alta/baja/modificación -CRUD/ABM- de objetos en una base de datos integrada, llamada **[SQLite](X)**). 56 | - **Services** (usada para la lógica de negocio de la aplicación). 57 | - **Transport** (utilizada para el consumo de la API en cuestión). 58 | 59 | Si bien no es un parámetro de evaluación dónde colocan las funciones, es altamente recomendado que las funciones que se agreguen estén en las capas que correspondan (consultar con los docentes en caso de dudas). 60 | 61 | ### ¿Qué voy a ver al iniciar la app? 62 | - Al iniciar la aplicación y hacer clic sobre **Galería**, verás lo siguiente: 63 | ![imagen](https://i.ibb.co/bN6bhVG/galeria-1.png) 64 | 65 | 66 | ### Lo que falta hacer 67 | 68 | - Aún faltan implementar ciertas funciones de los archivos **views.py** y **services_nasa_image_gallery.py**. Éstas son las encargadas de hacer que las imágenes de la galería se muestren/rendericen: 69 | 70 | - **views.py**: 71 | - *home(request):* invoca a getAllImagesAndFavouriteList(request) para obtener 2 listados que utilizará para renderizar el template. 72 | ![imagen](https://i.ibb.co/0mMLRrv/galeria-4.png) 73 | - *getAllImagesAndFavouriteList(request):* invoca al servicio correspondiente para obtener 2 listados, uno de las imágenes de la API y otro -si corresponde- de los favoritos del usuario. 74 | ![imagen](https://i.ibb.co/DpRXXj5/galeria-4.png) 75 | 76 | 77 | - **services_nasa_image_gallery.py**: 78 | - *getAllImages(input=None):* obtiene un listado de imágenes de la API. El parámetro *input*, si está presente, indica sobre qué imágenes debe filtrar/traer. 79 | ![imagen](https://i.ibb.co/mqGRNfR/galeria-3.png) 80 | 81 | **Concluido su desarrollo, deberían ver algo como lo siguiente:** 82 | ![imagen](https://i.ibb.co/yptbG3g/galeria-2.png) 83 | 84 | ### Condiciones de entrega 85 | 86 | - Requisitos de aprobación y criterio de corrección 87 | - El TP debe realizarse en grupos de 2 o 3 integrantes (no 1). Para aprobar el trabajo se deberán reunir los siguientes ítems: 88 | - La galería de imágenes se muestra adecuadamente (**imagen**, **título** y **descripción**). 89 | - El código debe ser **claro**. Las variables y funciones deben tener nombres que hagan fácil de entender el código a quien lo lea -de ser necesario, incluir comentarios que clarifiquen-. **Reutilizar el código mediante funciones todas las veces que se amerite.** 90 | - No deben haber variables que no se usan, funciones que tomen parámetros que no necesitan, ciclos innecesarios, etc. 91 | - **El 'correcto' funcionamiento del código NO es suficiente para la aprobación del TP, son necesarios todos los ítems mencionados arriba.** 92 | 93 | 94 | 95 | ### Opcionales 96 | Las siguientes funcionalidades del juego NO son necesarias para la aprobación (con nota mínima), pero sirven para mejorar la nota del trabajo. De optar por hacerlas, se aplican las mismas reglas y criterios de corrección que para las funcionalidades básicas. Cualquier otra funcionalidad extra que se desee implementar debe ser antes consultada con los docentes. 97 | 98 | **Los opcionales notados con ⭐ ya están parcialmente resueltos.** Se sugiere comenzar con ellos y luego seguir con los demás. 99 | 100 | **⚠️NO ES NECESARIO REALIZAR TODOS LOS OPCIONALES.⚠️** 101 | Enfóquense en los más relevantes, teniendo en cuenta el tiempo de desarrollo y pruebas. 102 | 103 | - #### **Buscador** ⭐ 104 | - Se debe **completar** la funcionalidad para que el buscador filtre adecuadamente las imágenes, según los siguientes criterios: 105 | - Si el usuario **NO** ingresa dato alguno y hace clic sobre el botón 'Buscar', debe filtrar por el valor predeterminado (*space*). 106 | - Si el usuario ingresa algún dato (ej. *sun* -sol, en inglés-), al hacer clic se deben desplegar las imágenes filtradas relacionadas a dicho valor. 107 | 108 | Ejemplo para *moon* (luna). **ATENCIÓN, las imágenes pueden variar**: 109 | ![imagen](https://i.ibb.co/pz5fTqk/galeria13.png) 110 | 111 | --- 112 | 113 | - #### **Inicio de sesión** ⭐⭐ 114 | - Se debe **completar** la *feature* de inicio de sesión de la app. El usuario y contraseña a utilizar, preliminarmente, es **admin**/**admin** (ya se encuentra guardado sobre la base SQLite, tabla *auth_user*). 115 | - Consideraciones: 116 | - **NO** se permite utilizar *Django Admin* para emular la autenticación de los usuarios, la sección **Iniciar sesión** debe funcionar adecuadamente. 117 | - Solo los usuarios que hayan iniciado sesión podrán añadir las imágenes como favoritos y visualizarlas en su sección correspondiente. 118 | - Ayuda: [tutorial de autenticación login/logout básica](https://www.youtube.com/watch?v=oKuZQ238Ncc) 119 | 120 | Una posible visualización del inicio de sesión es: 121 | ![imagen](https://i.ibb.co/nMHGFD9/session-1.png) 122 | ![imagen](https://i.ibb.co/cwzcBNx/session-2.png) 123 | 124 | --- 125 | 126 | - #### **Favoritos** ⭐⭐ 127 | - Se debe **completar** la lógica presente para permitir que un usuario logueado pueda almacenar una o varias imágenes de la galería como **favoritos**, mediante el clic de un botón en la parte inferior. 128 | - **Observaciones** 129 | - Este punto puede realizarse SOLO si el ítem anterior (inicio de sesión) está desarrollado/funcionando bien. 130 | - Si el favorito ya fue añadido, debe mostrarse un botón que impida reañadirlo. 131 | - Debe existir una sección llamada 'Favoritos' que permita listar todos los agregados por el usuario, mediante una tabla. Además, debe existir un botón que permita removerlo del listado (**si fue removido, desde la galería de imágenes podrá ser agregado otra vez**). 132 | - **Parte del código ya está resuelto**. Revisar los archivos *views.py*, *repositories.py* y *services_nasa_image_gallery.py*. 133 | 134 | Una posible visualización de este ítem resuelto es: 135 | ![imagen](https://i.ibb.co/09LknXN/galeria-11.png) 136 | ![imagen](https://i.ibb.co/nDQrXLc/galeria12.png) 137 | 138 | --- 139 | 140 | - #### Paginación de resultados (A) ó ***infinite scroll*** (B) (estilo Instagram ó Facebook). 141 | - (A) Se desea implementar la paginación de los resultados de búsqueda, de forma tal que: 142 | - Por cada página, se muestren 5* imágenes. Es de interés que este número lo pueda escoger el usuario (definir/investigar la mejor forma de lograrlo). 143 | - Se deben listar TODAS las imágenes de la API. Si el número de imágenes no es **múltiplo** de la cantidad escogida, el **resto** debe figurar en una página adicional. 144 | 145 | ![imagen](https://cdn.hashnode.com/res/hashnode/image/upload/v1629807013323/uyllDChXl.gif) 146 | 147 | - (B) Se desea implementar un algoritmo que permita mostrar cierta cantidad de imágenes y, a partir del *swap*/deslizamiento de la barra de *scroll* vertical, cargue las demás hasta completar su tope (similar al *scroll* infinito que poseen aplicaciones como Instagram, Facebook o X). **Es de interés que el usuario pueda configurar la cantidad de imágenes mostradas en un principio.** 148 | 149 | ![imagen](https://i.makeagif.com/media/11-10-2014/dXAbu_.gif) 150 | 151 | --- 152 | 153 | - #### Añadir comentarios en imágenes marcadas en favoritos 154 | - Se desea que, cada vez que se añada una nueva imagen a favoritos, se visualice un mensaje cargado por el usuario al hacer clic sobre el botón correspondiente. **Este mensaje debe visualizarse en la tabla de la sección en cuestión.** 155 | 156 | ![imagen](https://images.vexels.com/media/users/3/144066/isolated/preview/00c9f19169fbda083382d2d1bbaa5d37-burbuja-de-comentario.png) 157 | 158 | --- 159 | 160 | - #### ALTA de nuevos usuarios 161 | - Actualmente la aplicación no permite el registro/alta de nuevos usuarios. Se desea implementar esta sección, para permitir que cualquier persona pueda registrarse en la aplicación. 162 | - Consideraciones: 163 | - Se debe solicitar nombre, apellido, usuario, contraseña y correo electrónico. **Si dos personas poseen el mismo nombre de usuario se anulará el alta, visualizando un mensaje descriptivo del error.** 164 | - El registro exitoso debe disparar un correo a la casilla indicada por el usuario, que indique en el cuerpo del mismo las credenciales de acceso. 165 | - Ayuda: [envío de emails usando cuenta @gmail a través de Django](https://github.com/akjasim/cb_django-sending-emails) 166 | 167 | ![imagen](https://mantpress.com/wp-content/uploads/02-03-22-X-plugins-para-el-registro-de-usuarios-en-tu-sitio-web-1200x630.png) 168 | 169 | --- 170 | 171 | - #### Internacionalizar (i18n) aplicación para soportar múltiples idiomas 172 | - Se debe desarrollar una lógica que permita *switchear* el idioma de la aplicación, de español a inglés o portugués y viceversa. 173 | - El *switch* debe ejecutarse desde la misma página, según el usuario lo requiera. 174 | 175 | ![imagen](https://images.unsplash.com/photo-1512076249812-fd58fb2c8748?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ) 176 | 177 | --- 178 | 179 | - #### *Loading Spinner* para la carga de imágenes 180 | - Se desea implementar una pantalla de carga/*loading spinner* que indique el usuario que espere hasta que la carga de imágenes se complete. 181 | 182 | ![imagen](https://media.tenor.com/tEBoZu1ISJ8AAAAC/spinning-loading.gif) 183 | 184 | --- 185 | 186 | - #### Eliminar resultado de búsqueda no interesante (botón 'no mostrar esta img') 187 | - El usuario tendrá la posibilidad de marcar una imagen de la galería como **no interesante**. Si una imagen fue marcada de esta forma, además de eliminarse automáticamente de la galería, NO se mostrará en futuras búsquedas. 188 | - Ayuda: pensar a las imágenes **no interesantes** como una **lista** de NASACards, con un **atributo especial** que permita decidir, para determinado usuario, si resulta o no relevante en la búsqueda. 189 | 190 | 191 | Debería verse algo similar a la siguiente idea: 192 | ![imagen](https://i.ibb.co/GpDWGMf/anuncio.png) 193 | 194 | --- 195 | 196 | - #### Renovar interfaz gráfica 197 | - Se debe proponer una nueva interfaz gráfica para los distintos *templates* de la aplicación. 198 | - Recomendaciones: 199 | - Pueden usar el *framework CSS* que deseen, sea [Bootstrap](https://getbootstrap.com/), [Tailwind](https://tailwindcss.com/), [Foundation](https://get.foundation/), etc., siempre y cuando **consideren que el código debe resultar LEGIBLE para su corrección**. 200 | - Verificar que la lógica implementada en los *templates* funcione bien a medida que se modifica la interfaz. 201 | 202 | ![imagen](https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcShH3gYS_Po2eaj0zb9qsjWrSJttgdJe2C2PEKsrRGVhRrP89i968HBir68O_2PiUiGmn4&usqp=CAU) 203 | 204 | --- 205 | 206 | - #### Mapeo/traducción automática de palabras ingresadas en español, a inglés, para el correcto funcionamiento del buscador 207 | - En la actualidad, el buscador solo admite palabras escritas en inglés, lo cual puede dificultar su uso para personas que no hablen ese idioma. Se debe implementar un algoritmo que permita *mapear* las palabras ingresadas en el buscador, de español a inglés, permitiendo que aún si el usuario ingresa algo en su idioma nativo lo traduzca antes de enviar la petición a la API, no alterando el normal funcionamiento de ésta y ampliando la utilidad del mismo. 208 | 209 | - *Tips*: 210 | - Opción 1: pensar un archivo tipo JSON (*key, value*), que contenga TODAS las posibles palabras que pueda ingresar la persona en el buscador (español e inglés). 211 | - Opción 2: usar la [API de Google Translate](https://codeloop.org/google-translate-api-with-python/) para traducir los ingresos de los usuarios internamente, antes de enviar la petición. 212 | 213 | ![imagen](https://is1-ssl.mzstatic.com/image/thumb/Purple126/v4/08/31/22/0831220c-39ba-c659-04a1-fbda53a8bd68/AppIcon-1x_U007emarketing-0-7-0-85-220.png/256x256bb.jpg) 214 | 215 | --- 216 | 217 | ### Fecha de entrega 218 | El trabajo debe ser entregado en la fecha estipulada en el cronograma. **Recordar que es requisito hacer pre-entregas.** 219 | 220 | ### Formato de entrega 221 | - La entrega se dividirá de 2 partes: **código** e **informe**: 222 | 223 | - **Parte 1: código:** todo el desarrollo debe estar en un repositorio interno del grupo (*fork* del repo base del TP). Se deben añadir a los docentes de la comisión con motivo de verificar los avances del mismo (corregir funciones, brindar sugerencias o recomendaciones, etc). Dado el caudal de alumnos, **serán responsables los estudiantes de notificar a los docentes para evaluar una pre-entrega, corregir alguna duda o similar que bloquee/impida del avance del TP**. 224 | 225 | Sugerimos: 226 | - Que cada integrante tenga su propia cuenta de GitHub, NO usar una única en el proyecto. 227 | - Cada integrante debe *commitear* una o varias porciones de código, dependiendo cómo distribuyan el trabajo. **Se debe visualizar el aporte individual al TP.** 228 | 229 | 230 | - **Parte 2: informe:** deben redactar un documento donde exista una introducción que explique de qué se trata el trabajo (sin utilizar lenguaje técnico), que incluya el código de las funciones implementadas y una breve explicación de cada una de ellas junto con las **dificultades de implementación** y **decisiones tomadas** -con su correspondiente justificación-. **NO incluir explicaciones de funcionalidades de Python, Django o similares**. Este documento debe estar en formato PDF anexo dentro de la carpeta del TP. 231 | 232 | 🔥 **Se DEBE cumplir con ambas partes (código + informe) para aprobar el trabajo práctico.** 233 | 234 | 235 | ### Documentación adicional 236 | - Documentación oficial de Django disponible aquí: https://docs.djangoproject.com/en/4.2/ 237 | - Sección **GIT** 238 | - Introducción a GIT: [clic acá](https://www.youtube.com/watch?v=mzHWafbVRyU). 239 | - Manejo de ramas/branches: [clic acá](https://www.youtube.com/watch?v=BRY9gamL9PE). 240 | - Merge & resolución de conflictos: [clic acá](https://www.youtube.com/watch?v=9YUaf-uxuRM). 241 | --------------------------------------------------------------------------------