Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add published field to Release #17257

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 2 additions & 1 deletion tests/common/db/packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ class Meta:
lambda o: hashlib.blake2b(o.filename.encode("utf8"), digest_size=32).hexdigest()
)
upload_time = factory.Faker(
"date_time_between_dates", datetime_start=datetime.datetime(2008, 1, 1)
"date_time_between_dates",
datetime_start=datetime.datetime(2008, 1, 1),
)
path = factory.LazyAttribute(
lambda o: "/".join(
Expand Down
1 change: 1 addition & 0 deletions tests/unit/forklift/test_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -3613,6 +3613,7 @@ def test_upload_succeeds_creates_release(
else None
),
"uploaded_via_trusted_publisher": not test_with_user,
"published": True,
}

fileadd_event = {
Expand Down
16 changes: 16 additions & 0 deletions tests/unit/legacy/api/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ def test_all_non_prereleases_yanked(self, monkeypatch, db_request):
db_request.matchdict = {"name": project.normalized_name}
assert json.latest_release_factory(db_request) == release

def test_with_unpublished(self, db_request):
project = ProjectFactory.create()
release = ReleaseFactory.create(project=project, version="1.0")
ReleaseFactory.create(project=project, version="2.0", published=False)
db_request.matchdict = {"name": project.normalized_name}
assert json.latest_release_factory(db_request) == release

def test_project_quarantined(self, monkeypatch, db_request):
project = ProjectFactory.create(
lifecycle_status=LifecycleStatus.QuarantineEnter
Expand Down Expand Up @@ -191,6 +198,15 @@ def test_renders(self, pyramid_config, db_request, db_session):
)
]

ReleaseFactory.create(
project=project,
version="3.1",
description=DescriptionFactory.create(
content_type=description_content_type
),
published=False,
)

for urlspec in project_urls:
label, _, purl = urlspec.partition(",")
db_session.add(
Expand Down
33 changes: 32 additions & 1 deletion tests/unit/packaging/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ def test_only_yanked_release(self, monkeypatch, db_request):
assert resp is response
assert release_detail.calls == [pretend.call(release, db_request)]

def test_with_unpublished(self, monkeypatch, db_request):
project = ProjectFactory.create()
release = ReleaseFactory.create(project=project, version="1.0")
ReleaseFactory.create(project=project, version="1.1", published=False)

response = pretend.stub()
release_detail = pretend.call_recorder(lambda ctx, request: response)
monkeypatch.setattr(views, "release_detail", release_detail)

resp = views.project_detail(project, db_request)
assert resp is response
assert release_detail.calls == [pretend.call(release, db_request)]


class TestReleaseDetail:
def test_normalizing_name_redirects(self, db_request):
Expand Down Expand Up @@ -202,14 +215,27 @@ def test_detail_rendered(self, db_request):
yanked_reason="plaintext yanked reason",
)
]

# Add an unpublished version
staged_release = ReleaseFactory.create(
project=project,
version="5.1",
description=DescriptionFactory.create(
raw="unrendered description",
html="rendered description",
content_type="text/html",
),
published=False,
)

files = [
FileFactory.create(
release=r,
filename=f"{project.name}-{r.version}.tar.gz",
python_version="source",
packagetype="sdist",
)
for r in releases
for r in releases + [staged_release]
]

# Create a role for each user
Expand All @@ -226,6 +252,7 @@ def test_detail_rendered(self, db_request):
"bdists": [],
"description": "rendered description",
"latest_version": project.latest_version,
# Non published version are not listed here
"all_versions": [
(r.version, r.created, r.is_prerelease, r.yanked, r.yanked_reason)
for r in reversed(releases)
Expand Down Expand Up @@ -324,6 +351,10 @@ def test_long_singleline_license(self, db_request):
"characters, it's really so lo..."
)

def test_created_with_published(self, db_request):
release = ReleaseFactory.create()
assert release.published is True


class TestPEP740AttestationViewer:

Expand Down
1 change: 1 addition & 0 deletions warehouse/forklift/legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,7 @@ def file_upload(request):
else None
),
"uploaded_via_trusted_publisher": bool(request.oidc_publisher),
"published": True,
},
)

