diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 0000000..8f17202 --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,23 @@ +{ + "name": "Django", + "dockerComposeFile": ["local.yml", "docker-compose.extend.yml"], + "service": "workspace", + "shutdownAction": "stopCompose", + "workspaceFolder": "/workspace/", + "forwardPorts": [8000], + "onCreateCommand": "pip install -r requirements/local.txt && pre-commit install", + "postAttachCommand": "python -m pytest", + "remoteEnv": { + "DATABASE_URL": "postgres://${containerEnv:POSTGRES_USER}:${containerEnv:POSTGRES_PASSWORD}@${containerEnv:POSTGRES_HOST}:${containerEnv:POSTGRES_PORT}/${containerEnv:POSTGRES_DB}" + }, + "customizations": { + "vscode": { + "settings": { + "python.testing.pytestArgs": ["tests"], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true + }, + "extensions": ["ms-python.python"] + } + } +} diff --git a/.envs/.local/.django b/.envs/.local/.django index bcde257..4b06fef 100644 --- a/.envs/.local/.django +++ b/.envs/.local/.django @@ -2,3 +2,4 @@ # ------------------------------------------------------------------------------ USE_DOCKER=yes IPYTHONDIR=/app/.ipython +LANG=es_CO.UTF-8 diff --git a/.github/ISSUE_TEMPLATE/pregunta.md b/.github/ISSUE_TEMPLATE/pregunta.md new file mode 100644 index 0000000..07d8966 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/pregunta.md @@ -0,0 +1,10 @@ +--- +name: Pregunta +about: Si tienes dudas comunicate con nosotros a través de meetup o discord +--- + +# Preguntas, inquietudes + +Puedes escribirnos tus dudas en nuestra página de meetup [pythonbaq](https://www.meetup.com/es-ES/pythonbaq/) o al correo djangoquilla@gmail.com + +Nos encuentras en el Discord de Python Colombia dentro del [#temii](https://discord.gg/fDsZ6mrdtC). diff --git a/.github/ISSUE_TEMPLATE/reporte_error.md b/.github/ISSUE_TEMPLATE/reporte_error.md new file mode 100644 index 0000000..cb80f67 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/reporte_error.md @@ -0,0 +1,21 @@ +--- +name: Reporte de error +about: Genera un reporte de error para ayudarnos a mejorar +--- + +# Error en sitio web de Python Barranquilla + +## Comportamiento esperado + +En esta zona escribe o adjunta una imagen de como debería funcionar la aplicación web. + +## Comportamiento actual + +En esta zona escribe o adjunta una imagen de la salida actual que se observa de la aplicación web. +Es preferible el uso de capturas de pantalla además de una descripción paso a paso de como reproducir el error. + +## Pasos para reproducir el error + +1. En que URL aparece el error. +2. Definir paso a paso las acciones para reproducir el error. +3. En este ejemplo se utiliza una lista numerada. diff --git a/.github/ISSUE_TEMPLATE/solicitud_mejora.md b/.github/ISSUE_TEMPLATE/solicitud_mejora.md new file mode 100644 index 0000000..89c5467 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/solicitud_mejora.md @@ -0,0 +1,16 @@ +--- +name: Solicitud de mejora +about: Encontraste algo que crees que debería tener la app de Temii +--- + +# Solicitud de mejora en Temii + +Especifica como se puede implementar la mejora que sugieres para Temii. + +## Ventajas + +Que ventajas supone lo que estás sugiriendo a futuro. + +## Desventajas + +Cuales crees que podrían ser los inconvenientes que pueda ocasionar la implementación de lo que estás sugiriendo a futuro. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..cb53a34 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ +# Pull request + +Closes #(número del issue) + +## Observaciones + +Escribe o adjunta una imagen de la información que considere pertinente del cambio que se está subiendo. Por ejemplo una captura de pantalla en algún cambio visual. diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d969f96 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.testing.pytestArgs": ["tests"], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} diff --git a/README.md b/README.md index ea59dd8..924b395 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,11 @@ Para ejecutar docker compose localmente toca especificarle que use el archivo lo - Para crear una **cuenta de superadministrador**, usa el comando: - python manage.py createsuperuser +```bash +docker compose -f local.yml run --rm django python manage.py shell_plus + +python manage.py createsuperuser +``` Por conveniencia, puedes abrir el usuario normal en un navegador, y el super-administrador en otro y ver los comportamientos para cada tipo de usuario. @@ -36,12 +40,24 @@ Puedes correr los checks de tipos de datos con el comando mypy temii +### Debugging con ipdb + +Levanta los contenedores de forma _detached_, detén el de django y luego habilita el puerto donde corre. De la siguiente manera: + +```bash +docker compose -f local.yml up -d +docker compose -f local.yml down django +docker compose -f local.yml run --rm --service-ports django +``` + +De esta forma podrás usar `import ipdb; ipdb.set_trace()` en tu código de Python sin problemas. + ### Cobertura de pruebas Para ejecutar los tests, verificar el coverage y generar un reporte de coverage en HTML: - coverage run -m pytest - coverage html + docker compose -f local.yml run --rm django coverage run -m pytest + docker compose -f local.yml run --rm django coverage html open htmlcov/index.html #### Ejecutar los test usando pytest diff --git a/config/settings/base.py b/config/settings/base.py index b502ed1..39e2668 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -77,6 +77,7 @@ LOCAL_APPS = [ "temii.users", + "temii.talks" # Your stuff: custom apps go here ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps diff --git a/config/settings/local.py b/config/settings/local.py index 7619ec0..c652e02 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -60,3 +60,4 @@ # Your stuff... # ------------------------------------------------------------------------------ +CSRF_TRUSTED_ORIGINS = ["https://localhost:8000", "http://localhost:8000"] diff --git a/config/urls.py b/config/urls.py index d560716..94a0f23 100644 --- a/config/urls.py +++ b/config/urls.py @@ -14,6 +14,7 @@ path(settings.ADMIN_URL, admin.site.urls), # User management path("users/", include("temii.users.urls", namespace="users")), + path("talks/", include("temii.talks.urls", namespace="talks")), path("accounts/", include("allauth.urls")), # Your stuff: custom urls includes go here ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/docker-compose.extend.yml b/docker-compose.extend.yml new file mode 100644 index 0000000..ee5d349 --- /dev/null +++ b/docker-compose.extend.yml @@ -0,0 +1,12 @@ +version: '3' +services: + workspace: + image: mcr.microsoft.com/vscode/devcontainers/python:3.11 + volumes: + # Mounts the project folder to '/workspace'. While this file is in .devcontainer, + # mounts are relative to the first file in the list, which is a level up. + - .:/workspace:cached + command: /bin/sh -c "while sleep 1000; do :; done" + env_file: + - ./.envs/.local/.django + - ./.envs/.local/.postgres diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000..9843bcc --- /dev/null +++ b/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,494 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-27 18:39-0500\n" +"PO-Revision-Date: 2023-09-05 23:34-0500\n" +"Last-Translator: Sergio Orozco \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.0.1\n" + +#: htmlcov/d_afce42a2cf3d92da_base_html.html:140 temii/templates/base.html:58 +msgid "Admin" +msgstr "Administrador" + +#: htmlcov/d_afce42a2cf3d92da_base_html.html:148 temii/templates/base.html:66 +msgid "My Profile" +msgstr "Mi perfil" + +#: htmlcov/d_afce42a2cf3d92da_base_html.html:155 +#: temii/templates/account/logout.html:5 temii/templates/account/logout.html:8 +#: temii/templates/account/logout.html:17 temii/templates/base.html:73 +msgid "Sign Out" +msgstr "Salir" + +#: htmlcov/d_afce42a2cf3d92da_base_html.html:161 +#: temii/templates/account/signup.html:9 temii/templates/account/signup.html:19 +#: temii/templates/base.html:79 +msgid "Sign Up" +msgstr "Inscribirse" + +#: htmlcov/d_afce42a2cf3d92da_base_html.html:166 +#: temii/templates/account/login.html:7 temii/templates/account/login.html:11 +#: temii/templates/account/login.html:56 temii/templates/base.html:84 +msgid "Sign In" +msgstr "Iniciar sesión" + +#: temii/talks/apps.py:7 +msgid "Talks" +msgstr "Charlas" + +#: temii/talks/models.py:9 +msgid "Beginner" +msgstr "Principiante" + +#: temii/talks/models.py:10 +msgid "Intermediate" +msgstr "Intermedio" + +#: temii/talks/models.py:11 +msgid "Advanced" +msgstr "Avanzado" + +#: temii/talks/models.py:14 +msgid "Spanish" +msgstr "Español" + +#: temii/talks/models.py:15 +msgid "English" +msgstr "Inglés" + +#: temii/talks/models.py:18 +msgid "On site" +msgstr "En sitio" + +#: temii/talks/models.py:19 +msgid "Virtual" +msgstr "Virtual" + +#: temii/talks/models.py:22 +msgid "Name" +msgstr "Nombre" + +#: temii/talks/models.py:23 +msgid "Description" +msgstr "Descripción" + +#: temii/talks/models.py:24 +msgid "Level" +msgstr "Nivel" + +#: temii/talks/models.py:25 +msgid "Language" +msgstr "Lenguaje" + +#: temii/talks/models.py:27 +msgid "Timezone" +msgstr "Zona horaria" + +#: temii/talks/models.py:30 +msgid "" +"Timezone of the place you are located. E.g., Colombia (UTC-5), Argentina or " +"Chile (UTC-3), Mexico (UTC-6), etc." +msgstr "Zona horaria del lugar donde se encuentra. Por ejemplo," +"Colombia (UTC-5), Argentina o Chile (UTC-3), México (UTC-6), etc." + +#: temii/talks/models.py:34 +msgid "Comments" +msgstr "Comentarios" + +#: temii/talks/models.py:35 +msgid "Precense" +msgstr "Presencialidad" + +#: temii/talks/models.py:37 +msgid "Months" +msgstr "Meses" + +#: temii/talks/models.py:41 +msgid "" +"Please write your time availability in months.The more specific, the better. " +"E.g., October and November, any day after 6pm." +msgstr "Por favor escribe tu disponibilidad de tiempo en meses. Cuanto más específico, mejor." +"Por ejemplo, octubre y noviembre, cualquier día después de las 6 p.m." + +#: temii/talks/models.py:47 +msgid "Talk" +msgstr "Charla" + +#: temii/templates/account/account_inactive.html:5 +#: temii/templates/account/account_inactive.html:8 +msgid "Account Inactive" +msgstr "Cuenta inactiva" + +#: temii/templates/account/account_inactive.html:10 +msgid "This account is inactive." +msgstr "Esta cuenta está inactiva." + +#: temii/templates/account/email.html:7 +msgid "Account" +msgstr "Cuenta" + +#: temii/templates/account/email.html:10 +msgid "E-mail Addresses" +msgstr "Dirección de correo" + +#: temii/templates/account/email.html:13 +msgid "The following e-mail addresses are associated with your account:" +msgstr "Los siguientes correos electrónicos están asociados a su cuenta:" + +#: temii/templates/account/email.html:27 +msgid "Verified" +msgstr "Verificado" + +#: temii/templates/account/email.html:29 +msgid "Unverified" +msgstr "Sin verificar" + +#: temii/templates/account/email.html:31 +msgid "Primary" +msgstr "Principal" + +#: temii/templates/account/email.html:37 +msgid "Make Primary" +msgstr "Hacer principal" + +#: temii/templates/account/email.html:38 +msgid "Re-send Verification" +msgstr "Re-enviar verificación" + +#: temii/templates/account/email.html:39 +msgid "Remove" +msgstr "Eliminar" + +#: temii/templates/account/email.html:46 +msgid "Warning:" +msgstr "Advertencia:" + +#: temii/templates/account/email.html:46 +msgid "" +"You currently do not have any e-mail address set up. You should really add " +"an e-mail address so you can receive notifications, reset your password, etc." +msgstr "" +"Usted actualmente no tiene alguna dirección de correo electrónico asociada, " +"sugerimos que añadas tu cuenta para poder recibir notificaciones, " +"restablecer su contraseña, etc." + +#: temii/templates/account/email.html:51 +msgid "Add E-mail Address" +msgstr "Adicionar dirección de correo electrónico" + +#: temii/templates/account/email.html:56 +msgid "Add E-mail" +msgstr "Adicionar correo electrónico" + +#: temii/templates/account/email.html:66 +msgid "Do you really want to remove the selected e-mail address?" +msgstr "" +"¿Realmente desea eliminar la dirección de correo electrónica seleccionada?" + +#: temii/templates/account/email_confirm.html:6 +#: temii/templates/account/email_confirm.html:10 +msgid "Confirm E-mail Address" +msgstr "Confirmar dirección de correo electrónico" + +#: temii/templates/account/email_confirm.html:16 +#, python-format +msgid "" +"Please confirm that %(email)s is an e-mail " +"address for user %(user_display)s." +msgstr "" +"Por favor confirme que %(email)s es una " +"dirección de correo valida para el usuario %(user_display)s." + +#: temii/templates/account/email_confirm.html:20 +msgid "Confirm" +msgstr "Confirmar" + +#: temii/templates/account/email_confirm.html:27 +#, python-format +msgid "" +"This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request." +msgstr "" +"Este enlace de confirmación de correo electrónico expiro o es invalido. Por " +"favor solicite una nueva confirmación de correo " +"electrónico." + +#: temii/templates/account/login.html:17 +msgid "Please sign in with one of your existing third party accounts:" +msgstr "" +"Por favor inicie sesión con alguna de sus cuentas existente de terceros:" + +#: temii/templates/account/login.html:19 +#, python-format +msgid "" +"Or, sign up for a %(site_name)s account and " +"sign in below:" +msgstr "" +"O, registrese para una nueva cuenta en " +"%(site_name)s a continuación:" + +#: temii/templates/account/login.html:32 +msgid "or" +msgstr "o" + +#: temii/templates/account/login.html:41 +#, python-format +msgid "" +"If you have not created an account yet, then please sign up first." +msgstr "" +"Si no has creado tu cuenta aún, por favor registrate." + +#: temii/templates/account/login.html:55 +msgid "Forgot Password?" +msgstr "Olvido su contraseña?" + +#: temii/templates/account/logout.html:10 +msgid "Are you sure you want to sign out?" +msgstr "¿Estás seguro que deseas salir?" + +#: temii/templates/account/password_change.html:6 +#: temii/templates/account/password_change.html:9 +#: temii/templates/account/password_change.html:14 +#: temii/templates/account/password_reset_from_key.html:5 +#: temii/templates/account/password_reset_from_key.html:8 +#: temii/templates/account/password_reset_from_key_done.html:4 +#: temii/templates/account/password_reset_from_key_done.html:7 +msgid "Change Password" +msgstr "Cambiar contraseña" + +#: temii/templates/account/password_reset.html:7 +#: temii/templates/account/password_reset.html:11 +#: temii/templates/account/password_reset_done.html:6 +#: temii/templates/account/password_reset_done.html:9 +msgid "Password Reset" +msgstr "Restablecer contraseña" + +#: temii/templates/account/password_reset.html:16 +msgid "" +"Forgotten your password? Enter your e-mail address below, and we'll send you " +"an e-mail allowing you to reset it." +msgstr "" +"¿Olvido su contraseña?, Ingrese su dirección de correo electrónico a " +"continuación, y le estaremos enviando un enlace para restablecerla." + +#: temii/templates/account/password_reset.html:21 +msgid "Reset My Password" +msgstr "Restablecer mi contraseña" + +#: temii/templates/account/password_reset.html:24 +msgid "Please contact us if you have any trouble resetting your password." +msgstr "" +"Si tiene alguna dificultad para restablecer su contraseña, por favor " +"contáctenos." + +#: temii/templates/account/password_reset_done.html:15 +msgid "" +"We have sent you an e-mail. Please contact us if you do not receive it " +"within a few minutes." +msgstr "" + +#: temii/templates/account/password_reset_from_key.html:8 +msgid "Bad Token" +msgstr "" + +#: temii/templates/account/password_reset_from_key.html:12 +#, python-format +msgid "" +"The password reset link was invalid, possibly because it has already been " +"used. Please request a new password reset." +msgstr "" + +#: temii/templates/account/password_reset_from_key.html:18 +msgid "change password" +msgstr "cambiar contraseña" + +#: temii/templates/account/password_reset_from_key.html:21 +#: temii/templates/account/password_reset_from_key_done.html:8 +msgid "Your password is now changed." +msgstr "" + +#: temii/templates/account/password_set.html:6 +#: temii/templates/account/password_set.html:9 +#: temii/templates/account/password_set.html:14 +msgid "Set Password" +msgstr "" + +#: temii/templates/account/signup.html:6 +msgid "Signup" +msgstr "" + +#: temii/templates/account/signup.html:11 +#, python-format +msgid "" +"Already have an account? Then please sign in." +msgstr "" + +#: temii/templates/account/signup_closed.html:5 +#: temii/templates/account/signup_closed.html:8 +msgid "Sign Up Closed" +msgstr "" + +#: temii/templates/account/signup_closed.html:10 +msgid "We are sorry, but the sign up is currently closed." +msgstr "" + +#: temii/templates/account/verification_sent.html:5 +#: temii/templates/account/verification_sent.html:8 +#: temii/templates/account/verified_email_required.html:5 +#: temii/templates/account/verified_email_required.html:8 +msgid "Verify Your E-mail Address" +msgstr "Verificar su dirección de correo electrónico" + +#: temii/templates/account/verification_sent.html:10 +msgid "" +"We have sent an e-mail to you for verification. Follow the link provided to " +"finalize the signup process. Please contact us if you do not receive it " +"within a few minutes." +msgstr "" + +#: temii/templates/account/verified_email_required.html:12 +msgid "" +"This part of the site requires us to verify that\n" +"you are who you claim to be. For this purpose, we require that you\n" +"verify ownership of your e-mail address. " +msgstr "" + +#: temii/templates/account/verified_email_required.html:16 +msgid "" +"We have sent an e-mail to you for\n" +"verification. Please click on the link inside this e-mail. Please\n" +"contact us if you do not receive it within a few minutes." +msgstr "" + +#: temii/templates/account/verified_email_required.html:20 +#, python-format +msgid "" +"Note: you can still change your e-" +"mail address." +msgstr "" + +#: temii/templates/base.html:62 +msgid "About" +msgstr "" + +#: temii/templates/base.html:69 temii/templates/talks/talk_form.html:8 +msgid "Propose a talk" +msgstr "Proponer una charla" + +#: temii/templates/talks/talk_form.html:4 +msgid "Create Talk" +msgstr "Crear una charla" + +#: temii/templates/talks/talk_form.html:10 +msgid "Here you can propose a future talk" +msgstr "Aquí puedes proponer una futura charla" + +#: temii/templates/talks/talk_form.html:11 +msgid "" +"If you wish to make more than one proposal, you must fill out the form again" +msgstr "Si deseas hacer más de una propuesta debes diligenciar nuevamente el formulario" + +#: temii/templates/talks/talk_form.html:18 +#: temii/templates/users/user_form.html:13 +msgid "Save" +msgstr "Guardar" + +#: temii/templates/talks/talk_thanks.html:4 +#: temii/templates/talks/talk_thanks.html:11 +msgid "Thanks for your proposal" +msgstr "Gracias por tu propuesta" + +#: temii/templates/talks/talk_thanks.html:13 +msgid "Your contribution greatly contributes to the community" +msgstr "Tu aporte contribuye enormemente a la comunidad" + +#: temii/templates/talks/talk_thanks.html:16 +msgid "If you wish, you can" +msgstr "Si deseas, puedes" + +#: temii/templates/talks/talk_thanks.html:17 +msgid "propose another talk again" +msgstr "volver a proponer otra charla" + +#: temii/templates/users/user_detail.html:19 temii/users/forms.py:37 +#: temii/users/models.py:23 +msgid "Bio" +msgstr "Biografia" + +#: temii/templates/users/user_detail.html:22 temii/users/forms.py:36 +#: temii/users/models.py:22 +msgid "Phone" +msgstr "Telefono" + +#: temii/templates/users/user_detail.html:32 +msgid "Update" +msgstr "Actualizar" + +#: temii/templates/users/user_detail.html:33 +msgid "E-Mail" +msgstr "Coreo electrónico" + +#: temii/users/admin.py:17 +msgid "Personal info" +msgstr "Información personal" + +#: temii/users/admin.py:19 +msgid "Permissions" +msgstr "Permisos" + +#: temii/users/admin.py:30 +msgid "Important dates" +msgstr "Fechas importantes" + +#: temii/users/apps.py:7 +msgid "Users" +msgstr "Usuarios" + +#: temii/users/forms.py:25 temii/users/tests/test_forms.py:36 +msgid "This username has already been taken." +msgstr "Este nombre de usuario ya ha sido tomado" + +#: temii/users/forms.py:38 temii/users/models.py:24 +msgid "Image" +msgstr "Imagen" + +#: temii/users/models.py:19 +msgid "Name of User" +msgstr "Nombre" + +#: temii/users/views.py:23 +msgid "Information successfully updated" +msgstr "Información guardada con exito" + +#~ msgid "User" +#~ msgstr "Usuario" + +#, fuzzy +#~ msgid "IPv4 address" +#~ msgstr "Adicionar dirección de correo electrónico" + +#, fuzzy +#~ msgid "IP address" +#~ msgstr "Adicionar dirección de correo electrónico" + +#, fuzzy +#~ msgid "URL" +#~ msgstr "" +#~ "Este enlace de confirmación de correo electrónico expiro o es invalido. " +#~ "Por favor solicite una nueva confirmación de " +#~ "correo electrónico." diff --git a/temii/conftest.py b/temii/conftest.py index a818b6b..6d7a0c3 100644 --- a/temii/conftest.py +++ b/temii/conftest.py @@ -1,5 +1,7 @@ import pytest +from temii.talks.models import Talk +from temii.talks.tests.factories import TalkFactory from temii.users.models import User from temii.users.tests.factories import UserFactory @@ -12,3 +14,8 @@ def media_storage(settings, tmpdir): @pytest.fixture def user(db) -> User: return UserFactory() + + +@pytest.fixture +def talk(db) -> Talk: + return TalkFactory() diff --git a/temii/talks/__init__.py b/temii/talks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/temii/talks/admin.py b/temii/talks/admin.py new file mode 100644 index 0000000..ed0c7e9 --- /dev/null +++ b/temii/talks/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +from .models import Talk + + +@admin.register(Talk) +class TalkAdmin(admin.ModelAdmin): + list_display = ["name", "user", "months"] + search_fields = ["name"] + list_filter = ["level", "precense", "language"] diff --git a/temii/talks/apps.py b/temii/talks/apps.py new file mode 100644 index 0000000..b1e6e7c --- /dev/null +++ b/temii/talks/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class TalksConfig(AppConfig): + name = "temii.talks" + verbose_name = _("Talks") + default_auto_field = "django.db.models.BigAutoField" diff --git a/temii/talks/forms.py b/temii/talks/forms.py new file mode 100644 index 0000000..3a28b71 --- /dev/null +++ b/temii/talks/forms.py @@ -0,0 +1,18 @@ +from django import forms + +from .models import Talk + + +class NewTalkForm(forms.ModelForm): + class Meta: + model = Talk + fields = [ + "name", + "description", + "level", + "language", + "timezone", + "comments", + "precense", + "months", + ] diff --git a/temii/talks/migrations/0001_initial.py b/temii/talks/migrations/0001_initial.py new file mode 100644 index 0000000..fe6c5f4 --- /dev/null +++ b/temii/talks/migrations/0001_initial.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.1 on 2023-10-07 18:36 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Talk", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=60, verbose_name="Name")), + ("description", models.CharField(max_length=300, verbose_name="Description")), + ( + "level", + models.PositiveIntegerField( + choices=[(1, "Beginner"), (2, "Intermediate"), (3, "Advanced")], + default=1, + verbose_name="Level", + ), + ), + ( + "language", + models.CharField( + choices=[("es", "Spanish"), ("en", "English")], + default="es", + max_length=2, + verbose_name="Language", + ), + ), + ("timezone", models.CharField(max_length=60, verbose_name="Timezone")), + ("comments", models.CharField(max_length=300, verbose_name="Comments")), + ( + "precense", + models.PositiveIntegerField( + choices=[(1, "On site"), (2, "Virtual")], default=1, verbose_name="Precense" + ), + ), + ("months", models.CharField(blank=True, max_length=100, verbose_name="Months")), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + options={ + "verbose_name": "Talk", + }, + ), + ] diff --git a/temii/talks/migrations/0002_alter_talk_months_alter_talk_timezone.py b/temii/talks/migrations/0002_alter_talk_months_alter_talk_timezone.py new file mode 100644 index 0000000..483480a --- /dev/null +++ b/temii/talks/migrations/0002_alter_talk_months_alter_talk_timezone.py @@ -0,0 +1,31 @@ +# Generated by Django 4.2.1 on 2023-10-31 01:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("talks", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="talk", + name="months", + field=models.CharField( + blank=True, + help_text="Please write your time availability in months.The more specific, the better. E.g., October and November, any day after 6pm.", + max_length=100, + verbose_name="Months", + ), + ), + migrations.AlterField( + model_name="talk", + name="timezone", + field=models.CharField( + help_text="Timezone of the place you are located. E.g., Colombia (UTC-5), Argentina or Chile (UTC-3), Mexico (UTC-6), etc.", + max_length=60, + verbose_name="Timezone", + ), + ), + ] diff --git a/temii/talks/migrations/__init__.py b/temii/talks/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/temii/talks/models.py b/temii/talks/models.py new file mode 100644 index 0000000..78df7af --- /dev/null +++ b/temii/talks/models.py @@ -0,0 +1,47 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from temii.users.models import User + + +class Talk(models.Model): + class Level(models.IntegerChoices): + BEGINNER = 1, _("Beginner") + INTERMEDIATE = 2, _("Intermediate") + ADVANCED = 3, _("Advanced") + + class Language(models.TextChoices): + ES = "es", _("Spanish") + EN = "en", _("English") + + class InPerson(models.IntegerChoices): + ON_SITE = 1, _("On site") + VIRTUAL = 2, _("Virtual") + + user = models.ForeignKey(User, on_delete=models.PROTECT) + name = models.CharField(_("Name"), max_length=60) + description = models.CharField(_("Description"), max_length=300) + level = models.PositiveIntegerField(_("Level"), choices=Level.choices, default=Level.BEGINNER) + language = models.CharField(_("Language"), max_length=2, choices=Language.choices, default=Language.ES) + timezone = models.CharField( + _("Timezone"), + max_length=60, + help_text=_( + "Timezone of the place you are located. E.g., " + "Colombia (UTC-5), Argentina or Chile (UTC-3), Mexico (UTC-6), etc." + ), + ) + comments = models.CharField(_("Comments"), max_length=300) + precense = models.PositiveIntegerField(_("Precense"), choices=InPerson.choices, default=InPerson.ON_SITE) + months = models.CharField( + _("Months"), + max_length=100, + blank=True, + help_text=_( + "Please write your time availability in months." + "The more specific, the better. E.g., October and November, any day after 6pm." + ), + ) + + class Meta: + verbose_name = _("Talk") diff --git a/temii/talks/tests/__init__.py b/temii/talks/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/temii/talks/tests/factories.py b/temii/talks/tests/factories.py new file mode 100644 index 0000000..267e0f1 --- /dev/null +++ b/temii/talks/tests/factories.py @@ -0,0 +1,19 @@ +from factory import Faker, Iterator, SubFactory +from factory.django import DjangoModelFactory + +from temii.talks.models import Talk +from temii.users.tests.factories import UserFactory + + +class TalkFactory(DjangoModelFactory): + name = Faker("name") + description = Faker("text") + user = SubFactory(UserFactory) + language = Iterator(["es", "en"]) + level = Iterator([1, 2, 3]) + comments = Faker("text") + precense = Iterator([1, 2]) + months = Faker("text", max_nb_chars=100) + + class Meta: + model = Talk diff --git a/temii/talks/tests/test_admin.py b/temii/talks/tests/test_admin.py new file mode 100644 index 0000000..9914d0e --- /dev/null +++ b/temii/talks/tests/test_admin.py @@ -0,0 +1,23 @@ +from django.urls import reverse + + +class TestTalkAdmin: + def test_changelist(self, admin_client): + url = reverse("admin:talks_talk_changelist") + response = admin_client.get(url) + assert response.status_code == 200 + + def test_search(self, admin_client): + url = reverse("admin:talks_talk_changelist") + response = admin_client.get(url, data={"q": "test"}) + assert response.status_code == 200 + + def test_add(self, admin_client): + url = reverse("admin:talks_talk_add") + response = admin_client.get(url) + assert response.status_code == 200 + + def test_view_user(self, admin_client, talk): + url = reverse("admin:talks_talk_change", kwargs={"object_id": talk.pk}) + response = admin_client.get(url) + assert response.status_code == 200 diff --git a/temii/talks/tests/test_models.py b/temii/talks/tests/test_models.py new file mode 100644 index 0000000..e69de29 diff --git a/temii/talks/tests/test_urls.py b/temii/talks/tests/test_urls.py new file mode 100644 index 0000000..e69de29 diff --git a/temii/talks/tests/test_views.py b/temii/talks/tests/test_views.py new file mode 100644 index 0000000..31c82ef --- /dev/null +++ b/temii/talks/tests/test_views.py @@ -0,0 +1,26 @@ +import pytest +from django.test import RequestFactory + +from temii.talks.forms import NewTalkForm +from temii.talks.tests.factories import TalkFactory +from temii.talks.views import TalkCreateView +from temii.users.models import User + +pytestmark = pytest.mark.django_db + + +class TestTalkCreateView: + def test_form_valid(self, user: User, rf: RequestFactory): + view = TalkCreateView() + talk = TalkFactory() + request = rf.get("/fake-url/") + request.user = user + + view.request = request + + # Initialize the form + form = NewTalkForm(instance=talk) + form.cleaned_data = {} + view.form_valid(form) + + assert form.instance.user == request.user diff --git a/temii/talks/urls.py b/temii/talks/urls.py new file mode 100644 index 0000000..6a83092 --- /dev/null +++ b/temii/talks/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from django.views.generic import TemplateView + +from .views import talk_create_view + +app_name = "talks" +urlpatterns = [ + path("", TemplateView.as_view(template_name="pages/about.html"), name="talks"), + path("~create/", view=talk_create_view, name="create"), + path("thanks/", TemplateView.as_view(template_name="talks/talk_thanks.html"), name="thanks"), +] diff --git a/temii/talks/views.py b/temii/talks/views.py new file mode 100644 index 0000000..e7dac81 --- /dev/null +++ b/temii/talks/views.py @@ -0,0 +1,22 @@ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.urls import reverse_lazy +from django.views.generic.edit import CreateView + +from .forms import NewTalkForm +from .models import Talk + + +class TalkCreateView(LoginRequiredMixin, CreateView): + """Propose a new talk.""" + + model = Talk + form_class = NewTalkForm + success_url = reverse_lazy("talks:thanks") + + def form_valid(self, form): + # Se agrega el usuario que hace la petición al formulario + form.instance.user = self.request.user + return super().form_valid(form) + + +talk_create_view = TalkCreateView.as_view() diff --git a/temii/templates/account/signup.html b/temii/templates/account/signup.html index 189ab9e..ebc77ef 100644 --- a/temii/templates/account/signup.html +++ b/temii/templates/account/signup.html @@ -10,7 +10,7 @@

