diff --git a/base-requirements.txt b/base-requirements.txt index 2495153db..06e1990e4 100644 --- a/base-requirements.txt +++ b/base-requirements.txt @@ -13,10 +13,10 @@ psycopg2-binary==2.8.6 python3-openid==3.2.0 python-decouple==3.4 # lxml used by BeautifulSoup. -lxml==4.6.3 +lxml==4.9.2 cssselect==1.1.0 feedparser==6.0.8 -beautifulsoup4==4.9.3 +beautifulsoup4==4.11.2 icalendar==4.0.7 chardet==4.0.0 # TODO: We may drop 'django-imagekit' completely. diff --git a/blogs/tests/test_views.py b/blogs/tests/test_views.py index ee7df723b..5c6c5053f 100644 --- a/blogs/tests/test_views.py +++ b/blogs/tests/test_views.py @@ -27,12 +27,3 @@ def test_blog_home(self): latest = BlogEntry.objects.latest() self.assertEqual(resp.context['latest_entry'], latest) - - def test_blog_redirects(self): - """ - Test that when '/blog/' is hit, it redirects '/blogs/' - """ - response = self.client.get('/blog/') - self.assertRedirects(response, - '/blogs/', - status_code=301) diff --git a/config/nginx.conf.erb b/config/nginx.conf.erb index 527fdc0df..42bbf94cc 100644 --- a/config/nginx.conf.erb +++ b/config/nginx.conf.erb @@ -84,6 +84,10 @@ http { return 301 https://www.python.org/psf; } + location ~ ^/community-landing/?(.*)$ { + return 301 https://www.python.org/community/; + } + location /doc/Summary { return 301 http://legacy.python.org/doc/intros/summary; } @@ -204,6 +208,22 @@ http { return 301 https://www.python.org/download/windows/; } + location /download/ { + return 301 https://www.python.org/downloads/; + } + + location /download/source/ { + return 301 https://www.python.org/downloads/source/; + } + + location /download/mac/ { + return 301 https://www.python.org/downloads/macos/; + } + + location /download/windows/ { + return 301 https://www.python.org/downloads/windows/; + } + location /Mirrors.html { return 301 https://www.python.org/mirrors/; } @@ -292,6 +312,10 @@ http { return 302 /blogs/; } + location /blog/ { + return 301 https://python.org/blogs/; + } + location /static/ { alias /app/static-root/; add_header Cache-Control "max-age=604800, public"; # 604800 is 7 days diff --git a/downloads/api.py b/downloads/api.py index e58023dbf..73eb9b7bf 100644 --- a/downloads/api.py +++ b/downloads/api.py @@ -69,7 +69,7 @@ class Meta(GenericResource.Meta): 'creator', 'last_modified_by', 'os', 'release', 'description', 'is_source', 'url', 'gpg_signature_file', 'md5_sum', 'filesize', 'download_button', 'sigstore_signature_file', - 'sigstore_cert_file', 'sigstore_bundle_file', + 'sigstore_cert_file', 'sigstore_bundle_file', 'sbom_spdx2_file', ] filtering = { 'name': ('exact',), diff --git a/downloads/migrations/0010_releasefile_sbom_spdx2_file.py b/downloads/migrations/0010_releasefile_sbom_spdx2_file.py new file mode 100644 index 000000000..f3a4784e9 --- /dev/null +++ b/downloads/migrations/0010_releasefile_sbom_spdx2_file.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2024-01-12 21:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('downloads', '0009_releasefile_sigstore_bundle_file'), + ] + + operations = [ + migrations.AddField( + model_name='releasefile', + name='sbom_spdx2_file', + field=models.URLField(blank=True, help_text='SPDX-2 SBOM URL', verbose_name='SPDX-2 SBOM URL'), + ), + ] diff --git a/downloads/models.py b/downloads/models.py index 6d91534ac..4a9c5781c 100644 --- a/downloads/models.py +++ b/downloads/models.py @@ -332,6 +332,9 @@ class ReleaseFile(ContentManageable, NameSlugModel): sigstore_bundle_file = models.URLField( "Sigstore Bundle URL", blank=True, help_text="Sigstore Bundle URL" ) + sbom_spdx2_file = models.URLField( + "SPDX-2 SBOM URL", blank=True, help_text="SPDX-2 SBOM URL" + ) md5_sum = models.CharField('MD5 Sum', max_length=200, blank=True) filesize = models.IntegerField(default=0) download_button = models.BooleanField(default=False, help_text="Use for the supernav download button for this OS") diff --git a/downloads/serializers.py b/downloads/serializers.py index 67bde5b5c..1ff57049f 100644 --- a/downloads/serializers.py +++ b/downloads/serializers.py @@ -49,4 +49,5 @@ class Meta: 'sigstore_signature_file', 'sigstore_cert_file', 'sigstore_bundle_file', + 'sbom_spdx2_file', ) diff --git a/downloads/templatetags/download_tags.py b/downloads/templatetags/download_tags.py index fb3496787..c72f6d58c 100644 --- a/downloads/templatetags/download_tags.py +++ b/downloads/templatetags/download_tags.py @@ -14,3 +14,42 @@ def has_sigstore_materials(files): f.sigstore_bundle_file or f.sigstore_cert_file or f.sigstore_signature_file for f in files ) + + +@register.filter +def has_sbom(files): + return any(f.sbom_spdx2_file for f in files) + + +@register.filter +def sort_windows(files): + if not files: + return files + + # Put Windows files in preferred order + files = list(files) + windows_files = [] + other_files = [] + for preferred in ( + 'Windows installer (64-bit)', + 'Windows installer (32-bit)', + 'Windows installer (ARM64)', + 'Windows help file', + 'Windows embeddable package (64-bit)', + 'Windows embeddable package (32-bit)', + 'Windows embeddable package (ARM64)', + ): + for file in files: + if file.name == preferred: + windows_files.append(file) + files.remove(file) + break + + # Then append any remaining Windows files + for file in files: + if file.name.startswith('Windows'): + windows_files.append(file) + else: + other_files.append(file) + + return other_files + windows_files diff --git a/downloads/tests/base.py b/downloads/tests/base.py index e19ffe03a..bcb7905c4 100644 --- a/downloads/tests/base.py +++ b/downloads/tests/base.py @@ -64,6 +64,7 @@ def setUp(self): is_source=True, description='Gzipped source', url='ftp/python/2.7.5/Python-2.7.5.tgz', + filesize=12345678, ) self.draft_release = Release.objects.create( diff --git a/downloads/tests/test_views.py b/downloads/tests/test_views.py index 75fe76693..50270c556 100644 --- a/downloads/tests/test_views.py +++ b/downloads/tests/test_views.py @@ -40,6 +40,9 @@ def test_download_release_detail(self): response = self.client.get(url) self.assertEqual(response.status_code, 200) + with self.subTest("Release file sizes should be human-readable"): + self.assertInHTML("11.8 MB", response.content.decode()) + url = reverse('download:download_release_detail', kwargs={'release_slug': 'fake_slug'}) response = self.client.get(url) self.assertEqual(response.status_code, 404) diff --git a/fixtures/boxes.json b/fixtures/boxes.json index b0e965011..bc3816cc7 100644 --- a/fixtures/boxes.json +++ b/fixtures/boxes.json @@ -174,9 +174,9 @@ "created": "2013-10-28T19:27:20.963Z", "updated": "2022-01-05T15:42:59.645Z", "label": "widget-use-python-for", - "content": "

