Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ exclude_lines = [
"@abstractmethod",
"class .*(Protocol):",
]
fail_under = 94
fail_under = 100
Comment thread
JacobCoffee marked this conversation as resolved.
Resolved

[tool.coverage.run]
concurrency = ["multiprocessing", "thread"]
Expand Down
4 changes: 2 additions & 2 deletions src/django_program/manage/reports_analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from decimal import Decimal
from typing import TYPE_CHECKING, Any

from django.db.models import Avg, Count, Q, Sum, Value
from django.db.models import Avg, Count, DecimalField, Q, Sum, Value
from django.db.models.functions import Coalesce

if TYPE_CHECKING:
Expand Down Expand Up @@ -212,7 +212,7 @@ def get_session_rating_analytics(conference: Conference) -> dict[str, Any]:

agg = ratings_qs.aggregate(
total_ratings=Count("id"),
average_score=Coalesce(Avg("score"), Value(_ZERO)),
average_score=Coalesce(Avg("score", output_field=DecimalField()), Value(_ZERO)),
)
total_ratings: int = agg["total_ratings"]
average_score: Decimal = Decimal(str(agg["average_score"]))
Expand Down
24 changes: 7 additions & 17 deletions src/django_program/manage/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -910,24 +910,14 @@ def post(self, request: HttpRequest, *args: object, **kwargs: object) -> HttpRes
return self.render_to_response(self.get_context_data(form=form, kpi_form=kpi_form))

def _forms_valid(self, form: ConferenceForm, kpi_form: KPITargetsForm) -> HttpResponse:
"""Save both the conference and KPI targets forms."""
response = super().form_valid(form)
kpi = kpi_form.save(commit=False)
kpi.conference = self.conference
kpi.save()
return response

def form_valid(self, form: ConferenceForm) -> HttpResponse:
"""Save the form and add a success message.

Args:
form: The validated conference form.

Returns:
A redirect response to the success URL.
"""
"""Save the conference and its KPI targets together."""
with transaction.atomic():
response = super().form_valid(form)
kpi = kpi_form.save(commit=False)
kpi.conference = self.conference
kpi.save()
messages.success(self.request, "Conference updated successfully.")
return super().form_valid(form)
return response


class SectionListView(ManagePermissionMixin, ListView):
Expand Down
181 changes: 181 additions & 0 deletions tests/test_conference/test_models_coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"""Coverage backfill tests for:

- ``conference/management/commands/setup_groups.py`` (the "missing
permissions" warning branch)
- Simple ``__str__`` representations in ``conference/models.py``,
``pretalx/models.py``, and ``programs/models.py`` that had no direct
coverage.
"""

from datetime import date
from decimal import Decimal
from io import StringIO
from unittest.mock import patch

import pytest
from django.contrib.auth import get_user_model
from django.core.management import call_command

from django_program.conference.models import Conference, Expense, ExpenseCategory
from django_program.pretalx.models import SessionRating, Talk
from django_program.programs.models import Survey, SurveyResponse

User = get_user_model()


# =============================================================================
# setup_groups command — missing permissions warning
# =============================================================================


@pytest.mark.django_db
def test_setup_groups_warns_when_permissions_are_missing():
"""When Permission.objects.filter returns no matches, every requested
permission is reported as missing and the warning line is printed."""
out = StringIO()

with patch("django_program.conference.management.commands.setup_groups.Permission.objects.filter") as mock_filter:
mock_filter.return_value = []
call_command("setup_groups", stdout=out, verbosity=1)

output = out.getvalue()
assert "missing permissions" in output
assert "Conference Organizer" in output


@pytest.mark.django_db
def test_setup_groups_silent_when_verbosity_zero():
"""Verbosity 0 suppresses the missing-permissions warning line entirely."""
out = StringIO()

with patch("django_program.conference.management.commands.setup_groups.Permission.objects.filter") as mock_filter:
mock_filter.return_value = []
call_command("setup_groups", stdout=out, verbosity=0)

assert out.getvalue() == ""


@pytest.mark.django_db
def test_setup_groups_creates_groups_without_warning_when_all_matched():
out = StringIO()
call_command("setup_groups", stdout=out, verbosity=1)
output = out.getvalue()

assert "missing permissions" not in output
assert "Done." in output


# =============================================================================
# conference/models.py — __str__ methods
# =============================================================================


@pytest.fixture
def conference(db):
return Conference.objects.create(
name="ModelCoverageCon",
slug="model-coverage-con",
start_date=date(2027, 8, 1),
end_date=date(2027, 8, 3),
timezone="UTC",
)


@pytest.mark.unit
@pytest.mark.django_db
class TestExpenseCategoryStr:
def test_str_includes_name_and_conference_slug(self, conference):
category = ExpenseCategory.objects.create(
conference=conference,
name="Venue",
slug="venue",
)
assert str(category) == f"Venue ({conference.slug})"


@pytest.mark.unit
@pytest.mark.django_db
class TestExpenseStr:
def test_str_includes_description_and_amount(self, conference):
category = ExpenseCategory.objects.create(
conference=conference,
name="Catering",
slug="catering",
)
expense = Expense.objects.create(
conference=conference,
category=category,
description="Lunch buffet",
amount=Decimal("1250.50"),
date=date(2027, 8, 2),
)
assert str(expense) == "Lunch buffet ($1250.50)"


# =============================================================================
# pretalx/models.py — SessionRating.__str__
# =============================================================================


@pytest.mark.unit
@pytest.mark.django_db
class TestSessionRatingStr:
def test_str_includes_score_talk_and_user(self, conference):
user = User.objects.create_user(
username="raterperson",
email="rater@example.com",
password="testpass123",
)
talk = Talk.objects.create(
conference=conference,
pretalx_code="ABC123",
title="A Great Talk",
)
rating = SessionRating.objects.create(
conference=conference,
talk=talk,
user=user,
score=4,
)
expected = f"Rating 4/5 for {talk} by {user}"
assert str(rating) == expected


# =============================================================================
# programs/models.py — Survey.__str__ / SurveyResponse.__str__
# =============================================================================


@pytest.mark.unit
@pytest.mark.django_db
class TestSurveyStr:
def test_str_includes_name_and_conference_slug(self, conference):
survey = Survey.objects.create(
conference=conference,
name="Post-Event Feedback",
slug="post-event-feedback",
)
assert str(survey) == f"Post-Event Feedback ({conference.slug})"


@pytest.mark.unit
@pytest.mark.django_db
class TestSurveyResponseStr:
def test_str_includes_score_survey_and_user(self, conference):
user = User.objects.create_user(
username="surveyresponder",
email="responder@example.com",
password="testpass123",
)
survey = Survey.objects.create(
conference=conference,
name="NPS Survey",
slug="nps-survey",
)
response = SurveyResponse.objects.create(
survey=survey,
user=user,
score=9,
)
expected = f"Response 9 to {survey} by {user}"
assert str(response) == expected
Loading
Loading