From 6c53bb9ce60932038e67a1722b7d93acf2acd958 Mon Sep 17 00:00:00 2001 From: Andrew Norman Date: Thu, 25 Jun 2026 12:23:18 -0500 Subject: [PATCH] Add Entry Type taxonomy to entries and reports Add a new extensible "Entry Type" taxonomy table (Weekly Report, Milestone, Activity seeded) so submissions can be classified by type. Entry Type is optional on WorkItem (like Group), so existing entries need no backfill. - taxonomy: new EntryType(TaxonomyBase) + migration, admin registration, management views/URLs/templates, "Entry Types" nav tab on all taxonomy pages, JSON export/import, and seed_taxonomy entries. - entries: nullable entry_type FK + index, form dropdown, detail display, admin list/filter. - reports: Entry Type filter on the report page (filter-only, matching the Lab Priority pattern). - audit: track entry_type_id changes. - docs: API page lists the new /taxonomy/entry-types/ routes. - tests: form save + optional, management page access + create, seed count, reports filter (204 passing). Closes fermitools/SCD-Reporting#16 Co-Authored-By: Claude Opus 4.8 --- apps/audit/signals.py | 2 +- apps/core/templates/core/api.html | 4 + apps/entries/admin.py | 4 +- apps/entries/forms.py | 6 +- .../0008_workitem_entry_type_and_more.py | 35 +++++ apps/entries/models.py | 7 + apps/entries/templates/entries/detail.html | 6 + apps/entries/templates/entries/form.html | 20 ++- apps/reports/filters.py | 7 +- apps/reports/templates/reports/index.html | 15 ++ apps/taxonomy/admin.py | 10 +- apps/taxonomy/forms.py | 11 +- .../management/commands/seed_taxonomy.py | 16 ++- apps/taxonomy/migrations/0004_entrytype.py | 41 ++++++ apps/taxonomy/models.py | 6 + .../templates/taxonomy/categories.html | 2 + .../templates/taxonomy/entry_type_form.html | 29 ++++ .../templates/taxonomy/entry_types.html | 129 ++++++++++++++++++ apps/taxonomy/templates/taxonomy/groups.html | 2 + .../templates/taxonomy/lab_priorities.html | 2 + .../taxonomy/templates/taxonomy/projects.html | 2 + apps/taxonomy/urls.py | 2 + apps/taxonomy/views.py | 38 +++++- tests/test_entries.py | 34 +++++ tests/test_reports.py | 15 +- tests/test_taxonomy.py | 25 +++- 26 files changed, 456 insertions(+), 14 deletions(-) create mode 100644 apps/entries/migrations/0008_workitem_entry_type_and_more.py create mode 100644 apps/taxonomy/migrations/0004_entrytype.py create mode 100644 apps/taxonomy/templates/taxonomy/entry_type_form.html create mode 100644 apps/taxonomy/templates/taxonomy/entry_types.html diff --git a/apps/audit/signals.py b/apps/audit/signals.py index 601696c..e3434a3 100644 --- a/apps/audit/signals.py +++ b/apps/audit/signals.py @@ -8,7 +8,7 @@ from .service import log_event _TRACKED_FIELDS = ( - 'title', 'project_id', 'category_id', 'group_id', + 'title', 'project_id', 'category_id', 'entry_type_id', 'group_id', 'period_kind', 'period_start', 'period_end', 'description', 'is_private', 'is_critical', 'is_highlight', 'highlight_stars', 'is_division_head_only', 'author_id', 'is_archived', diff --git a/apps/core/templates/core/api.html b/apps/core/templates/core/api.html index 55c021c..1c7f1b8 100644 --- a/apps/core/templates/core/api.html +++ b/apps/core/templates/core/api.html @@ -244,6 +244,10 @@

Taxonomy

