Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e320f6f
Add missing migrations after changes on the field names and texts
berinhard Oct 26, 2021
668609d
Add new benefit feature to flag benefits with required image assets
berinhard Oct 26, 2021
9975431
Add new configuration to admin
berinhard Oct 26, 2021
0be8809
Validate min/max ranges
berinhard Oct 26, 2021
129d2ed
Model class to hold generic img assets
berinhard Oct 26, 2021
d7355d1
Use UUID to format file paths
berinhard Oct 26, 2021
e21da03
Add new class to require text inputs
berinhard Oct 26, 2021
9366efd
Add asset to store text input from user
berinhard Oct 26, 2021
acba567
Do not use UUID field as primary key
berinhard Oct 26, 2021
97cf490
Move benefit feature creation to specific method under feature cfg
berinhard Oct 27, 2021
afc4be5
Remove duplicated tests
berinhard Oct 27, 2021
bf4e2b7
Create empty ImgAsset during sponsorship creation
berinhard Oct 27, 2021
1bb967e
Create empty TextAsset during sponsorship creation
berinhard Oct 27, 2021
5d61930
Check if asset relates to sponsor or sponsorship before creating it
berinhard Oct 27, 2021
1a5a879
Add generic relation to iter over all assets from sponsor/sponsorship…
berinhard Oct 27, 2021
879cc0e
Assets base models Meta should inherit too
berinhard Oct 29, 2021
bc98fe5
Prevent same required asset from being created twice
berinhard Oct 29, 2021
74604dc
Optimizes query to list sponsorship benefits
berinhard Oct 29, 2021
7b501c8
List sponsorship assets under sponsor/sponsorship detail admin
berinhard Oct 29, 2021
30a8672
Add extra card on sponsorship detail page to link to assets form
berinhard Nov 1, 2021
60ac592
Revert "Add extra card on sponsorship detail page to link to assets f…
berinhard Nov 2, 2021
083ca71
Update the docs of the sponsors app to give a broader vision of the m…
berinhard Nov 3, 2021
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
19 changes: 14 additions & 5 deletions docs/source/administration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,21 @@ The jobs application is using to display Python jobs on the site. The data items
Sponsors
--------

The Sponsors app is a place to store PSF Sponsors. Sponsors have to be associated to a Company model from
the companies app. If they are marked as `is_published` they will be shown on the main sponsor page which
is located at /sponsors/.
The Sponsors app is a place to store PSF Sponsors and Sponsorships. This is the most complex app in the
project due to the multiple possibilities on how to configure a sponsorship and, to support this, the
app has a lot of models that are grouped by context. Here's a list of the group of models and what do
they represent:

:sponsorship.py: The `Sponsorship` model and all the related information to configure a new sponsorship
appplication like programs, packages and benefits;
:benefits.py: List models that are used to configure benefits. Here you'll find models that forces a
benefit to have an asset or controls it maximum quantity;
:assets.py: Models that are used to configure the type of assets that a benefit can have;
:sponsors.py: Has the `Sponsor` model and all related information such as their contacts and benefits;
:notifications.py: Any type of sponsor notification that's configurable via admin;
:contract.py: The `Contract` model which is used to generate the final contract document and other
support models;

If a Sponsor is marked as `featured` they will be included in the sponsor rotation on the main PSF landing
page. In the fourth "Sponsors" column.

Events
------
Expand Down
77 changes: 53 additions & 24 deletions sponsors/admin.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,41 @@
from django.contrib.contenttypes.admin import GenericTabularInline
from ordered_model.admin import OrderedModelAdmin
from polymorphic.admin import PolymorphicInlineSupportMixin, StackedPolymorphicInline

from django.db.models import Subquery
from django.template import Context, Template
from django.contrib import admin
from django.contrib.humanize.templatetags.humanize import intcomma
from django.urls import path, reverse
from django.urls import path, reverse, resolve
from django.utils.functional import cached_property
from django.utils.html import mark_safe

