Skip to content

Commit

Permalink
Merge pull request #2503 from rtfd/add-payment-form
Browse files Browse the repository at this point in the history
Add a way for sponsors to pay without asking for logos etc.
  • Loading branch information
ericholscher committed Mar 3, 2017
2 parents 3188181 + 0384faf commit 24c215b
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 24 deletions.
17 changes: 0 additions & 17 deletions readthedocs/donate/admin.py
Expand Up @@ -40,12 +40,6 @@ class ImpressionInline(admin.TabularInline):
can_delete = False
max_num = 15

def view_ratio(self, instance):
return instance.view_ratio * 100

def click_ratio(self, instance):
return instance.click_ratio * 100


class SupporterPromoAdmin(admin.ModelAdmin):
model = SupporterPromo
Expand All @@ -58,17 +52,6 @@ class SupporterPromoAdmin(admin.ModelAdmin):
inlines = [ImpressionInline, GeoFilterInline]
actions = [set_default_countries]

def view_ratio(self, instance):
return instance.view_ratio() * 100

def click_ratio(self, instance):
return instance.click_ratio() * 100

def total_views(self, instance):
return sum(imp.views for imp in instance.impressions.all())

def total_clicks(self, instance):
return sum(imp.clicks for imp in instance.impressions.all())

admin.site.register(Supporter, SupporterAdmin)
admin.site.register(SupporterPromo, SupporterPromoAdmin)
Expand Down
43 changes: 43 additions & 0 deletions readthedocs/donate/forms.py
Expand Up @@ -86,3 +86,46 @@ def save(self, commit=True):
supporter.user = self.user
supporter.save()
return supporter


class EthicalAdForm(StripeResourceMixin, StripeModelForm):

"""Payment form for ethical ads
This extends the basic payment form, giving fields for credit card number,
expiry, and CVV. The proper Knockout data bindings are established on
:py:class:`StripeModelForm`
"""

class Meta:
model = Supporter
fields = (
'last_4_digits',
'name',
'email',
'dollars',
)
help_texts = {
'email': _('Your email is used so we can send you a receipt'),
}
widgets = {
'dollars': forms.HiddenInput(attrs={
'data-bind': 'value: dollars'
}),
'last_4_digits': forms.TextInput(attrs={
'data-bind': 'valueInit: card_digits, value: card_digits'
}),
}

last_4_digits = forms.CharField(widget=forms.HiddenInput(), required=True)
name = forms.CharField(required=True)
email = forms.CharField(required=True)

def validate_stripe(self):
stripe.Charge.create(
amount=int(self.cleaned_data['dollars']) * 100,
currency='usd',
source=self.cleaned_data['stripe_token'],
description='Read the Docs Sponsorship Payment',
receipt_email=self.cleaned_data['email']
)
23 changes: 20 additions & 3 deletions readthedocs/donate/models.py
Expand Up @@ -78,7 +78,7 @@ class SupporterPromo(models.Model):
live = models.BooleanField(_('Live'), default=False)

class Meta:
ordering = ('-live',)
ordering = ('analytics_id', '-live')

def __str__(self):
return self.name
Expand Down Expand Up @@ -157,6 +157,19 @@ def views_needed_today(self):
return 0
return ret

def total_views(self):
return sum(imp.views for imp in self.impressions.all())

def total_clicks(self):
return sum(imp.clicks for imp in self.impressions.all())

def total_click_ratio(self):
if self.total_views() == 0:
return float(0)
return '%.4f' % float(
(float(self.total_clicks()) / float(self.total_views())) * 100
)


class BaseImpression(models.Model):
date = models.DateField(_('Date'))
Expand All @@ -173,13 +186,17 @@ class Meta:
def view_ratio(self):
if self.offers == 0:
return 0 # Don't divide by 0
return float(self.views) / float(self.offers)
return float(
float(self.views) / float(self.offers) * 100
)

@property
def click_ratio(self):
if self.views == 0:
return 0 # Don't divide by 0
return float(self.clicks) / float(self.views)
return float(
float(self.clicks) / float(self.views) * 100
)


