mirror of
https://github.com/simple-login/app.git
synced 2024-11-10 21:27:10 +01:00
Admin panel improvements (#2179)
This commit is contained in:
parent
ab26dd3cb4
commit
8dfa886024
@ -2,6 +2,7 @@ from typing import Optional
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
from flask_admin import BaseView
|
||||||
from flask_admin.form import SecureForm
|
from flask_admin.form import SecureForm
|
||||||
from flask_admin.model.template import EndpointLinkRowAction
|
from flask_admin.model.template import EndpointLinkRowAction
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
@ -28,10 +29,24 @@ from app.models import (
|
|||||||
Alias,
|
Alias,
|
||||||
Newsletter,
|
Newsletter,
|
||||||
PADDLE_SUBSCRIPTION_GRACE_DAYS,
|
PADDLE_SUBSCRIPTION_GRACE_DAYS,
|
||||||
|
Mailbox,
|
||||||
)
|
)
|
||||||
from app.newsletter_utils import send_newsletter_to_user, send_newsletter_to_address
|
from app.newsletter_utils import send_newsletter_to_user, send_newsletter_to_address
|
||||||
|
|
||||||
|
|
||||||
|
def _admin_action_formatter(view, context, model, name):
|
||||||
|
action_name = AuditLogActionEnum.get_name(model.action)
|
||||||
|
return "{} ({})".format(action_name, model.action)
|
||||||
|
|
||||||
|
|
||||||
|
def _admin_date_formatter(view, context, model, name):
|
||||||
|
return model.created_at.format()
|
||||||
|
|
||||||
|
|
||||||
|
def _user_upgrade_channel_formatter(view, context, model, name):
|
||||||
|
return Markup(model.upgrade_channel)
|
||||||
|
|
||||||
|
|
||||||
class SLModelView(sqla.ModelView):
|
class SLModelView(sqla.ModelView):
|
||||||
column_default_sort = ("id", True)
|
column_default_sort = ("id", True)
|
||||||
column_display_pk = True
|
column_display_pk = True
|
||||||
@ -96,10 +111,6 @@ class SLAdminIndexView(AdminIndexView):
|
|||||||
return redirect("/admin/user")
|
return redirect("/admin/user")
|
||||||
|
|
||||||
|
|
||||||
def _user_upgrade_channel_formatter(view, context, model, name):
|
|
||||||
return Markup(model.upgrade_channel)
|
|
||||||
|
|
||||||
|
|
||||||
class UserAdmin(SLModelView):
|
class UserAdmin(SLModelView):
|
||||||
form_base_class = SecureForm
|
form_base_class = SecureForm
|
||||||
column_searchable_list = ["email", "id"]
|
column_searchable_list = ["email", "id"]
|
||||||
@ -120,6 +131,8 @@ class UserAdmin(SLModelView):
|
|||||||
|
|
||||||
column_formatters = {
|
column_formatters = {
|
||||||
"upgrade_channel": _user_upgrade_channel_formatter,
|
"upgrade_channel": _user_upgrade_channel_formatter,
|
||||||
|
"created_at": _admin_date_formatter,
|
||||||
|
"updated_at": _admin_date_formatter,
|
||||||
}
|
}
|
||||||
|
|
||||||
@action(
|
@action(
|
||||||
@ -353,12 +366,22 @@ class EmailLogAdmin(SLModelView):
|
|||||||
can_edit = False
|
can_edit = False
|
||||||
can_create = False
|
can_create = False
|
||||||
|
|
||||||
|
column_formatters = {
|
||||||
|
"created_at": _admin_date_formatter,
|
||||||
|
"updated_at": _admin_date_formatter,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AliasAdmin(SLModelView):
|
class AliasAdmin(SLModelView):
|
||||||
form_base_class = SecureForm
|
form_base_class = SecureForm
|
||||||
column_searchable_list = ["id", "user.email", "email", "mailbox.email"]
|
column_searchable_list = ["id", "user.email", "email", "mailbox.email"]
|
||||||
column_filters = ["id", "user.email", "email", "mailbox.email"]
|
column_filters = ["id", "user.email", "email", "mailbox.email"]
|
||||||
|
|
||||||
|
column_formatters = {
|
||||||
|
"created_at": _admin_date_formatter,
|
||||||
|
"updated_at": _admin_date_formatter,
|
||||||
|
}
|
||||||
|
|
||||||
@action(
|
@action(
|
||||||
"disable_email_spoofing_check",
|
"disable_email_spoofing_check",
|
||||||
"Disable email spoofing protection",
|
"Disable email spoofing protection",
|
||||||
@ -385,6 +408,11 @@ class MailboxAdmin(SLModelView):
|
|||||||
column_searchable_list = ["id", "user.email", "email"]
|
column_searchable_list = ["id", "user.email", "email"]
|
||||||
column_filters = ["id", "user.email", "email"]
|
column_filters = ["id", "user.email", "email"]
|
||||||
|
|
||||||
|
column_formatters = {
|
||||||
|
"created_at": _admin_date_formatter,
|
||||||
|
"updated_at": _admin_date_formatter,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# class LifetimeCouponAdmin(SLModelView):
|
# class LifetimeCouponAdmin(SLModelView):
|
||||||
# can_edit = True
|
# can_edit = True
|
||||||
@ -396,12 +424,22 @@ class CouponAdmin(SLModelView):
|
|||||||
can_edit = False
|
can_edit = False
|
||||||
can_create = True
|
can_create = True
|
||||||
|
|
||||||
|
column_formatters = {
|
||||||
|
"created_at": _admin_date_formatter,
|
||||||
|
"updated_at": _admin_date_formatter,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ManualSubscriptionAdmin(SLModelView):
|
class ManualSubscriptionAdmin(SLModelView):
|
||||||
form_base_class = SecureForm
|
form_base_class = SecureForm
|
||||||
can_edit = True
|
can_edit = True
|
||||||
column_searchable_list = ["id", "user.email"]
|
column_searchable_list = ["id", "user.email"]
|
||||||
|
|
||||||
|
column_formatters = {
|
||||||
|
"created_at": _admin_date_formatter,
|
||||||
|
"updated_at": _admin_date_formatter,
|
||||||
|
}
|
||||||
|
|
||||||
@action(
|
@action(
|
||||||
"extend_1y",
|
"extend_1y",
|
||||||
"Extend for 1 year",
|
"Extend for 1 year",
|
||||||
@ -445,12 +483,22 @@ class CustomDomainAdmin(SLModelView):
|
|||||||
column_exclude_list = ["ownership_txt_token"]
|
column_exclude_list = ["ownership_txt_token"]
|
||||||
can_edit = False
|
can_edit = False
|
||||||
|
|
||||||
|
column_formatters = {
|
||||||
|
"created_at": _admin_date_formatter,
|
||||||
|
"updated_at": _admin_date_formatter,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ReferralAdmin(SLModelView):
|
class ReferralAdmin(SLModelView):
|
||||||
form_base_class = SecureForm
|
form_base_class = SecureForm
|
||||||
column_searchable_list = ["id", "user.email", "code", "name"]
|
column_searchable_list = ["id", "user.email", "code", "name"]
|
||||||
column_filters = ["id", "user.email", "code", "name"]
|
column_filters = ["id", "user.email", "code", "name"]
|
||||||
|
|
||||||
|
column_formatters = {
|
||||||
|
"created_at": _admin_date_formatter,
|
||||||
|
"updated_at": _admin_date_formatter,
|
||||||
|
}
|
||||||
|
|
||||||
def scaffold_list_columns(self):
|
def scaffold_list_columns(self):
|
||||||
ret = super().scaffold_list_columns()
|
ret = super().scaffold_list_columns()
|
||||||
ret.insert(0, "nb_user")
|
ret.insert(0, "nb_user")
|
||||||
@ -466,15 +514,6 @@ class ReferralAdmin(SLModelView):
|
|||||||
# can_delete = True
|
# can_delete = True
|
||||||
|
|
||||||
|
|
||||||
def _admin_action_formatter(view, context, model, name):
|
|
||||||
action_name = AuditLogActionEnum.get_name(model.action)
|
|
||||||
return "{} ({})".format(action_name, model.action)
|
|
||||||
|
|
||||||
|
|
||||||
def _admin_created_at_formatter(view, context, model, name):
|
|
||||||
return model.created_at.format()
|
|
||||||
|
|
||||||
|
|
||||||
class AdminAuditLogAdmin(SLModelView):
|
class AdminAuditLogAdmin(SLModelView):
|
||||||
form_base_class = SecureForm
|
form_base_class = SecureForm
|
||||||
column_searchable_list = ["admin.id", "admin.email", "model_id", "created_at"]
|
column_searchable_list = ["admin.id", "admin.email", "model_id", "created_at"]
|
||||||
@ -487,7 +526,8 @@ class AdminAuditLogAdmin(SLModelView):
|
|||||||
|
|
||||||
column_formatters = {
|
column_formatters = {
|
||||||
"action": _admin_action_formatter,
|
"action": _admin_action_formatter,
|
||||||
"created_at": _admin_created_at_formatter,
|
"created_at": _admin_date_formatter,
|
||||||
|
"updated_at": _admin_date_formatter,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -516,8 +556,8 @@ class ProviderComplaintAdmin(SLModelView):
|
|||||||
can_delete = False
|
can_delete = False
|
||||||
|
|
||||||
column_formatters = {
|
column_formatters = {
|
||||||
"created_at": _admin_created_at_formatter,
|
"created_at": _admin_date_formatter,
|
||||||
"updated_at": _admin_created_at_formatter,
|
"updated_at": _admin_date_formatter,
|
||||||
"state": _transactionalcomplaint_state_formatter,
|
"state": _transactionalcomplaint_state_formatter,
|
||||||
"phase": _transactionalcomplaint_phase_formatter,
|
"phase": _transactionalcomplaint_phase_formatter,
|
||||||
"refused_email": _transactionalcomplaint_refused_email_id_formatter,
|
"refused_email": _transactionalcomplaint_refused_email_id_formatter,
|
||||||
@ -687,3 +727,38 @@ class InvalidMailboxDomainAdmin(SLModelView):
|
|||||||
form_base_class = SecureForm
|
form_base_class = SecureForm
|
||||||
can_create = True
|
can_create = True
|
||||||
can_delete = True
|
can_delete = True
|
||||||
|
|
||||||
|
|
||||||
|
class EmailSearchAdmin(BaseView):
|
||||||
|
def is_accessible(self):
|
||||||
|
return current_user.is_authenticated and current_user.is_admin
|
||||||
|
|
||||||
|
def inaccessible_callback(self, name, **kwargs):
|
||||||
|
# redirect to login page if user doesn't have access
|
||||||
|
flash("You don't have access to the admin page", "error")
|
||||||
|
return redirect(url_for("dashboard.index", next=request.url))
|
||||||
|
|
||||||
|
@expose("/", methods=["GET", "POST"])
|
||||||
|
def index(self):
|
||||||
|
alias = None
|
||||||
|
user = None
|
||||||
|
mailbox = None
|
||||||
|
no_match = False
|
||||||
|
email = None
|
||||||
|
if request.form and request.form["email"]:
|
||||||
|
email = request.form["email"]
|
||||||
|
alias = Alias.get_by(email=email)
|
||||||
|
user = User.get_by(email=email)
|
||||||
|
mailbox = Mailbox.get_by(email=email)
|
||||||
|
if not alias and not user and not mailbox:
|
||||||
|
no_match = True
|
||||||
|
|
||||||
|
return self.render(
|
||||||
|
"admin/alias_search.html",
|
||||||
|
email=email,
|
||||||
|
no_match=no_match,
|
||||||
|
alias=alias,
|
||||||
|
mailbox=mailbox,
|
||||||
|
user=user,
|
||||||
|
user_aliases=lambda user_id: Alias.filter_by(user_id=user_id).all(),
|
||||||
|
)
|
||||||
|
@ -45,6 +45,7 @@ from app.admin_model import (
|
|||||||
DailyMetricAdmin,
|
DailyMetricAdmin,
|
||||||
MetricAdmin,
|
MetricAdmin,
|
||||||
InvalidMailboxDomainAdmin,
|
InvalidMailboxDomainAdmin,
|
||||||
|
EmailSearchAdmin,
|
||||||
)
|
)
|
||||||
from app.api.base import api_bp
|
from app.api.base import api_bp
|
||||||
from app.auth.base import auth_bp
|
from app.auth.base import auth_bp
|
||||||
@ -786,6 +787,7 @@ def init_admin(app):
|
|||||||
admin.add_view(UserAdmin(User, Session))
|
admin.add_view(UserAdmin(User, Session))
|
||||||
admin.add_view(AliasAdmin(Alias, Session))
|
admin.add_view(AliasAdmin(Alias, Session))
|
||||||
admin.add_view(MailboxAdmin(Mailbox, Session))
|
admin.add_view(MailboxAdmin(Mailbox, Session))
|
||||||
|
admin.add_view(EmailSearchAdmin(name="Email Search", endpoint="email_search"))
|
||||||
admin.add_view(CouponAdmin(Coupon, Session))
|
admin.add_view(CouponAdmin(Coupon, Session))
|
||||||
admin.add_view(ManualSubscriptionAdmin(ManualSubscription, Session))
|
admin.add_view(ManualSubscriptionAdmin(ManualSubscription, Session))
|
||||||
admin.add_view(CustomDomainAdmin(CustomDomain, Session))
|
admin.add_view(CustomDomainAdmin(CustomDomain, Session))
|
||||||
|
300
templates/admin/alias_search.html
Normal file
300
templates/admin/alias_search.html
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
{% extends 'admin/master.html' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
||||||
|
<form method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email to search:</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="email"
|
||||||
|
value="{{ email or '' }}"/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% if no_match %}
|
||||||
|
|
||||||
|
<div class="border border-dark border-2 mt-1 mb-2 p-3 alert alert-warning"
|
||||||
|
role="alert">
|
||||||
|
No user, alias or mailbox found for {{ email }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if alias %}
|
||||||
|
|
||||||
|
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
||||||
|
<h3>Alias {{ alias.email }} found</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>
|
||||||
|
Alias id
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ alias.id }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Email
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ alias.email }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Created at
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ alias.created_at }}
|
||||||
|
</dd>
|
||||||
|
<dt class="mb-2">
|
||||||
|
User
|
||||||
|
</dt>
|
||||||
|
<dd class="ml-4 border-secondary border p-2">
|
||||||
|
<dl>
|
||||||
|
<dt>
|
||||||
|
User id
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ alias.user.id }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Email
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ alias.user.email }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Premium
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ alias.user.is_premium() }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Disabled
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ alias.user.disabled }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Crated At
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ alias.user.created_at }}
|
||||||
|
</dd>
|
||||||
|
<dt class="border-dark border-top-2 mb-2">
|
||||||
|
Mailboxes
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{% for mailbox in alias.mailboxes %}
|
||||||
|
|
||||||
|
<dl class="border border-grey border-2 ml-4 p-2">
|
||||||
|
<dt>
|
||||||
|
Mailbox id
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.id }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Email
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.email }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Verified
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.verified }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Created At
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.created_at }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
{% endfor %}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if user %}
|
||||||
|
|
||||||
|
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
||||||
|
<h3>User {{ user.email }} found</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>
|
||||||
|
User id
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ user.id }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Email
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ user.email }}
|
||||||
|
</dd>
|
||||||
|
{% if user.is_paid() %}
|
||||||
|
|
||||||
|
<dt>
|
||||||
|
Paid
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
Yes
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Subscription
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ user.get_active_subscription() }}
|
||||||
|
</dd>
|
||||||
|
{% else %}
|
||||||
|
<dt>
|
||||||
|
Paid
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
No
|
||||||
|
</dd>
|
||||||
|
{% endif %}
|
||||||
|
<dt>
|
||||||
|
Created at
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ user.created_at }}
|
||||||
|
</dd>
|
||||||
|
<dt class="mb-2">
|
||||||
|
Mailboxes
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{% for mailbox in user.mailboxes() %}
|
||||||
|
|
||||||
|
<dl class="border border-dark p-2 ml-4">
|
||||||
|
<dt>
|
||||||
|
Mailbox id
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.id }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Email
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.email }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Verified
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.verified }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Created At
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.created_at }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
{% endfor %}
|
||||||
|
</dd>
|
||||||
|
<dt class="mb-2">
|
||||||
|
Aliases
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{% for mailbox in user_aliases(user.id) %}
|
||||||
|
|
||||||
|
<dl class="border border-dark p-2 ml-4">
|
||||||
|
<dt>
|
||||||
|
Mailbox id
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.id }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Email
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.email }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Verified
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.verified }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Created At
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.created_at }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
{% endfor %}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if mailbox %}
|
||||||
|
|
||||||
|
<div class="border border-dark mt-1 mb-2 p-3">
|
||||||
|
<h3>Mailbox {{ mailbox.email }} found</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>
|
||||||
|
Mailbox id
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.id }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Email
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.email }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Created at
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.created_at }}
|
||||||
|
</dd>
|
||||||
|
<dt class="mb-2">
|
||||||
|
User
|
||||||
|
</dt>
|
||||||
|
<dd class="ml-4">
|
||||||
|
<dl class="border-dark border p-2">
|
||||||
|
<dt>
|
||||||
|
User id
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.user.id }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Email
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.user.email }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Premium
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.user.is_premium() }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Disabled
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.user.disabled }}
|
||||||
|
</dd>
|
||||||
|
<dt>
|
||||||
|
Crated At
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{ mailbox.user.created_at }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user