{% translate "Sign Up" %}

{% blocktranslate %}Already have an account? Then please sign in.{% endblocktranslate %}

- + +{% endblock content %} diff --git a/temii/templates/talks/talk_thanks.html b/temii/templates/talks/talk_thanks.html new file mode 100644 index 0000000..87a3d3e --- /dev/null +++ b/temii/templates/talks/talk_thanks.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block title %}{% translate "Thanks for your proposal" %}{% endblock %} + +{% block content %} + +
+
+
+

{% translate "Thanks for your proposal" %}

+

+ {% translate "Your contribution greatly contributes to the community" %}. +

+

+ {% translate "If you wish, you can" %} + {% translate "propose another talk again" %}. +

+
+
+
+{% endblock content %} diff --git a/temii/templates/users/user_detail.html b/temii/templates/users/user_detail.html index 79b8233..c280c81 100644 --- a/temii/templates/users/user_detail.html +++ b/temii/templates/users/user_detail.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load static %} +{% load static i18n %} {% block title %}User: {{ object.username }}{% endblock %} @@ -8,11 +8,19 @@
-

{{ object.username }}

+ {% if object.image %} + + {% endif %} {% if object.name %}

{{ object.name }}

{% endif %} + {% if object.bio %} +

{% translate "Bio" %}: {{ object.bio }}

