Django Task — Project Detail Template
Build a Django HTML template for project detail — extends base.html, no inline styles or scripts, data-* attributes for JS.
Scenario
Continuing from Task 01, you will build the HTML templates for the Project List page and the Project Detail page. Every template must follow the 3-folder rule (CSS in static/css/, JS in static/js/, HTML in templates/), extend base.html, use zero inline styles or scripts, and pass any Django data to JavaScript only via data-* attributes.
What to Build
Three template files and their supporting static files:
| Template | Path |
|---|---|
| Project list page | templates/projects/list.html |
| Project create form page | templates/projects/form.html |
| Project detail page | templates/projects/detail.html |
Plus supporting static files:
static/css/pages/projects.cssstatic/js/pages/project-detail.js
Part 1 — templates/projects/list.html
Requirements
- Extends
base.html - Sets
{% block title %}Projects — WorkFlow{% endblock %} - Lists all projects passed in context as a grid of cards
- Each card shows: project name, status badge (color-coded), start date, number of tasks (if available)
- Cards link to the project detail page using
{% url 'projects:detail' workspace_id=workspace.id pk=project.id %} - An "Add Project" button links to the create page
- If no projects: show an empty state message ("No projects yet. Create your first project.")
- Status badges use CSS classes only — no inline
style="color: ..."orstyle="background: ..."
Status Badge Classes (defined in CSS, not inline)
/* static/css/components/badge.css */
.badge-active { background-color: var(--color-success); color: white; }
.badge-on-hold { background-color: var(--color-warning); color: black; }
.badge-archived { background-color: var(--color-neutral); color: white; }
Use {{ project.status|lower|slugify }} to generate the class name dynamically.
Part 2 — templates/projects/form.html
Requirements
- Extends
base.html - Title block: "Add Project — WorkFlow" (or "Edit Project" in edit mode)
- Renders the
ProjectFormpassed in context - Shows field-level errors below each field using
{{ form.field_name.errors }} - Shows non-field errors (from
form.non_field_errors()) as a banner above the form - Required fields show an asterisk (
*) next to their label - Submit button label: "Create Project"
- "Cancel" link goes back to the project list
- CSRF token included:
{% csrf_token %} - No
<style>blocks — all styles instatic/css/pages/projects.css - No
<script>blocks — JS instatic/js/pages/project-form.jsif needed
Form Field Layout
<div class="form-field {% if form.name.errors %}has-error{% endif %}">
<label for="{{ form.name.id_for_label }}">
Project Name <span class="required-star">*</span>
</label>
{{ form.name }}
{% if form.name.errors %}
<p class="field-error">{{ form.name.errors.0 }}</p>
{% endif %}
</div>
Part 3 — templates/projects/detail.html
Requirements
- Extends
base.html - Title block:
{{ project.name }} — WorkFlow - Shows all project fields: name, description, status, dates, created by, created at
- An "Edit Project" button — shown only to the project creator and workspace admins
- A "Archive Project" button — only for admins, triggers a JavaScript confirmation modal before submitting
- Project task list (if tasks are passed in context)
JavaScript Interaction — Archive Confirmation
The Archive button must trigger a confirmation dialog before the form is submitted. The project name must be passed to the JavaScript — via a data-* attribute, NOT via string interpolation in a <script> block.
Correct pattern:
<!-- In the template -->
<div
id="project-actions"
data-project-name="{{ project.name|escapejs }}"
data-archive-url="{% url 'projects:archive' workspace_id=workspace.id pk=project.id %}"
>
<button id="archive-btn" class="btn btn-danger">Archive Project</button>
</div>
// static/js/pages/project-detail.js
const actions = document.getElementById('project-actions');
const projectName = actions.dataset.projectName;
const archiveUrl = actions.dataset.archiveUrl;
document.getElementById('archive-btn').addEventListener('click', () => {
const confirmed = confirm(`Archive "${projectName}"? This cannot be undone.`);
if (confirmed) {
// Submit archive form or redirect
window.location.href = archiveUrl;
}
});
Static Files to Create
static/css/pages/projects.css
Must define styles for:
- Project card grid layout (responsive — 1 column mobile, 2 tablet, 3 desktop)
- Status badges (use CSS variables from
base.css, not hardcoded colors) - Empty state styling
- Form field layout (
form-field,has-error,field-error,required-star) - Detail page layout
Must use CSS variables from base.css — no hardcoded colors or spacing values.
static/js/pages/project-detail.js
Must contain:
- Archive confirmation logic (reads
data-project-namefrom the DOM) - No global variables
- No inline event handlers in the HTML
- All DOM lookups after
DOMContentLoaded
What You Should NOT Do
- Do not use
<style>blocks anywhere in templates - Do not use
<script>blocks with logic in templates - Do not use
onclick="..."or any inline event handlers - Do not interpolate Django data into
<script>tags: NOT<script>var name = "{{ project.name }}";</script> - Do not hardcode colors in CSS — use CSS variables
- Do not duplicate the navbar or footer — use
{% include "components/navbar.html" %} - Do not use
/static/hardcoded paths — always use{% static 'path' %}
Checklist to Run When Done
Use the Django Template Checklist.
Done When
FOLDER STRUCTURE
[ ] Templates in templates/projects/
[ ] CSS in static/css/pages/projects.css
[ ] JS in static/js/pages/project-detail.js
TEMPLATE INHERITANCE
[ ] All templates start with {% extends "base.html" %}
[ ] {% load static %} at top of each template
[ ] Each template sets {% block title %}
[ ] No duplicate <html>, <head>, or <body> tags
CSS
[ ] Zero <style> blocks in any template
[ ] All styles in static/css/ files
[ ] CSS uses variables from base.css (no hardcoded colors)
[ ] Status badges styled with CSS classes (not inline style attributes)
[ ] Grid layout is responsive (1 col mobile, 2 tablet, 3 desktop)
JAVASCRIPT
[ ] Zero <script> blocks with logic in any template
[ ] No inline event handlers (onclick, etc.)
[ ] Archive button reads project name from data-project-name attribute
[ ] Confirmation fires before any destructive action
STATIC FILES
[ ] {% load static %} before any {% static %} usage
[ ] All static file paths use {% static 'path' %}
[ ] No hardcoded /static/ paths
FORMS
[ ] {% csrf_token %} inside every <form>
[ ] Field errors displayed below each field
[ ] Non-field errors shown as banner
[ ] Required fields have asterisk indicator
MOBILE
[ ] List page tested: cards stack on mobile
[ ] Form page tested: inputs usable on mobile, no overflow
[ ] Detail page tested: layout readable on mobile