send email for put your password

This commit is contained in:
Cayo Puigdefabregas 2023-10-18 17:30:11 +02:00
parent d9352a1f9f
commit fc226bd5df
17 changed files with 456 additions and 157 deletions

View File

@ -1,4 +1,5 @@
import logging import logging
from smtplib import SMTPException
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
@ -9,6 +10,7 @@ from django.urls import reverse_lazy
from django.contrib import messages from django.contrib import messages
from idhub.models import Membership, Rol, Service, UserRol from idhub.models import Membership, Rol, Service, UserRol
from idhub.mixins import AdminView from idhub.mixins import AdminView
from idhub.email.views import NotifyActivateUserByEmail
from idhub.admin.forms import ( from idhub.admin.forms import (
ProfileForm, ProfileForm,
MembershipForm, MembershipForm,
@ -121,7 +123,7 @@ class AdminPeopleEditView(AdminPeopleView, UpdateView):
success_url = reverse_lazy('idhub:admin_people_list') success_url = reverse_lazy('idhub:admin_people_list')
class AdminPeopleRegisterView(People, CreateView): class AdminPeopleRegisterView(NotifyActivateUserByEmail, People, CreateView):
template_name = "idhub/admin/people_register.html" template_name = "idhub/admin/people_register.html"
subtitle = _('People Register') subtitle = _('People Register')
icon = 'bi bi-person' icon = 'bi bi-person'
@ -137,6 +139,16 @@ class AdminPeopleRegisterView(People, CreateView):
) )
return self.success_url return self.success_url
def form_valid(self, form):
user = form.save()
messages.success(self.request, _('The account is created successfully'))
if user.is_active:
try:
self.send_email(user)
except SMTPException as e:
messages.error(self.request, e)
return super().form_valid(form)
class AdminPeopleMembershipRegisterView(People, CreateView): class AdminPeopleMembershipRegisterView(People, CreateView):
template_name = "idhub/admin/people_membership_register.html" template_name = "idhub/admin/people_membership_register.html"

0
idhub/email/__init__.py Normal file
View File

55
idhub/email/views.py Normal file
View File

@ -0,0 +1,55 @@
from django.conf import settings
from django.template import loader
from django.core.mail import EmailMultiAlternatives
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
class NotifyActivateUserByEmail:
def get_email_context(self, user):
"""
Define a new context with a token for put in a email
when send a email for add a new password
"""
protocol = 'https' if self.request.is_secure() else 'http'
current_site = get_current_site(self.request)
site_name = current_site.name
domain = current_site.domain
context = {
'email': user.email,
'domain': domain,
'site_name': site_name,
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'user': user,
'token': default_token_generator.make_token(user),
'protocol': protocol,
}
return context
def send_email(self, user):
"""
Send a email when a user is activated.
"""
context = self.get_email_context(user)
subject_template_name = 'idhub/admin/registration/activate_user_subject.txt'
email_template_name = 'idhub/admin/registration/activate_user_email.txt'
html_email_template_name = 'idhub/admin/registration/activate_user_email.html'
subject = loader.render_to_string(subject_template_name, context)
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
body = loader.render_to_string(email_template_name, context)
from_email = settings.DEFAULT_FROM_EMAIL
to_email = user.email
email_message = EmailMultiAlternatives(
subject, body, from_email, [to_email])
html_email = loader.render_to_string(html_email_template_name, context)
email_message.attach_alternative(html_email, 'text/html')
if settings.DEVELOPMENT:
print(to_email)
print(body)
return
email_message.send()

View File

