v20260108-28-admin-all-mail-open-fix #63
@ -1 +1 @@
|
|||||||
v20260108-27-admin-all-mail-audit-page
|
v20260108-28-admin-all-mail-open-fix
|
||||||
|
|||||||
@ -0,0 +1,135 @@
|
|||||||
|
from .routes_shared import * # noqa: F401,F403
|
||||||
|
from .routes_shared import _format_datetime
|
||||||
|
|
||||||
|
|
||||||
|
@main_bp.route("/admin/all-mail")
|
||||||
|
@login_required
|
||||||
|
@roles_required("admin")
|
||||||
|
def admin_all_mail_page():
|
||||||
|
# Pagination
|
||||||
|
try:
|
||||||
|
page = int(request.args.get("page", "1"))
|
||||||
|
except ValueError:
|
||||||
|
page = 1
|
||||||
|
if page < 1:
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
per_page = 50
|
||||||
|
|
||||||
|
# Filters (AND combined)
|
||||||
|
from_q = (request.args.get("from_q") or "").strip()
|
||||||
|
subject_q = (request.args.get("subject_q") or "").strip()
|
||||||
|
backup_q = (request.args.get("backup_q") or "").strip()
|
||||||
|
type_q = (request.args.get("type_q") or "").strip()
|
||||||
|
job_name_q = (request.args.get("job_name_q") or "").strip()
|
||||||
|
received_from = (request.args.get("received_from") or "").strip()
|
||||||
|
received_to = (request.args.get("received_to") or "").strip()
|
||||||
|
only_unlinked = (request.args.get("only_unlinked") or "").strip().lower() in (
|
||||||
|
"1",
|
||||||
|
"true",
|
||||||
|
"yes",
|
||||||
|
"on",
|
||||||
|
)
|
||||||
|
|
||||||
|
query = db.session.query(MailMessage).outerjoin(Job, MailMessage.job_id == Job.id)
|
||||||
|
|
||||||
|
if from_q:
|
||||||
|
query = query.filter(MailMessage.from_address.ilike(f"%{from_q}%"))
|
||||||
|
|
||||||
|
if subject_q:
|
||||||
|
query = query.filter(MailMessage.subject.ilike(f"%{subject_q}%"))
|
||||||
|
|
||||||
|
if backup_q:
|
||||||
|
query = query.filter(MailMessage.backup_software.ilike(f"%{backup_q}%"))
|
||||||
|
|
||||||
|
if type_q:
|
||||||
|
query = query.filter(MailMessage.backup_type.ilike(f"%{type_q}%"))
|
||||||
|
|
||||||
|
if job_name_q:
|
||||||
|
# Prefer stored job_name, but also allow matching the linked Job name.
|
||||||
|
query = query.filter(
|
||||||
|
or_(
|
||||||
|
MailMessage.job_name.ilike(f"%{job_name_q}%"),
|
||||||
|
Job.name.ilike(f"%{job_name_q}%"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if only_unlinked:
|
||||||
|
query = query.filter(MailMessage.job_id.is_(None))
|
||||||
|
|
||||||
|
# Datetime window (received_at)
|
||||||
|
# Use dateutil.parser when available, otherwise a simple ISO parse fallback.
|
||||||
|
def _parse_dt(value: str):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
from dateutil import parser as dtparser # type: ignore
|
||||||
|
|
||||||
|
return dtparser.parse(value)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
# Accept "YYYY-MM-DDTHH:MM" from datetime-local.
|
||||||
|
return datetime.fromisoformat(value)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
dt_from = _parse_dt(received_from)
|
||||||
|
dt_to = _parse_dt(received_to)
|
||||||
|
|
||||||
|
if dt_from is not None:
|
||||||
|
query = query.filter(MailMessage.received_at >= dt_from)
|
||||||
|
if dt_to is not None:
|
||||||
|
query = query.filter(MailMessage.received_at <= dt_to)
|
||||||
|
|
||||||
|
total_items = query.count()
|
||||||
|
total_pages = max(1, math.ceil(total_items / per_page)) if total_items else 1
|
||||||
|
if page > total_pages:
|
||||||
|
page = total_pages
|
||||||
|
|
||||||
|
messages = (
|
||||||
|
query.order_by(
|
||||||
|
MailMessage.received_at.desc().nullslast(),
|
||||||
|
MailMessage.id.desc(),
|
||||||
|
)
|
||||||
|
.offset((page - 1) * per_page)
|
||||||
|
.limit(per_page)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for msg in messages:
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"id": msg.id,
|
||||||
|
"from_address": msg.from_address or "",
|
||||||
|
"subject": msg.subject or "",
|
||||||
|
"received_at": _format_datetime(msg.received_at),
|
||||||
|
"backup_software": msg.backup_software or "",
|
||||||
|
"backup_type": msg.backup_type or "",
|
||||||
|
"job_name": (msg.job_name or "") or (msg.job.name if msg.job else ""),
|
||||||
|
"linked": bool(msg.job_id),
|
||||||
|
"has_eml": bool(getattr(msg, "eml_stored_at", None)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
has_prev = page > 1
|
||||||
|
has_next = page < total_pages
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"main/admin_all_mail.html",
|
||||||
|
rows=rows,
|
||||||
|
page=page,
|
||||||
|
total_pages=total_pages,
|
||||||
|
has_prev=has_prev,
|
||||||
|
has_next=has_next,
|
||||||
|
filters={
|
||||||
|
"from_q": from_q,
|
||||||
|
"subject_q": subject_q,
|
||||||
|
"backup_q": backup_q,
|
||||||
|
"type_q": type_q,
|
||||||
|
"job_name_q": job_name_q,
|
||||||
|
"received_from": received_from,
|
||||||
|
"received_to": received_to,
|
||||||
|
"only_unlinked": only_unlinked,
|
||||||
|
},
|
||||||
|
)
|
||||||
@ -219,10 +219,15 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
function initAdminAllMailPopup() {
|
||||||
var table = document.getElementById('mailAuditTable');
|
var table = document.getElementById('mailAuditTable');
|
||||||
var modalEl = document.getElementById('mailMessageModal');
|
var modalEl = document.getElementById('mailMessageModal');
|
||||||
if (!table || !modalEl) return;
|
if (!table || !modalEl) return;
|
||||||
|
|
||||||
|
// base.html loads Bootstrap JS after the page content. Initialize after DOMContentLoaded
|
||||||
|
// so bootstrap.Modal is guaranteed to be available.
|
||||||
|
if (typeof bootstrap === 'undefined' || !bootstrap.Modal) return;
|
||||||
|
|
||||||
var modal = new bootstrap.Modal(modalEl);
|
var modal = new bootstrap.Modal(modalEl);
|
||||||
|
|
||||||
function setText(id, value) {
|
function setText(id, value) {
|
||||||
@ -297,6 +302,9 @@
|
|||||||
if (!id) return;
|
if (!id) return;
|
||||||
openMessage(id);
|
openMessage(id);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initAdminAllMailPopup);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,12 @@
|
|||||||
- Reused the existing Inbox message detail modal behavior to open and inspect messages from the All Mail page.
|
- Reused the existing Inbox message detail modal behavior to open and inspect messages from the All Mail page.
|
||||||
- Added navigation entry for admins to access the All Mail page.
|
- Added navigation entry for admins to access the All Mail page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v20260108-28-admin-all-mail-open-fix
|
||||||
|
- Fixed All Mail row-click handling by switching to event delegation, ensuring message rows reliably open the detail modal.
|
||||||
|
- Ensured EML link clicks no longer trigger row-click modal opening.
|
||||||
|
|
||||||
|
|
||||||
================================================================================================================================================
|
================================================================================================================================================
|
||||||
## v0.1.18
|
## v0.1.18
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user