Skip to content

Commit

Permalink
Sponsorship application detail view (#1733)
Browse files Browse the repository at this point in the history
* Querset method and property shortcut to list user's visible sponsorships

* New detail view to display sponsorship application data

* Add sponsorship detail link in user's navbar

* Detail sponsorship application data

* Style user action buttons

* Add missing .user-profile-controls class as a button

* Do not display sponsorship fee if has customization

* Introduce agreed_fee property to deal with internal display logics

* Add unit tests to fix broken and naive implementation of agreed fee property
  • Loading branch information
berinhard committed Apr 20, 2021
1 parent a9ebf2f commit 8a9d173
Show file tree
Hide file tree
Showing 14 changed files with 324 additions and 36 deletions.
12 changes: 10 additions & 2 deletions pydotorg/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,16 @@ def blog_url(request):
def user_nav_bar_links(request):
nav = {}
if request.user.is_authenticated:
user = request.user.username
user = request.user
sponsorship_urls = [
{"url": sp.detail_url, "label": f"{sp.sponsor.name}'s sponsorship"}
for sp in user.sponsorships
]
nav = {
"account": {
"label": "Your Account",
"urls": [
{"url": reverse("users:user_detail", args=[user]), "label": "View profile"},
{"url": reverse("users:user_detail", args=[user.username]), "label": "View profile"},
{"url": reverse("users:user_profile_edit"), "label": "Edit profile"},
{"url": reverse("account_change_password"), "label": "Change password"},
],
Expand All @@ -48,6 +52,10 @@ def user_nav_bar_links(request):
"urls": [
{"url": reverse("users:user_nominations_view"), "label": "Nominations"},
],
},
"sponsorships": {
"label": "Sponsorships",
"urls": sponsorship_urls,
}
}

Expand Down
26 changes: 26 additions & 0 deletions pydotorg/tests/test_context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ def test_user_nav_bar_links_for_non_psf_members(self):
{"url": reverse("users:user_nominations_view"), "label": "Nominations"},
{"url": reverse("users:user_membership_create"), "label": "Become a PSF member"},
],
},
"sponsorships": {
"label": "Sponsorships",
"urls": [],
}
}

Expand Down Expand Up @@ -78,6 +82,10 @@ def test_user_nav_bar_links_for_psf_members(self):
{"url": reverse("users:user_nominations_view"), "label": "Nominations"},
{"url": reverse("users:user_membership_edit"), "label": "Edit PSF membership"},
],
},
"sponsorships": {
"label": "Sponsorships",
"urls": [],
}
}

Expand All @@ -86,6 +94,24 @@ def test_user_nav_bar_links_for_psf_members(self):
context_processors.user_nav_bar_links(request)
)

def test_user_nav_bar_sponsorship_links(self):
request = self.factory.get('/about/')
request.user = baker.make(settings.AUTH_USER_MODEL, username='foo')
sponsorships = baker.make("sponsors.Sponsorship", submited_by=request.user, _quantity=2, _fill_optional=True)

expected_sponsorships = {
"label": "Sponsorships",
"urls": [
{"url": sp.detail_url, "label": f"{sp.sponsor.name}'s sponsorship"}
for sp in request.user.sponsorships
]
}

self.assertEqual(
expected_sponsorships,
context_processors.user_nav_bar_links(request)['USER_NAV_BAR']['sponsorships']
)

def test_user_nav_bar_links_for_anonymous_user(self):
request = self.factory.get('/about/')
request.user = AnonymousUser()
Expand Down
9 changes: 9 additions & 0 deletions sponsors/managers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
from django.db.models import Q, Subquery
from django.db.models.query import QuerySet


class SponsorshipQuerySet(QuerySet):
def in_progress(self):
status = [self.model.APPLIED, self.model.APPROVED]
return self.filter(status__in=status)