{% include "core/partials/_route_row.html" with method="POST" path="/taxonomy/categories/" badge="manager" desc="Create a new category" %} {% include "core/partials/_route_row.html" with method="GET" path="/taxonomy/categories/<id>/edit/" badge="manager" desc="Edit category form" %} {% include "core/partials/_route_row.html" with method="POST" path="/taxonomy/categories/<id>/edit/" badge="manager" desc="Save category changes" %} + {% include "core/partials/_route_row.html" with method="GET" path="/taxonomy/entry-types/" badge="manager" desc="Manage entry types — list and add" %} + {% include "core/partials/_route_row.html" with method="POST" path="/taxonomy/entry-types/" badge="manager" desc="Create a new entry type" %} + {% include "core/partials/_route_row.html" with method="GET" path="/taxonomy/entry-types/<id>/edit/" badge="manager" desc="Edit entry type form" %} + {% include "core/partials/_route_row.html" with method="POST" path="/taxonomy/entry-types/<id>/edit/" badge="manager" desc="Save entry type changes" %} {% include "core/partials/_route_row.html" with method="GET" path="/taxonomy/groups/" badge="manager" desc="Manage work groups — list and add" %} {% include "core/partials/_route_row.html" with method="POST" path="/taxonomy/groups/" badge="manager" desc="Create a new work group" %} {% include "core/partials/_route_row.html" with method="GET" path="/taxonomy/groups/<id>/edit/" badge="manager" desc="Edit work group form" %} diff --git a/apps/entries/admin.py b/apps/entries/admin.py index 7009eb9..480fec7 100644 --- a/apps/entries/admin.py +++ b/apps/entries/admin.py @@ -5,9 +5,9 @@ @admin.register(WorkItem) class WorkItemAdmin(admin.ModelAdmin): - list_display = ('title', 'author', 'project', 'category', + list_display = ('title', 'author', 'project', 'category', 'entry_type', 'period_start', 'period_end', 'is_private', 'created_at') - list_filter = ('project', 'category', 'is_private') + list_filter = ('project', 'category', 'entry_type', 'is_private') search_fields = ('title', 'description', 'author__email', 'author__display_name') raw_id_fields = ('author',) filter_horizontal = ('tags',) diff --git a/apps/entries/forms.py b/apps/entries/forms.py index 5d8d006..9e563b8 100644 --- a/apps/entries/forms.py +++ b/apps/entries/forms.py @@ -1,7 +1,7 @@ from django import forms from django.db.models import F -from apps.taxonomy.models import Category, LabPriority, Project, Tag, WorkGroup +from apps.taxonomy.models import Category, EntryType, LabPriority, Project, Tag, WorkGroup from .models import WorkItem @@ -12,7 +12,7 @@ class WorkItemForm(forms.ModelForm): class Meta: model = WorkItem fields = [ - 'title', 'project', 'category', 'group', 'lab_priority', + 'title', 'project', 'category', 'entry_type', 'group', 'lab_priority', 'period_kind', 'period_start', 'period_end', 'description', 'is_private', 'is_critical', 'is_highlight', 'highlight_stars', 'is_division_head_only', @@ -29,6 +29,8 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['project'].queryset = Project.objects.filter(is_active=True).order_by('sort_order', 'name') self.fields['category'].queryset = Category.objects.filter(is_active=True).order_by('sort_order', 'name') + self.fields['entry_type'].queryset = EntryType.objects.filter(is_active=True).order_by('sort_order', 'name') + self.fields['entry_type'].required = False self.fields['group'].queryset = WorkGroup.objects.filter(is_active=True).order_by('sort_order', 'name') self.fields['group'].required = False self.fields['lab_priority'].queryset = LabPriority.objects.filter(is_active=True).order_by('sort_order', 'name') diff --git a/apps/entries/migrations/0008_workitem_entry_type_and_more.py b/apps/entries/migrations/0008_workitem_entry_type_and_more.py new file mode 100644 index 0000000..e5c34f7 --- /dev/null +++ b/apps/entries/migrations/0008_workitem_entry_type_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.14 on 2026-06-25 17:07 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("entries", "0007_add_entry_template"), + ("taxonomy", "0004_entrytype"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name="workitem", + name="entry_type", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="work_items", + to="taxonomy.entrytype", + ), + ), + migrations.AddIndex( + model_name="workitem", + index=models.Index( + fields=["entry_type", "-period_end"], + name="entries_wor_entry_t_d54d2e_idx", + ), + ), + ] diff --git a/apps/entries/models.py b/apps/entries/models.py index af5ccbb..fba1411 100644 --- a/apps/entries/models.py +++ b/apps/entries/models.py @@ -27,6 +27,12 @@ class PeriodKind(models.TextChoices): on_delete=models.PROTECT, related_name='work_items', ) + entry_type = models.ForeignKey( + 'taxonomy.EntryType', + null=True, blank=True, + on_delete=models.PROTECT, + related_name='work_items', + ) group = models.ForeignKey( 'taxonomy.WorkGroup', null=True, blank=True, @@ -66,6 +72,7 @@ class Meta: models.Index(fields=['author', '-period_end']), models.Index(fields=['project', '-period_end']), models.Index(fields=['category', '-period_end']), + models.Index(fields=['entry_type', '-period_end']), models.Index(fields=['period_start', 'period_end']), models.Index(fields=['is_private', '-period_end']), ] diff --git a/apps/entries/templates/entries/detail.html b/apps/entries/templates/entries/detail.html index 6fdda3e..bcf2b1a 100644 --- a/apps/entries/templates/entries/detail.html +++ b/apps/entries/templates/entries/detail.html @@ -55,6 +55,12 @@

