Files: prototype and documentation files on features, with versions. Adding, replacing or removing a file is a feature revision with a change note. Types come from the extension; uploads are capped at 25 MB and stored under opaque paths. Every file URL checks project access, then redirects to a short-lived signed URL on the bucket, or streams with nosniff and a CSP sandbox when no bucket is configured. HTML prototypes run in a sandboxed iframe without same-origin access. Threads: whole-file threads and pins at a point on an image, drawn as an SVG overlay in percentages. Pins are placed with number fields, or by clicking the image with JS. Implementations: name, link and status, plus a per-feature status grid stamped with the feature revision, marked behind when the feature changes, and confirmable without changing the status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
4.2 KiB
HTML
97 lines
4.2 KiB
HTML
{% extends "projects/base.html" %}
|
|
|
|
{% block page_title %}{{ impl.name }}{% endblock %}
|
|
|
|
{% block project_content %}
|
|
<h1>{{ impl.name }}{% if impl.status == "archived" %} <span class="muted">(archived)</span>{% endif %}</h1>
|
|
<p><a href="{{ impl.url }}" rel="noopener noreferrer">{{ impl.url }}</a></p>
|
|
{% if access.can_edit %}
|
|
<p><a href="{% url 'implementations:edit' org_slug=project.org.slug project_slug=project.slug impl_id=impl.pk %}">Edit implementation</a></p>
|
|
{% endif %}
|
|
|
|
{% if summary %}
|
|
<ul>
|
|
{% for label, count in summary %}<li>{{ label }}: {{ count }}</li>{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
|
|
<section aria-labelledby="features-heading">
|
|
<h2 id="features-heading">Features</h2>
|
|
<p class="muted">
|
|
A status applies to the feature revision it was set at. When the feature changes after that, the status is marked behind until someone updates or confirms it.
|
|
</p>
|
|
{% if rows %}
|
|
<form method="post" action="{% url 'implementations:statuses' org_slug=project.org.slug project_slug=project.slug impl_id=impl.pk %}">
|
|
{% csrf_token %}
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Feature</th>
|
|
<th scope="col">Status</th>
|
|
<th scope="col">As of revision</th>
|
|
{% if access.can_edit %}<th scope="col">Still true now</th>{% endif %}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for row in rows %}
|
|
{% with f=row.feature %}
|
|
<tr id="{{ f.key }}">
|
|
<th scope="row">
|
|
<a href="{% url 'features:detail' org_slug=project.org.slug project_slug=project.slug feature_key=f.key %}">{{ f.key }} {{ f.title }}</a>
|
|
{% if f.status != "active" %}<span class="muted">({{ f.get_status_display|lower }})</span>{% endif %}
|
|
</th>
|
|
<td>
|
|
{% if access.can_edit %}
|
|
<label for="status-{{ f.pk }}" class="visually-hidden">Status of {{ f.key }}</label>
|
|
<select id="status-{{ f.pk }}" name="status-{{ f.pk }}">
|
|
{% for value, label in choices %}
|
|
<option value="{{ value }}"{% if value == row.status %} selected{% endif %}>{{ label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
{% else %}
|
|
{{ row.label }}
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if row.revision %}
|
|
{{ row.revision }}{% if row.behind %}, <strong>behind</strong> (now {{ f.current_revision }}){% endif %}
|
|
{% else %}—{% endif %}
|
|
</td>
|
|
{% if access.can_edit %}
|
|
<td>
|
|
{% if row.behind %}
|
|
<input type="checkbox" id="confirm-{{ f.pk }}" name="confirm-{{ f.pk }}">
|
|
<label for="confirm-{{ f.pk }}">Still {{ row.label|lower }} at revision {{ f.current_revision }}</label>
|
|
{% endif %}
|
|
</td>
|
|
{% endif %}
|
|
</tr>
|
|
{% endwith %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% if access.can_edit %}<p><button type="submit">Save statuses</button></p>{% endif %}
|
|
</form>
|
|
{% else %}
|
|
<p>This project has no features yet.</p>
|
|
{% endif %}
|
|
</section>
|
|
|
|
{% if access.can_admin %}
|
|
<section id="delete" aria-labelledby="delete-heading">
|
|
<h2 id="delete-heading">Delete</h2>
|
|
<details>
|
|
<summary>Delete {{ impl.name }}</summary>
|
|
<form method="post" action="{% url 'implementations:delete' org_slug=project.org.slug project_slug=project.slug impl_id=impl.pk %}">
|
|
{% csrf_token %}
|
|
<p>This removes the implementation and every status recorded for it. To keep the history, archive it instead.</p>
|
|
<p>
|
|
<label for="confirm-delete">Type “{{ impl.name }}” to confirm</label>
|
|
<input type="text" id="confirm-delete" name="confirm" required>
|
|
</p>
|
|
<button type="submit">Delete implementation</button>
|
|
</form>
|
|
</details>
|
|
</section>
|
|
{% endif %}
|
|
{% endblock %}
|