@ -1,162 +1,54 @@
{% extends "auth/login_base.html" %}
{% load i18n static %} {% load i18n static %}
<!doctype html> {% block login_content %}
<html lang="en"> <form action="{% url 'idhub:login' %}" role="form" method="post">
<head> {% csrf_token %}
{% block head %} <input type="hidden" name="next" value="{{ next }}" />
{% block meta %}
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="robots" content="NONE,NOARCHIVE" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="Pangea">
{% endblock %}
<title>{% block title %}{% if title %}{{ title }} {% endif %}IdHub{% endblock %}</title>
<!-- Bootstrap core CSS --> <div id="div_id_username"
{% block style %} class="clearfix control-group {% if form.username.errors %}error{% endif %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> <div class="form-group">
<link rel="stylesheet" href= "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"> <input type="text" name="username" maxlength="100" autocapitalize="off"
<link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet"> autocorrect="off" class="form-control textinput textInput" id="id_username" required
autofocus placeholder="{{ form.username.label }}"
<style> {% if form.username.value %}value="{{ form.username.value }}" {% endif %}>
.bd-placeholder-img { {% if form.username.errors %}
font-size: 1.125rem; <p class="text-error">
text-anchor: middle; {{ form.username.errors|striptags }}
-webkit-user-select: none; </p>
-moz-user-select: none; {% endif %}
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
<link href="{% static "/css/dashboard.css" %}" rel="stylesheet">
{% endblock %}
{% endblock %}
</head>
<body id="body-login">
<header class="navbar navbar-dark sticky-top bg-grey flex-md-nowrap p-0 shadow">
<div class="navbar-nav navbar-sub-brand">
</div>
<div class="navbar-nav">
</div>
</header>
<div class="container-fluid">
<div class="row">
<main class="col-md-12 bt-5">
{% block messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endfor %}
{% endblock messages %}
<div class="jumbotron vertical-center">
<div id="login-wrapper" class="container" style="width: 430px;">
<div id="login-content" class="rounded">
<div id="login-branding">
{% block branding %}
<h1>
<img class="img-fluid" src="{% static '/images/logo-pangea-monocrome-h.png' %}"
alt="Pangea.org - Internet etic i solidari" />
</h1>
{% endblock %}
</div><!-- /login-branding -->
<div class="mt-5">
<form action="{% url 'idhub:login' %}" role="form" method="post">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}" />
<div id="div_id_username"
class="clearfix control-group {% if form.username.errors %}error{% endif %}">
<div class="form-group">
<input type="text" name="username" maxlength="100" autocapitalize="off"
autocorrect="off" class="form-control textinput textInput" id="id_username" required
autofocus placeholder="{{ form.username.label }}"
{% if form.username.value %}value="{{ form.username.value }}" {% endif %}>
{% if form.username.errors %}
<p class="text-error">
{{ form.username.errors|striptags }}
</p>
{% endif %}
</div>
</div>
<div id="div_id_password"
class="clearfix control-group {% if form.password.errors %}error{% endif %}">
<div class="form-group">
<input type="password" name="password" maxlength="100" autocapitalize="off"
autocorrect="off" class="form-control textinput textInput" id="id_password"
placeholder="{{ form.password.label }}" required>
{% if form.password.errors %}
<p class="text-error">
{{ form.password.errors|striptags }}
</p>
{% endif %}
</div>
</div>
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<div class="well well-small text-error" style="border: none">{{ error }}</div>
{% endfor %}
{% endif %}
<input name="next" type="hidden" value="{{ success_url }}">
<div class="form-actions-no-box">
<input type="submit" name="submit" value="{% trans 'Log in' %}"
class="btn btn-primary form-control" id="submit-id-submit">
</div>
</form>
</div><!-- /.row-fluid -->
</div>
<!--/#login-content-->
<div id="login-footer">
<a href="#password_reset" data-toggle="modal" data-target="#forgotPasswordModal">{% trans "Forgot your password? Click here to recover" %}</a>
</div>
</div><!-- /#login-wrapper -->
</div><!-- /.jumbotron -->
<!-- Modal -->
<div class="modal fade" id="forgotPasswordModal" tabindex="-1" role="dialog" aria-labelledby="forgotPasswordModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="forgotPasswordModalLabel">{% trans "Forgot your password?" %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div> </div>
<div class="modal-body">
{% blocktrans trimmed with support_email="suport@pangea.org" %}
Send an email to <a href="mailto:{{ support_email }}">{{ support_email }}</a> including your username and we will provide instructions.
{% endblocktrans %}
</div>
</div>
</div>
</div>
</main>
</div>
</div> </div>
<script src="/static/js/bootstrap.bundle.min.js"></script> <div id="div_id_password"
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script> class="clearfix control-group {% if form.password.errors %}error{% endif %}">
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script> <div class="form-group">
<script src="/static/js/dashboard.js"></script> <input type="password" name="password" maxlength="100" autocapitalize="off"
</body> autocorrect="off" class="form-control textinput textInput" id="id_password"
</html> placeholder="{{ form.password.label }}" required>
{% if form.password.errors %}
<p class="text-error">
{{ form.password.errors|striptags }}
</p>
{% endif %}
</div>
</div>
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<div class="well well-small text-error" style="border: none">{{ error }}</div>
{% endfor %}
{% endif %}
<input name="next" type="hidden" value="{{ success_url }}">
<div class="form-actions-no-box">
<input type="submit" name="submit" value="{% trans 'Log in' %}"
class="btn btn-primary form-control" id="submit-id-submit">
</div>
</form>
<div id="login-footer">
<a href="{% url 'idhub:password_reset' %}" data-toggle="modal" data-target="#forgotPasswordModal">{% trans "Forgot your password? Click here to recover" %}</a>
</div>
{% endblock %}

View File

