diff --git a/.env.example b/.env.example index ec78dd8b..436b34ff 100644 --- a/.env.example +++ b/.env.example @@ -11,3 +11,10 @@ LOGGING_LOGGERS_LEVEL=INFO LOGGING_LOGGERS_DJANGO_LEVEL=INFO TESTING_DIR=/app/general/tests/files/ FEATURE_FLAG='' +EMAIL_HOST='' +EMAIL_USE_TLS=True +EMAIL_PORT=587 +EMAIL_HOST_USER='' +EMAIL_HOST_PASSWORD='' +EMAIL_BACKEND_CONSOLE='True/False' +EMAIL_USE_TLS=True diff --git a/README.md b/README.md index 5340f57e..5a1ea029 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,19 @@ About the project: --- +### Email Settings in Development + +.env file + +* EMAIL_HOST='sandbo x.smtp.mailtrap.io' +* EMAIL_HOST_USER='*********' +* EMAIL_HOST_PASSWORD='******' +* EMAIL_PORT='2525' +* EMAIL_BACKEND_CONSOLE=True + +By default, the email backend is set to console, so you can see the email in the console. +To send an email, you need to set the EMAIL_BACKEND_CONSOLE to False. + ### Plugins installed #### Django Simple History @@ -58,3 +71,8 @@ Docker Volumes for production: * /logging * /pdf_uploads * /pdf_upload_completed + +### Email Settings in Production + +.env file +* EMAIL_BACKEND_CONSOLE=False diff --git a/app/accounts/__init__.py b/app/accounts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/accounts/admin.py b/app/accounts/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/app/accounts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/app/accounts/apps.py b/app/accounts/apps.py new file mode 100644 index 00000000..0cb51e63 --- /dev/null +++ b/app/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "accounts" diff --git a/app/accounts/forms.py b/app/accounts/forms.py new file mode 100644 index 00000000..e8cb29e7 --- /dev/null +++ b/app/accounts/forms.py @@ -0,0 +1,22 @@ +from django import forms +from django.contrib.auth.forms import UserCreationForm + +from users.models import CustomUser + + +class CustomUserCreationForm(UserCreationForm): + email = forms.EmailField(required=True, help_text="Required. Add a valid email address.") + username = forms.CharField(required=True, help_text="Required. Add a valid username.") + first_name = forms.CharField(required=True, help_text="Required. Add a valid first name.") + last_name = forms.CharField(required=True, help_text="Required. Add a valid last name.") + + class Meta: + model = CustomUser + fields = ("username", "email", "first_name", "last_name") + + def __init__(self, *args, **kwargs): + super(CustomUserCreationForm, self).__init__(*args, **kwargs) + self.fields["email"].required = True + self.fields["username"].required = True + self.fields["first_name"].required = True + self.fields["last_name"].required = True diff --git a/app/accounts/migrations/__init__.py b/app/accounts/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/accounts/models.py b/app/accounts/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/app/accounts/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/app/accounts/tests/__init__.py b/app/accounts/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/accounts/tests/test_signup_form.py b/app/accounts/tests/test_signup_form.py new file mode 100644 index 00000000..250c6dcd --- /dev/null +++ b/app/accounts/tests/test_signup_form.py @@ -0,0 +1,88 @@ +import unittest + +from django.test import TestCase + +from accounts.forms import CustomUserCreationForm + + +class CustomUserCreationFormTest(TestCase): + def setUp(self): + self.username = "testuser" + self.email = "testuser@gmail.com" + self.first_name = "Test" + self.last_name = "User" + self.password1 = "sadilar2024" + self.password2 = "sadilar2024" + + def test_valid_data(self): + form = CustomUserCreationForm( + { + "username": self.username, + "email": self.email, + "first_name": self.first_name, + "last_name": self.last_name, + "password1": self.password1, + "password2": self.password2, + } + ) + + self.assertTrue(form.is_valid()) + + def test_blank_data(self): + form = CustomUserCreationForm({}) + self.assertFalse(form.is_valid()) + + self.assertEqual( + form.errors, + { + "username": ["This field is required."], + "email": ["This field is required."], + "first_name": ["This field is required."], + "last_name": ["This field is required."], + "password1": ["This field is required."], + "password2": ["This field is required."], + }, + ) + + def test_invalid_email(self): + form = CustomUserCreationForm( + { + "username": self.username, + "email": "not a valid email", + "first_name": self.first_name, + "last_name": self.last_name, + "password1": self.password1, + "password2": self.password2, + } + ) + self.assertFalse(form.is_valid()) + self.assertEqual( + form.errors, + { + "email": ["Enter a valid email address."], + }, + ) + + def test_passwords_do_not_match(self): + form = CustomUserCreationForm( + { + "username": self.username, + "email": self.email, + "first_name": self.first_name, + "last_name": self.last_name, + "password1": self.password1, + "password2": "wrong password", + } + ) + + self.assertFalse(form.is_valid()) + self.assertEqual( + form.errors, + { + "password2": ["The two password fields didn’t match."], + }, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/app/accounts/urls.py b/app/accounts/urls.py new file mode 100644 index 00000000..eb6d59cb --- /dev/null +++ b/app/accounts/urls.py @@ -0,0 +1,33 @@ +from django.contrib.auth import views as auth_views +from django.urls import path + +from . import views + +urlpatterns = [ + path("register/", views.register, name="accounts_register"), + path("login/", auth_views.LoginView.as_view(template_name="accounts/login.html"), name="login"), + path( + "password_reset/", + auth_views.PasswordResetView.as_view(template_name="accounts/password_reset_form.html"), + name="password_reset", + ), + path( + "password_reset/done/", + auth_views.PasswordResetDoneView.as_view(template_name="accounts/password_reset_done.html"), + name="password_reset_done", + ), + path( + "reset///", + auth_views.PasswordResetConfirmView.as_view( + template_name="accounts/password_reset_confirm.html" + ), + name="password_reset_confirm", + ), + path( + "reset/done/", + auth_views.PasswordResetCompleteView.as_view( + template_name="accounts/password_reset_complete.html" + ), + name="password_reset_complete", + ), +] diff --git a/app/accounts/views.py b/app/accounts/views.py new file mode 100644 index 00000000..b2eabcb4 --- /dev/null +++ b/app/accounts/views.py @@ -0,0 +1,18 @@ +from django.contrib.auth import login +from django.shortcuts import redirect, render + +from .forms import CustomUserCreationForm + + +def register(request): + if request.method == "POST": + form = CustomUserCreationForm(request.POST) + if form.is_valid(): + user = form.save(commit=False) + user.is_staff = True + user.save() + login(request, user) + return redirect("home") + else: + form = CustomUserCreationForm() + return render(request, "accounts/register.html", {"form": form}) diff --git a/app/app/settings.py b/app/app/settings.py index af92a265..e03a5c95 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -47,6 +47,7 @@ "users", "general", "simple_history", + "accounts", ] # Add django-extensions to the installed apps if DEBUG is True @@ -125,6 +126,20 @@ "host.docker.internal", ] +# Email settings +EMAIL_HOST = os.environ.get("EMAIL_HOST") +EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") +EMAIL_PORT = os.environ.get("EMAIL_PORT") +EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS") + +email_backend_env = os.environ.get("EMAIL_BACKEND_CONSOLE", "False").lower() in ["true", "1", "yes"] + +if DEBUG and email_backend_env: + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +LOGIN_REDIRECT_URL = "/" +LOGOUT_REDIRECT_URL = "/" # Password validation # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators diff --git a/app/app/urls.py b/app/app/urls.py index 74103c3c..649d0d9b 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -18,6 +18,7 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin +from django.contrib.auth import views as auth_views from django.urls import include, path from django.utils.translation import gettext_lazy as _ @@ -39,6 +40,8 @@ path("subject//", views.subject_detail, name="subject_detail"), path("search/", views.search, name="search"), path("i18n/", include("django.conf.urls.i18n")), + path("accounts/", include("accounts.urls")), + path("accounts/", include("django.contrib.auth.urls")), ] if settings.DEBUG: diff --git a/app/templates/accounts/login.html b/app/templates/accounts/login.html new file mode 100644 index 00000000..99b7250d --- /dev/null +++ b/app/templates/accounts/login.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block title %}Log In{% endblock %} + +{% block content %} +
+