+ {% endif %} + {% if object.phone %} +

{% translate "Phone" %}: {{ object.phone }}

+ {% endif %}
@@ -21,8 +29,8 @@

{{ object.username }}

diff --git a/temii/templates/users/user_form.html b/temii/templates/users/user_form.html index 467357a..4932cf8 100644 --- a/temii/templates/users/user_form.html +++ b/temii/templates/users/user_form.html @@ -1,16 +1,16 @@ {% extends "base.html" %} -{% load crispy_forms_tags %} +{% load crispy_forms_tags i18n %} {% block title %}{{ user.username }}{% endblock %} {% block content %}

{{ user.username }}

-
+ {% csrf_token %} {{ form|crispy }}
- +
diff --git a/temii/users/admin.py b/temii/users/admin.py index ea235c5..5e5c05e 100644 --- a/temii/users/admin.py +++ b/temii/users/admin.py @@ -14,7 +14,7 @@ class UserAdmin(auth_admin.UserAdmin): add_form = UserAdminCreationForm fieldsets = ( (None, {"fields": ("username", "password")}), - (_("Personal info"), {"fields": ("name", "email")}), + (_("Personal info"), {"fields": ("name", "email", "phone", "bio", "image")}), ( _("Permissions"), { diff --git a/temii/users/forms.py b/temii/users/forms.py index c0946bf..b3076e2 100644 --- a/temii/users/forms.py +++ b/temii/users/forms.py @@ -1,5 +1,6 @@ from allauth.account.forms import SignupForm from allauth.socialaccount.forms import SignupForm as SocialSignupForm +from django import forms from django.contrib.auth import forms as admin_forms from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ @@ -32,6 +33,18 @@ class UserSignupForm(SignupForm): Check UserSocialSignupForm for accounts created from social. """ + phone = forms.CharField(max_length=20, label=_("Phone"), required=False) + bio = forms.CharField(max_length=255, label=_("Bio"), required=False) + image = forms.ImageField(label=_("Image"), required=False) + + def save(self, request): + user = super().save(request) + user.phone = self.cleaned_data["phone"] + user.bio = self.cleaned_data["bio"] + user.image = self.cleaned_data["image"] + user.save() + return user + class UserSocialSignupForm(SocialSignupForm): """ diff --git a/temii/users/migrations/0002_user_bio_user_image_user_phone.py b/temii/users/migrations/0002_user_bio_user_image_user_phone.py new file mode 100644 index 0000000..d7e908d --- /dev/null +++ b/temii/users/migrations/0002_user_bio_user_image_user_phone.py @@ -0,0 +1,30 @@ +# Generated by Django 4.1.8 on 2023-08-12 16:41 + +from django.db import migrations, models +import temii.users.models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="bio", + field=models.CharField(blank=True, max_length=255, verbose_name="Bio"), + ), + migrations.AddField( + model_name="user", + name="image", + field=models.ImageField( + default="default.jpg", upload_to=temii.users.models.upload_to, verbose_name="Image" + ), + ), + migrations.AddField( + model_name="user", + name="phone", + field=models.CharField(blank=True, max_length=50, verbose_name="Phone"), + ), + ] diff --git a/temii/users/models.py b/temii/users/models.py index 376ff22..e06cc9c 100644 --- a/temii/users/models.py +++ b/temii/users/models.py @@ -1,9 +1,13 @@ from django.contrib.auth.models import AbstractUser -from django.db.models import CharField +from django.db.models import CharField, ImageField from django.urls import reverse from django.utils.translation import gettext_lazy as _ +def upload_to(instance, filename): + return f"profile_pics/{instance.username}/{filename}" + + class User(AbstractUser): """ Default custom user model for temii. @@ -15,6 +19,9 @@ class User(AbstractUser): name = CharField(_("Name of User"), blank=True, max_length=255) first_name = None # type: ignore last_name = None # type: ignore + phone = CharField(_("Phone"), blank=True, max_length=50) + bio = CharField(_("Bio"), blank=True, max_length=255) + image = ImageField(_("Image"), default="default.jpg", upload_to=upload_to) def get_absolute_url(self) -> str: """Get URL for user's detail view. diff --git a/temii/users/tests/factories.py b/temii/users/tests/factories.py index 36f9634..9654a73 100644 --- a/temii/users/tests/factories.py +++ b/temii/users/tests/factories.py @@ -2,14 +2,18 @@ from typing import Any from django.contrib.auth import get_user_model -from factory import Faker, post_generation -from factory.django import DjangoModelFactory +from django.core.files.base import ContentFile +from factory import Faker, LazyAttribute, post_generation +from factory.django import DjangoModelFactory, ImageField class UserFactory(DjangoModelFactory): username = Faker("user_name") email = Faker("email") name = Faker("name") + phone = Faker("phone_number") + bio = Faker("text") + image = LazyAttribute(lambda _: ContentFile(ImageField()._make_data({"width": 300, "height": 300}), "example.jpg")) @post_generation def password(self, create: bool, extracted: Sequence[Any], **kwargs): diff --git a/temii/users/views.py b/temii/users/views.py index 5b7bc89..e6cb5af 100644 --- a/temii/users/views.py +++ b/temii/users/views.py @@ -19,7 +19,7 @@ class UserDetailView(LoginRequiredMixin, DetailView): class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = User - fields = ["name"] + fields = ["name", "phone", "bio", "image"] success_message = _("Information successfully updated") def get_success_url(self):