@ -0,0 +1,114 @@
{% load i18n static %}
<!doctype html>
<html lang="en">
<head>
{% block head %}
{% block meta %}
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="robots" content="NONE,NOARCHIVE" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="Pangea">
{% endblock %}
<title>{% block title %}{% if title %}{{ title }} {% endif %}IdHub{% endblock %}</title>
<!-- Bootstrap core CSS -->
{% block style %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href= "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
<link href="{% static "/css/dashboard.css" %}" rel="stylesheet">
{% endblock %}
{% endblock %}
</head>
<body id="body-login">
<header class="navbar navbar-dark sticky-top bg-grey flex-md-nowrap p-0 shadow">
<div class="navbar-nav navbar-sub-brand">
</div>
<div class="navbar-nav">
</div>
</header>
<div class="container-fluid">
<div class="row">
<main class="col-md-12 bt-5">
{% block messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endfor %}
{% endblock messages %}
<div class="jumbotron vertical-center">
<div id="login-wrapper" class="container" style="width: 430px;">
<div id="login-content" class="rounded">
<div id="login-branding">
{% block branding %}
<h1>
<img class="img-fluid" src="{% static '/images/logo-pangea-monocrome-h.png' %}"
alt="Pangea.org - Internet etic i solidari" />
</h1>
{% endblock %}
</div><!-- /login-branding -->
<div class="mt-5">
{% block login_content %}
{% endblock %}
</div><!-- /.row-fluid -->
</div>
<!--/#login-content-->
</div><!-- /#login-wrapper -->
</div><!-- /.jumbotron -->
<!-- Modal -->
<div class="modal fade" id="forgotPasswordModal" tabindex="-1" role="dialog" aria-labelledby="forgotPasswordModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="forgotPasswordModalLabel">{% trans "Forgot your password?" %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{% blocktrans trimmed with support_email="suport@pangea.org" %}
Send an email to <a href="mailto:{{ support_email }}">{{ support_email }}</a> including your username and we will provide instructions.
{% endblocktrans %}
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script>
<script src="/static/js/dashboard.js"></script>
</body>
</html>

View File

@ -0,0 +1,27 @@
{% extends "auth/login_base.html" %}
{% load i18n django_bootstrap5 %}
{% block login_content %}
<div class="well">
<div class="row-fluid">
<h2>{% trans 'Password reset' %}</h2>
<span>{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}</span>
</div>
</div>
<div class="well">
<div class="row-fluid">
<div>
<form action="{% url 'idhub:password_reset' %}" role="form" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% bootstrap_form_errors form type='non_fields' %}
<div class="form-actions-no-box">
<input type="submit" name="submit" value="{% trans 'Reset my password' %}" class="btn btn-primary form-control" id="submit-id-submit">
</div>
</form>
</div>
</div><!-- /.row-fluid -->
</div><!--/.well-->
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "auth/login_base.html" %}
{% load i18n %}
{% block login_content %}
<div class="container-fluid" style="margin-top: 30px">
<div class="row-fluid">
<div class="well" style="width: 800px; margin: auto auto 50px auto">
<div class="row-fluid">
<h2>{% trans 'Password reset complete' %}</h2>
<p>{% trans 'Your password has been set. You may go ahead and log in now.' %}</p>
<a href="{% url 'idhub:login' %}">{% trans 'Login' %}</a>
</div>
</div><!--/.well-->
</div><!-- /.row-fluid -->
</div><!-- /.container-fluid -->
{% endblock %}

View File

@ -0,0 +1,36 @@
{% extends "auth/login_base.html" %}
{% load i18n django_bootstrap5 %}
{% block login_content %}
<div class="container-fluid" style="margin-top: 30px">
<div class="row-fluid">
{% if validlink %}
<div class="well" style="width: 600px; margin-left: auto; margin-right: auto">
<h2>{% trans 'Enter new password' %}</h2>
<p>{% trans 'Please enter your new password twice so we can verify you typed it in correctly.' %}</p>
</div><!-- /well -->
<div class="well" style="width: 320px; margin-left: auto; margin-right: auto">
<div class="row-fluid">
<div>
<form role="form" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% bootstrap_form_errors form type='non_fields' %}
<div class="form-actions-no-box">
<input type="submit" name="submit" value="{% trans 'Change my password' %}" class="btn btn-primary form-control" id="submit-id-submit">
</div>
</form>
</div>
</div><!-- /.row-fluid -->
</div><!--/.well-->
{% else %}
<div class="well" style="width: 800px; margin-left: auto; margin-right: auto">
<h2>{% trans 'Password reset unsuccessful' %}</h2>
<p>{% trans 'The password reset link was invalid, possibly because it has already been used.' %}<br />
{% trans 'Please request a new password reset.' %}</p>
</div><!-- /well -->
{% endif %}
</div><!-- /.row-fluid -->
</div><!-- /.container-fluid -->
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "auth/login_base.html" %}
{% load i18n %}
{% block login_content %}
<div class="well">
<div class="row-fluid">
<h2>{% trans 'Password reset sent' %}</h2>
<p>{% trans "We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly." %}</p>
<p>{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}</p>
</div><!-- /.row-fluid -->
</div><!--/.well-->
{% endblock %}

