Skip to content

Benefit Features db modeling #1813

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

Merged
merged 17 commits into from
Jul 22, 2021
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
1 change: 1 addition & 0 deletions base-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ django-countries==6.1.3
xhtml2pdf==0.2.5
django-easy-pdf==0.1.1
num2words==0.5.10
django-polymorphic==2.1.2
1 change: 1 addition & 0 deletions pydotorg/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
'rest_framework',
'rest_framework.authtoken',
'django_filters',
'polymorphic',
]

# Fixtures
Expand Down
16 changes: 15 additions & 1 deletion sponsors/admin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from ordered_model.admin import OrderedModelAdmin
from polymorphic.admin import PolymorphicInlineSupportMixin, StackedPolymorphicInline

from django.contrib import admin
from django.contrib.humanize.templatetags.humanize import intcomma
Expand All @@ -15,6 +16,8 @@
SponsorBenefit,
LegalClause,
Contract,
BenefitFeatureConfiguration,
LogoPlacementConfiguration,
)
from sponsors import views_admin
from sponsors.forms import SponsorshipReviewAdminForm, SponsorBenefitAdminInlineForm
Expand All @@ -30,8 +33,19 @@ class SponsorshipProgramAdmin(OrderedModelAdmin):
]


class BenefitFeatureConfigurationInline(StackedPolymorphicInline):
class LogoPlacementConfigurationInline(StackedPolymorphicInline.Child):
model = LogoPlacementConfiguration

model = BenefitFeatureConfiguration
child_inlines = [
LogoPlacementConfigurationInline
]


@admin.register(SponsorshipBenefit)
class SponsorshipBenefitAdmin(OrderedModelAdmin):
class SponsorshipBenefitAdmin(PolymorphicInlineSupportMixin, OrderedModelAdmin):
inlines = [BenefitFeatureConfigurationInline]
ordering = ("program", "order")
list_display = [
"program",
Expand Down
18 changes: 18 additions & 0 deletions sponsors/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from enum import Enum


class LogoPlacementChoices(Enum):
SIDEBAR = "sidebar"
SPONSORS_PAGE = "sponsors"
JOBS = "jobs"
BLOG = "blogpost"
FOOTER = "footer"
DOCS = "docs"
DOWNLOAD_PAGE = "download"
DEV_GUIDE = "devguide"

class PublisherChoices(Enum):
FOUNDATION = "psf"
PYCON = "pycon"
PYPI = "pypi"
CORE_DEV = "core"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have other choice fields in the sponsors app, such as sponsorship's status or contract's status. For the sake of integrity, I'd like to refactor them to also use Enum too if you think it's a good idea.

Copy link
Member

Choose a reason for hiding this comment

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

yes! i think this is a wonderful idea!

89 changes: 89 additions & 0 deletions sponsors/management/commands/gen_sponsors_csv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import csv
from django.core.management import BaseCommand
from django.template.defaultfilters import slugify

from sponsors.models import Sponsorship, Contract

# This command works as a faster entrypoint to generate a CSV file
# with all the sponsor data required by the sponsor placement server
# Python.org will use to display sponsors' logo. This command can
# become an optional one once such operation is enabled via admin.

class Command(BaseCommand):
"""
Generate CSV with sponsors data to be sent to sponsor placement server
"""
help = "Generate CSV with sponsors data to be sent to sponsor placement server"

def handle(self, **options):
qs = Sponsorship.objects.finalized().select_related('sponsor')
if not qs.exists():
print("There's no finalized Sponsorship.")
return

rows = []
for sponsorship in qs.iterator():
base_row = {
"sponsor": sponsorship.sponsor.name,
"description": sponsorship.sponsor.description,
"logo": sponsorship.sponsor.web_logo.url,
"sponsor_url": sponsorship.sponsor.landing_page_url,
"start_date": sponsorship.start_date.isoformat(),
"end_date": sponsorship.end_date.isoformat(),
}

benefits = sponsorship.benefits.select_related('sponsorship_benefit')
for benefit in benefits.iterator():
# TODO implement this as DB objects not hardcoded checks
# - check for logo placements
# - use the program to determine the publisher
# - use the flight to determine the placement (footer/sidebar/sponsor etc)
flight_mapping = {
# Foundation
"Logo on python.org": "sponsors",
"jobs.python.org support": "jobs",
"Logo listed on PSF blog": "blogspot",
# Pycon
"PyCon website Listing": "sponsors",
# Pypi
"Logo on the PyPI sponsors page": "sponsors",
"Logo in a prominent position on the PyPI project detail page": "sidebar",
"Logo on the PyPI footer": "footer",
# Core dev
"docs.python.org recognition": "docs",
"Logo on python.org/downloads/": "docs-download",
"Logo recognition on devguide.python.org/": "devguide"
}

publisher = benefit.program.name
flight = flight_mapping.get(benefit.name)
if publisher and flight:
row = base_row.copy()

if not sponsorship.sponsor.web_logo:
print(f"WARNING: sponsor {sponsorship.sponsor} without logo")
continue

if publisher == "Foundation" and flight in ["jobs", "blogspot"]:
row["sponsor_url"] = "https://www.python.org/psf/sponsorship/sponsors/"

row["publisher"] = publisher
row["flight"] = slugify(publisher) + '-' + flight
rows.append(row)

columns = [
"publisher",
"flight",
"sponsor",
"description",
"logo",
"start_date",
"end_date",
"sponsor_url",
]
with open('sponsors.csv', 'w') as fd:
writer = csv.DictWriter(fd, fieldnames=columns)
writer.writeheader()
writer.writerows(rows)

print(f"Done!")
23 changes: 23 additions & 0 deletions sponsors/managers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from django.db.models import Count
from ordered_model.models import OrderedModelManager
from django.db.models import Q, Subquery
from django.db.models.query import QuerySet

Expand All @@ -18,10 +20,31 @@ def visible_to(self, user):
status__in=status,
).select_related('sponsor')