from mailing.admin import BaseEmailTemplateAdmin
from .models import (
SponsorshipPackage,
SponsorshipProgram,
SponsorshipBenefit,
Sponsor,
Sponsorship,
SponsorContact,
SponsorBenefit,
LegalClause,
Contract,
BenefitFeature,
BenefitFeatureConfiguration,
LogoPlacementConfiguration,
TieredQuantityConfiguration,
EmailTargetableConfiguration,
SponsorEmailNotificationTemplate,
)
from sponsors.models import *
from sponsors import views_admin
from sponsors.forms import SponsorshipReviewAdminForm, SponsorBenefitAdminInlineForm
from sponsors.forms import SponsorshipReviewAdminForm, SponsorBenefitAdminInlineForm, RequiredImgAssetConfigurationForm
from cms.admin import ContentManageableModelAdmin


class AssetsInline(GenericTabularInline):
model = GenericAsset
extra = 0
max_num = 0
has_delete_permission = lambda self, request, obj: False
readonly_fields = ["internal_name", "user_submitted_info", "value"]

def value(self, request, obj=None):
if not obj or not obj.value:
return ""
return obj.value
value.short_description = "Submitted information"

def user_submitted_info(self, request, obj=None):
return bool(self.value(request, obj))
user_submitted_info.short_description = "Fullfilled data?"
user_submitted_info.boolean = True


@admin.register(SponsorshipProgram)
class SponsorshipProgramAdmin(OrderedModelAdmin):
ordering = ("order",)
Expand All @@ -55,11 +59,20 @@ class EmailTargetableConfigurationInline(StackedPolymorphicInline.Child):
def display(self, obj):
return "Enabled"

class RequiredImgAssetConfigurationInline(StackedPolymorphicInline.Child):
model = RequiredImgAssetConfiguration
form = RequiredImgAssetConfigurationForm

class RequiredTextAssetConfigurationInline(StackedPolymorphicInline.Child):
model = RequiredTextAssetConfiguration

model = BenefitFeatureConfiguration
child_inlines = [
LogoPlacementConfigurationInline,
TieredQuantityConfigurationInline,
EmailTargetableConfigurationInline,
RequiredImgAssetConfigurationInline,
RequiredTextAssetConfigurationInline,
]


Expand Down Expand Up @@ -144,7 +157,7 @@ class SponsorContactInline(admin.TabularInline):

@admin.register(Sponsor)
class SponsorAdmin(ContentManageableModelAdmin):
inlines = [SponsorContactInline]
inlines = [SponsorContactInline, AssetsInline]
search_fields = ["name"]


Expand All @@ -153,6 +166,7 @@ class SponsorBenefitInline(admin.TabularInline):
form = SponsorBenefitAdminInlineForm
fields = ["sponsorship_benefit", "benefit_internal_value"]
extra = 0
max_num = 0

def has_add_permission(self, request, obj=None):
has_add_permission = super().has_add_permission(request, obj=obj)
Expand All @@ -172,6 +186,10 @@ 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")


class TargetableEmailBenefitsFilter(admin.SimpleListFilter):
title = "targetable email benefits"
Expand All @@ -185,7 +203,7 @@ def benefits(self):

def lookups(self, request, model_admin):
return [
(k, b.name) for k, b in self.benefits.items()
(k, b.name) for k, b in self.benefits.items()
]

