ixg_platform/templates/ops/document_list.html
ixgadmin cdefbd010c Serve document and certificate files through a permission-checked download
Media was unreachable from either portal, which is why the certificate button on
the buyer compliance page returned page-not-found and no document row had
anything to click. The cause was structural: the portal and ops hosts proxy
location / to the app with a path prefix, so a stored /media/... URL arrives as
/portal/media/... and matches no route.

Add a download view per portal behind the same role gate as the page that links
to it — buyer-visible documents and published certificates for buyers, anything
the ops list shows for operations — resolving the stored path safely under
MEDIA_ROOT so a traversal or an absolute path is refused. Point the seven
templates that list files at the new routes.

Also add a portal-wide render sweep: nothing in the suite was fetching the ops
or portal pages, so a view could lose its template_name and 500 in front of a
user with every test still green.
2026-09-17 12:44:52 +00:00

57 lines
2.6 KiB
HTML

{% extends 'ops/base_ops.html' %}
{% block title %}Documents - IXG Operations{% endblock %}
{% block content %}
<div class=page-header d-flex justify-content-between align-items-center>
<h2>Documents</h2>
<span class=text-secondary>{{ page_obj.paginator.count }} total</span>
</div>
<div class=filter-bar>
<form method=get class=row g-2 align-items-end>
<div class=col-auto>
<select name=doc_type class=form-select form-select-sm>
<option value=>All Types</option>
{% for val, label in doc_type_choices %}
<option value={{ val }} {% if current_doc_type == val %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class=col-auto>
<button type=submit class=btn btn-sm btn-primary>Filter</button>
<a href={% url 'ops:document_list' %} class=btn btn-sm btn-outline-secondary>Clear</a>
</div>
</form>
</div>
<div class=stat-card p-0>
<table class=table table-hover mb-0>
<thead>
<tr><th>File Name</th><th>Type</th><th>Batch</th><th>Visibility</th><th>Uploaded By</th><th>Uploaded</th></tr>
</thead>
<tbody>
{% for d in documents %}
<tr>
<td><a href="{% url 'ops:document_download' d.id %}" target=_blank class=text-decoration-none><i class=bi bi-file-earmark></i> {{ d.file_name }}</a></td>
<td><span class=badge bg-light text-dark>{{ d.get_doc_type_display }}</span></td>
<td>{{ d.batch.batch_reference|default:'-' }}</td>
<td><span class=badge bg-info text-dark>{{ d.get_visibility_display }}</span></td>
<td>{% if d.uploaded_by %}{{ d.uploaded_by.full_name|default:d.uploaded_by.email }}{% else %}-{% endif %}</td>
<td>{{ d.uploaded_at|date:'d/m/Y' }}</td>
</tr>
{% empty %}
<tr><td colspan=6 class=text-muted text-center py-3>No documents</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<nav class=mt-3><ul class=pagination pagination-sm justify-content-center>
{% if page_obj.has_previous %}<li class=page-item><a class=page-link href=?page=1>&laquo;</a></li><li class=page-item><a class=page-link href=?page={{ page_obj.previous_page_number }}>{{ page_obj.previous_page_number }}</a></li>{% endif %}
<li class=page-item active><span class=page-link>{{ page_obj.number }}</span></li>
{% if page_obj.has_next %}<li class=page-item><a class=page-link href=?page={{ page_obj.next_page_number }}>{{ page_obj.next_page_number }}</a></li><li class=page-item><a class=page-link href=?page={{ page_obj.paginator.num_pages }}>&raquo;</a></li>{% endif %}
</ul></nav>
{% endif %}
{% endblock %}