This repository has been archived by the owner on Feb 13, 2019. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #157 from theonion/payroll-email
Payroll email
- Loading branch information
Showing
9 changed files
with
325 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
__version__ = "0.10.0" | ||
__version__ = "0.10.1" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
"""Module to generate and send a report to contributors containing a log of their contributions.""" | ||
from django.conf import settings | ||
from django.contrib.auth import get_user_model | ||
from django.core.mail import EmailMultiAlternatives | ||
from django.template import loader | ||
from django.utils import timezone | ||
|
||
|
||
User = get_user_model() | ||
|
||
|
||
# Define constants. | ||
CONTRIBUTION_SETTINGS = getattr(settings, "CONTRIBUTIONS", {}) | ||
EMAIL_SETTINGS = CONTRIBUTION_SETTINGS.get("EMAIL", {}) | ||
DEFAULT_SUBJECT = "Contribution Report." | ||
TEMPLATE = "reporting/__contribution_report.html" | ||
|
||
|
||
class EmailReport(object): | ||
"""Generate an email report for contributors.""" | ||
|
||
def __init__(self, **kwargs): | ||
# if "start" in kwargs: | ||
self.now = timezone.now() | ||
self.month = kwargs.get("month", self.now.month) | ||
self.year = kwargs.get("year", self.now.year) | ||
|
||
self._contributors = [] | ||
self._deadline = kwargs.get("deadline") | ||
self._start = kwargs.get("start") | ||
self._end = kwargs.get("end") | ||
|
||
def send_contributor_email(self, contributor): | ||
"""Send an EmailMessage object for a given contributor.""" | ||
body = self.get_email_body(contributor) | ||
mail = EmailMultiAlternatives( | ||
subject=EMAIL_SETTINGS.get("SUBJECT", DEFAULT_SUBJECT), | ||
from_email=EMAIL_SETTINGS.get("FROM"), | ||
headers={"Reply-To": EMAIL_SETTINGS.get("REPLY_TO")} | ||
) | ||
mail.attach_alternative(body, "text/html") | ||
if EMAIL_SETTINGS.get("ACTIVE", False): | ||
mail.to = [contributor.email] | ||
else: | ||
mail.to = EMAIL_SETTINGS.get("TO") | ||
mail.send() | ||
|
||
def send_mass_contributor_emails(self): | ||
"""Send report email to all relevant contributors.""" | ||
# If the report configuration is not active we only send to the debugging user. | ||
if EMAIL_SETTINGS.get("ACTIVE", False): | ||
for contributor in self.contributors: | ||
self.send_contributor_email(contributor) | ||
else: | ||
for email in EMAIL_SETTINGS.get("TO", []): | ||
self.send_contributor_email(User.objects.get(email=email)) | ||
|
||
def get_email_body(self, contributor): | ||
contributions = self.get_contributions_by_contributor(contributor) | ||
total = sum([contribution.pay for contribution in contributions if contribution.pay]) | ||
contribution_types = {} | ||
for contribution in contributions: | ||
contribution_types[contribution] = contribution.content._meta.concrete_model.__name__ | ||
context = { | ||
"content_type": contribution.content._meta.concrete_model.__name__, | ||
"contributor": contributor, | ||
"contributions": contribution_types, | ||
"deadline": self.deadline, | ||
"total": total | ||
} | ||
return loader.render_to_string(TEMPLATE, context) | ||
|
||
def get_contributors(self): | ||
"""Return a list of contributors with contributions between the start/end dates.""" | ||
return User.objects.filter( | ||
contributions__content__published__gte=self.start, | ||
contributions__content__published__lt=self.end | ||
).distinct() | ||
|
||
def get_contributions_by_contributor(self, contributor, **kwargs): | ||
"""Return a list of all contributions associated with a contributor for a given month.""" | ||
return contributor.contributions.filter( | ||
content__published__gte=self.start, content__published__lt=self.end | ||
) | ||
|
||
@property | ||
def contributors(self): | ||
"""Property to retrieve or access the list of contributors.""" | ||
if not self._contributors: | ||
self._contributors = self.get_contributors() | ||
return self._contributors | ||
|
||
@property | ||
def deadline(self): | ||
"""Set deadline to next day if no deadline provided.""" | ||
if not self._deadline: | ||
self.now + timezone.timedelta(days=1) | ||
return self._deadline | ||
|
||
@property | ||
def start(self): | ||
if not self._start: | ||
self._start = timezone.datetime(day=1, month=self.month, year=self.year) | ||
return self._start | ||
|
||
@property | ||
def end(self): | ||
if not self._end: | ||
next_month = (self.start.month + 1) % 12 | ||
year = self.start.year | ||
if next_month == 1: | ||
year += 1 | ||
elif next_month == 0: | ||
next_month = 12 | ||
self._end = timezone.datetime(day=1, month=next_month, year=year) | ||
return self._end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,17 @@ | ||
"""celery tasks for contributions.""" | ||
from celery import shared_task | ||
|
||
from .email import EmailReport | ||
from .models import Contribution | ||
|
||
|
||
@shared_task(default_retry_delay=5) | ||
def update_role_rates(contributor_role_pk): | ||
for contribution in Contribution.objects.filter(contributor__pk=contributor_role_pk): | ||
contribution.index() | ||
|
||
|
||
@shared_task(default_retry_delay=5) | ||
def run_contributor_email_report(**kwargs): | ||
report = EmailReport(**kwargs) | ||
report.send_mass_contributor_emails() |
53 changes: 53 additions & 0 deletions
53
bulbs/contributions/templates/reporting/__contribution_report.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | ||
<html xmlns="http://www.w3.org/1999/xhtml"> | ||
<head> | ||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | ||
<title>A Simple Responsive HTML Email</title> | ||
<style type="text/css"> | ||
body {margin: 0; padding: 0; min-width: 100%!important;} | ||
p { | ||
margin-bottom: 10px; | ||
} | ||
table { | ||
margin: 5px; | ||
} | ||
td, th { | ||
text-align: left; | ||
border: 1px solid #999; | ||
padding: 3px; | ||
} | ||
.content {width: 100%; max-width: 600px;} | ||
</style> | ||
</head> | ||
<body> | ||
{% autoescape on %} | ||
<p>Hey, <b>{{ contributor.get_full_name }}</b></p> | ||
|
||
<p> | ||
Please check the amount below and make sure it matches your records. | ||
</p> | ||
|
||
<p>The total is <b>${{ total }}</b></p> | ||
|
||
<p> | ||
If you have any question about it, please hit reply-all and let us know by <b>{{ deadline }}</b> | ||
</p> | ||
<table> | ||
<tr> | ||
<th>Content Type</th> | ||
<th>Freelancer Name</th> | ||
<th>Date</th> | ||
<th>Article Title</th> | ||
</tr> | ||
{% for contribution, content_type in contributions.items %} | ||
<tr> | ||
<td>{{ content_type }}</td> | ||
<td>{{ contribution.contributor.get_full_name }}</td> | ||
<td>{{ contribution.content.published }}</td> | ||
<td><a href="{{ contribution.content.get_absolute_url }}">{{ contribution.content.title }}</a></td> | ||
</tr> | ||
{% endfor %} | ||
</table> | ||
{% endautoescape %} | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
"""Tests for bulbs.contributions.email.""" | ||
import mock | ||
import random | ||
|
||
from django.contrib.auth import get_user_model | ||
from django.core.urlresolvers import reverse | ||
from django.test.utils import override_settings | ||
from django.utils import timezone | ||
|
||
from bulbs.contributions.email import EmailReport | ||
from bulbs.contributions.models import ContributorRole | ||
from bulbs.utils.test import make_content, BaseAPITestCase | ||
|
||
from example.testcontent.models import TestContentObj | ||
|
||
|
||
User = get_user_model() | ||
|
||
|
||
# class EmailReportTestCase(BaseIndexableTestCase): | ||
class EmailReportTestCase(BaseAPITestCase): | ||
"""TestCase for bulbs.contributions.email.EmailReport.""" | ||
|
||
def setUp(self): | ||
super(EmailReportTestCase, self).setUp() | ||
# Define relevant variables. | ||
self.endpoint = reverse("contributor-email-list") | ||
self.last_month = (self.now.month - 1) % 12 | ||
self.next_month = (self.now.month + 1) % 12 | ||
|
||
# Add Users. | ||
self.tony_sarpino = User.objects.create( | ||
first_name="Tony", | ||
last_name="Sarpino", | ||
username="Tone", | ||
email="admin@theonion.com" | ||
) | ||
self.buddy_sarpino = User.objects.create( | ||
first_name="Buddy", last_name="Sarpino", username="Buddy" | ||
) | ||
|
||
# Add Roles. | ||
self.draft_writer = ContributorRole.objects.create(name="Draft Writer", payment_type=0) | ||
|
||
# Add Rates. | ||
self.draft_writer.flat_rates.create(rate=60) | ||
|
||
# Make Content with contributions. | ||
make_content( | ||
TestContentObj, | ||
authors=[self.tony_sarpino], | ||
published=timezone.datetime( | ||
day=random.randrange(1, 28), month=self.now.month, year=self.now.year | ||
), | ||
_quantity=25 | ||
) | ||
make_content( | ||
TestContentObj, | ||
authors=[self.tony_sarpino, self.buddy_sarpino], | ||
published=timezone.datetime( | ||
day=random.randrange(1, 28), month=self.last_month, year=self.now.year | ||
), | ||
_quantity=25 | ||
) | ||
make_content( | ||
TestContentObj, | ||
authors=[self.tony_sarpino], | ||
published=timezone.datetime( | ||
day=random.randrange(1, 28), month=self.next_month, year=self.now.year | ||
), | ||
_quantity=25 | ||
) | ||
|
||
# Refresh the index. | ||
TestContentObj.search_objects.refresh() | ||
|
||
def test_get_contributors_default(self): | ||
report = EmailReport() | ||
contributors = report.get_contributors() | ||
self.assertEqual(contributors.count(), 1) | ||
|
||
def test_get_contributors_last_month(self): | ||
report = EmailReport(month=self.last_month) | ||
contributors = report.get_contributors() | ||
self.assertEqual(contributors.count(), 2) | ||
|
||
def test_get_contributor_contributions_default(self): | ||
report = EmailReport() | ||
contributions = report.get_contributions_by_contributor(self.tony_sarpino) | ||
self.assertEqual(contributions.count(), 25) | ||
|
||
def test_get_contributor_contributions_next_month(self): | ||
report = EmailReport(month=self.next_month) | ||
contributions = report.get_contributions_by_contributor(self.tony_sarpino) | ||
self.assertEqual(contributions.count(), 25) | ||
|
||
def test_email_body(self): | ||
report = EmailReport(month=self.next_month) | ||
body = report.get_email_body(self.tony_sarpino) | ||
self.assertTrue(body) | ||
|
||
@override_settings(EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend') | ||
def test_email_api(self): | ||
with mock.patch("django.core.mail.EmailMultiAlternatives.send") as mock_send: | ||
mock_send.return_value = None | ||
data = { | ||
"deadline": self.now + timezone.timedelta(days=5), | ||
"start": timezone.datetime(day=1, month=self.last_month, year=self.now.year) | ||
} | ||
resp = self.api_client.post(self.endpoint, data=data) | ||
self.assertEqual(resp.status_code, 200) | ||
self.assertTrue(mock_send.called) |
Oops, something went wrong.