class PromoImpressions(BaseImpression):
Expand Down
15 changes: 15 additions & 0 deletions readthedocs/donate/templates/donate/ethicalads-success.html
@@ -0,0 +1,15 @@
{% extends "base.html" %}

{% load i18n %}
{% load static %}

{% block title %}{% trans "Sponsorship" %}{% endblock %}

{% block content %}
<h2>Thanks for your support</h2>

<p>
We think that our <a href="http://docs.readthedocs.io/en/latest/ethical-advertising.html">Ethical Advertising</a> campaign is a wonderful step forward for running ads on open source projects.
Thanks for your support.
</p>
{% endblock %}
92 changes: 92 additions & 0 deletions readthedocs/donate/templates/donate/ethicalads.html
@@ -0,0 +1,92 @@
{% extends "base.html" %}

{% load i18n %}
{% load static %}

{% block title %}{% trans "Pay for your ad" %}{% endblock %}

{% block extra_links %}
<link rel="stylesheet" href="{% static 'payments/css/form.css' %}" />
{% endblock %}

{% block extra_scripts %}
<script src="https://js.stripe.com/v2/" type="text/javascript"></script>
<script type="text/javascript" src="{% static 'vendor/knockout.js' %}"></script>
<script type="text/javascript" src="{% static 'donate/js/donate.js' %}"></script>
<script type="text/javascript">
var donate_views = require('donate/donate');
$(document).ready(function () {
var key;
//<![CDATA[
key = '{{ stripe_publishable }}';
//]]>

var view = donate_views.DonateView.init({
key: key,
form: $('form#donate-payment')
});
});
</script>
{% endblock %}


{% block content %}
<h2>Pay for your Sponsorship</h2>

<p>
This form can be used to pay for your sponsorship of Read the Docs.
We think that our <a href="http://docs.readthedocs.io/en/latest/ethical-advertising.html">Ethical Advertising</a> campaign is a wonderful step forward for running ads on open source projects.
You can pay for these ads below.
</p>

<form action="" method="post" id="donate-payment" class="payment">
{% csrf_token %}

{{ form.non_field_errors }}

{% for field in form.fields_with_cc_group %}
{% if field.is_cc_group %}
<div class="subscription-card">
{% for groupfield in field.fields %}
{% include 'core/ko_form_field.html' with field=groupfield %}
{% endfor %}
</div>
{% elif field.name == 'dollars' %}
{{ field.errors }}
<p>
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
<input
type="hidden"
name="{{ field.name }}"
id="{{ field.id_for_label }}"
data-bind="value: dollars" />
<input
type="number"
data-bind="textInput: dollars_input, visible: dollars_select() == 'custom'"
value="50"
id="id_dollars_input"
style="display: none;" />
<select data-bind="value: dollars_select, visible: dollars_select() != 'custom'">
<option value="custom">{% trans "Custom amount" %}</option>
<option value="2000">$2,000</option>
<option value="3000">$3,000</option>
<option value="5000" selected>$5,000</option>
<option value="10000">$10,000</option>
</select>
{% if field.help_text %}
<span class="helptext">{{ field.help_text }}</span>
{% endif %}
</p>
{% else %}
{% include 'core/ko_form_field.html' with field=field %}
{% endif %}
{% endfor %}

{% trans "Pay" as form_submit_text %}
<input type="submit" value="{{ form_submit_text }}" data-bind="click: process_form" />
<p>
We do not store your credit card details,
payment is processed directly through <a href="https://stripe.com">Stripe</a>.
</p>
</form>
{% endblock %}
66 changes: 66 additions & 0 deletions readthedocs/donate/templates/donate/promo_detail.html
@@ -0,0 +1,66 @@
{% extends "base.html" %}

{% load i18n %}
{% load static %}

{% block title %}{% trans "Promo Detail" %}{% endblock %}

{% block content %}

<h1> Promo Results </h1>

{% if promos %}

<p>
Total Clicks for all shown promos: {{ total_clicks }}
</p>

{% for promo in promos %}

<h3>
Results for {{ promo.name }} ({{ promo.analytics_id }}) over last {{ days }} days.
</h3>

<div class="example" style="width: 30%;">
<a href="{{ promo.link }}"><img width=120 height=90 src="{{ promo.image }}"></a>
<br>
{{ promo.text|safe }}