Log In

+
+ {% csrf_token %} + {{ form }} + +
+ +
+
+
+ {% csrf_token %} + +
+
+ +{% endblock %} diff --git a/app/templates/accounts/password_reset_complete.html b/app/templates/accounts/password_reset_complete.html new file mode 100644 index 00000000..caec7e04 --- /dev/null +++ b/app/templates/accounts/password_reset_complete.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block title %}Password reset complete{% endblock %} + +{% block content %} +
+

Your password has been set. You may go ahead and log in now.

+
+

Log in

+
+{% endblock %} diff --git a/app/templates/accounts/password_reset_confirm.html b/app/templates/accounts/password_reset_confirm.html new file mode 100644 index 00000000..74974523 --- /dev/null +++ b/app/templates/accounts/password_reset_confirm.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block title %}Enter new password{% endblock %} + +{% block content %} + + {% if validlink %} + +
+

Please enter your new password twice so we can verify you typed it in correctly.

+ +
{% csrf_token %} +
+
+ {{ form.new_password1.errors }} +
+ + {{ form.new_password1 }} +
+
+
+ {{ form.new_password2.errors }} +
+ + {{ form.new_password2 }} +
+
+
+
+
+
+ +
+
+ + {% else %} + +

"The password reset link was invalid, possibly because it has already been used. Please request a new + password reset.