View File

@ -0,0 +1,31 @@
{% load i18n %}{% autoescape off %}
{% trans "IdHub" as site %}
<p>
{% blocktrans %}You're receiving this email because your user account at {{site}} has been activated.{% endblocktrans %}
</p>
<p>
{% trans "Your username is:" %} {{ user.username }}
</p>
<p>
{% trans "Please go to the following page and choose a password:" %}
</p>
<p>
{% block reset_link %}
<a href="{{ protocol }}://{{ domain }}{% url 'idhub:password_reset_confirm' uidb64=uid token=token %}">
{{ protocol }}://{{ domain }}{% url 'idhub:password_reset_confirm' uidb64=uid token=token %}
</a>
{% endblock %}
</p>
<p>
{% trans "Thanks for using our site!" %}
</p>
<p>
{% blocktrans %}The {{site}} team{% endblocktrans %}
</p>
{% endautoescape %}

View File

@ -0,0 +1,19 @@
{% load i18n %}{% autoescape off %}
{% trans "Idhub" as site %}
{% blocktrans %}You're receiving this email because your user account at {{site}} has been activated.{% endblocktrans %}
{% trans "Your username is:" %} {{ user.username }}
{% trans "Please go to the following page and choose a password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'idhub:password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{site}} team{% endblocktrans %}
{% endautoescape %}

View File

@ -0,0 +1,4 @@
{% load i18n %}{% autoescape off %}
{% trans "IdHub" as site %}
{% blocktrans %}User activation on {{site}}{% endblocktrans %}
{% endautoescape %}

View File

@ -0,0 +1,30 @@
{% load i18n %}{% autoescape off %}
<p>
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}
</p>
<p>
{% trans "Please go to the following page and choose a new password:" %}
</p>
<p>
{% block reset_link %}
<a href="{{ protocol }}://{{ domain }}{% url 'idhub:password_reset_confirm' uidb64=uid token=token %}">
{{ protocol }}://{{ domain }}{% url 'idhub:password_reset_confirm' uidb64=uid token=token %}
</a>
{% endblock %}
</p>
<p>
{% trans "Your username, in case you've forgotten:" %} {{ user.username }}
</p>
<p>
{% trans "Thanks for using our site!" %}
</p>
<p>
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
</p>
{% endautoescape %}

View File

@ -0,0 +1,14 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'idhub:password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.username }}
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
{% endautoescape %}

View File

@ -0,0 +1,3 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}Password reset on {{ site_name }}{% endblocktrans %}
{% endautoescape %}

View File

@ -28,6 +28,35 @@ urlpatterns = [
permanent=False)), permanent=False)),
path('login/', LoginView.as_view(), name='login'), path('login/', LoginView.as_view(), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'), path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('auth/password_reset/',
auth_views.PasswordResetView.as_view(
template_name='auth/password_reset.html',
email_template_name='auth/registration/password_reset_email.txt',
html_email_template_name='auth/registration/password_reset_email.html',
subject_template_name='auth/registration/password_reset_subject.txt',
success_url=reverse_lazy('auth:password_reset_done')
),
name='password_reset'
),
path('auth/password_reset/done/',
auth_views.PasswordResetDoneView.as_view(
template_name='auth/password_reset_done.html'
),
name='password_reset_done'
),
path('auth/reset/<uidb64>/<token>/',
auth_views.PasswordResetConfirmView.as_view(
template_name='auth/password_reset_confirm.html',
success_url=reverse_lazy('idhub:password_reset_complete')
),
name='password_reset_confirm'
),
path('auth/reset/done/',
auth_views.PasswordResetCompleteView.as_view(
template_name='auth/password_reset_complete.html'
),
name='password_reset_complete'
),
# User # User
path('user/dashboard/', views_user.UserDashboardView.as_view(), path('user/dashboard/', views_user.UserDashboardView.as_view(),

View File

@ -30,6 +30,7 @@ SECRET_KEY = config('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool) DEBUG = config('DEBUG', default=False, cast=bool)
DEVELOPMENT = config('DEVELOPMENT', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv()) ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv())
@ -173,3 +174,4 @@ MESSAGE_TAGS = {
messages.WARNING: 'alert-warning', messages.WARNING: 'alert-warning',
messages.ERROR: 'alert-danger', messages.ERROR: 'alert-danger',
} }