def visible_to(self, user):
contacts = user.sponsorcontact_set.values_list('sponsor_id', flat=True)
status = [self.model.APPLIED, self.model.APPROVED, self.model.FINALIZED]
return self.filter(
Q(submited_by=user) | Q(sponsor_id__in=Subquery(contacts)),
status__in=status,
).select_related('sponsor')
19 changes: 19 additions & 0 deletions sponsors/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ def new(cls, sponsor, benefits, package=None, submited_by=None):
"""
for_modified_package = False
package_benefits = []

if package and package.has_user_customization(benefits):
package_benefits = package.benefits.all()
for_modified_package = True
Expand Down Expand Up @@ -345,6 +346,20 @@ def estimated_cost(self):
or 0
)

@property
def agreed_fee(self):
valid_status = [Sponsorship.APPROVED, Sponsorship.FINALIZED]
if self.status in valid_status:
return self.sponsorship_fee
try:
package = SponsorshipPackage.objects.get(name=self.level_name)
benefits = [sb.sponsorship_benefit for sb in self.package_benefits.all().select_related('sponsorship_benefit')]
if package and not package.has_user_customization(benefits):
return self.sponsorship_fee
except SponsorshipPackage.DoesNotExist: # sponsorship level names can change over time
return None


def reject(self):
if self.REJECTED not in self.next_status:
msg = f"Can't reject a {self.get_status_display()} sponsorship."
Expand Down Expand Up @@ -379,6 +394,10 @@ def verified_emails(self):
def admin_url(self):
return reverse("admin:sponsors_sponsorship_change", args=[self.pk])

@property
def detail_url(self):
return reverse("sponsorship_application_detail", args=[self.pk])

@cached_property
def package_benefits(self):
return self.benefits.filter(added_by_user=False)
Expand Down
29 changes: 29 additions & 0 deletions sponsors/tests/test_managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from model_bakery import baker

from django.conf import settings
from django.test import TestCase

from ..models import Sponsorship


class SponsorshipQuerySetTests(TestCase):

def setUp(self):
self.user = baker.make(settings.AUTH_USER_MODEL)
self.contact = baker.make('sponsors.SponsorContact', user=self.user)

def test_visible_to_user(self):
visible = [
baker.make(Sponsorship, submited_by=self.user, status=Sponsorship.APPLIED),
baker.make(Sponsorship, sponsor=self.contact.sponsor, status=Sponsorship.APPROVED),
baker.make(Sponsorship, submited_by=self.user, status=Sponsorship.FINALIZED),
]
baker.make(Sponsorship) # should not be visible because it's from other sponsor
baker.make(Sponsorship, submited_by=self.user, status=Sponsorship.REJECTED) # don't list rejected

qs = Sponsorship.objects.visible_to(self.user)

self.assertEqual(len(visible), qs.count())
for sp in visible:
self.assertIn(sp, qs)
self.assertEqual(list(qs), list(self.user.sponsorships))
16 changes: 16 additions & 0 deletions sponsors/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def test_create_new_sponsorship(self):
self.assertIsNone(sponsorship.end_date)
self.assertEqual(sponsorship.level_name, "")
self.assertIsNone(sponsorship.sponsorship_fee)
self.assertIsNone(sponsorship.agreed_fee)
self.assertTrue(sponsorship.for_modified_package)

self.assertEqual(sponsorship.benefits.count(), len(self.benefits))
Expand All @@ -97,6 +98,7 @@ def test_create_new_sponsorship_with_package(self):

self.assertEqual(sponsorship.level_name, "PSF Sponsorship Program")
self.assertEqual(sponsorship.sponsorship_fee, 100)
self.assertEqual(sponsorship.agreed_fee, 100) # can display the price because there's not customizations
self.assertFalse(sponsorship.for_modified_package)
for benefit in sponsorship.benefits.all():
self.assertFalse(benefit.added_by_user)
Expand All @@ -109,6 +111,7 @@ def test_create_new_sponsorship_with_package_modifications(self):

self.assertTrue(sponsorship.for_modified_package)
self.assertEqual(sponsorship.benefits.count(), 2)
self.assertIsNone(sponsorship.agreed_fee) # can't display the price with customizations
for benefit in sponsorship.benefits.all():
self.assertFalse(benefit.added_by_user)

Expand Down Expand Up @@ -193,6 +196,19 @@ def test_raise_exception_when_trying_to_create_sponsorship_for_same_sponsor(self
with self.assertRaises(SponsorWithExistingApplicationException):
Sponsorship.new(self.sponsor, self.benefits)

def test_display_agreed_fee_for_approved_and_finalized_status(self):
sponsorship = Sponsorship.new(self.sponsor, self.benefits)
sponsorship.sponsorship_fee = 2000
sponsorship.save()

finalized_status = [Sponsorship.APPROVED, Sponsorship.FINALIZED]
for status in finalized_status:
sponsorship.status = status
sponsorship.save()

self.assertEqual(sponsorship.agreed_fee, 2000)



class SponsorshipPackageTests(TestCase):
def setUp(self):
Expand Down
39 changes: 39 additions & 0 deletions sponsors/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,3 +613,42 @@ def test_message_user_if_approving_invalid_sponsorship(self):
self.assertEqual(self.sponsorship.status, Sponsorship.FINALIZED)
msg = list(get_messages(response.wsgi_request))[0]
assertMessage(msg, "Can't approve a Finalized sponsorship.", messages.ERROR)


class SponsorshipDetailViewTests(TestCase):

def setUp(self):
self.user = baker.make(settings.AUTH_USER_MODEL)
self.client.force_login(self.user)
self.sponsorship = baker.make(
Sponsorship, submited_by=self.user, status=Sponsorship.APPLIED, _fill_optional=True
)
self.url = reverse(
"sponsorship_application_detail", args=[self.sponsorship.pk]
)

def test_display_template_with_sponsorship_info(self):
response = self.client.get(self.url)
context = response.context

self.assertTemplateUsed(response, "sponsors/sponsorship_detail.html")
self.assertEqual(context["sponsorship"], self.sponsorship)

def test_404_if_sponsorship_does_not_exist(self):
self.sponsorship.delete()
response = self.client.get(self.url)
self.assertEqual(response.status_code, 404)

def test_login_required(self):
login_url = settings.LOGIN_URL
redirect_url = f"{login_url}?next={self.url}"
self.client.logout()

r = self.client.get(self.url)

self.assertRedirects(r, redirect_url)

def test_404_if_sponsorship_does_not_belong_to_user(self):
self.client.force_login(baker.make(settings.AUTH_USER_MODEL)) # log in with a new user
response = self.client.get(self.url)
self.assertEqual(response.status_code, 404)
7 changes: 6 additions & 1 deletion sponsors/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.conf.urls import url
from django.views.generic.base import TemplateView
from django.urls import path

from . import views

Expand All @@ -15,4 +15,9 @@
views.SelectSponsorshipApplicationBenefitsView.as_view(),
name="select_sponsorship_application_benefits",
),
path(
"application/<int:pk>/detail/",
views.SponsorshipDetailView.as_view(),
name="sponsorship_application_detail",
),
]
15 changes: 12 additions & 3 deletions sponsors/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import transaction
from django.forms.utils import ErrorList
from django.utils.decorators import method_decorator
from django.http import JsonResponse
from django.views.generic import ListView, FormView
from django.urls import reverse_lazy, reverse
from django.shortcuts import redirect, render
from django.urls import reverse_lazy, reverse
from django.utils.decorators import method_decorator
from django.views.generic import ListView, FormView, DetailView

from .models import (
Sponsor,
Expand Down Expand Up @@ -174,3 +174,12 @@ def form_valid(self, form):
)
cookies.delete_sponsorship_selected_benefits(response)
return response


@method_decorator(login_required(login_url=settings.LOGIN_URL), name="dispatch")
class SponsorshipDetailView(DetailView):
context_object_name = 'sponsorship'
template_name = 'sponsors/sponsorship_detail.html'

def get_queryset(self):
return self.request.user.sponsorships

0 comments on commit 8a9d173

Please sign in to comment.