def queryset(self, request, queryset):
Expand All @@ -202,7 +220,7 @@ def queryset(self, request, queryset):
class SponsorshipAdmin(admin.ModelAdmin):
change_form_template = "sponsors/admin/sponsorship_change_form.html"
form = SponsorshipReviewAdminForm
inlines = [SponsorBenefitInline]
inlines = [SponsorBenefitInline, AssetsInline]
search_fields = ["sponsor__name"]
list_display = [
"sponsor",
Expand Down Expand Up @@ -264,7 +282,7 @@ class SponsorshipAdmin(admin.ModelAdmin):

def get_queryset(self, *args, **kwargs):
qs = super().get_queryset(*args, **kwargs)
return qs.select_related("sponsor", "package")
return qs.select_related("sponsor", "package", "submited_by")

def send_notifications(self, request, queryset):
return views_admin.send_sponsorship_notifications_action(self, request, queryset)
Expand Down Expand Up @@ -306,6 +324,7 @@ def get_estimated_cost(self, obj):
cost = intcomma(obj.estimated_cost)
html = f"{cost} USD <br/><b>Important: </b> {msg}"
return mark_safe(html)

get_estimated_cost.short_description = "Estimated cost"

def get_contract(self, obj):
Expand All @@ -314,6 +333,7 @@ def get_contract(self, obj):
url = reverse("admin:sponsors_contract_change", args=[obj.contract.pk])
html = f"<a href='{url}' target='_blank'>{obj.contract}</a>"
return mark_safe(html)

get_contract.short_description = "Contract"

def get_urls(self):
Expand Down Expand Up @@ -346,14 +366,17 @@ def get_urls(self):

def get_sponsor_name(self, obj):
return obj.sponsor.name

get_sponsor_name.short_description = "Name"

def get_sponsor_description(self, obj):
return obj.sponsor.description

get_sponsor_description.short_description = "Description"

def get_sponsor_landing_page_url(self, obj):
return obj.sponsor.landing_page_url

get_sponsor_landing_page_url.short_description = "Landing Page URL"

def get_sponsor_web_logo(self, obj):
Expand All @@ -362,6 +385,7 @@ def get_sponsor_web_logo(self, obj):
context = Context({'sponsor': obj.sponsor})
html = template.render(context)
return mark_safe(html)

get_sponsor_web_logo.short_description = "Web Logo"

def get_sponsor_print_logo(self, obj):
Expand All @@ -373,10 +397,12 @@ def get_sponsor_print_logo(self, obj):
context = Context({'img': img})
html = template.render(context)
return mark_safe(html) if html else "---"

get_sponsor_print_logo.short_description = "Print Logo"

def get_sponsor_primary_phone(self, obj):
return obj.sponsor.primary_phone

get_sponsor_primary_phone.short_description = "Primary Phone"

def get_sponsor_mailing_address(self, obj):
Expand All @@ -395,6 +421,7 @@ def get_sponsor_mailing_address(self, obj):
html += f"<p>{mail_row}</p>"
html += f"<p>{sponsor.postal_code}</p>"
return mark_safe(html)

get_sponsor_mailing_address.short_description = "Mailing/Billing Address"

def get_sponsor_contacts(self, obj):
Expand All @@ -415,6 +442,7 @@ def get_sponsor_contacts(self, obj):
)
html += "</ul>"
return mark_safe(html)

get_sponsor_contacts.short_description = "Contacts"

def rollback_to_editing_view(self, request, pk):
Expand Down Expand Up @@ -538,15 +566,16 @@ def document_link(self, obj):
if url and msg:
html = f'<a href="{url}" target="_blank">{msg}</a>'
return mark_safe(html)
document_link.short_description = "Contract document"

document_link.short_description = "Contract document"

def get_sponsorship_url(self, obj):
if not obj.sponsorship:
return "---"
url = reverse("admin:sponsors_sponsorship_change", args=[obj.sponsorship.pk])
html = f"<a href='{url}' target='_blank'>{obj.sponsorship}</a>"
return mark_safe(html)

get_sponsorship_url.short_description = "Sponsorship"

def get_urls(self):
Expand Down
22 changes: 21 additions & 1 deletion sponsors/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
SponsorContact,
Sponsorship,
SponsorBenefit,
SponsorEmailNotificationTemplate
SponsorEmailNotificationTemplate,
RequiredImgAssetConfiguration,
)


Expand Down Expand Up @@ -526,3 +527,22 @@ def clean(self):
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.contacts_formset.save()


class RequiredImgAssetConfigurationForm(forms.ModelForm):

def clean(self):
data = super().clean()

min_width, max_width = data.get("min_width"), data.get("max_width")
if min_width and max_width and max_width < min_width:
raise forms.ValidationError("Max width must be greater than min width")
min_height, max_height = data.get("min_height"), data.get("max_height")
if min_height and max_height and max_height < min_height:
raise forms.ValidationError("Max height must be greater than min height")

return data

