feat: Enhance proposal creation and display

- Improve proposal creation form with input validation and
  default date settings for a better user experience.
- Add context variables to the proposals template for
  consistent display across governance pages.
- Enhance proposal detail page with visual improvements,
  voting results display, and user voting functionality.
- Add styles for better visual presentation of proposal details
  and voting information.
This commit is contained in:
Mahmoud-Emad 2025-05-22 16:31:11 +03:00
parent fad288f67d
commit 52fbc77e3e
3 changed files with 459 additions and 149 deletions

View File

@ -340,6 +340,11 @@ impl GovernanceController {
}
};
ctx.insert("proposals", &proposals);
// Add the required context variables for the proposals template
ctx.insert("active_tab", "proposals");
ctx.insert("status_filter", &None::<String>);
ctx.insert("search_filter", &None::<String>);
render_template(&tmpl, "governance/proposals.html", &ctx)
}

View File

@ -23,21 +23,24 @@
</ul>
</div>
</div>
<!-- Info Alert -->
<div class="row">
<div class="col-12">
<div class="alert alert-info alert-dismissible fade show">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<h5><i class="bi bi-info-circle"></i> About Creating Proposals</h5>
<p>Creating a proposal is an important step in our community governance process. Well-crafted proposals clearly state the problem, solution, and implementation details. The community will review and vote on your proposal, so be thorough and thoughtful in your submission.</p>
<p>Creating a proposal is an important step in our community governance process. Well-crafted proposals
clearly state the problem, solution, and implementation details. The community will review and vote
on your proposal, so be thorough and thoughtful in your submission.</p>
<div class="mt-2">
<a href="/governance/proposal-templates" class="btn btn-sm btn-outline-primary"><i class="bi bi-file-earmark-text"></i> Proposal Templates</a>
<a href="/governance/proposal-templates" class="btn btn-sm btn-outline-primary"><i
class="bi bi-file-earmark-text"></i> Proposal Templates</a>
</div>
</div>
</div>
</div>
<!-- Proposal Form and Guidelines in Flex Layout -->
<div class="row mb-4">
<!-- Proposal Form Column -->
@ -47,34 +50,42 @@
<h5 class="mb-0">New Proposal</h5>
</div>
<div class="card-body">
<form action="/governance/create" method="post">
<form action="/governance/create" method="post" id="proposalForm" novalidate>
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control" id="title" name="title" required
placeholder="Enter a clear, concise title for your proposal">
<input type="text" class="form-control" id="title" name="title" required minlength="5"
maxlength="100" placeholder="Enter a clear, concise title for your proposal">
<div class="invalid-feedback">Please provide a title (5-100 characters).</div>
<div class="form-text">Make it descriptive and specific</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="8" required
placeholder="Provide a detailed description of your proposal..."></textarea>
minlength="50" maxlength="5000"
placeholder="Provide a detailed description of your proposal..."></textarea>
<div class="invalid-feedback">Please provide a detailed description (at least 50
characters).</div>
<div class="form-text">Explain the purpose, benefits, and implementation details</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="voting_start_date" class="form-label">Voting Start Date</label>
<input type="date" class="form-control" id="voting_start_date" name="voting_start_date">
<div class="invalid-feedback" id="start_date_feedback">Please select a valid start date.
</div>
<div class="form-text">When should voting begin?</div>
</div>
<div class="col-md-6">
<label for="voting_end_date" class="form-label">Voting End Date</label>
<input type="date" class="form-control" id="voting_end_date" name="voting_end_date">
<div class="invalid-feedback" id="end_date_feedback">End date must be after start date.
</div>
<div class="form-text">When should voting end?</div>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="draft" name="draft" value="true">
@ -83,7 +94,7 @@
</label>
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Submit Proposal</button>
<a href="/governance" class="btn btn-outline-secondary">Cancel</a>
@ -92,7 +103,7 @@
</div>
</div>
</div>
<!-- Guidelines Column -->
<div class="col-lg-4">
<div class="card bg-light h-100">
@ -122,4 +133,111 @@
</div>
</div>
</div>
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
const form = document.getElementById('proposalForm');
const startDateInput = document.getElementById('voting_start_date');
const endDateInput = document.getElementById('voting_end_date');
const startDateFeedback = document.getElementById('start_date_feedback');
const endDateFeedback = document.getElementById('end_date_feedback');
// Set default dates
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const nextWeek = new Date(today);
nextWeek.setDate(nextWeek.getDate() + 7);
// Format dates for input fields
const formatDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
// Set default values
startDateInput.value = formatDate(tomorrow);
endDateInput.value = formatDate(nextWeek);
// Validate dates when they change
function validateDates() {
const startDate = new Date(startDateInput.value);
const endDate = new Date(endDateInput.value);
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0); // Reset time to start of day
let startValid = true;
let endValid = true;
// Validate start date is not in the past
if (startDate < currentDate) {
startDateInput.classList.add('is-invalid');
startDateFeedback.textContent = 'Start date cannot be in the past.';
startValid = false;
} else {
startDateInput.classList.remove('is-invalid');
}
// Validate end date is after start date
if (endDate < startDate) {
endDateInput.classList.add('is-invalid');
endDateFeedback.textContent = 'End date must be after start date.';
endValid = false;
} else {
endDateInput.classList.remove('is-invalid');
}
return startValid && endValid;
}
// Validate on input
startDateInput.addEventListener('change', validateDates);
endDateInput.addEventListener('change', validateDates);
// Form submission validation
form.addEventListener('submit', function (event) {
let formValid = true;
// Validate required fields
const requiredFields = form.querySelectorAll('[required]');
requiredFields.forEach(field => {
if (!field.value.trim()) {
field.classList.add('is-invalid');
formValid = false;
} else {
field.classList.remove('is-invalid');
}
// Check minlength if specified
if (field.minLength && field.value.length < field.minLength) {
field.classList.add('is-invalid');
formValid = false;
}
});
// Validate dates
const datesValid = validateDates();
formValid = formValid && datesValid;
// If form is not valid, prevent submission
if (!formValid) {
event.preventDefault();
// Scroll to the first invalid element
const firstInvalid = form.querySelector('.is-invalid');
if (firstInvalid) {
firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
firstInvalid.focus();
}
}
});
// Initial validation
validateDates();
});
</script>
{% endblock %}
{% endblock %}