</div>

<br>

<table>
<tr>
<th><strong>Day (UTC)</strong></th>
<th><strong>Views</strong></th>
<th><strong>Clicks</strong></th>
<th><strong>CTR</strong></th>
</tr>
{% for day in promo.impressions.all|slice:days_slice %}
{% if day.views > 0 %}
<tr>
<td>{{ day.date }}</td>
<td><code>{{ day.views }}</code></td>
<td><code>{{ day.clicks }}</code></td>
<td><code>{{ day.click_ratio }}%</code></td>
</tr>
{% endif %}
{% endfor %}
<tr>
<td><strong>Total (over all time)</strong> </td>
<td><strong>{{ promo.total_views }}</strong></td>
<td><strong>{{ promo.total_clicks }}</strong></td>
<td><strong>{{ promo.total_click_ratio }}%</strong></td>
</tr>
</table>

{% endfor %}

{% else %}

No promos match this query.

{% endif %}

{% endblock %}
8 changes: 5 additions & 3 deletions readthedocs/donate/urls.py
@@ -1,13 +1,15 @@
from django.conf.urls import url, include

from .views import DonateCreateView
from .views import DonateListView
from .views import DonateSuccessView
from .views import DonateCreateView, DonateListView, DonateSuccessView
from .views import PayAdsView, PaySuccess, PromoDetailView
from .views import click_proxy, view_proxy


urlpatterns = [
url(r'^$', DonateListView.as_view(), name='donate'),
url(r'^pay/$', PayAdsView.as_view(), name='pay_ads'),
url(r'^pay/success/$', PaySuccess.as_view(), name='pay_success'),
url(r'^report/(?P<promo_slug>.+)/$', PromoDetailView.as_view(), name='donate_promo_detail'),
url(r'^contribute/$', DonateCreateView.as_view(), name='donate_add'),
url(r'^contribute/thanks$', DonateSuccessView.as_view(), name='donate_success'),
url(r'^view/(?P<promo_id>\d+)/(?P<hash>.+)/$', view_proxy, name='donate_view_proxy'),
Expand Down
45 changes: 44 additions & 1 deletion readthedocs/donate/views.py
Expand Up @@ -15,12 +15,28 @@
from readthedocs.projects.models import Project

from .models import Supporter, SupporterPromo, CLICKS, VIEWS
from .forms import SupporterForm
from .forms import SupporterForm, EthicalAdForm
from .mixins import DonateProgressMixin

log = logging.getLogger(__name__)


class PayAdsView(StripeMixin, CreateView):

"""Create a payment locally and in Stripe"""

form_class = EthicalAdForm
success_message = _('Your payment has been received')
template_name = 'donate/ethicalads.html'

def get_success_url(self):
return reverse('pay_success')


class PaySuccess(TemplateView):
template_name = 'donate/ethicalads-success.html'


class DonateCreateView(StripeMixin, CreateView):

"""Create a donation locally and in Stripe"""
Expand Down Expand Up @@ -61,6 +77,33 @@ def get_template_names(self):
return [self.template_name]


class PromoDetailView(TemplateView):
template_name = 'donate/promo_detail.html'

def get_context_data(self, **kwargs):
promo_slug = kwargs['promo_slug']
days = int(self.request.GET.get('days', 90))

if promo_slug == 'live' and self.request.user.is_staff:
promos = SupporterPromo.objects.filter(live=True)
elif '*' in promo_slug:
promos = SupporterPromo.objects.filter(
analytics_id__contains=promo_slug.replace('*', '')
)
else:
slugs = promo_slug.split(',')
promos = SupporterPromo.objects.filter(analytics_id__in=slugs)

total_clicks = sum(promo.total_clicks() for promo in promos)

return {
'promos': promos,
'total_clicks': total_clicks,
'days': days,
'days_slice': ':%s' % days,
}


def click_proxy(request, promo_id, hash):
promo = get_object_or_404(SupporterPromo, pk=promo_id)
count = cache.get(promo.cache_key(type=CLICKS, hash=hash), None)
Expand Down

0 comments on commit 24c215b

Please sign in to comment.