class Meta:
model = RequiredImgAssetConfiguration
fields = "__all__"
54 changes: 54 additions & 0 deletions sponsors/migrations/0051_auto_20211022_1403.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Generated by Django 2.2.24 on 2021-10-22 14:03

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


class Migration(migrations.Migration):

dependencies = [
('sponsors', '0050_emailtargetable_emailtargetableconfiguration'),
]

operations = [
migrations.AlterField(
model_name='sponsor',
name='description',
field=models.TextField(help_text='Brief description of the sponsor for public display.', verbose_name='Description'),
),
migrations.AlterField(
model_name='sponsor',
name='landing_page_url',
field=models.URLField(blank=True, help_text='Landing page URL. This may be provided by the sponsor, however the linked page may not contain any sales or marketing information.', null=True, verbose_name='Landing page URL'),
),
migrations.AlterField(
model_name='sponsor',
name='name',
field=models.CharField(help_text='Name of the sponsor, for public display.', max_length=100, verbose_name='Name'),
),
migrations.AlterField(
model_name='sponsor',
name='primary_phone',
field=models.CharField(max_length=32, verbose_name='Primary Phone'),
),
migrations.AlterField(
model_name='sponsor',
name='print_logo',
field=models.FileField(blank=True, help_text='For printed materials, signage, and projection. SVG or EPS', null=True, upload_to='sponsor_print_logos', verbose_name='Print logo'),
),
migrations.AlterField(
model_name='sponsor',
name='twitter_handle',
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Twitter handle'),
),
migrations.AlterField(
model_name='sponsor',
name='web_logo',
field=models.ImageField(help_text='For display on our sponsor webpage. High resolution PNG or JPG, smallest dimension no less than 256px', upload_to='sponsor_web_logos', verbose_name='Web logo'),
),
migrations.AlterField(
model_name='sponsorcontact',
name='primary',
field=models.BooleanField(default=False, help_text='The primary contact for a sponsorship will be responsible for managing deliverables we need to fulfill benefits. Primary contacts will receive all email notifications regarding sponsorship. '),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Generated by Django 2.2.24 on 2021-10-22 14:04

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


class Migration(migrations.Migration):

dependencies = [
('sponsors', '0051_auto_20211022_1403'),
]

operations = [
migrations.CreateModel(
name='RequiredImgAsset',
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')),
('related_to', models.CharField(choices=[('sponsor', 'Sponsor'), ('sponsorship', 'Sponsorship')], help_text='To which instance (Sponsor or Sponsorship) should this asset relate to.', max_length=30, verbose_name='Related To')),
('internal_name', models.CharField(db_index=True, help_text='Unique name used internally to control if the sponsor/sponsorship already has the asset', max_length=128, unique=True, verbose_name='Internal Name')),
('min_width', models.PositiveIntegerField()),
('max_width', models.PositiveIntegerField()),
('min_height', models.PositiveIntegerField()),
('max_height', models.PositiveIntegerField()),
],
options={
'verbose_name': 'Require Image Benefit',
'verbose_name_plural': 'Require Image Benefits',
'abstract': False,
'base_manager_name': 'objects',
},
bases=('sponsors.benefitfeature', models.Model),
),
migrations.CreateModel(
name='RequiredImgAssetConfiguration',
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')),
('related_to', models.CharField(choices=[('sponsor', 'Sponsor'), ('sponsorship', 'Sponsorship')], help_text='To which instance (Sponsor or Sponsorship) should this asset relate to.', max_length=30, verbose_name='Related To')),
('internal_name', models.CharField(db_index=True, help_text='Unique name used internally to control if the sponsor/sponsorship already has the asset', max_length=128, unique=True, verbose_name='Internal Name')),
('min_width', models.PositiveIntegerField()),
('max_width', models.PositiveIntegerField()),
('min_height', models.PositiveIntegerField()),
('max_height', models.PositiveIntegerField()),
],
options={
'verbose_name': 'Require Image Configuration',
'verbose_name_plural': 'Require Image Configurations',
'abstract': False,
'base_manager_name': 'objects',
},
bases=('sponsors.benefitfeatureconfiguration', models.Model),
),
]
Loading