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
30 changes: 16 additions & 14 deletions .github/workflows/coverage_comment.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
name: Coverage Comment

on:
workflow_run:
# The so-called dangerous workflow_run trigger is necessary for letting forks
# gain workflow-scoped pull-requests: write permission. The only
# user-controlled content is the content of diff-cover-report.md.
workflow_run: # zizmor: ignore[dangerous-triggers]
workflows: ["Coverage Tests"]
types:
- completed
Expand All @@ -27,15 +30,7 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

- name: Read PR number
id: pr
run: |
pr_number=$(cat pr_number.txt)
echo "number=$pr_number" >> $GITHUB_OUTPUT

- name: Post/update PR comment
env:
PR_NUMBER: ${{ steps.pr.outputs.number }}
uses: actions/github-script@v9
with:
script: |
Expand All @@ -54,16 +49,23 @@ jobs:
'**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)) {
core.setFailed('PR number is not available or invalid.');
const headSha = context.payload.workflow_run.head_sha;
const { data: prs } =
await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: headSha,
});
const pr = prs.find((p) => p.head.sha === headSha);
if (!pr) {
core.setFailed(`No PR found for commit ${headSha}.`);
return;
}

const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
issue_number: pr.number,
});

for (const c of comments) {
Expand All @@ -79,6 +81,6 @@ jobs:
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
issue_number: pr.number,
body: commentBody,
});
8 changes: 1 addition & 7 deletions .github/workflows/coverage_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,10 @@ jobs:
echo "No coverage data available." > diff-cover-report.md
fi

- name: Save PR number
if: success()
run: echo "${{ github.event.pull_request.number }}" > pr_number.txt

- name: Upload artifacts
if: success()
uses: actions/upload-artifact@v7
with:
name: coverage-artifacts
path: |
diff-cover-report.md
pr_number.txt
path: diff-cover-report.md
retention-days: 1
2 changes: 1 addition & 1 deletion django/contrib/admin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False, form=None)
except FieldDoesNotExist:
if name == "__str__":
label = str(model._meta.verbose_name)
attr = str
attr = model.__str__
else:
if callable(name):
attr = name
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/admin/views/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ def get_ordering_field(self, field_name):
# that allows sorting.
if callable(field_name):
attr = field_name
elif hasattr(self.model_admin, field_name):
elif hasattr(self.model_admin, field_name) and field_name != "__str__":
attr = getattr(self.model_admin, field_name)
else:
try:
Expand Down
33 changes: 0 additions & 33 deletions docs/ref/forms/widgets.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,39 +79,6 @@ widget on the field. In the following example, the
See the :ref:`built-in widgets` for more information about which widgets
are available and which arguments they accept.

Widgets inheriting from the ``Select`` widget
=============================================

Widgets inheriting from the :class:`Select` widget deal with choices. They
present the user with a list of options to choose from. The different widgets
present this choice differently; the :class:`Select` widget itself uses a
``<select>`` HTML list representation, while :class:`RadioSelect` uses radio
buttons.

:class:`Select` widgets are used by default on :class:`ChoiceField` fields. The
choices displayed on the widget are inherited from the :class:`ChoiceField` and
changing :attr:`ChoiceField.choices` will update :attr:`Select.choices`. For
example:

.. code-block:: pycon

>>> from django import forms
>>> CHOICES = {"1": "First", "2": "Second"}
>>> choice_field = forms.ChoiceField(widget=forms.RadioSelect, choices=CHOICES)
>>> choice_field.choices
[('1', 'First'), ('2', 'Second')]
>>> choice_field.widget.choices
[('1', 'First'), ('2', 'Second')]
>>> choice_field.widget.choices = []
>>> choice_field.choices = [("1", "First and only")]
>>> choice_field.widget.choices
[('1', 'First and only')]

Widgets which offer a :attr:`~Select.choices` attribute can however be used
with fields which are not based on choice -- such as a :class:`CharField` --
but it is recommended to use a :class:`ChoiceField`-based field when the
choices are inherent to the model and not just the representational widget.

Customizing widget instances
============================

Expand Down
1 change: 1 addition & 0 deletions tests/admin_utils/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class Cascade(models.Model):
num = models.PositiveSmallIntegerField()
parent = models.ForeignKey("self", models.CASCADE, null=True)

@admin.display(ordering="num")
def __str__(self):
return str(self.num)

Expand Down
6 changes: 6 additions & 0 deletions tests/admin_utils/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,12 @@ def test_from_property(self):
"property short description",
)

def test_label_for_field_str_admin_order_field(self):
_, attr = label_for_field("__str__", Cascade, return_attr=True)
self.assertIs(attr, Cascade.__str__)
self.assertTrue(hasattr(attr, "admin_order_field"))
self.assertEqual(attr.admin_order_field, "num")

def test_help_text_for_field(self):
tests = [
("article", ""),
Expand Down
1 change: 1 addition & 0 deletions tests/admin_views/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class Article(models.Model):
Section, models.SET_NULL, null=True, blank=True, related_name="+"
)

@admin.display(ordering="title")
def __str__(self):
return self.title

Expand Down
15 changes: 15 additions & 0 deletions tests/admin_views/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,21 @@ def test_change_list_sorting_model(self):
"Results of sorting on Model method are out of order.",
)

def test_change_list_sorting_model_str(self):
"""Ensure we can sort on a Model.__str__ list_display field."""

class ArticleStrAdmin(admin.ModelAdmin):
list_display = ["__str__"]

model_admin = ArticleStrAdmin(Article, site)
request = RequestFactory().get("/?o=1")
request.user = self.superuser
cl = model_admin.get_changelist_instance(request)
self.assertEqual(cl.get_ordering_field("__str__"), "title")
self.assertQuerySetEqual(
cl.get_queryset(request), Article.objects.order_by("title", "-pk")
)

def test_change_list_sorting_model_admin(self):
"""
Ensure we can sort on a list_display field that is a ModelAdmin method
Expand Down
1 change: 0 additions & 1 deletion zizmor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ rules:
# Before ignoring a file, assume all inputs are malicious, assign explicit
# minimal permissions, and do not use actions/checkout.
ignore:
- coverage_comment.yml
- labels.yml
- new_contributor_pr.yml
- check_pr_quality.yml
Expand Down
Loading