Expand Down
6 changes: 4 additions & 2 deletions warehouse/legacy/api/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ def _json_data(request, project, release, *, all_releases):
)
)
.outerjoin(File)
.filter(Release.project == project)
.filter(
Release.project == project,
)
)

# If we're not looking for all_releases, then we'll filter this further
Expand Down Expand Up @@ -206,7 +208,7 @@ def latest_release_factory(request):
.filter(
Project.lifecycle_status.is_distinct_from(
LifecycleStatus.QuarantineEnter
)
),
)
.order_by(
Release.yanked.asc(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
add published field
Revision ID: 6cac7b706953
Revises: 2a2c32c47a8f
Create Date: 2025-01-22 08:49:17.030343
"""

import sqlalchemy as sa

from alembic import op

revision = "6cac7b706953"
down_revision = "2a2c32c47a8f"


def upgrade():
conn = op.get_bind()
conn.execute(sa.text("SET statement_timeout = 120000"))
conn.execute(sa.text("SET lock_timeout = 120000"))

op.add_column(
"releases",
sa.Column(
"published", sa.Boolean(), server_default=sa.text("true"), nullable=False
),
)


def downgrade():
op.drop_column("releases", "published")
20 changes: 19 additions & 1 deletion warehouse/packaging/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import (
Mapped,
ORMExecuteState,
attribute_keyed_dict,
declared_attr,
mapped_column,
validates,
with_loader_criteria,
)
from urllib3.exceptions import LocationParseError
from urllib3.util import parse_url
Expand All @@ -79,7 +81,7 @@
from warehouse.sitemap.models import SitemapMixin
from warehouse.utils import dotted_navigator, wheel
from warehouse.utils.attrs import make_repr
from warehouse.utils.db.types import bool_false, datetime_now
from warehouse.utils.db.types import bool_false, bool_true, datetime_now

if typing.TYPE_CHECKING:
from warehouse.oidc.models import OIDCPublisher
Expand Down Expand Up @@ -633,6 +635,7 @@ def __table_args__(cls): # noqa
_pypi_ordering: Mapped[int | None]
requires_python: Mapped[str | None] = mapped_column(Text)
created: Mapped[datetime_now] = mapped_column()
published: Mapped[bool_true] = mapped_column()

description_id: Mapped[UUID] = mapped_column(
ForeignKey("release_descriptions.id", onupdate="CASCADE", ondelete="CASCADE"),
Expand Down Expand Up @@ -1057,6 +1060,21 @@ def ensure_monotonic_journals(config, session, flush_context, instances):
return


@db.listens_for(db.Session, "do_orm_execute")
def filter_staged_release(config, state: ORMExecuteState):
if (
state.is_select
and not state.is_column_load
and not state.is_relationship_load
and not state.statement.get_execution_options().get("include_staged", False)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FLAG: This is added to allow querying for the published=False query but is not used in this PR.

):
state.statement = state.statement.options(
with_loader_criteria(
Release, lambda cls: cls.published, include_aliases=True
)
)


class ProhibitedProjectName(db.Model):
__tablename__ = "prohibited_project_names"
__table_args__ = (
Expand Down
3 changes: 2 additions & 1 deletion warehouse/packaging/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ def _simple_detail(project, request):
.join(Release)
.filter(Release.project == project)
# Exclude projects that are in the `quarantine-enter` lifecycle status.
# And exclude un-published releases from the index
.join(Project)
.filter(
Project.lifecycle_status.is_distinct_from(LifecycleStatus.QuarantineEnter)
Project.lifecycle_status.is_distinct_from(LifecycleStatus.QuarantineEnter),
)
.all(),
key=lambda f: (packaging_legacy.version.parse(f.release.version), f.filename),
Expand Down
8 changes: 8 additions & 0 deletions warehouse/templates/manage/project/history.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ <h2>{% trans %}Security history{% endtrans %}</h2>
<a href="{{ event.additional.publisher_url }}">{{ event.additional.publisher_url }}</a>
{% endif %}
</small>
<small>
{% trans %}Published:{% endtrans %}
{% if event.additional.published is defined and event.additional.published is false %}
{% trans %}No{% endtrans %}
{% else %}
{% trans %}Yes{% endtrans %}
{% endif %}
</small>
{% elif event.tag == EventTag.Project.ReleaseRemove %}
<strong>
{# No link to removed release #}
Expand Down
Loading