{{ object.title }}

Category
{{ object.category.name }}
+ {% if object.entry_type %} +
+
Entry Type
+
{{ object.entry_type.name }}
+
+ {% endif %} {% if object.group %}
Group
diff --git a/apps/entries/templates/entries/form.html b/apps/entries/templates/entries/form.html index 44e7d13..0aaf5a2 100644 --- a/apps/entries/templates/entries/form.html +++ b/apps/entries/templates/entries/form.html @@ -42,7 +42,7 @@

{% endfor %}

- {# ── Project / Category / Group / Lab Priority ────────────────────────── #} + {# ── Project / Category / Entry Type / Group / Lab Priority ───────────── #}
+
+ + + {% for error in form.entry_type.errors %} +

{{ error }}

+ {% endfor %} +
+ {# Entry Type #} +
+ + +
+ {# Lab Priority #}
diff --git a/apps/taxonomy/admin.py b/apps/taxonomy/admin.py index acc99e8..5c33b95 100644 --- a/apps/taxonomy/admin.py +++ b/apps/taxonomy/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Category, LabPriority, Project, Tag, WorkGroup +from .models import Category, EntryType, LabPriority, Project, Tag, WorkGroup @admin.register(Project) @@ -19,6 +19,14 @@ class CategoryAdmin(admin.ModelAdmin): search_fields = ('name', 'short_code') +@admin.register(EntryType) +class EntryTypeAdmin(admin.ModelAdmin): + list_display = ('name', 'short_code', 'slug', 'is_active', 'sort_order') + list_editable = ('is_active', 'sort_order') + prepopulated_fields = {'slug': ('name',)} + search_fields = ('name', 'short_code') + + @admin.register(Tag) class TagAdmin(admin.ModelAdmin): list_display = ('name', 'use_count') diff --git a/apps/taxonomy/forms.py b/apps/taxonomy/forms.py index ef9e656..e173b75 100644 --- a/apps/taxonomy/forms.py +++ b/apps/taxonomy/forms.py @@ -1,6 +1,6 @@ from django import forms -from .models import Category, LabPriority, Project, WorkGroup +from .models import Category, EntryType, LabPriority, Project, WorkGroup class ProjectForm(forms.ModelForm): @@ -21,6 +21,15 @@ class Meta: } +class EntryTypeForm(forms.ModelForm): + class Meta: + model = EntryType + fields = ['name', 'short_code', 'is_active', 'sort_order'] + widgets = { + 'sort_order': forms.NumberInput(attrs={'min': 0}), + } + + class WorkGroupForm(forms.ModelForm): class Meta: model = WorkGroup diff --git a/apps/taxonomy/management/commands/seed_taxonomy.py b/apps/taxonomy/management/commands/seed_taxonomy.py index 132dc03..2e1d6c4 100644 --- a/apps/taxonomy/management/commands/seed_taxonomy.py +++ b/apps/taxonomy/management/commands/seed_taxonomy.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand -from apps.taxonomy.models import Category, LabPriority, Project +from apps.taxonomy.models import Category, EntryType, LabPriority, Project PROJECTS = [ {'name': 'DUNE', 'short_code': 'DUNE', 'sort_order': 10}, @@ -17,6 +17,12 @@ {'name': 'Training', 'short_code': 'TRN', 'sort_order': 40}, ] +ENTRY_TYPES = [ + {'name': 'Weekly Report', 'short_code': 'WEEKLY', 'sort_order': 10}, + {'name': 'Milestone', 'short_code': 'MILESTONE', 'sort_order': 20}, + {'name': 'Activity', 'short_code': 'ACTIVITY', 'sort_order': 30}, +] + LAB_PRIORITIES = [ {'name': 'Science Mission', 'short_code': 'SCI-MISS', 'sort_order': 10}, {'name': 'DUNE/LBNF', 'short_code': 'DUNE', 'sort_order': 20}, @@ -46,6 +52,14 @@ def handle(self, *args, **options): status = 'created' if created else 'exists ' self.stdout.write(f' [{status}] {name}') + self.stdout.write('Seeding entry types…') + for data in ENTRY_TYPES: + name = data['name'] + defaults = {k: v for k, v in data.items() if k != 'name'} + _, created = EntryType.objects.get_or_create(name=name, defaults=defaults) + status = 'created' if created else 'exists ' + self.stdout.write(f' [{status}] {name}') + self.stdout.write('Seeding lab priorities…') for data in LAB_PRIORITIES: name = data['name'] diff --git a/apps/taxonomy/migrations/0004_entrytype.py b/apps/taxonomy/migrations/0004_entrytype.py new file mode 100644 index 0000000..436d050 --- /dev/null +++ b/apps/taxonomy/migrations/0004_entrytype.py @@ -0,0 +1,41 @@ +# Generated by Django 5.2.14 on 2026-06-25 17:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("taxonomy", "0003_labpriority"), + ] + + operations = [ + migrations.CreateModel( + name="EntryType", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, unique=True)), + ("slug", models.SlugField(blank=True, max_length=100, unique=True)), + ("short_code", models.CharField(blank=True, max_length=20)), + ("is_active", models.BooleanField(db_index=True, default=True)), + ( + "sort_order", + models.PositiveSmallIntegerField(db_index=True, default=0), + ), + ], + options={ + "verbose_name": "Entry Type", + "verbose_name_plural": "Entry Types", + "ordering": ["sort_order", "name"], + "abstract": False, + }, + ), + ] diff --git a/apps/taxonomy/models.py b/apps/taxonomy/models.py index 8cba0d2..600bdb7 100644 --- a/apps/taxonomy/models.py +++ b/apps/taxonomy/models.py @@ -32,6 +32,12 @@ class Meta(TaxonomyBase.Meta): verbose_name_plural = 'categories' +class EntryType(TaxonomyBase): + class Meta(TaxonomyBase.Meta): + verbose_name = 'Entry Type' + verbose_name_plural = 'Entry Types' + + class WorkGroup(TaxonomyBase): class Meta(TaxonomyBase.Meta): verbose_name = 'Group' diff --git a/apps/taxonomy/templates/taxonomy/categories.html b/apps/taxonomy/templates/taxonomy/categories.html index a0789c3..676500a 100644 --- a/apps/taxonomy/templates/taxonomy/categories.html +++ b/apps/taxonomy/templates/taxonomy/categories.html @@ -15,6 +15,8 @@

Taxonomy

class="px-4 py-2 text-sm font-medium text-slate-500 hover:text-slate-700 border-b-2 border-transparent -mb-px">Projects Categories + Entry Types Groups +
+ ← Back to entry types +

Edit entry type

+
+ +
+
+ {% csrf_token %} + {% for field in form %} + {% include "components/_form_field.html" %} + {% endfor %} +
+ + Cancel +
+
+
+
+{% endblock %} diff --git a/apps/taxonomy/templates/taxonomy/entry_types.html b/apps/taxonomy/templates/taxonomy/entry_types.html new file mode 100644 index 0000000..33c6c85 --- /dev/null +++ b/apps/taxonomy/templates/taxonomy/entry_types.html @@ -0,0 +1,129 @@ +{% extends "base.html" %} +{% block title %}Entry Types — SCD Reporting{% endblock %} + +{% block content %} +
+ +
+

Taxonomy

+

Manage the labels used to classify effort entries.

+
+ +
+ +
+ + ↓ Export JSON + + +
+
+ + {# Inline import form — hidden by default #} + + +
+ + + + + + + + + + + + {% for t in entry_types %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
#NameShort CodeStatusActions
{{ t.sort_order }}{{ t.name }}{{ t.short_code|default:"—" }} + {% if t.is_active %} + Active + {% else %} + Inactive + {% endif %} + + Edit +
+ No entry types yet. Add one below. +
+
+ +
+
+

Add entry type

+
+
+ {% csrf_token %} +
+ {% for field in form %} + {% include "components/_form_field.html" %} + {% endfor %} +
+ +
+
+ +
+ + +{% endblock %} diff --git a/apps/taxonomy/templates/taxonomy/groups.html b/apps/taxonomy/templates/taxonomy/groups.html index 76d9707..fd6962c 100644 --- a/apps/taxonomy/templates/taxonomy/groups.html +++ b/apps/taxonomy/templates/taxonomy/groups.html @@ -15,6 +15,8 @@

Taxonomy

class="px-4 py-2 text-sm font-medium text-slate-500 hover:text-slate-700 border-b-2 border-transparent -mb-px">Projects Categories + Entry Types Groups Taxonomy class="px-4 py-2 text-sm font-medium text-slate-500 hover:text-slate-700 border-b-2 border-transparent -mb-px">Projects Categories + Entry Types Groups Taxonomy class="px-4 py-2 text-sm font-medium text-scd-primary border-b-2 border-scd-primary -mb-px">Projects Categories + Entry Types Groups /edit/', views.ProjectEditView.as_view(), name='project-edit'), path('categories/', views.CategoryManageView.as_view(), name='categories'), path('categories//edit/', views.CategoryEditView.as_view(), name='category-edit'), + path('entry-types/', views.EntryTypeManageView.as_view(), name='entry-types'), + path('entry-types//edit/', views.EntryTypeEditView.as_view(), name='entry-type-edit'), path('groups/', views.WorkGroupManageView.as_view(), name='groups'), path('groups//edit/', views.WorkGroupEditView.as_view(), name='group-edit'), path('lab-priorities/', views.LabPriorityManageView.as_view(), name='lab-priorities'), diff --git a/apps/taxonomy/views.py b/apps/taxonomy/views.py index 9ae4735..6d4968b 100644 --- a/apps/taxonomy/views.py +++ b/apps/taxonomy/views.py @@ -11,8 +11,8 @@ from apps.accounts.permissions import TaxonomyEditorRequiredMixin as AdminRequiredMixin -from .forms import CategoryForm, LabPriorityForm, ProjectForm, WorkGroupForm -from .models import Category, LabPriority, Project, Tag, WorkGroup +from .forms import CategoryForm, EntryTypeForm, LabPriorityForm, ProjectForm, WorkGroupForm +from .models import Category, EntryType, LabPriority, Project, Tag, WorkGroup class ProjectManageView(AdminRequiredMixin, View): @@ -79,6 +79,38 @@ def form_valid(self, form): return super().form_valid(form) +class EntryTypeManageView(AdminRequiredMixin, View): + template_name = 'taxonomy/entry_types.html' + + def _ctx(self, form=None): + return { + 'entry_types': EntryType.objects.order_by('sort_order', 'name'), + 'form': form or EntryTypeForm(), + } + + def get(self, request): + return render(request, self.template_name, self._ctx()) + + def post(self, request): + form = EntryTypeForm(request.POST) + if form.is_valid(): + obj = form.save() + messages.success(request, f'Entry Type "{obj.name}" added.') + return redirect('taxonomy:entry-types') + return render(request, self.template_name, self._ctx(form)) + + +class EntryTypeEditView(AdminRequiredMixin, UpdateView): + model = EntryType + form_class = EntryTypeForm + template_name = 'taxonomy/entry_type_form.html' + success_url = reverse_lazy('taxonomy:entry-types') + + def form_valid(self, form): + messages.success(self.request, f'Entry Type "{form.instance.name}" saved.') + return super().form_valid(form) + + class WorkGroupManageView(AdminRequiredMixin, View): template_name = 'taxonomy/groups.html' @@ -161,6 +193,7 @@ def rows(qs): 'exported_at': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'), 'projects': rows(Project.objects.order_by('sort_order', 'name')), 'categories': rows(Category.objects.order_by('sort_order', 'name')), + 'entry_types': rows(EntryType.objects.order_by('sort_order', 'name')), 'groups': rows(WorkGroup.objects.order_by('sort_order', 'name')), 'lab_priorities': rows(LabPriority.objects.order_by('sort_order', 'name')), } @@ -177,6 +210,7 @@ class TaxonomyImportView(AdminRequiredMixin, View): TABLES = { 'projects': Project, 'categories': Category, + 'entry_types': EntryType, 'groups': WorkGroup, 'lab_priorities': LabPriority, } diff --git a/tests/test_entries.py b/tests/test_entries.py index c614dbb..b1deb5b 100644 --- a/tests/test_entries.py +++ b/tests/test_entries.py @@ -161,6 +161,40 @@ def test_sets_author_to_current_user(self, client, user, project, category): }) assert WorkItem.objects.get(title='Authored').author == user + def test_post_with_entry_type_sets_it(self, client, user, project, category): + from apps.taxonomy.models import EntryType + et = EntryType.objects.create(name='Weekly Report', slug='weekly-report') + client.force_login(user) + today = date.today() + client.post(reverse('entries:create'), { + 'title': 'Typed entry', + 'project': project.pk, + 'category': category.pk, + 'entry_type': et.pk, + 'period_kind': 'week', + 'period_start': (today - timedelta(days=today.weekday())).isoformat(), + 'period_end': (today - timedelta(days=today.weekday()) + timedelta(days=6)).isoformat(), + 'description': 'desc', + 'tags_input': '', + }) + assert WorkItem.objects.get(title='Typed entry').entry_type == et + + def test_entry_type_is_optional(self, client, user, project, category): + client.force_login(user) + today = date.today() + resp = client.post(reverse('entries:create'), { + 'title': 'No type', + 'project': project.pk, + 'category': category.pk, + 'period_kind': 'week', + 'period_start': (today - timedelta(days=today.weekday())).isoformat(), + 'period_end': (today - timedelta(days=today.weekday()) + timedelta(days=6)).isoformat(), + 'description': 'desc', + 'tags_input': '', + }) + assert resp.status_code == 302 + assert WorkItem.objects.get(title='No type').entry_type is None + def test_post_with_tags_increments_use_count(self, client, user, project, category): tag = Tag.objects.create(name='mytag') client.force_login(user) diff --git a/tests/test_reports.py b/tests/test_reports.py index 4186c63..71362fb 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -11,7 +11,7 @@ from apps.accounts.models import User from apps.entries.models import WorkItem -from apps.taxonomy.models import Category, Project, WorkGroup +from apps.taxonomy.models import Category, EntryType, Project, WorkGroup @pytest.fixture @@ -102,6 +102,19 @@ def test_author_email_filter(self, client, admin_user, entry): resp = client.post(reverse('reports:preview'), {'author_email': 'nomatch@x.com'}) assert b'Test entry' not in resp.content + def test_entry_type_filter(self, client, admin_user, entry, db): + weekly = EntryType.objects.create(name='Weekly Report', slug='weekly-report') + milestone = EntryType.objects.create(name='Milestone', slug='milestone') + entry.entry_type = weekly + entry.save(update_fields=['entry_type']) + client.force_login(admin_user) + # Matching type shows the entry… + resp = client.post(reverse('reports:preview'), {'entry_type': weekly.pk}) + assert b'Test entry' in resp.content + # …a different type filters it out. + resp = client.post(reverse('reports:preview'), {'entry_type': milestone.pk}) + assert b'Test entry' not in resp.content + def test_no_match_shows_empty_message(self, client, admin_user): client.force_login(admin_user) resp = client.post(reverse('reports:preview'), {'author_email': 'nobody@nowhere.com'}) diff --git a/tests/test_taxonomy.py b/tests/test_taxonomy.py index e1c0c13..9b390a6 100644 --- a/tests/test_taxonomy.py +++ b/tests/test_taxonomy.py @@ -1,7 +1,7 @@ import pytest from django.contrib.auth import get_user_model -from apps.taxonomy.models import Category, Project, Tag +from apps.taxonomy.models import Category, EntryType, Project, Tag User = get_user_model() @@ -91,6 +91,27 @@ def test_create_project_via_post(client, admin_user): assert Project.objects.filter(name='Test').exists() +@pytest.mark.django_db +def test_entry_types_page_requires_admin(client, regular_user): + client.force_login(regular_user) + assert client.get('/taxonomy/entry-types/').status_code == 403 + + +@pytest.mark.django_db +def test_entry_types_page_accessible_to_admin(client, admin_user): + client.force_login(admin_user) + assert client.get('/taxonomy/entry-types/').status_code == 200 + + +@pytest.mark.django_db +def test_create_entry_type_via_post(client, admin_user): + client.force_login(admin_user) + resp = client.post('/taxonomy/entry-types/', {'name': 'Milestone', 'short_code': 'MS', + 'is_active': True, 'sort_order': 5}) + assert resp.status_code == 302 + assert EntryType.objects.filter(name='Milestone').exists() + + @pytest.mark.django_db def test_edit_project(client, admin_user): p = Project.objects.create(name='OldName') @@ -119,8 +140,10 @@ def test_seed_taxonomy_creates_expected_data(): call_command('seed_taxonomy', verbosity=0) assert Project.objects.count() == 5 assert Category.objects.count() == 4 + assert EntryType.objects.count() == 3 assert Project.objects.filter(name='MicroBooNE').exists() assert Category.objects.filter(name='Outreach').exists() + assert EntryType.objects.filter(name='Weekly Report').exists() # ── Tag autocomplete content ──────────────────────────────────────────────────