June 25 - Optimize codes
This commit is contained in:
@@ -18,7 +18,8 @@ def dashboard():
|
|||||||
stats = get_admin_dashboard_stats()
|
stats = get_admin_dashboard_stats()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Dashboard stats error: {e}")
|
logger.error(f"Dashboard stats error: {e}")
|
||||||
stats = {"user_stats": [], "total_sites": 0, "total_users": 0, "active_today": 0}
|
stats = {"user_stats": [], "total_sites": 0, "total_users": 0, "active_today": 0,
|
||||||
|
"open_bids": 0, "bids_due_soon": 0, "ai_analyses_30d": 0}
|
||||||
try:
|
try:
|
||||||
missed = get_missed_shifts_today()
|
missed = get_missed_shifts_today()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from flask import Blueprint, render_template, request, redirect, url_for, flash,
|
|||||||
from models import (
|
from models import (
|
||||||
get_all_users, create_user, update_user, delete_user,
|
get_all_users, create_user, update_user, delete_user,
|
||||||
get_user_by_id, create_password_reset_token, log_action,
|
get_user_by_id, create_password_reset_token, log_action,
|
||||||
|
invalidate_admin_stats_cache,
|
||||||
)
|
)
|
||||||
from utils.decorators import admin_required
|
from utils.decorators import admin_required
|
||||||
from utils.email import send_email
|
from utils.email import send_email
|
||||||
@@ -38,6 +39,7 @@ def create():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
create_user(admin_id, username, password, role, full_name, email)
|
create_user(admin_id, username, password, role, full_name, email)
|
||||||
|
invalidate_admin_stats_cache()
|
||||||
flash(f"User '{username}' created successfully.", "success")
|
flash(f"User '{username}' created successfully.", "success")
|
||||||
logger.info(f"User '{username}' created by admin_id={admin_id}.")
|
logger.info(f"User '{username}' created by admin_id={admin_id}.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -60,6 +62,7 @@ def edit(user_id):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
update_user(admin_id, user_id, username, role, full_name, is_active, password, email)
|
update_user(admin_id, user_id, username, role, full_name, is_active, password, email)
|
||||||
|
invalidate_admin_stats_cache()
|
||||||
flash(f"User '{username}' updated successfully.", "success")
|
flash(f"User '{username}' updated successfully.", "success")
|
||||||
logger.info(f"User id={user_id} updated by admin_id={admin_id}.")
|
logger.info(f"User id={user_id} updated by admin_id={admin_id}.")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -77,6 +80,7 @@ def delete(user_id):
|
|||||||
admin_id = session["user"]["id"]
|
admin_id = session["user"]["id"]
|
||||||
try:
|
try:
|
||||||
delete_user(admin_id, user_id)
|
delete_user(admin_id, user_id)
|
||||||
|
invalidate_admin_stats_cache()
|
||||||
flash("User deleted successfully.", "success")
|
flash("User deleted successfully.", "success")
|
||||||
logger.info(f"User id={user_id} deleted by admin_id={admin_id}.")
|
logger.info(f"User id={user_id} deleted by admin_id={admin_id}.")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from flask import Blueprint, render_template, request, redirect, url_for, flash,
|
|||||||
from models import (
|
from models import (
|
||||||
get_all_websites, get_website_by_id, get_website_credentials,
|
get_all_websites, get_website_by_id, get_website_credentials,
|
||||||
get_website_assigned_users, create_website, update_website, delete_website,
|
get_website_assigned_users, create_website, update_website, delete_website,
|
||||||
get_all_users,
|
get_all_users, invalidate_admin_stats_cache,
|
||||||
)
|
)
|
||||||
from utils.decorators import admin_required
|
from utils.decorators import admin_required
|
||||||
|
|
||||||
@@ -78,6 +78,7 @@ def create():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
create_website(admin_id, name, url, check_type, note, creds, visibility, assigned)
|
create_website(admin_id, name, url, check_type, note, creds, visibility, assigned)
|
||||||
|
invalidate_admin_stats_cache()
|
||||||
flash(f"Website '{name}' created successfully.", "success")
|
flash(f"Website '{name}' created successfully.", "success")
|
||||||
logger.info(f"Website '{name}' created by admin_id={admin_id}.")
|
logger.info(f"Website '{name}' created by admin_id={admin_id}.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -101,6 +102,7 @@ def edit(website_id):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
update_website(admin_id, website_id, name, url, check_type, note, creds, visibility, assigned)
|
update_website(admin_id, website_id, name, url, check_type, note, creds, visibility, assigned)
|
||||||
|
invalidate_admin_stats_cache()
|
||||||
flash(f"Website '{name}' updated successfully.", "success")
|
flash(f"Website '{name}' updated successfully.", "success")
|
||||||
logger.info(f"Website id={website_id} updated by admin_id={admin_id}.")
|
logger.info(f"Website id={website_id} updated by admin_id={admin_id}.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -116,6 +118,7 @@ def delete(website_id):
|
|||||||
admin_id = session["user"]["id"]
|
admin_id = session["user"]["id"]
|
||||||
try:
|
try:
|
||||||
delete_website(admin_id, website_id)
|
delete_website(admin_id, website_id)
|
||||||
|
invalidate_admin_stats_cache()
|
||||||
flash("Website deleted (soft) successfully.", "success")
|
flash("Website deleted (soft) successfully.", "success")
|
||||||
logger.info(f"Website id={website_id} soft-deleted by admin_id={admin_id}.")
|
logger.info(f"Website id={website_id} soft-deleted by admin_id={admin_id}.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from models import (
|
|||||||
get_all_bids, get_bid, create_bid, update_bid, delete_bid,
|
get_all_bids, get_bid, create_bid, update_bid, delete_bid,
|
||||||
get_bid_updates, add_bid_update, delete_bid_update, BID_STATUSES,
|
get_bid_updates, add_bid_update, delete_bid_update, BID_STATUSES,
|
||||||
log_action, get_bids_due_soon, get_admin_emails,
|
log_action, get_bids_due_soon, get_admin_emails,
|
||||||
|
invalidate_admin_stats_cache,
|
||||||
)
|
)
|
||||||
from utils.decorators import login_required, admin_required
|
from utils.decorators import login_required, admin_required
|
||||||
from utils.email import send_email
|
from utils.email import send_email
|
||||||
@@ -78,6 +79,7 @@ def create():
|
|||||||
bid_id = create_bid(user["id"], title, url, source, sol_no, status, due_date, notes)
|
bid_id = create_bid(user["id"], title, url, source, sol_no, status, due_date, notes)
|
||||||
log_action(user["id"], "CREATE_BID", "bid_tracker", bid_id,
|
log_action(user["id"], "CREATE_BID", "bid_tracker", bid_id,
|
||||||
f"Created bid '{title}' status='{status}'.")
|
f"Created bid '{title}' status='{status}'.")
|
||||||
|
invalidate_admin_stats_cache()
|
||||||
flash(f"Bid '{title}' added.", "success")
|
flash(f"Bid '{title}' added.", "success")
|
||||||
logger.info(f"Bid '{title}' created by user_id={user['id']}.")
|
logger.info(f"Bid '{title}' created by user_id={user['id']}.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -103,6 +105,7 @@ def edit(bid_id):
|
|||||||
update_bid(user["id"], bid_id, title, url, source, sol_no, status, due_date, notes)
|
update_bid(user["id"], bid_id, title, url, source, sol_no, status, due_date, notes)
|
||||||
log_action(user["id"], "UPDATE_BID", "bid_tracker", bid_id,
|
log_action(user["id"], "UPDATE_BID", "bid_tracker", bid_id,
|
||||||
f"Updated bid id={bid_id} status='{status}'.")
|
f"Updated bid id={bid_id} status='{status}'.")
|
||||||
|
invalidate_admin_stats_cache()
|
||||||
flash(f"Bid '{title}' updated.", "success")
|
flash(f"Bid '{title}' updated.", "success")
|
||||||
logger.info(f"Bid id={bid_id} updated by user_id={user['id']}.")
|
logger.info(f"Bid id={bid_id} updated by user_id={user['id']}.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -120,6 +123,7 @@ def delete(bid_id):
|
|||||||
delete_bid(user["id"], bid_id)
|
delete_bid(user["id"], bid_id)
|
||||||
log_action(user["id"], "DELETE_BID", "bid_tracker", bid_id,
|
log_action(user["id"], "DELETE_BID", "bid_tracker", bid_id,
|
||||||
f"Deleted bid id={bid_id}.")
|
f"Deleted bid id={bid_id}.")
|
||||||
|
invalidate_admin_stats_cache()
|
||||||
flash("Bid deleted.", "success")
|
flash("Bid deleted.", "success")
|
||||||
logger.info(f"Bid id={bid_id} deleted by user_id={user['id']}.")
|
logger.info(f"Bid id={bid_id} deleted by user_id={user['id']}.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+8
-32
@@ -183,6 +183,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const SESSION_MS = 30 * 60 * 1000;
|
const SESSION_MS = 30 * 60 * 1000;
|
||||||
|
|
||||||
var warningTimer = null, expireTimer = null, countdownInterval = null;
|
var warningTimer = null, expireTimer = null, countdownInterval = null;
|
||||||
|
var _lastPing = 0;
|
||||||
|
|
||||||
function isWarningShowing() {
|
function isWarningShowing() {
|
||||||
var m = document.getElementById('modal-session-warning');
|
var m = document.getElementById('modal-session-warning');
|
||||||
@@ -208,8 +209,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetTimers() {
|
function resetTimers() {
|
||||||
|
var now = Date.now();
|
||||||
clearTimeout(warningTimer); clearTimeout(expireTimer); clearCountdown();
|
clearTimeout(warningTimer); clearTimeout(expireTimer); clearCountdown();
|
||||||
if (isWarningShowing()) closeModal('modal-session-warning');
|
if (isWarningShowing()) closeModal('modal-session-warning');
|
||||||
|
// Ping the server at most once per minute so the server-side session
|
||||||
|
// stays alive while the user is active on the page.
|
||||||
|
if (now - _lastPing > 60000) {
|
||||||
|
_lastPing = now;
|
||||||
|
fetch('/ping', { credentials: 'same-origin' }).catch(function(){});
|
||||||
|
}
|
||||||
|
|
||||||
warningTimer = setTimeout(function() {
|
warningTimer = setTimeout(function() {
|
||||||
openModal('modal-session-warning');
|
openModal('modal-session-warning');
|
||||||
@@ -311,38 +319,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ── Password strength meter ───────────────────────────────── */
|
|
||||||
function _pwStrengthLevel(pw) {
|
|
||||||
var score = 0;
|
|
||||||
if (pw.length >= 8) score++;
|
|
||||||
if (pw.length >= 12) score++;
|
|
||||||
if (/[A-Z]/.test(pw)) score++;
|
|
||||||
if (/[0-9]/.test(pw)) score++;
|
|
||||||
if (/[^A-Za-z0-9]/.test(pw)) score++;
|
|
||||||
var map = ['pw-weak','pw-weak','pw-fair','pw-strong','pw-great','pw-great'];
|
|
||||||
var lbl = ['Weak','Weak','Fair','Strong','Very strong','Very strong'];
|
|
||||||
return { cls: map[score], label: lbl[score] };
|
|
||||||
}
|
|
||||||
|
|
||||||
function attachPasswordStrength(inputId, fillId, labelId) {
|
|
||||||
var input = document.getElementById(inputId);
|
|
||||||
var fill = document.getElementById(fillId);
|
|
||||||
var lbl = document.getElementById(labelId);
|
|
||||||
if (!input || !fill || !lbl) return;
|
|
||||||
input.addEventListener('input', function() {
|
|
||||||
if (!this.value) {
|
|
||||||
fill.className = 'pw-strength-fill';
|
|
||||||
lbl.className = 'pw-strength-label';
|
|
||||||
lbl.textContent = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var r = _pwStrengthLevel(this.value);
|
|
||||||
fill.className = 'pw-strength-fill ' + r.cls;
|
|
||||||
lbl.className = 'pw-strength-label ' + r.cls;
|
|
||||||
lbl.textContent = r.label;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── HTML escape (safe for attributes and text nodes) ──────── */
|
/* ── HTML escape (safe for attributes and text nodes) ──────── */
|
||||||
function esc(str) {
|
function esc(str) {
|
||||||
return String(str || '').replace(/[&<>"']/g, function(c) {
|
return String(str || '').replace(/[&<>"']/g, function(c) {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/* ── Password strength meter (shared by base.html and standalone pages) ── */
|
||||||
|
function _pwStrengthLevel(pw) {
|
||||||
|
var score = 0;
|
||||||
|
if (pw.length >= 8) score++;
|
||||||
|
if (pw.length >= 12) score++;
|
||||||
|
if (/[A-Z]/.test(pw)) score++;
|
||||||
|
if (/[0-9]/.test(pw)) score++;
|
||||||
|
if (/[^A-Za-z0-9]/.test(pw)) score++;
|
||||||
|
var map = ['pw-weak','pw-weak','pw-fair','pw-strong','pw-great','pw-great'];
|
||||||
|
var lbl = ['Weak','Weak','Fair','Strong','Very strong','Very strong'];
|
||||||
|
return { cls: map[score], label: lbl[score] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachPasswordStrength(inputId, fillId, labelId) {
|
||||||
|
var input = document.getElementById(inputId);
|
||||||
|
var fill = document.getElementById(fillId);
|
||||||
|
var lbl = document.getElementById(labelId);
|
||||||
|
if (!input || !fill || !lbl) return;
|
||||||
|
input.addEventListener('input', function() {
|
||||||
|
if (!this.value) {
|
||||||
|
fill.className = 'pw-strength-fill';
|
||||||
|
lbl.className = 'pw-strength-label';
|
||||||
|
lbl.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var r = _pwStrengthLevel(this.value);
|
||||||
|
fill.className = 'pw-strength-fill ' + r.cls;
|
||||||
|
lbl.className = 'pw-strength-label ' + r.cls;
|
||||||
|
lbl.textContent = r.label;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -31,8 +31,8 @@
|
|||||||
<td><span class="badge badge-{{ 'success' if u.is_active else 'danger' }}">{{ 'Yes' if u.is_active else 'No' }}</span></td>
|
<td><span class="badge badge-{{ 'success' if u.is_active else 'danger' }}">{{ 'Yes' if u.is_active else 'No' }}</span></td>
|
||||||
<td class="text-muted">{{ u.created_at.strftime('%Y-%m-%d') if u.created_at else '—' }}</td>
|
<td class="text-muted">{{ u.created_at.strftime('%Y-%m-%d') if u.created_at else '—' }}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<button class="btn btn-ghost btn-sm"
|
<button class="btn btn-ghost btn-sm js-edit-user"
|
||||||
onclick='openEditUser({{ u|tojson }})'>✎ Edit</button>
|
data-user="{{ u|tojson|e }}">✎ Edit</button>
|
||||||
{% if u.email %}
|
{% if u.email %}
|
||||||
<button class="btn btn-ghost btn-sm js-send-reset"
|
<button class="btn btn-ghost btn-sm js-send-reset"
|
||||||
data-id="{{ u.id }}" data-username="{{ u.username }}" data-email="{{ u.email }}"
|
data-id="{{ u.id }}" data-username="{{ u.username }}" data-email="{{ u.email }}"
|
||||||
@@ -170,6 +170,13 @@ document.querySelectorAll('.js-delete-user').forEach(function(btn) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Edit user — data-user attribute avoids single-quote apostrophe breakage */
|
||||||
|
document.querySelectorAll('.js-edit-user').forEach(function(btn) {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
openEditUser(JSON.parse(btn.dataset.user));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function openEditUser(u) {
|
function openEditUser(u) {
|
||||||
document.getElementById('edit-username').value = u.username;
|
document.getElementById('edit-username').value = u.username;
|
||||||
document.getElementById('edit-full-name').value = u.full_name || '';
|
document.getElementById('edit-full-name').value = u.full_name || '';
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/pw-strength.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -46,23 +46,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script src="{{ url_for('static', filename='js/pw-strength.js') }}"></script>
|
||||||
(function() {
|
<script>attachPasswordStrength('password', 'pw-fill', 'pw-label');</script>
|
||||||
var input = document.getElementById('password');
|
|
||||||
var fill = document.getElementById('pw-fill');
|
|
||||||
var lbl = document.getElementById('pw-label');
|
|
||||||
var map = ['pw-weak','pw-weak','pw-fair','pw-strong','pw-great','pw-great'];
|
|
||||||
var lbls = ['Weak','Weak','Fair','Strong','Very strong','Very strong'];
|
|
||||||
input.addEventListener('input', function() {
|
|
||||||
if (!this.value) { fill.className='pw-strength-fill'; lbl.className='pw-strength-label'; lbl.textContent=''; return; }
|
|
||||||
var pw=this.value, s=0;
|
|
||||||
if (pw.length>=8) s++; if (pw.length>=12) s++;
|
|
||||||
if (/[A-Z]/.test(pw)) s++; if (/[0-9]/.test(pw)) s++; if (/[^A-Za-z0-9]/.test(pw)) s++;
|
|
||||||
fill.className='pw-strength-fill '+map[s];
|
|
||||||
lbl.className='pw-strength-label '+map[s];
|
|
||||||
lbl.textContent=lbls[s];
|
|
||||||
});
|
|
||||||
}());
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -293,13 +293,22 @@ document.getElementById('site-list').addEventListener('click', function(e) {
|
|||||||
method: 'POST', credentials: 'same-origin',
|
method: 'POST', credentials: 'same-origin',
|
||||||
headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': getCsrfToken()},
|
headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': getCsrfToken()},
|
||||||
body: 'user_note='
|
body: 'user_note='
|
||||||
}).then(function() {
|
}).then(function(r) {
|
||||||
|
if (!r.ok) throw new Error('Server error ' + r.status);
|
||||||
var toast = document.createElement('div');
|
var toast = document.createElement('div');
|
||||||
toast.className = 'alert alert-success';
|
toast.className = 'alert alert-success';
|
||||||
toast.style.cssText = 'position:fixed;top:1rem;right:1rem;z-index:9999;min-width:220px;box-shadow:var(--shadow)';
|
toast.style.cssText = 'position:fixed;top:1rem;right:1rem;z-index:9999;min-width:220px;box-shadow:var(--shadow)';
|
||||||
toast.textContent = '✔ ' + siteName + ' marked checked';
|
toast.textContent = '✔ ' + siteName + ' marked checked';
|
||||||
document.body.appendChild(toast);
|
document.body.appendChild(toast);
|
||||||
setTimeout(function() { location.reload(); }, 700);
|
setTimeout(function() { location.reload(); }, 700);
|
||||||
|
}).catch(function(err) {
|
||||||
|
btn.disabled = false;
|
||||||
|
var toast = document.createElement('div');
|
||||||
|
toast.className = 'alert alert-danger';
|
||||||
|
toast.style.cssText = 'position:fixed;top:1rem;right:1rem;z-index:9999;min-width:220px;box-shadow:var(--shadow)';
|
||||||
|
toast.textContent = '✖ Failed to mark checked. Please try again.';
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
setTimeout(function() { toast.remove(); }, 4000);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -353,13 +362,22 @@ document.getElementById('site-list').addEventListener('click', function(e) {
|
|||||||
method: 'POST', credentials: 'same-origin',
|
method: 'POST', credentials: 'same-origin',
|
||||||
headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': getCsrfToken()},
|
headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': getCsrfToken()},
|
||||||
body: ''
|
body: ''
|
||||||
}).then(function() {
|
}).then(function(r) {
|
||||||
|
if (!r.ok) throw new Error('Server error ' + r.status);
|
||||||
var toast = document.createElement('div');
|
var toast = document.createElement('div');
|
||||||
toast.className = 'alert alert-info';
|
toast.className = 'alert alert-info';
|
||||||
toast.style.cssText = 'position:fixed;top:1rem;right:1rem;z-index:9999;min-width:220px;box-shadow:var(--shadow)';
|
toast.style.cssText = 'position:fixed;top:1rem;right:1rem;z-index:9999;min-width:220px;box-shadow:var(--shadow)';
|
||||||
toast.textContent = '↩ Check removed for ' + siteName;
|
toast.textContent = '↩ Check removed for ' + siteName;
|
||||||
document.body.appendChild(toast);
|
document.body.appendChild(toast);
|
||||||
setTimeout(function() { location.reload(); }, 700);
|
setTimeout(function() { location.reload(); }, 700);
|
||||||
|
}).catch(function(err) {
|
||||||
|
btn.disabled = false;
|
||||||
|
var toast = document.createElement('div');
|
||||||
|
toast.className = 'alert alert-danger';
|
||||||
|
toast.style.cssText = 'position:fixed;top:1rem;right:1rem;z-index:9999;min-width:220px;box-shadow:var(--shadow)';
|
||||||
|
toast.textContent = '✖ Failed to remove check. Please try again.';
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
setTimeout(function() { toast.remove(); }, 4000);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user