def finalized(self):
return self.filter(status=self.model.FINALIZED)


class SponsorContactQuerySet(QuerySet):
def get_primary_contact(self, sponsor):
contact = self.filter(sponsor=sponsor, primary=True).first()
if not contact:
raise self.model.DoesNotExist()
return contact


class SponsorshipBenefitManager(OrderedModelManager):
def with_conflicts(self):
return self.exclude(conflicts__isnull=True)

def without_conflicts(self):
return self.filter(conflicts__isnull=True)

def add_ons(self):
return self.annotate(num_packages=Count("packages")).filter(num_packages=0)

def with_packages(self):
return (
self.annotate(num_packages=Count("packages"))
.exclude(num_packages=0)
.order_by("-num_packages")
)
48 changes: 48 additions & 0 deletions sponsors/migrations/0029_auto_20210715_2015.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 2.0.13 on 2021-07-15 20:15

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('sponsors', '0028_auto_20210707_1426'),
]

operations = [
migrations.CreateModel(
name='BenefitFeatureConfiguration',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
options={
'verbose_name': 'Benefit Feature Configuration',
'verbose_name_plural': 'Benefit Feature Configurations',
},
),
migrations.CreateModel(
name='LogoPlacementConfiguration',
fields=[
('benefitfeatureconfiguration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='sponsors.BenefitFeatureConfiguration')),
('publisher', models.CharField(choices=[('psf', 'Foundation'), ('pycon', 'Pycon'), ('pypi', 'Pypi'), ('core', 'Core Dev')], help_text='On which site should the logo be displayed?', max_length=30, verbose_name='Publisher')),
('logo_place', models.CharField(choices=[('sidebar', 'Sidebar'), ('sponsors', 'Sponsors Page'), ('jobs', 'Jobs'), ('blogpost', 'Blog'), ('footer', 'Footer'), ('docs', 'Docs'), ('download', 'Download Page'), ('devguide', 'Dev Guide')], help_text='Where the logo should be placed?', max_length=30, verbose_name='Logo Placement')),
],
options={
'verbose_name': 'Logo Placement Configuration',
'verbose_name_plural': 'Logo Placement Configurations',
},
bases=('sponsors.benefitfeatureconfiguration',),
),
migrations.AddField(
model_name='benefitfeatureconfiguration',
name='benefit',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sponsors.SponsorshipBenefit'),
),
migrations.AddField(
model_name='benefitfeatureconfiguration',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_sponsors.benefitfeatureconfiguration_set+', to='contenttypes.ContentType'),
),
]
54 changes: 54 additions & 0 deletions sponsors/migrations/0030_auto_20210715_2023.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Generated by Django 2.0.13 on 2021-07-15 20:23

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('sponsors', '0029_auto_20210715_2015'),
]

operations = [
migrations.CreateModel(
name='BenefitFeature',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
options={
'verbose_name': 'Benefit Feature',
'verbose_name_plural': 'Benefit Features',
},
),
migrations.AlterModelOptions(
name='logoplacementconfiguration',
options={'base_manager_name': 'objects', 'verbose_name': 'Logo Placement Configuration', 'verbose_name_plural': 'Logo Placement Configurations'},
),
migrations.CreateModel(
name='LogoPlacement',
fields=[
('benefitfeature_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='sponsors.BenefitFeature')),
('publisher', models.CharField(choices=[('psf', 'Foundation'), ('pycon', 'Pycon'), ('pypi', 'Pypi'), ('core', 'Core Dev')], help_text='On which site should the logo be displayed?', max_length=30, verbose_name='Publisher')),
('logo_place', models.CharField(choices=[('sidebar', 'Sidebar'), ('sponsors', 'Sponsors Page'), ('jobs', 'Jobs'), ('blogpost', 'Blog'), ('footer', 'Footer'), ('docs', 'Docs'), ('download', 'Download Page'), ('devguide', 'Dev Guide')], help_text='Where the logo should be placed?', max_length=30, verbose_name='Logo Placement')),
],
options={
'verbose_name': 'Logo Placement',
'verbose_name_plural': 'Logo Placement',
'abstract': False,
'base_manager_name': 'objects',
},
bases=('sponsors.benefitfeature', models.Model),
),
migrations.AddField(
model_name='benefitfeature',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_sponsors.benefitfeature_set+', to='contenttypes.ContentType'),
),
migrations.AddField(
model_name='benefitfeature',
name='sponsor_benefit',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sponsors.SponsorBenefit'),
),
]
Loading