View File

@ -2,6 +2,37 @@
{% block title %}{{ proposal.title }} - Governance Proposal{% endblock %}
{% block styles %}
<style>
.avatar-circle {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.comment-text {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.comment-text:hover {
white-space: normal;
overflow: visible;
}
.progress {
border-radius: 10px;
overflow: hidden;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
@ -30,44 +61,62 @@
<!-- Proposal Details -->
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<div class="col-lg-8">
<div class="card h-100 shadow-sm">
<div class="card-header bg-light">
<h4 class="mb-0">{{ proposal.title }}</h4>
</div>
<div class="card-body">
<div class="d-flex justify-content-between mb-3">
<div class="card-body d-flex flex-column">
<div class="d-flex justify-content-between align-items-center mb-3">
<span
class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %} p-2">
<i
class="bi {% if proposal.status == 'Active' %}bi-check-circle{% elif proposal.status == 'Approved' %}bi-trophy{% elif proposal.status == 'Rejected' %}bi-x-circle{% elif proposal.status == 'Draft' %}bi-pencil{% else %}bi-exclamation-circle{% endif %} me-1"></i>
{{ proposal.status }}
</span>
<small class="text-muted">Created by {{ proposal.creator_name }}
<!-- on {{ proposal.created_at | date(format="%Y-%m-%d") }} --></small>
<span class="text-muted"><i class="bi bi-person me-1"></i>Created by {{ proposal.creator_name
}}</span>
</div>
<h5>Description</h5>
<p class="mb-4">{{ proposal.description }}</p>
<div class="flex-grow-1">
<h5><i class="bi bi-file-text me-2"></i>Description</h5>
<div class="p-3 bg-light rounded mb-4">{{ proposal.description }}</div>
</div>
<h5>Voting Period</h5>
<p>
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
<strong>Start:</strong> {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} <br>
<strong>End:</strong> {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
{% else %}
Not set
{% endif %}
</p>
<div class="mt-auto">
<h5><i class="bi bi-calendar-event me-2"></i>Voting Period</h5>
<div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
<div>
<div class="text-muted mb-1">Start Date</div>
<div class="fw-bold">{{ proposal.voting_starts_at | date(format="%Y-%m-%d") }}</div>
</div>
<div class="text-center">
<i class="bi bi-arrow-right fs-4 text-muted"></i>
</div>
<div>
<div class="text-muted mb-1">End Date</div>
<div class="fw-bold">{{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}</div>
</div>
{% else %}
<div class="text-center w-100">Not set</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Voting Results</h5>
<div class="col-lg-4">
<div class="card mb-4 shadow-sm h-100">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-bar-chart-fill me-2"></i>Voting Dashboard</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="card-body d-flex flex-column">
<!-- Voting Results Section -->
<div class="mb-4">
<h6 class="border-bottom pb-2 mb-3">Results</h6>
{% set yes_percent = 0 %}
{% set no_percent = 0 %}
{% set abstain_percent = 0 %}
@ -78,131 +127,269 @@
{% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %}
{% endif %}
<p class="mb-1">Yes: {{ results.yes_count }} ({{ yes_percent }}%)</p>
<div class="progress mb-3">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%">
</div>
<!-- Yes votes -->
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-bold text-success"><i class="bi bi-check-circle-fill me-1"></i> Yes</span>
<span class="badge bg-success rounded-pill">{{ results.yes_count }}</span>
</div>
<div class="progress mb-3" style="height: 12px;">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%"
aria-valuenow="{{ yes_percent }}" aria-valuemin="0" aria-valuemax="100"
title="{{ yes_percent }}% of votes"></div>
</div>
<p class="mb-1">No: {{ results.no_count }} ({{ no_percent }}%)</p>
<div class="progress mb-3">
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%">
</div>
<!-- No votes -->
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-bold text-danger"><i class="bi bi-x-circle-fill me-1"></i> No</span>
<span class="badge bg-danger rounded-pill">{{ results.no_count }}</span>
</div>
<div class="progress mb-3" style="height: 12px;">
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%"
aria-valuenow="{{ no_percent }}" aria-valuemin="0" aria-valuemax="100"
title="{{ no_percent }}% of votes"></div>
</div>
<p class="mb-1">Abstain: {{ results.abstain_count }} ({{ abstain_percent }}%)</p>
<div class="progress mb-3">
<!-- Abstain votes -->
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-bold text-secondary"><i class="bi bi-dash-circle-fill me-1"></i>
Abstain</span>
<span class="badge bg-secondary rounded-pill">{{ results.abstain_count }}</span>
</div>
<div class="progress mb-3" style="height: 12px;">
<div class="progress-bar bg-secondary" role="progressbar"
style="width: {{ abstain_percent }}%"></div>
style="width: {{ abstain_percent }}%" aria-valuenow="{{ abstain_percent }}"
aria-valuemin="0" aria-valuemax="100" title="{{ abstain_percent }}% of votes"></div>
</div>
</div>
<p class="text-center"><strong>Total Votes:</strong> {{ results.total_votes }}</p>
</div>
</div>
<!-- Vote Form -->
{% if proposal.status == "Active" and user and user.id %}
<div class="card">
<div class="card-header">
<h5 class="mb-0">Cast Your Vote</h5>
</div>
<div class="card-body">
<form action="/governance/proposals/{{ proposal.base_data.id }}/vote" method="post">
<div class="mb-3">
<label class="form-label">Vote Type</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteYes" value="Yes"
checked>
<label class="form-check-label" for="voteYes">
Yes - I support this proposal
</label>
<div class="mt-auto">
<div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
<div class="text-center">
<h4 class="mb-0">{{ results.total_votes }}</h4>
<small class="text-muted">Total Votes</small>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteNo" value="No">
<label class="form-check-label" for="voteNo">
No - I oppose this proposal
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteAbstain"
value="Abstain">
<label class="form-check-label" for="voteAbstain">
Abstain - I choose not to vote
</label>
</div>
</div>
<div class="mb-3">
<label for="comment" class="form-label">Comment (Optional)</label>
<textarea class="form-control" id="comment" name="comment" rows="3"
placeholder="Explain your vote..."></textarea>
</div>
<button type="submit" class="btn btn-primary w-100">Submit Vote</button>
</form>
</div>
</div>
{% elif not user or not user.id %}
<div class="card">
<div class="card-body text-center">
<p>You must be logged in to vote.</p>
<a href="/login" class="btn btn-primary">Login to Vote</a>
</div>
</div>
{% elif proposal.status != "Active" %}
<div class="card">
<div class="card-body">
<div class="alert alert-warning mb-0">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Note:</strong> Voting is only available for proposals with an Active status.
This proposal's current status is <strong>{{ proposal.status }}</strong>.
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Votes List -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Votes ({{ votes | length }})</h5>
</div>
<div class="card-body">
{% if votes | length > 0 %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Voter</th>
<th>Vote</th>
<th>Comment</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{% for vote in votes %}
<tr>
<td>{{ vote.voter_name }}</td>
<td>
<span
class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
{{ vote.vote_type }}
</span>
</td>
<td>{% if vote.comment %}{{ vote.comment }}{% else %}No comment{% endif %}</td>
<td>{{ vote.created_at | date(format="%Y-%m-%d %H:%M") }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if proposal.status == "Active" %}
<div class="text-center">
<div class="position-relative d-inline-block" style="width: 60px; height: 60px;">
<svg width="60" height="60">
<circle cx="30" cy="30" r="25" fill="none" stroke="#e9ecef" stroke-width="5">
</circle>
<circle cx="30" cy="30" r="25" fill="none" stroke="#0d6efd" stroke-width="5"
stroke-dasharray="157"
stroke-dashoffset="{{ 157 - (157 * yes_percent / 100) }}"
transform="rotate(-90 30 30)"></circle>
</svg>
<div
class="position-absolute top-50 start-50 translate-middle text-primary fw-bold">
{{ yes_percent }}%</div>
</div>
<small class="text-muted">Approval Rate</small>
</div>
{% endif %}
</div>
</div>
<!-- Vote Form Section -->
{% if proposal.status == "Active" and user and user.id %}
<div class="mt-auto">
<h6 class="border-bottom pb-2 mb-3"><i class="bi bi-check2-square me-2"></i>Cast Your Vote</h6>
<form action="/governance/proposals/{{ proposal.base_data.id }}/vote" method="post">
<div class="mb-3">
<div class="d-flex gap-2 mb-2">
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteYes"
value="Yes" required>
<label class="form-check-label text-success" for="voteYes"><i
class="bi bi-check-circle-fill me-1"></i>Yes</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteNo"
value="No">
<label class="form-check-label text-danger" for="voteNo"><i
class="bi bi-x-circle-fill me-1"></i>No</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteAbstain"
value="Abstain">
<label class="form-check-label text-secondary" for="voteAbstain"><i
class="bi bi-dash-circle-fill me-1"></i>Abstain</label>
</div>
</div>
<textarea class="form-control" id="comment" name="comment" rows="2"
placeholder="Add your thoughts about this proposal (optional)..."></textarea>
</div>
<button type="submit" class="btn btn-primary w-100"><i class="bi bi-send me-2"></i>Submit
Vote</button>
</form>
</div>
{% elif proposal.status != "Active" %}
<div class="mt-auto text-center p-3 bg-light rounded">
<i class="bi bi-info-circle fs-4 text-muted"></i>
<p class="mb-0 mt-2">Voting is {{ proposal.status | lower }} for this proposal</p>
</div>
{% elif not user or not user.id %}
<div class="mt-auto text-center p-3 bg-light rounded">
<i class="bi bi-person-lock fs-4 text-muted"></i>
<p class="mb-0 mt-2">You must be logged in to vote</p>
<a href="/login" class="btn btn-primary btn-sm mt-2">Login to Vote</a>
</div>
{% else %}
<p class="text-center">No votes have been cast yet.</p>
{% endif %}
</div>
</div>
</div>
<!-- Votes List -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center flex-wrap">
<h5 class="mb-0 mb-md-0"><i class="bi bi-list-check me-2"></i>Votes</h5>
<div class="d-flex align-items-center">
<div class="input-group input-group-sm me-2 d-none d-md-flex" style="width: 200px;">
<span class="input-group-text bg-white">
<i class="bi bi-search"></i>
</span>
<input type="text" class="form-control border-start-0" id="voteSearch"
placeholder="Search votes...">
</div>
<div class="btn-group" role="group" aria-label="Filter votes">
<button type="button" class="btn btn-sm btn-outline-primary active"
data-filter="all">All</button>
<button type="button" class="btn btn-sm btn-outline-success"
data-filter="yes">Yes</button>
<button type="button" class="btn btn-sm btn-outline-danger" data-filter="no">No</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
data-filter="abstain">Abstain</button>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-3">Voter</th>
<th>Vote</th>
<th>Comment</th>
<th class="text-end pe-3">Date</th>
</tr>
</thead>
<tbody>
{% if votes | length == 0 %}
<tr>
<td colspan="4" class="text-center py-4">
<div class="py-3">
<i class="bi bi-inbox fs-1 text-muted"></i>
<p class="mt-2 mb-0">No votes have been cast yet</p>
</div>
</td>
</tr>
{% else %}
{% for vote in votes %}
<tr class="vote-row" data-vote-type="{{ vote.vote_type | lower }}">
<td class="ps-3">
<div class="d-flex align-items-center">
<div class="avatar-circle me-2 bg-primary text-white">
U
</div>
<span>{{ vote.voter_name }}</span>
</div>
</td>
<td>
<span
class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %} rounded-pill px-3 py-2">
{% if vote.vote_type == 'Yes' %}
<i class="bi bi-check-circle-fill me-1"></i>
{% elif vote.vote_type == 'No' %}
<i class="bi bi-x-circle-fill me-1"></i>
{% else %}
<i class="bi bi-dash-circle-fill me-1"></i>
{% endif %}
{{ vote.vote_type }}
</span>
</td>
<td>
{% if vote.comment %}
<div class="comment-text">{{ vote.comment }}</div>
{% else %}
<span class="text-muted fst-italic">No comment provided</span>
{% endif %}
</td>
<td class="text-end pe-3">
<div class="d-flex flex-column align-items-end">
<span>{{ vote.created_at | date(format="%Y-%m-%d") }}</span>
<small class="text-muted">{{ vote.created_at | date(format="%H:%M")
}}</small>
</div>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Vote filtering using data-filter attributes
const filterButtons = document.querySelectorAll('[data-filter]');
const voteRows = document.querySelectorAll('.vote-row');
const searchInput = document.getElementById('voteSearch');
// Filter votes by type
filterButtons.forEach(button => {
button.addEventListener('click', function () {
// Update active button
filterButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
const filterType = this.getAttribute('data-filter');
voteRows.forEach(row => {
if (filterType === 'all') {
row.style.display = '';
} else {
const voteType = row.getAttribute('data-vote-type');
row.style.display = (voteType === filterType) ? '' : 'none';
}
});
});
});
// Search functionality
if (searchInput) {
searchInput.addEventListener('input', function () {
const searchTerm = this.value.toLowerCase();
voteRows.forEach(row => {
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
if (voterName.includes(searchTerm) || comment.includes(searchTerm)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
}
// Initialize tooltips for all elements with title attributes
const tooltipElements = document.querySelectorAll('[title]');
if (tooltipElements.length > 0) {
[].slice.call(tooltipElements).map(function (el) {
return new bootstrap.Tooltip(el);
});
}
});
</script>
{% endblock scripts %}
{% endblock content %}