Skip to content
Merged
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
9 changes: 8 additions & 1 deletion .github/workflows/coverage_comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,15 @@ jobs:
let body = 'No coverage data available.';
if (fs.existsSync(reportPath)) {
body = fs.readFileSync(reportPath, 'utf8');
const divider = '-------------';
// Start after the second divder, where changed files are listed.
const start = body.indexOf(divider, 1);
body = body.substring(start);
}
const commentBody = '### 📊 Coverage Report for Changed Files\n\n```\n' + body + '\n```\n\n**Note:** Missing lines are warnings only. Some lines may not be covered by SQLite tests as they are database-specific.\n\nFor more information about code coverage on pull requests, see the [contributing documentation](https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-tests/#code-coverage-on-pull-requests).';
const commentBody = (
'#### Uncovered lines in changed files\n\n```\n' + body + '```\n' +
+ '**Note:** Missing lines are warnings only. Some database-specific lines may not be measured. [More information](https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-tests/#code-coverage-on-pull-requests)'
);

const prNumber = parseInt(process.env.PR_NUMBER);
if (isNaN(prNumber)) {
Expand Down
40 changes: 31 additions & 9 deletions .github/workflows/coverage_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Coverage Tests

on:
pull_request:
types: [labeled, synchronize, opened, reopened]
paths-ignore:
- 'docs/**'
- '**/*.md'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand All @@ -15,9 +15,24 @@ permissions:

jobs:
coverage:
name: Coverage Tests (Windows)
runs-on: windows-latest
if: contains(github.event.pull_request.labels.*.name, 'coverage')
runs-on: ubuntu-latest
name: Coverage Tests (PostgreSQL)
timeout-minutes: 60
services:
postgres:
image: postgres:18-alpine
env:
POSTGRES_DB: django
POSTGRES_USER: user
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v6
Expand All @@ -32,19 +47,26 @@ jobs:
cache: 'pip'
cache-dependency-path: 'tests/requirements/py3.txt'

- name: Update apt repo
run: sudo apt update
- name: Install libmemcached-dev for pylibmc
run: sudo apt install -y libmemcached-dev
- name: Install dependencies
run: |
python -m pip install --upgrade pip wheel
python -m pip install -r tests/requirements/py3.txt -e .
python -m pip install -r tests/requirements/py3.txt -r tests/requirements/postgres.txt -e .
python -m pip install 'coverage[toml]' diff-cover

- name: Create PostgreSQL settings file
run: mv ./.github/workflows/data/test_postgres.py.tpl ./tests/test_postgres.py

- name: Run tests with coverage
env:
PYTHONPATH: ${{ github.workspace }}/tests
COVERAGE_PROCESS_START: ${{ github.workspace }}/tests/.coveragerc
RUNTESTS_DIR: ${{ github.workspace }}/tests
run: |
python -Wall tests/runtests.py -v2
python -Wall tests/runtests.py --settings=test_postgres -v2

- name: Generate coverage report
if: success()
Expand All @@ -59,11 +81,11 @@ jobs:
- name: Generate diff coverage report
if: success()
run: |
if (Test-Path 'tests/coverage.xml') {
if [ -f tests/coverage.xml ]; then
diff-cover tests/coverage.xml --compare-branch=origin/main --fail-under=0 > diff-cover-report.md
} else {
Set-Content -Path diff-cover-report.md -Value 'No coverage data available.'
}
else
echo "No coverage data available." > diff-cover-report.md
fi

- name: Save PR number
if: success()
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ permissions:

jobs:
windows:
if: github.event_name == 'push'
runs-on: windows-latest
strategy:
matrix:
Expand Down
34 changes: 18 additions & 16 deletions django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -1419,7 +1419,7 @@ def response_add(self, request, obj, post_url_continue=None):

# Find the optgroup for the new item, if available
source_model_name = request.POST.get(SOURCE_MODEL_VAR)

source_admin = None
if source_model_name:
app_label, model_name = source_model_name.split(".", 1)
try:
Expand All @@ -1428,21 +1428,23 @@ def response_add(self, request, obj, post_url_continue=None):
msg = _('The app "%s" could not be found.') % source_model_name
self.message_user(request, msg, messages.ERROR)
else:
source_admin = self.admin_site._registry[source_model]
form = source_admin.get_form(request)()
if self.opts.verbose_name_plural in form.fields:
field = form.fields[self.opts.verbose_name_plural]
for option_value, option_label in field.choices:
# Check if this is an optgroup (label is a sequence
# of choices rather than a single string value).
if isinstance(option_label, (list, tuple)):
# It's an optgroup:
# (group_name, [(value, label), ...])
optgroup_label = option_value
for choice_value, choice_display in option_label:
if choice_display == str(obj):
popup_response["optgroup"] = str(optgroup_label)
break
source_admin = self.admin_site._registry.get(source_model)

if source_admin:
form = source_admin.get_form(request)()
if self.opts.verbose_name_plural in form.fields:
field = form.fields[self.opts.verbose_name_plural]
for option_value, option_label in field.choices:
# Check if this is an optgroup (label is a sequence
# of choices rather than a single string value).
if isinstance(option_label, (list, tuple)):
# It's an optgroup:
# (group_name, [(value, label), ...])
optgroup_label = option_value
for choice_value, choice_display in option_label:
if choice_display == str(obj):
popup_response["optgroup"] = str(optgroup_label)
break

popup_response_data = json.dumps(popup_response)
return TemplateResponse(
Expand Down
40 changes: 36 additions & 4 deletions tests/admin_views/tests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
import os
import re
import sys
import unittest
import zoneinfo
from unittest import mock
Expand Down Expand Up @@ -588,6 +589,31 @@ def test_popup_add_POST_with_invalid_source_model(self):
self.assertIn("admin_views.nonexistent", str(messages[0]))
self.assertIn("could not be found", str(messages[0]))

def test_popup_add_POST_with_unregistered_source_model(self):
"""
Popup add where source_model is a valid Django model but is not
registered in the admin site (e.g. a model only used as an inline)
should succeed without raising a KeyError.
"""
post_data = {
IS_POPUP_VAR: "1",
# Chapter exists as a model but is not registered in site (only
# in site6), simulating a model used only as an inline.
SOURCE_MODEL_VAR: "admin_views.chapter",
"title": "Test Article",
"content": "some content",
"date_0": "2010-09-10",
"date_1": "14:55:39",
}
response = self.client.post(reverse("admin:admin_views_article_add"), post_data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "data-popup-response")
# No error messages - unregistered model is silently skipped.
messages = list(response.wsgi_request._messages)
self.assertEqual(len(messages), 0)
# No optgroup in the response.
self.assertNotContains(response, ""optgroup"")

def test_basic_edit_POST(self):
"""
A smoke test to ensure POST on edit_view works.
Expand Down Expand Up @@ -5983,6 +6009,12 @@ def setUp(self):
title="A Long Title", published=True, slug="a-long-title"
)

@property
def modifier_key(self):
from selenium.webdriver.common.keys import Keys

return Keys.COMMAND if sys.platform == "darwin" else Keys.CONTROL

@screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
def test_login_button_centered(self):
from selenium.webdriver.common.by import By
Expand Down Expand Up @@ -6406,8 +6438,8 @@ def test_selectbox_selected_rows(self):
elem = self.selenium.find_element(
By.CSS_SELECTOR, f"#id_user_permissions_from option[value='{perm.id}']"
)
ActionChains(self.selenium).key_down(Keys.CONTROL).click(elem).key_up(
Keys.CONTROL
ActionChains(self.selenium).key_down(self.modifier_key).click(elem).key_up(
self.modifier_key
).perform()

# Move focus to other element.
Expand All @@ -6425,8 +6457,8 @@ def test_selectbox_selected_rows(self):
elem = self.selenium.find_element(
By.CSS_SELECTOR, f"#id_user_permissions_to option[value='{perm.id}']"
)
ActionChains(self.selenium).key_down(Keys.CONTROL).click(elem).key_up(
Keys.CONTROL
ActionChains(self.selenium).key_down(self.modifier_key).click(elem).key_up(
self.modifier_key
).perform()

# Move focus to other element.
Expand Down