+ + {% endif %} +
+{% endblock %} diff --git a/app/templates/accounts/password_reset_done.html b/app/templates/accounts/password_reset_done.html new file mode 100644 index 00000000..0083930c --- /dev/null +++ b/app/templates/accounts/password_reset_done.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block title %}Email Sent{% endblock %} + +{% block content %} +
+

We’ve emailed you instructions for setting your password, if an account exists with the email you entered. + You should receive them shortly.

+ +

If you don’t receive an email, please make sure you’ve entered the address you registered with, and check + your spam folder.

+
+{% endblock %} diff --git a/app/templates/accounts/password_reset_form.html b/app/templates/accounts/password_reset_form.html new file mode 100644 index 00000000..1f4ea298 --- /dev/null +++ b/app/templates/accounts/password_reset_form.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block title %}Forgot Your Password?{% endblock %} + +{% block content %} +
+

Forgotten your password? Enter your email address below, and we’ll email instructions for setting a new + one.

+ +
+ {% csrf_token %} +
+
+ {{ form.email.errors }} +
+ + {{ form.email }} +
+
+
+
+
+
+ +
+
+
+{% endblock %} diff --git a/app/templates/accounts/register.html b/app/templates/accounts/register.html new file mode 100644 index 00000000..8cfaafb3 --- /dev/null +++ b/app/templates/accounts/register.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block title %}Sign Up{% endblock %} + +{% block content %} +
+

Register

+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+{% endblock %} diff --git a/docker-compose.yml b/docker-compose.yml index 07be97f8..b18cb7c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,3 +43,9 @@ services: - DB_PASSWORD=sadilar # see POSTGRES_PASSWORD above - TESTING_DIR=/app/general/tests/files/ - FEATURE_FLAG=search_feature + - EMAIL_HOST=${EMAIL_HOST-} + - EMAIL_HOST_USER=${EMAIL_HOST_USER-} + - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD-"none"} + - EMAIL_PORT=${EMAIL_PORT-"none"} + - EMAIL_BACKEND_CONSOLE=${EMAIL_BACKEND_CONSOLE:-True} + - EMAIL_USE_TLS=${EMAIL_USE_TLS:-True}