diff --git a/base-requirements.txt b/base-requirements.txt index 66b693491..e42f8c654 100644 --- a/base-requirements.txt +++ b/base-requirements.txt @@ -47,3 +47,4 @@ num2words==0.5.10 django-polymorphic==3.0.0 sorl-thumbnail==12.7.0 docxtpl==0.12.0 +django-extensions==3.1.2 diff --git a/dev-requirements.txt b/dev-requirements.txt index 182beea18..2ded7e005 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -6,10 +6,14 @@ factory-boy==3.1.0 Faker==0.8.1 tblib==1.7.0 responses==0.13.3 +model-bakery==1.3.2 # Extra stuff required for local dev django-debug-toolbar==3.2.1 coverage ddt -model-bakery==1.3.2 + +# Extra stuff required by django-extensions Graph models command line +pyparsing==3.0.6 +pydot==1.4.2 diff --git a/docs/source/_images/sponsors-db.png b/docs/source/_images/sponsors-db.png new file mode 100644 index 000000000..66a3b5d88 Binary files /dev/null and b/docs/source/_images/sponsors-db.png differ diff --git a/docs/source/administration.rst b/docs/source/administration.rst index 254c36a20..7096e96a8 100644 --- a/docs/source/administration.rst +++ b/docs/source/administration.rst @@ -106,6 +106,13 @@ they represent: :contract.py: The `Contract` model which is used to generate the final contract document and other support models; +.. image:: _images/sponsors-db.png + :alt: Sponsors app's database schema + +The sponsors app is mostly an administrative one. Its only part that regular uses can interact with is +the sponsorship application form, available at ``/sponsors/application/new/``. Despite that, every +administrative operation should be done via admin actions buttons in both ``Sponsorship`` and ``Contract`` +models. Events ------ diff --git a/pydotorg/settings/base.py b/pydotorg/settings/base.py index 0b66143f5..8fc794b44 100644 --- a/pydotorg/settings/base.py +++ b/pydotorg/settings/base.py @@ -166,6 +166,7 @@ 'django_countries', 'easy_pdf', 'sorl.thumbnail', + 'django_extensions', 'banners', 'blogs', diff --git a/pydotorg/settings/local.py b/pydotorg/settings/local.py index 61c98583d..1be56697b 100644 --- a/pydotorg/settings/local.py +++ b/pydotorg/settings/local.py @@ -71,3 +71,8 @@ REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] += ( 'rest_framework.renderers.BrowsableAPIRenderer', ) + +# detailed info https://django-extensions.readthedocs.io/en/latest/graph_models.html +GRAPH_MODELS = { + 'app_labels': ["sponsors"], +} diff --git a/sponsors/use_cases.py b/sponsors/use_cases.py index d38ea27b4..e4ad49b57 100644 --- a/sponsors/use_cases.py +++ b/sponsors/use_cases.py @@ -1,11 +1,35 @@ +""" +This module holds Use Cases (UCs) implementations. These are indirections to trigger business rules +and avoid the sponsors core logic and state management to be spread across views codes. +""" +from abc import ABC, abstractmethod + from sponsors import notifications from sponsors.models import Sponsorship, Contract, SponsorContact, SponsorEmailNotificationTemplate from sponsors.pdf import render_contract_to_pdf_file, render_contract_to_docx_file -class BaseUseCaseWithNotifications: +class BaseUseCaseWithNotifications(ABC): + """ + Abstract base class to be used to implement use cases. + It holds a list of notifications to be dispatched by the UC if needed + """ notifications = [] + @classmethod + def build(cls): + """ + Factory method to explicity handle complex logic and/or dependency injection + """ + return cls(cls.notifications) + + @abstractmethod + def execute(self, *args, **kwargs): + """ + Abstract method to implement specific UC business rules + """ + pass + def __init__(self, notifications): self.notifications = notifications @@ -13,12 +37,12 @@ def notify(self, **kwargs): for notification in self.notifications: notification.notify(**kwargs) - @classmethod - def build(cls): - return cls(cls.notifications) - class CreateSponsorshipApplicationUseCase(BaseUseCaseWithNotifications): + """ + Use case called to create a new sponsorships application for submitted by a user + """ + notifications = [ notifications.AppliedSponsorshipNotificationToPSF(), notifications.AppliedSponsorshipNotificationToSponsors(), @@ -31,6 +55,9 @@ def execute(self, user, sponsor, benefits, package=None, request=None): class RejectSponsorshipApplicationUseCase(BaseUseCaseWithNotifications): + """ + Use case to enable PSF staff to reject an application + """ notifications = [ notifications.RejectedSponsorshipNotificationToPSF(), notifications.RejectedSponsorshipNotificationToSponsors(), @@ -44,6 +71,9 @@ def execute(self, sponsorship, request=None): class ApproveSponsorshipApplicationUseCase(BaseUseCaseWithNotifications): + """ + Use case to enable PSF staff to approve an application + """ notifications = [ notifications.SponsorshipApprovalLogger(), ] @@ -71,6 +101,10 @@ def execute(self, sponsorship, start_date, end_date, **kwargs): class SendContractUseCase(BaseUseCaseWithNotifications): + """ + Use case to enable PSF staff to generate the contract .docx + file and sent it over email + """ notifications = [ notifications.ContractNotificationToPSF(), # TODO: sponsor's notification will be enabled again once @@ -92,6 +126,14 @@ def execute(self, contract, **kwargs): class ExecuteExistingContractUseCase(BaseUseCaseWithNotifications): + """ + Use case to PSF Staff to finalize a sponsorship by "executing" a contract. + This UC was created to enable to enable to upload existing contracts documents + that weren't generated by the sponsors app. + + It's probable that this UC will become a legacy one once all the new + contracts and sponsorships were created via the Django application + """ notifications = [ notifications.ExecutedExistingContractLogger(), ] @@ -107,6 +149,12 @@ def execute(self, contract, contract_file, **kwargs): class ExecuteContractUseCase(ExecuteExistingContractUseCase): + """ + Use case to PSF Staff to execute a contract created by the sponsors app. + Execute a contract requires the admin user to upload the signed contract + and this will flag the contract as Executed and the corresponding Sponsorship + as Finalized. + """ notifications = [ notifications.ExecutedContractLogger(), ] @@ -114,6 +162,9 @@ class ExecuteContractUseCase(ExecuteExistingContractUseCase): class NullifyContractUseCase(BaseUseCaseWithNotifications): + """ + Use case to enable PSF staff to nullify non-executed contracts + """ notifications = [ notifications.NullifiedContractLogger(), ] @@ -127,6 +178,10 @@ def execute(self, contract, **kwargs): class SendSponsorshipNotificationUseCase(BaseUseCaseWithNotifications): + """ + Use case to enable PSF staff to send DB stored email notifications + to a list of selected sponsorships. + """ notifications = [ notifications.SendSponsorNotificationLogger(), ]