Use Python for…

\r\n

More

\r\n\r\n", + "content": "

Use Python for…

\r\n

More

\r\n\r\n", "content_markup_type": "html", - "_content_rendered": "

Use Python for…

\r\n

More

\r\n\r\n" + "_content_rendered": "

Use Python for…

\r\n

More

\r\n\r\n" } }, { diff --git a/pydotorg/urls.py b/pydotorg/urls.py index 5fc6b3f12..c112b2e19 100644 --- a/pydotorg/urls.py +++ b/pydotorg/urls.py @@ -3,7 +3,7 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.conf.urls.static import static from django.urls import path, re_path -from django.views.generic.base import TemplateView, RedirectView +from django.views.generic.base import TemplateView from django.conf import settings from cms.views import custom_404 @@ -24,18 +24,11 @@ # python section landing pages path('about/', TemplateView.as_view(template_name="python/about.html"), name='about'), - # Redirect old download links to new downloads pages - path('download/', RedirectView.as_view(url='https://www.python.org/downloads/', permanent=True)), - path('download/source/', RedirectView.as_view(url='https://www.python.org/downloads/source/', permanent=True)), - path('download/mac/', RedirectView.as_view(url='https://www.python.org/downloads/macos/', permanent=True)), - path('download/windows/', RedirectView.as_view(url='https://www.python.org/downloads/windows/', permanent=True)), - # duplicated downloads to getit to bypass China's firewall. See # https://github.com/python/pythondotorg/issues/427 for more info. path('getit/', include('downloads.urls', namespace='getit')), path('downloads/', include('downloads.urls', namespace='download')), path('doc/', views.DocumentationIndexView.as_view(), name='documentation'), - path('blog/', RedirectView.as_view(url='/blogs/', permanent=True)), path('blogs/', include('blogs.urls')), path('inner/', TemplateView.as_view(template_name="python/inner.html"), name='inner'), diff --git a/sponsors/admin.py b/sponsors/admin.py index d6601140f..88aff8c57 100644 --- a/sponsors/admin.py +++ b/sponsors/admin.py @@ -246,9 +246,13 @@ def has_delete_permission(self, request, obj=None): return True return obj.open_for_editing - def get_queryset(self, *args, **kwargs): - qs = super().get_queryset(*args, **kwargs) - return qs.select_related("sponsorship_benefit__program", "program") + def get_queryset(self, request): + #filters the available benefits by the benefits for the year of the sponsorship + match = request.resolver_match + sponsorship = self.parent_model.objects.get(pk=match.kwargs["object_id"]) + year = sponsorship.year + + return super().get_queryset(request).filter(sponsorship_benefit__year=year) class TargetableEmailBenefitsFilter(admin.SimpleListFilter): diff --git a/sponsors/forms.py b/sponsors/forms.py index 5a31605af..4ced017c9 100644 --- a/sponsors/forms.py +++ b/sponsors/forms.py @@ -221,6 +221,11 @@ class SponsorshipApplicationForm(forms.Form): help_text="For promotion of your sponsorship on social media.", required=False, ) + linked_in_page_url = forms.URLField( + label="LinkedIn page URL", + help_text="URL for your LinkedIn page.", + required=False, + ) web_logo = forms.ImageField( label="Sponsor web logo", help_text="For display on our sponsor webpage. High resolution PNG or JPG, smallest dimension no less than 256px", @@ -253,10 +258,17 @@ class SponsorshipApplicationForm(forms.Form): state = forms.CharField( label="State/Province/Region", max_length=64, required=False ) + state_of_incorporation = forms.CharField( + label="State of incorporation", help_text="US only, If different than mailing address", max_length=64, required=False + ) postal_code = forms.CharField( label="Zip/Postal Code", max_length=64, required=False ) - country = CountryField().formfield(required=False) + country = CountryField().formfield(required=False, help_text="For mailing/contact purposes") + + country_of_incorporation = CountryField().formfield( + label="Country of incorporation", help_text="For contractual purposes", required=False + ) def __init__(self, *args, **kwargs): self.user = kwargs.pop("user", None) @@ -372,7 +384,10 @@ def save(self): description=self.cleaned_data.get("description", ""), landing_page_url=self.cleaned_data.get("landing_page_url", ""), twitter_handle=self.cleaned_data["twitter_handle"], + linked_in_page_url=self.cleaned_data["linked_in_page_url"], print_logo=self.cleaned_data.get("print_logo"), + country_of_incorporation=self.cleaned_data.get("country_of_incorporation", ""), + state_of_incorporation=self.cleaned_data.get("state_of_incorporation", ""), ) contacts = [f.save(commit=False) for f in self.contacts_formset.forms] for contact in contacts: diff --git a/sponsors/management/commands/create_pycon_vouchers_for_sponsors.py b/sponsors/management/commands/create_pycon_vouchers_for_sponsors.py index f8b99855a..3e3b4973d 100644 --- a/sponsors/management/commands/create_pycon_vouchers_for_sponsors.py +++ b/sponsors/management/commands/create_pycon_vouchers_for_sponsors.py @@ -20,22 +20,26 @@ ) BENEFITS = { - 121: { - "internal_name": "full_conference_passes_2023_code", + 183: { + "internal_name": "full_conference_passes_code_2024", "voucher_type": "SPNS_COMP_", }, - 139: { - "internal_name": "expo_hall_only_passes_2023_code", + 201: { + "internal_name": "expo_hall_only_passes_code_2024", "voucher_type": "SPNS_EXPO_COMP_", }, - 148: { - "internal_name": "additional_full_conference_passes_2023_code", + 208: { + "internal_name": "additional_full_conference_passes_code_2024", "voucher_type": "SPNS_ADDL_DISC_REG_", }, - 166: { - "internal_name": "online_only_conference_passes_2023_code", + 225: { + "internal_name": "online_only_conference_passes_2024", "voucher_type": "SPNS_ONLINE_COMP_", }, + 237: { + "internal_name": "additional_expo_hall_only_passes_2024", + "voucher_type": "SPNS_EXPO_DISC_", + }, } diff --git a/sponsors/migrations/0100_auto_20240107_1054.py b/sponsors/migrations/0100_auto_20240107_1054.py new file mode 100644 index 000000000..8bad2bc92 --- /dev/null +++ b/sponsors/migrations/0100_auto_20240107_1054.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.24 on 2024-01-07 10:54 + +from django.db import migrations, models +import django_countries.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('sponsors', '0099_auto_20231224_1854'), + ] + + operations = [ + migrations.AddField( + model_name='sponsor', + name='country_of_incorporation', + field=django_countries.fields.CountryField(blank=True, help_text='For contractual purposes', max_length=2, null=True, verbose_name='Country of incorporation (If different)'), + ), + migrations.AddField( + model_name='sponsor', + name='state_of_incorporation', + field=models.CharField(blank=True, default='', max_length=64, null=True, verbose_name='US only: State of incorporation (If different)'), + ), + migrations.AlterField( + model_name='sponsor', + name='country', + field=django_countries.fields.CountryField(default='', help_text='For mailing/contact purposes', max_length=2), + ), + ] diff --git a/sponsors/migrations/0101_sponsor_linked_in_page_url.py b/sponsors/migrations/0101_sponsor_linked_in_page_url.py new file mode 100644 index 000000000..61041a08e --- /dev/null +++ b/sponsors/migrations/0101_sponsor_linked_in_page_url.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2024-02-09 13:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sponsors', '0100_auto_20240107_1054'), + ] + + operations = [ + migrations.AddField( + model_name='sponsor', + name='linked_in_page_url', + field=models.URLField(blank=True, help_text='URL for your LinkedIn page.', null=True, verbose_name='LinkedIn page URL'), + ), + ] diff --git a/sponsors/models/sponsors.py b/sponsors/models/sponsors.py index eee7f585e..78d5d6e32 100644 --- a/sponsors/models/sponsors.py +++ b/sponsors/models/sponsors.py @@ -44,6 +44,12 @@ class Sponsor(ContentManageable): null=True, verbose_name="Twitter handle", ) + linked_in_page_url = models.URLField( + blank=True, + null=True, + verbose_name="LinkedIn page URL", + help_text="URL for your LinkedIn page." + ) web_logo = models.ImageField( upload_to="sponsor_web_logos", verbose_name="Web logo", @@ -73,8 +79,15 @@ class Sponsor(ContentManageable): postal_code = models.CharField( verbose_name="Zip/Postal Code", max_length=64, default="" ) - country = CountryField(default="") + country = CountryField(default="", help_text="For mailing/contact purposes") assets = GenericRelation(GenericAsset) + country_of_incorporation = CountryField( + verbose_name="Country of incorporation (If different)", help_text="For contractual purposes", blank=True, null=True + ) + state_of_incorporation = models.CharField( + verbose_name="US only: State of incorporation (If different)", + max_length=64, blank=True, null=True, default="" + ) class Meta: verbose_name = "sponsor" diff --git a/sponsors/tests/test_forms.py b/sponsors/tests/test_forms.py index 123dc1729..49b0515cd 100644 --- a/sponsors/tests/test_forms.py +++ b/sponsors/tests/test_forms.py @@ -423,14 +423,18 @@ def test_create_sponsor_with_valid_data(self): def test_create_sponsor_with_valid_data_for_non_required_inputs( self, ): + user = baker.make(settings.AUTH_USER_MODEL) + self.data["description"] = "Important company" self.data["landing_page_url"] = "https://companyx.com" self.data["twitter_handle"] = "@companyx" + self.data["country_of_incorporation"] = "US" + self.data["state_of_incorporation"] = "NY" self.files["print_logo"] = get_static_image_file_as_upload( "psf-logo_print.png", "logo_print.png" ) - form = SponsorshipApplicationForm(self.data, self.files) + form = SponsorshipApplicationForm(self.data, self.files, user=user) self.assertTrue(form.is_valid(), form.errors) sponsor = form.save() @@ -440,6 +444,8 @@ def test_create_sponsor_with_valid_data_for_non_required_inputs( self.assertFalse(form.user_with_previous_sponsors) self.assertEqual(sponsor.landing_page_url, "https://companyx.com") self.assertEqual(sponsor.twitter_handle, "@companyx") + self.assertEqual(sponsor.country_of_incorporation, "US") + self.assertEqual(sponsor.state_of_incorporation, "NY") def test_create_sponsor_with_svg_for_print_logo( self, diff --git a/static/sass/style.css b/static/sass/style.css index c3d2bb5f9..a58863817 100644 --- a/static/sass/style.css +++ b/static/sass/style.css @@ -3405,6 +3405,23 @@ span.highlighted { .icon-megaphone span, .icon-python-alt span, .icon-pypi span, .icon-news span, .icon-moderate span, .icon-mercurial span, .icon-jobs span, .icon-help span, .icon-download span, .icon-documentation span, .icon-community span, .icon-code span, .icon-close span, .icon-calendar span, .icon-beginner span, .icon-advanced span, .icon-sitemap span, .icon-search span, .icon-search-alt span, .icon-python span, .icon-github span, .icon-get-started span, .icon-feed span, .icon-facebook span, .icon-email span, .icon-arrow-up span, .icon-arrow-right span, .icon-arrow-left span, .icon-arrow-down span, .errorlist:before span, .icon-freenode span, .icon-alert span, .icon-versions span, .icon-twitter span, .icon-thumbs-up span, .icon-thumbs-down span, .icon-text-resize span, .icon-success-stories span, .icon-statistics span, .icon-stack-overflow span, .icon-mastodon span { display: none; } +.fa { + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + margin-right: .5em; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + /* Hide a unicode fallback character when we supply it by default. + * In fonts.scss, we hide the icon and show the fallback when other conditions are not met + */ } + .fa { + display: none; } + /* Keep this at the bottom since it will create a huge set of data */ /* * Would have liked to use Compass' built-in font-face mixin with the inline-font-files() helper, but it seems to be BROKEN in older versions! diff --git a/static/sass/style.scss b/static/sass/style.scss index 2e0ea8981..4fd9a3efd 100644 --- a/static/sass/style.scss +++ b/static/sass/style.scss @@ -2426,6 +2426,24 @@ span.highlighted { */ span { display: none; } } +.fa { + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + margin-right: .5em; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + /* Hide a unicode fallback character when we supply it by default. + * In fonts.scss, we hide the icon and show the fallback when other conditions are not met + */ + span { display: none; } +} /* Keep this at the bottom since it will create a huge set of data */ @import "fonts"; diff --git a/templates/base.html b/templates/base.html index 27daceb50..424df06f6 100644 --- a/templates/base.html +++ b/templates/base.html @@ -41,6 +41,7 @@ {% stylesheet 'style' %} {% stylesheet 'mq' %} + {% stylesheet 'font-awesome' %} {% comment %} {# equivalent to: #} @@ -235,10 +236,10 @@

  • Socialize
  • diff --git a/templates/downloads/os_list.html b/templates/downloads/os_list.html index 67db5233f..1e0177dca 100644 --- a/templates/downloads/os_list.html +++ b/templates/downloads/os_list.html @@ -1,6 +1,7 @@ {% extends "downloads/base.html" %} {% load boxes %} {% load sitetree %} +{% load sort_windows from download_tags %} {% block body_attributes %}class="python download"{% endblock %} @@ -45,7 +46,7 @@

    Stable Releases

    {% endif %} {% endif %}