Skip to content
This repository has been archived by the owner on Feb 13, 2019. It is now read-only.

Commit

Permalink
Merge pull request #157 from theonion/payroll-email
Browse files Browse the repository at this point in the history
Payroll email
  • Loading branch information
benghaziboy committed Apr 18, 2016
2 parents 98cc2ee + 2eba8d6 commit 428e4f4
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 8 deletions.
2 changes: 1 addition & 1 deletion bulbs/__init__.py
@@ -1 +1 @@
__version__ = "0.10.0"
__version__ = "0.10.1"
25 changes: 18 additions & 7 deletions bulbs/api/views.py
Expand Up @@ -13,24 +13,23 @@
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from djes.apps import indexable_registry

from djes.apps import indexable_registry
import elasticsearch
from elasticsearch_dsl.query import Q
from elasticsearch_dsl import filter as es_filter

from firebase_token_generator import create_token
from rest_framework import (
filters,
status,
viewsets,
routers
)
from firebase_token_generator import create_token

from rest_framework.decorators import detail_route, list_route
from rest_framework.metadata import BaseMetadata
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from rest_framework.views import APIView

from bulbs.content.custom_search import custom_search_model
from bulbs.content.filters import Authors
Expand All @@ -40,12 +39,11 @@
TagSerializer, UserSerializer, FeatureTypeSerializer,
ObfuscatedUrlInfoSerializer
)

from bulbs.contributions.serializers import ContributionSerializer
from bulbs.contributions.tasks import run_contributor_email_report
from bulbs.contributions.models import Contribution
from bulbs.contributions.serializers import ContributionSerializer, ContributorReportSerializer
from bulbs.special_coverage.models import SpecialCoverage
from bulbs.special_coverage.serializers import SpecialCoverageSerializer

from bulbs.utils.methods import get_query_params, get_request_data

from .mixins import UncachedResponse
Expand Down Expand Up @@ -602,6 +600,18 @@ def group_count(self, request, **kwargs):
return Response(dict(count=qs.count()))


class SendContributorReport(viewsets.GenericViewSet):
"""Send contribution report email to all relevant ."""

serializer_class = ContributorReportSerializer
permission_classes = [IsAdminUser, CanEditContent]

def create(self, request, *args, **kwargs):
data = ContributorReportSerializer().to_internal_value(self.request.DATA)
run_contributor_email_report.delay(**data)
return Response(status=status.HTTP_200_OK)


# api router for aforementioned/defined viewsets
# note: me view is registered in urls.py
api_v1_router = routers.DefaultRouter()
Expand All @@ -615,3 +625,4 @@ def group_count(self, request, **kwargs):
api_v1_router.register(r"author", AuthorViewSet, base_name="author")
api_v1_router.register(r"feature-type", FeatureTypeViewSet, base_name="feature-type")
api_v1_router.register(r"user", UserViewSet, base_name="user")
api_v1_router.register(r"contributor-email", SendContributorReport, base_name="contributor-email")
116 changes: 116 additions & 0 deletions bulbs/contributions/email.py
@@ -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
7 changes: 7 additions & 0 deletions bulbs/contributions/serializers.py
Expand Up @@ -585,3 +585,10 @@ def get_published(self, obj):

def get_authors(self, obj):
return ",".join([author.get_full_name() for author in obj.authors.all()])



class ContributorReportSerializer(serializers.Serializer):

deadline = serializers.DateTimeField()
start = serializers.DateTimeField()
8 changes: 8 additions & 0 deletions bulbs/contributions/tasks.py
@@ -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 bulbs/contributions/templates/reporting/__contribution_report.html
@@ -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>
9 changes: 9 additions & 0 deletions example/settings.py
Expand Up @@ -133,6 +133,15 @@
}
}

CONTRIBUTIONS = {
"EMAIL": {
"FROM": "",
"REPLY_TO": "",
"SUBJECT": "",
"TO": ["admin@theonion.com"]
}
}

SODAHEAD_BASE_URL = 'https://onion.sodahead.com'
SODAHEAD_TOKEN_VAULT_PATH = 'sodahead/token'

Expand Down
112 changes: 112 additions & 0 deletions tests/contributions/test_contributions_email.py
@@ -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)

0 comments on commit 428e4f4

Please sign in to comment.