diff --git a/readthedocs/donate/__init__.py b/readthedocs/donate/__init__.py deleted file mode 100644 index 8ba6910e693..00000000000 --- a/readthedocs/donate/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Django app for one-time donations and promotions.""" - -default_app_config = 'readthedocs.donate.apps.DonateAppConfig' diff --git a/readthedocs/donate/admin.py b/readthedocs/donate/admin.py deleted file mode 100644 index a90ae88b298..00000000000 --- a/readthedocs/donate/admin.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Django admin configuration for the donate app.""" - -from __future__ import absolute_import -from django.contrib import admin -from .models import (Supporter, SupporterPromo, Country, - PromoImpressions, GeoFilter) - - -DEFAULT_EXCLUDES = ['BD', 'CN', 'TF', 'GT', 'IN', 'ID', - 'IR', 'PK', 'PH', 'RU', 'TW', 'TH', 'TR', 'UA', 'VN'] - - -def set_default_countries(modeladmin, request, queryset): - del modeladmin, request # unused arguments - for project in queryset: - geo_filter = project.geo_filters.create(filter_type='exclude') - for country in Country.objects.filter(country__in=DEFAULT_EXCLUDES): - geo_filter.countries.add(country) -set_default_countries.short_description = "Add default exclude countries to this Promo" - - -class GeoFilterAdmin(admin.ModelAdmin): - model = GeoFilter - filter_horizontal = ('countries',) - - -class GeoFilterInline(admin.TabularInline): - model = GeoFilter - filter_horizontal = ('countries',) - extra = 0 - - -class SupporterAdmin(admin.ModelAdmin): - model = Supporter - raw_id_fields = ('user',) - list_display = ('name', 'email', 'dollars', 'public') - list_filter = ('name', 'email', 'dollars', 'public') - - -class ImpressionInline(admin.TabularInline): - model = PromoImpressions - readonly_fields = ('date', 'promo', 'offers', 'views', 'clicks', 'view_ratio', 'click_ratio') - extra = 0 - can_delete = False - max_num = 15 - - -class SupporterPromoAdmin(admin.ModelAdmin): - model = SupporterPromo - save_as = True - prepopulated_fields = {'analytics_id': ('name',)} - list_display = ('name', 'live', 'total_click_ratio', 'click_ratio', 'sold_impressions', - 'total_views', 'total_clicks') - list_filter = ('live', 'display_type') - list_editable = ('live', 'sold_impressions') - readonly_fields = ('total_views', 'total_clicks') - search_fields = ('name', 'text', 'analytics_id') - inlines = [ImpressionInline, GeoFilterInline] - actions = [set_default_countries] - - -admin.site.register(Supporter, SupporterAdmin) -admin.site.register(SupporterPromo, SupporterPromoAdmin) -admin.site.register(GeoFilter, GeoFilterAdmin) diff --git a/readthedocs/donate/apps.py b/readthedocs/donate/apps.py deleted file mode 100644 index 696f1f81f3f..00000000000 --- a/readthedocs/donate/apps.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Django app config for the donate app.""" - -from __future__ import absolute_import -from django.apps import AppConfig - - -class DonateAppConfig(AppConfig): - name = 'readthedocs.donate' - verbose_name = 'Donate' - - def ready(self): - import readthedocs.donate.signals # noqa diff --git a/readthedocs/donate/constants.py b/readthedocs/donate/constants.py deleted file mode 100644 index da8ae8fcc15..00000000000 --- a/readthedocs/donate/constants.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Constants used by the donate app.""" - -DISPLAY_CHOICES = ( - ('doc', 'Documentation Pages'), - ('site-footer', 'Site Footer'), - ('search', 'Search Pages'), - ('error', 'Error Pages'), -) - -INCLUDE = 'include' -EXCLUDE = 'exclude' - -FILTER_CHOICES = ( - (EXCLUDE, 'Exclude'), - (INCLUDE, 'Include'), -) - -OFFERS = 'offers' -VIEWS = 'views' -CLICKS = 'clicks' - -IMPRESSION_TYPES = ( - OFFERS, - VIEWS, - CLICKS -) - -ANY = 'any' -READTHEDOCS_THEME = 'sphinx_rtd_theme' -ALABASTER_THEME = 'alabaster' - -THEMES = ( - (ANY, 'Any'), - (ALABASTER_THEME, 'Alabaster Theme'), - (READTHEDOCS_THEME, 'Read the Docs Sphinx Theme'), -) diff --git a/readthedocs/donate/forms.py b/readthedocs/donate/forms.py deleted file mode 100644 index b9a7545d838..00000000000 --- a/readthedocs/donate/forms.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Forms for RTD donations""" - -from __future__ import absolute_import -from builtins import object -import logging - -from django import forms -from django.utils.translation import ugettext_lazy as _ - -from readthedocs.payments.forms import StripeModelForm, StripeResourceMixin -from readthedocs.payments.utils import stripe - -from .models import Supporter - -log = logging.getLogger(__name__) - - -class SupporterForm(StripeResourceMixin, StripeModelForm): - - """Donation support sign up form - - 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(object): - model = Supporter - fields = ( - 'last_4_digits', - 'name', - 'email', - 'dollars', - 'logo_url', - 'site_url', - 'public', - ) - labels = { - 'public': _('Make this donation public'), - } - help_texts = { - 'public': _('Your name and image will be displayed on the donation page'), - 'email': _('Your email is used for Gravatar and so we can send you a receipt'), - 'logo_url': _("URL of your company's logo, images should be 300x300 pixels or less"), - 'dollars': _('Companies donating over $400 can specify a logo URL and site link'), - } - widgets = { - 'dollars': forms.HiddenInput(attrs={ - 'data-bind': 'value: dollars' - }), - 'logo_url': forms.TextInput(attrs={ - 'data-bind': 'value: logo_url, enable: urls_enabled' - }), - 'site_url': forms.TextInput(attrs={ - 'data-bind': 'value: site_url, enable: urls_enabled' - }), - '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 __init__(self, *args, **kwargs): - self.user = kwargs.pop('user') - super(SupporterForm, self).__init__(*args, **kwargs) - - def validate_stripe(self): - """Call stripe for payment (not ideal here) and clean up logo < $200""" - dollars = self.cleaned_data['dollars'] - if dollars < 200: - self.cleaned_data['logo_url'] = None - self.cleaned_data['site_url'] = None - stripe.Charge.create( - amount=int(self.cleaned_data['dollars']) * 100, - currency='usd', - source=self.cleaned_data['stripe_token'], - description='Read the Docs Sustained Engineering', - receipt_email=self.cleaned_data['email'] - ) - - def save(self, commit=True): - supporter = super(SupporterForm, self).save(commit) - if commit and self.user is not None and self.user.is_authenticated(): - 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(object): - 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'] - ) diff --git a/readthedocs/donate/migrations/0001_initial.py b/readthedocs/donate/migrations/0001_initial.py deleted file mode 100644 index 574c3eece8f..00000000000 --- a/readthedocs/donate/migrations/0001_initial.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import models, migrations -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Supporter', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('pub_date', models.DateTimeField(auto_now_add=True, verbose_name='Publication date')), - ('modified_date', models.DateTimeField(auto_now=True, verbose_name='Modified date')), - ('public', models.BooleanField(default=True, verbose_name='Public')), - ('name', models.CharField(max_length=200, verbose_name='name', blank=True)), - ('email', models.EmailField(max_length=200, verbose_name='Email', blank=True)), - ('dollars', models.IntegerField(default=50, verbose_name='Amount', choices=[(5, b'$5'), (10, b'$10'), (25, b'$25'), (50, b'1 Hour ($50)'), (100, b'2 Hours ($100)'), (200, b'4 Hours ($200)'), (400, b'1 Day ($400)'), (800, b'2 Days ($800)'), (1200, b'3 Days ($1200)'), (1600, b'4 Days ($1600)'), (2000, b'5 Days ($2000)'), (4000, b'2 Weeks ($4000)'), (6000, b'3 Weeks ($6000)'), (8000, b'4 Weeks ($8000)')])), - ('logo_url', models.URLField(max_length=255, null=True, verbose_name='Logo URL', blank=True)), - ('site_url', models.URLField(max_length=255, null=True, verbose_name='Site URL', blank=True)), - ('last_4_digits', models.CharField(max_length=4)), - ('stripe_id', models.CharField(max_length=255)), - ('subscribed', models.BooleanField(default=False)), - ('user', models.ForeignKey(related_name='goldonce', verbose_name='User', blank=True, to=settings.AUTH_USER_MODEL, null=True)), - ], - ), - migrations.CreateModel( - name='SupporterPromo', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('pub_date', models.DateTimeField(auto_now_add=True, verbose_name='Publication date')), - ('modified_date', models.DateTimeField(auto_now=True, verbose_name='Modified date')), - ('name', models.CharField(max_length=200, verbose_name='Name')), - ('analytics_id', models.CharField(max_length=200, verbose_name='Analytics ID')), - ('text', models.TextField(verbose_name='Text', blank=True)), - ('link', models.URLField(max_length=255, null=True, verbose_name='Link URL', blank=True)), - ('image', models.URLField(max_length=255, null=True, verbose_name='Image URL', blank=True)), - ('display_type', models.CharField(default=b'doc', max_length=200, verbose_name='Display Type', choices=[(b'doc', b'Documentation Pages'), (b'site-footer', b'Site Footer'), (b'search', b'Search Pages')])), - ('live', models.BooleanField(default=False, verbose_name='Live')), - ], - ), - ] diff --git a/readthedocs/donate/migrations/0002_dollar-drop-choices.py b/readthedocs/donate/migrations/0002_dollar-drop-choices.py deleted file mode 100644 index c6b4d96055e..00000000000 --- a/readthedocs/donate/migrations/0002_dollar-drop-choices.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('donate', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='supporter', - name='dollars', - field=models.IntegerField(default=50, verbose_name='Amount'), - ), - ] diff --git a/readthedocs/donate/migrations/0003_add-impressions.py b/readthedocs/donate/migrations/0003_add-impressions.py deleted file mode 100644 index 1995f30451c..00000000000 --- a/readthedocs/donate/migrations/0003_add-impressions.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('donate', '0002_dollar-drop-choices'), - ] - - operations = [ - migrations.CreateModel( - name='SupporterImpressions', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('date', models.DateField(verbose_name='Date')), - ('offers', models.IntegerField(default=0, verbose_name='Offer')), - ('views', models.IntegerField(default=0, verbose_name='View')), - ('clicks', models.IntegerField(default=0, verbose_name='Clicks')), - ('promo', models.ForeignKey(related_name='impressions', blank=True, to='donate.SupporterPromo', null=True)), - ], - options={ - 'ordering': ('-date',), - }, - ), - migrations.AlterUniqueTogether( - name='supporterimpressions', - unique_together=set([('promo', 'date')]), - ), - ] diff --git a/readthedocs/donate/migrations/0004_rebase-impressions-on-base.py b/readthedocs/donate/migrations/0004_rebase-impressions-on-base.py deleted file mode 100644 index 845962b8216..00000000000 --- a/readthedocs/donate/migrations/0004_rebase-impressions-on-base.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('donate', '0003_add-impressions') - ] - - operations = [ - migrations.RenameModel( - 'SupporterImpressions', - 'PromoImpressions', - ), - migrations.CreateModel( - name='ProjectImpressions', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('date', models.DateField(verbose_name='Date')), - ('offers', models.IntegerField(default=0, verbose_name='Offer')), - ('views', models.IntegerField(default=0, verbose_name='View')), - ('clicks', models.IntegerField(default=0, verbose_name='Clicks')), - ('project', models.ForeignKey(related_name='impressions', blank=True, to='projects.Project', null=True)), - ('promo', models.ForeignKey(related_name='project_impressions', blank=True, to='donate.SupporterPromo', null=True)), - ], - ), - migrations.AlterUniqueTogether( - name='projectimpressions', - unique_together=set([('project', 'promo', 'date')]), - ), - ] diff --git a/readthedocs/donate/migrations/0005_add-geo-filters.py b/readthedocs/donate/migrations/0005_add-geo-filters.py deleted file mode 100644 index 807f5ab4507..00000000000 --- a/readthedocs/donate/migrations/0005_add-geo-filters.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import models, migrations -import django_countries.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('donate', '0004_rebase-impressions-on-base'), - ] - - operations = [ - migrations.CreateModel( - name='Country', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('country', django_countries.fields.CountryField(unique=True, max_length=2)), - ], - ), - migrations.CreateModel( - name='GeoFilter', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('filter_type', models.CharField(default=b'', max_length=20, verbose_name='Filter Type', choices=[(b'exclude', b'Exclude'), (b'include', b'Include')])), - ('countries', models.ManyToManyField(related_name='filters', null=True, to='donate.Country', blank=True)), - ('promo', models.ForeignKey(related_name='geo_filters', blank=True, to='donate.SupporterPromo', null=True)), - ], - ), - ] diff --git a/readthedocs/donate/migrations/0006_add-geo-data.py b/readthedocs/donate/migrations/0006_add-geo-data.py deleted file mode 100644 index 59dbd4d2dfc..00000000000 --- a/readthedocs/donate/migrations/0006_add-geo-data.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import models, migrations - -from django_countries import countries - - -def add_data(apps, schema_editor): - # We can't import the Person model directly as it may be a newer - # version than this migration expects. We use the historical version. - Country = apps.get_model("donate", "Country") - for code, name in list(countries): - Country.objects.get_or_create(country=code) - - -class Migration(migrations.Migration): - - dependencies = [ - ('donate', '0005_add-geo-filters'), - ] - - operations = [ - migrations.RunPython(add_data), - ] diff --git a/readthedocs/donate/migrations/0007_add-impression-totals.py b/readthedocs/donate/migrations/0007_add-impression-totals.py deleted file mode 100644 index 74a6cb5a5b7..00000000000 --- a/readthedocs/donate/migrations/0007_add-impression-totals.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('donate', '0006_add-geo-data'), - ] - - operations = [ - migrations.AddField( - model_name='supporterpromo', - name='sold_days', - field=models.IntegerField(default=30, verbose_name='Sold Days'), - ), - migrations.AddField( - model_name='supporterpromo', - name='sold_impressions', - field=models.IntegerField(default=1000, verbose_name='Sold Impressions'), - ), - migrations.AlterField( - model_name='geofilter', - name='countries', - field=models.ManyToManyField(related_name='filters', to='donate.Country', blank=True), - ), - ] diff --git a/readthedocs/donate/migrations/0008_add-programming-language-filter.py b/readthedocs/donate/migrations/0008_add-programming-language-filter.py deleted file mode 100644 index 6f33f3879e8..00000000000 --- a/readthedocs/donate/migrations/0008_add-programming-language-filter.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('donate', '0007_add-impression-totals'), - ] - - operations = [ - migrations.AlterModelOptions( - name='supporterpromo', - options={'ordering': ('-live',)}, - ), - migrations.AddField( - model_name='supporterpromo', - name='programming_language', - field=models.CharField(default=None, choices=[(b'words', b'Only Words'), (b'py', b'Python'), (b'js', b'Javascript'), (b'php', b'PHP'), (b'ruby', b'Ruby'), (b'perl', b'Perl'), (b'java', b'Java'), (b'go', b'Go'), (b'julia', b'Julia'), (b'c', b'C'), (b'csharp', b'C#'), (b'cpp', b'C++'), (b'objc', b'Objective-C'), (b'other', b'Other')], max_length=20, blank=True, null=True, verbose_name='Programming Language'), - ), - ] diff --git a/readthedocs/donate/migrations/0009_add-error-to-promos.py b/readthedocs/donate/migrations/0009_add-error-to-promos.py deleted file mode 100644 index 88ad5f1e47f..00000000000 --- a/readthedocs/donate/migrations/0009_add-error-to-promos.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.12 on 2017-03-24 16:03 -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('donate', '0008_add-programming-language-filter'), - ] - - operations = [ - migrations.AlterModelOptions( - name='supporterpromo', - options={'ordering': ('analytics_id', '-live')}, - ), - migrations.AlterField( - model_name='geofilter', - name='countries', - field=models.ManyToManyField(related_name='filters', to='donate.Country'), - ), - migrations.AlterField( - model_name='supporterpromo', - name='display_type', - field=models.CharField(choices=[(b'doc', b'Documentation Pages'), (b'site-footer', b'Site Footer'), (b'search', b'Search Pages'), (b'error', b'Error Pages')], default=b'doc', max_length=200, verbose_name='Display Type'), - ), - migrations.AlterField( - model_name='supporterpromo', - name='programming_language', - field=models.CharField(blank=True, choices=[(b'words', b'Only Words'), (b'py', b'Python'), (b'js', b'JavaScript'), (b'php', b'PHP'), (b'ruby', b'Ruby'), (b'perl', b'Perl'), (b'java', b'Java'), (b'go', b'Go'), (b'julia', b'Julia'), (b'c', b'C'), (b'csharp', b'C#'), (b'cpp', b'C++'), (b'objc', b'Objective-C'), (b'other', b'Other')], default=None, max_length=20, null=True, verbose_name='Programming Language'), - ), - ] diff --git a/readthedocs/donate/migrations/0010_add-sold-clicks.py b/readthedocs/donate/migrations/0010_add-sold-clicks.py deleted file mode 100644 index ece12aa2cc0..00000000000 --- a/readthedocs/donate/migrations/0010_add-sold-clicks.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.12 on 2017-04-04 13:48 -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('donate', '0009_add-error-to-promos'), - ] - - operations = [ - migrations.AddField( - model_name='supporterpromo', - name='sold_clicks', - field=models.IntegerField(default=0, verbose_name='Sold Clicks'), - ), - migrations.AlterField( - model_name='supporterpromo', - name='sold_impressions', - field=models.IntegerField(default=1000000, verbose_name='Sold Impressions'), - ), - ] diff --git a/readthedocs/donate/migrations/0011_add-theme-filter.py b/readthedocs/donate/migrations/0011_add-theme-filter.py deleted file mode 100644 index 241876140b5..00000000000 --- a/readthedocs/donate/migrations/0011_add-theme-filter.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.12 on 2017-04-12 13:05 -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('donate', '0010_add-sold-clicks'), - ] - - operations = [ - migrations.AddField( - model_name='supporterpromo', - name='theme', - field=models.CharField(blank=True, choices=[(b'any', b'Any'), (b'alabaster', b'Alabaster Theme'), (b'sphinx_rtd_theme', b'Read the Docs Sphinx Theme')], default=b'sphinx_rtd_theme', max_length=40, null=True, verbose_name='Theme'), - ), - ] diff --git a/readthedocs/donate/migrations/0012_add-community-ads.py b/readthedocs/donate/migrations/0012_add-community-ads.py deleted file mode 100644 index 860a7b665a7..00000000000 --- a/readthedocs/donate/migrations/0012_add-community-ads.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.12 on 2017-06-14 17:48 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('donate', '0011_add-theme-filter'), - ] - - operations = [ - migrations.AddField( - model_name='supporterpromo', - name='community', - field=models.BooleanField(default=False, verbose_name='Community Ad'), - ), - ] diff --git a/readthedocs/donate/migrations/__init__.py b/readthedocs/donate/migrations/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/readthedocs/donate/mixins.py b/readthedocs/donate/mixins.py deleted file mode 100644 index 58ad0d836fe..00000000000 --- a/readthedocs/donate/mixins.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Mixin classes for donation views""" - -from __future__ import absolute_import -from __future__ import division -from builtins import object -from past.utils import old_div -from django.db.models import Avg, Sum - -from .models import Supporter - - -class DonateProgressMixin(object): - - """Add donation progress to context data""" - - def get_context_data(self, **kwargs): - context = super(DonateProgressMixin, self).get_context_data(**kwargs) - sums = (Supporter.objects - .aggregate(dollars=Sum('dollars'))) - avgs = (Supporter.objects - .aggregate(dollars=Avg('dollars'))) - dollars = sums.get('dollars', None) or 0 - avg = int(avgs.get('dollars', None) or 0) - count = Supporter.objects.count() - percent = int((old_div(float(dollars), 24000.0)) * 100.0) - context.update({ - 'donate_amount': dollars, - 'donate_avg': avg, - 'donate_percent': percent, - 'donate_count': count, - }) - return context diff --git a/readthedocs/donate/models.py b/readthedocs/donate/models.py deleted file mode 100644 index 905c53c8270..00000000000 --- a/readthedocs/donate/models.py +++ /dev/null @@ -1,275 +0,0 @@ -"""Django models for the donate app.""" -# We use 'type' and 'hash' heavily in the API here. -# pylint: disable=redefined-builtin -from __future__ import (absolute_import, division) - -from past.utils import old_div -from builtins import object -from django.db import models -from django.utils.crypto import get_random_string -from django.utils.encoding import python_2_unicode_compatible -from django.utils.translation import ugettext_lazy as _, pgettext -from django.core.urlresolvers import reverse -from django.conf import settings -from django_countries.fields import CountryField -import six - -from readthedocs.donate.utils import get_ad_day -from readthedocs.donate.constants import ( - DISPLAY_CHOICES, FILTER_CHOICES, IMPRESSION_TYPES, THEMES, READTHEDOCS_THEME -) -from readthedocs.projects.models import Project -from readthedocs.projects.constants import PROGRAMMING_LANGUAGES - - -@python_2_unicode_compatible -class Supporter(models.Model): - pub_date = models.DateTimeField(_('Publication date'), auto_now_add=True) - modified_date = models.DateTimeField(_('Modified date'), auto_now=True) - public = models.BooleanField(_('Public'), default=True) - - name = models.CharField(_('name'), max_length=200, blank=True) - email = models.EmailField(_('Email'), max_length=200, blank=True) - user = models.ForeignKey('auth.User', verbose_name=_('User'), - related_name='goldonce', blank=True, null=True) - dollars = models.IntegerField(_('Amount'), default=50) - logo_url = models.URLField(_('Logo URL'), max_length=255, blank=True, - null=True) - site_url = models.URLField(_('Site URL'), max_length=255, blank=True, - null=True) - - last_4_digits = models.CharField(max_length=4) - stripe_id = models.CharField(max_length=255) - subscribed = models.BooleanField(default=False) - - def __str__(self): - return self.name - - -@python_2_unicode_compatible -class SupporterPromo(models.Model): - - """A banner advertisement.""" - - pub_date = models.DateTimeField(_('Publication date'), auto_now_add=True) - modified_date = models.DateTimeField(_('Modified date'), auto_now=True) - - name = models.CharField(_('Name'), max_length=200) - analytics_id = models.CharField(_('Analytics ID'), max_length=200) - text = models.TextField(_('Text'), blank=True) - link = models.URLField(_('Link URL'), max_length=255, blank=True, null=True) - image = models.URLField(_('Image URL'), max_length=255, blank=True, null=True) - display_type = models.CharField(_('Display Type'), max_length=200, - choices=DISPLAY_CHOICES, default='doc') - sold_impressions = models.IntegerField(_('Sold Impressions'), default=1000000) - sold_days = models.IntegerField(_('Sold Days'), default=30) - sold_clicks = models.IntegerField(_('Sold Clicks'), default=0) - programming_language = models.CharField(_('Programming Language'), max_length=20, - choices=PROGRAMMING_LANGUAGES, default=None, - blank=True, null=True) - theme = models.CharField(_('Theme'), max_length=40, - choices=THEMES, default=READTHEDOCS_THEME, - blank=True, null=True) - community = models.BooleanField(_('Community Ad'), default=False) - live = models.BooleanField(_('Live'), default=False) - - class Meta(object): - ordering = ('analytics_id', '-live') - - def __str__(self): - return self.name - - def as_dict(self): - """A dict respresentation of this for JSON encoding""" - hash = get_random_string() - domain = getattr(settings, 'PRODUCTION_DOMAIN', 'readthedocs.org') - if self.image: - image_url = '//{host}{url}'.format( - host=domain, - url=reverse( - 'donate_view_proxy', - kwargs={'promo_id': self.pk, 'hash': hash} - )) - else: - image_url = None - # TODO: Store this hash and confirm that a proper hash was sent later - link_url = '//{host}{url}'.format( - host=domain, - url=reverse( - 'donate_click_proxy', - kwargs={'promo_id': self.pk, 'hash': hash} - )) - return { - 'id': self.analytics_id, - 'text': self.text, - 'link': link_url, - 'image': image_url, - 'hash': hash, - } - - def cache_key(self, type, hash): - assert type in IMPRESSION_TYPES + ('project',) - return 'promo:{id}:{hash}:{type}'.format(id=self.analytics_id, hash=hash, type=type) - - def incr(self, type, project=None): - """Add to the number of times this action has been performed, stored in the DB""" - assert type in IMPRESSION_TYPES - day = get_ad_day() - if project: - impression, _ = self.project_impressions.get_or_create(date=day, project=project) - else: - impression, _ = self.impressions.get_or_create(date=day) - - setattr(impression, type, models.F(type) + 1) - impression.save() - - # TODO: Support redis, more info on this PR - # - # https://github.com/rtfd/readthedocs.org - # /pull/2105/files/1b5f8568ae0a7760f7247149bcff481efc000f32#r58253051 - - def view_ratio(self, day=None): - if not day: - day = get_ad_day() - impression = self.impressions.get_or_create(date=day)[0] - return impression.view_ratio - - def click_ratio(self, day=None): - if not day: - day = get_ad_day() - impression = self.impressions.get_or_create(date=day)[0] - return impression.click_ratio - - def views_per_day(self): - return int(old_div(float(self.sold_impressions), float(self.sold_days))) - - def views_shown_today(self, day=None): - if not day: - day = get_ad_day() - impression = self.impressions.get_or_create(date=day)[0] - return float(impression.views) - - def views_needed_today(self): - ret = self.views_per_day() - self.views_shown_today() - if ret < 0: - 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( - (old_div(float(self.total_clicks()), float(self.total_views()))) * 100 - ) - - def report_html_text(self): - """ - Include the link in the html text. - - Only used for reporting, - doesn't include any click fraud protection! - """ - return self.text.replace('', "" % self.link) - - -class BaseImpression(models.Model): - - """Statistics for tracking.""" - - date = models.DateField(_('Date')) - offers = models.IntegerField(_('Offer'), default=0) - views = models.IntegerField( - pgettext('View', 'Number of display on a screen that were sold'), default=0) - clicks = models.IntegerField(_('Clicks'), default=0) - - class Meta(object): - ordering = ('-date',) - unique_together = ('promo', 'date') - abstract = True - - @property - def view_ratio(self): - if self.offers == 0: - return 0 # Don't divide by 0 - 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 '%.3f' % float( - float(self.clicks) / float(self.views) * 100 - ) - - -@python_2_unicode_compatible -class PromoImpressions(BaseImpression): - - """ - Track stats around how successful this promo has been. - - Indexed one per promo per day. - """ - - promo = models.ForeignKey(SupporterPromo, related_name='impressions', - blank=True, null=True) - - def __str__(self): - return u'%s on %s' % (self.promo, self.date) - - -@python_2_unicode_compatible -class ProjectImpressions(BaseImpression): - - """ - Track stats for a specific project and promo. - - Indexed one per project per promo per day - """ - - promo = models.ForeignKey(SupporterPromo, related_name='project_impressions', - blank=True, null=True) - project = models.ForeignKey(Project, related_name='impressions', - blank=True, null=True) - - class Meta(object): - unique_together = ('project', 'promo', 'date') - - def __str__(self): - return u'%s / %s on %s' % (self.promo, self.project, self.date) - - -@python_2_unicode_compatible -class Country(models.Model): - country = CountryField(unique=True) - - def __str__(self): - return six.text_type(self.country.name) - - -@python_2_unicode_compatible -class GeoFilter(models.Model): - promo = models.ForeignKey(SupporterPromo, related_name='geo_filters', - blank=True, null=True) - filter_type = models.CharField(_('Filter Type'), max_length=20, - choices=FILTER_CHOICES, default='') - countries = models.ManyToManyField(Country, related_name='filters') - - @property - def codes(self): - ret = [] - for wrapped_code in self.countries.values_list('country'): - ret.append(wrapped_code[0]) - return ret - - def __str__(self): - return u"Filter for {promo} that {type}s: {countries}".format( - promo=self.promo.name, type=self.filter_type, countries=self.codes) diff --git a/readthedocs/donate/signals.py b/readthedocs/donate/signals.py deleted file mode 100644 index c6912c256e2..00000000000 --- a/readthedocs/donate/signals.py +++ /dev/null @@ -1,264 +0,0 @@ -"""Django signal plumbing for the donate app.""" - -from __future__ import absolute_import -import random -import logging - -from django.dispatch import receiver -from django.conf import settings -from django.core.cache import cache - -import redis - -from readthedocs.restapi.signals import footer_response -from readthedocs.donate.models import SupporterPromo -from readthedocs.donate.constants import INCLUDE, EXCLUDE -from readthedocs.donate.utils import offer_promo - - -log = logging.getLogger(__name__) - -PROMO_GEO_PATH = getattr(settings, 'PROMO_GEO_PATH', None) - -if PROMO_GEO_PATH: - import geoip2.database # noqa - from geoip2.errors import AddressNotFoundError # noqa - geo_reader = geoip2.database.Reader(PROMO_GEO_PATH) - - -def show_to_geo(promo, country_code): - # Remove promo's that exclude this country. - for geo_filter in promo.geo_filters.all(): - if geo_filter.filter_type == INCLUDE: - if country_code in geo_filter.codes: - continue - else: - return False - if geo_filter.filter_type == EXCLUDE: - if country_code in geo_filter.codes: - return False - - return True - - -def show_to_programming_language(promo, programming_language): - """ - Filter a promo by a specific programming language - - Return True if we haven't set a specific language, - which means show to all languages. - """ - if promo.programming_language: - return programming_language == promo.programming_language - return True - - -def choose_promo(promo_list): - """ - This is the algorithm to pick which promo to show. - - This takes into account how many remaining days this - promo has to be shown. - - The algorithm is currently as such: - - * Take the remaining number of views each promo has today - * Add them together, with each promo "assigned" to a range - * Pick a random number between 1 and that total - * Choose the ad whose range is in the chosen random number - - In the future, - we should take into account the expected views for today - (The number of views from this day last week) - Then we can scale the "total ads sold" against that "expected views", - and that will give us more spread throughout the day. - - """ - promo_range = [] - total_views_needed = 0 - for promo in promo_list: - promo_range.append([ - total_views_needed, - total_views_needed + promo.views_needed_today(), - promo - ]) - total_views_needed += promo.views_needed_today() - choice = random.randint(0, total_views_needed) - for range_list in promo_range: - if range_list[0] <= choice <= range_list[1]: - return range_list[2] - return None - - -def get_promo(country_code, programming_language, theme, - gold_project=False, gold_user=False, community_only=False): - """ - Get a proper promo. - - Takes into account: - - * Gold User status - * Gold Project status - * Geo - * Programming Language - - """ - promo_queryset = SupporterPromo.objects.filter(live=True, display_type='doc') - - if community_only: - promo_queryset = promo_queryset.filter(community=True) - - filtered_promos = [] - for promo in promo_queryset: - # Break out if we aren't meant to show to this language - if promo.programming_language and not show_to_programming_language(promo, programming_language): # noqa - continue - # Break out if we aren't meant to show to this country - if country_code and not show_to_geo(promo, country_code): - continue - # Don't show if the theme doesn't match - if promo.theme not in ['any', theme]: - continue - # If we haven't bailed because of language or country, possibly show the promo - filtered_promos.append(promo) - - promo_obj = choose_promo(filtered_promos) - - # Show a random house ad if we don't have anything else - if not promo_obj: - house_promo = SupporterPromo.objects.filter(live=True, - name='house').order_by('?') - if house_promo.exists(): - promo_obj = house_promo.first() - - # Support showing a "Thank you" message for gold folks - if gold_user: - gold_promo = SupporterPromo.objects.filter(live=True, - name='gold-user') - if gold_promo.exists(): - promo_obj = gold_promo.first() - - # Default to showing project-level thanks if it exists - if gold_project: - gold_promo = SupporterPromo.objects.filter(live=True, - name='gold-project') - if gold_promo.exists(): - promo_obj = gold_promo.first() - - return promo_obj - - -def is_gold_user(user): - """Return True if the user is a Gold supporter.""" - return user.is_authenticated() and ( - user.gold.count() or - user.goldonce.count() - ) - - -def is_gold_project(project): - """Return True if the project has been mapped by a Gold supporter.""" - return project.gold_owners.count() - - -def is_community_only(user, project): - """Return True is this project or user should only be shown community ads""" - if user.is_authenticated() and not user.profile.allow_ads: - return True - if not project.allow_promos: - return True - return False - - -def get_user_country(request): - """Return the ISO country code from geo-IP data, or None if not found.""" - if not PROMO_GEO_PATH: - return None - ip = request.META.get('REMOTE_ADDR') - if not ip: - return None - try: - geo_response = geo_reader.city(ip) - return geo_response.country.iso_code - except (AddressNotFoundError, ValueError): # Invalid IP - return None - - -@receiver(footer_response) -def attach_promo_data(sender, request, context, resp_data, **__): - """Insert promotion data keys into the footer API response.""" - del sender # unused - - project = context['project'] - theme = context['theme'] - - if getattr(settings, 'USE_PROMOS', True): - promo = lookup_promo(request, project, theme) - else: - promo = None - - if promo: - resp_data.update({ - 'promo': True, - 'promo_data': promo, - }) - else: - resp_data['promo'] = False - - -def lookup_promo(request, project, theme): - """Look up a promo to show for the given project. - - Return a dict of promo_data for inclusion in the footer response, or None - if no promo should be shown. - - """ - gold_user = is_gold_user(request.user) - gold_project = is_gold_project(project) - community_only = is_community_only(request.user, project) - - # Don't show promos to gold users or on gold projects for now - # (Some day we may show them something customised for them) - if gold_user or gold_project: - return None - - promo_obj = get_promo( - country_code=get_user_country(request), - programming_language=project.programming_language, - theme=theme, - gold_project=gold_project, - gold_user=gold_user, - community_only=community_only, - ) - - # If we don't have anything to show, don't show it. - if not promo_obj: - return None - - return offer_promo( - promo_obj=promo_obj, - project=project - ) - - -@receiver(footer_response) -def index_theme_data(sender, **kwargs): - """ - Keep track of which projects are using which theme. - - This is primarily used so we can send email to folks using alabaster, - and other themes we might want to display ads on. - This will allow us to give people fair warning before we put ads on their docs. - - """ - del sender # unused - context = kwargs['context'] - - project = context['project'] - theme = context['theme'] - - try: - redis_client = cache.get_client(None) - redis_client.sadd("readthedocs:v1:index:themes:%s" % theme, project.slug) - except (AttributeError, redis.exceptions.ConnectionError): - log.warning('Redis theme indexing error: %s', exc_info=True) diff --git a/readthedocs/donate/static-src/donate/js/donate.js b/readthedocs/donate/static-src/donate/js/donate.js deleted file mode 100644 index 3e3d7994735..00000000000 --- a/readthedocs/donate/static-src/donate/js/donate.js +++ /dev/null @@ -1,63 +0,0 @@ -// Donate payment views - -var jquery = require('jquery'), - payment = require('readthedocs/payments/static-src/payments/js/base'), - ko = require('knockout'); - -function DonateView (config) { - var self = this, - config = config || {}; - - self.constructor.call(self, config); - - self.dollars_select = ko.observable(); - self.dollars_input = ko.observable(); - self.dollars = ko.computed(function () { - var dollars; - dollars = self.dollars_select(); - if (dollars == 'custom') { - dollars = self.dollars_input(); - } - return dollars; - }); - self.logo_url = ko.observable(); - self.site_url = ko.observable(); - self.error_dollars = ko.observable(); - self.error_logo_url = ko.observable(); - self.error_site_url = ko.observable(); - - ko.computed(function () { - var input_logo = $('input#id_logo_url').closest('p'), - input_site = $('input#id_site_url').closest('p'); - if (self.dollars() < 400) { - self.logo_url(null); - self.site_url(null); - input_logo.hide(); - input_site.hide(); - } - else { - input_logo.show(); - input_site.show(); - } - }); - self.urls_enabled = ko.computed(function () { - return (self.dollars() >= 400); - }); -} - -DonateView.prototype = new payment.PaymentView(); - -DonateView.init = function (config, obj) { - var view = new DonateView(config), - obj = obj || $('#donate-payment')[0]; - ko.applyBindings(view, obj); - return view; -} - -DonateView.prototype.submit_form = function (card_digits, token) { - this.form.find('#id_last_4_digits').val(card_digits); - this.form.find('#id_stripe_token').val(token); - this.form.submit(); -}; - -module.exports.DonateView = DonateView; diff --git a/readthedocs/donate/static/donate/css/donate.css b/readthedocs/donate/static/donate/css/donate.css deleted file mode 100644 index 1509cac59fc..00000000000 --- a/readthedocs/donate/static/donate/css/donate.css +++ /dev/null @@ -1,15 +0,0 @@ -#promo_404 { - margin-left: auto; - margin-right: auto; - width: 900px; - text-align: center - -.promo { - margin-top: 1em; - margin-bottom: 1em; - width: 240px; -} - -.filters dt { - font-weight: bold; -} diff --git a/readthedocs/donate/static/donate/img/creditcard.png b/readthedocs/donate/static/donate/img/creditcard.png deleted file mode 100644 index 0fe91a8bb17..00000000000 Binary files a/readthedocs/donate/static/donate/img/creditcard.png and /dev/null differ diff --git a/readthedocs/donate/static/donate/js/donate.js b/readthedocs/donate/static/donate/js/donate.js deleted file mode 100644 index 39485ada060..00000000000 --- a/readthedocs/donate/static/donate/js/donate.js +++ /dev/null @@ -1 +0,0 @@ -require=function e(t,r,n){function o(a,c){if(!r[a]){if(!t[a]){var l="function"==typeof require&&require;if(!c&&l)return l(a,!0);if(i)return i(a,!0);var u=new Error("Cannot find module '"+a+"'");throw u.code="MODULE_NOT_FOUND",u}var s=r[a]={exports:{}};t[a][0].call(s.exports,function(e){var r=t[a][1][e];return o(r?r:e)},s,s.exports,e,t,r,n)}return r[a].exports}for(var i="function"==typeof require&&require,a=0;a9&&(t-=9),o+=t;return o%10===0},p=function(e){var t;return null!=e.prop("selectionStart")&&e.prop("selectionStart")!==e.prop("selectionEnd")||!(null==("undefined"!=typeof document&&null!==document&&null!=(t=document.selection)?t.createRange:void 0)||!document.selection.createRange().text)},C=function(e,t){var r,n,o,i,a,c;try{n=t.prop("selectionStart")}catch(l){i=l,n=null}if(a=t.val(),t.val(e),null!==n&&t.is(":focus"))return n===a.length&&(n=e.length),a!==e&&(c=a.slice(n-1,+n+1||9e9),r=e.slice(n-1,+n+1||9e9),o=e[n],/\d/.test(o)&&c===""+o+" "&&r===" "+o&&(n+=1)),t.prop("selectionStart",n),t.prop("selectionEnd",n)},y=function(e){var t,r,n,o,i,a,c,l;for(null==e&&(e=""),n="0123456789",o="0123456789",a="",t=e.split(""),c=0,l=t.length;c-1&&(r=o[i]),a+=r;return a},m=function(t){var r;return r=e(t.currentTarget),setTimeout(function(){var e;return e=r.val(),e=y(e),e=e.replace(/\D/g,""),C(e,r)})},h=function(t){var r;return r=e(t.currentTarget),setTimeout(function(){var t;return t=r.val(),t=y(t),t=e.payment.formatCardNumber(t),C(t,r)})},c=function(r){var n,o,i,a,c,l,u;if(i=String.fromCharCode(r.which),/^\d+$/.test(i)&&(n=e(r.currentTarget),u=n.val(),o=t(u+i),a=(u.replace(/\D/g,"")+i).length,l=16,o&&(l=o.length[o.length.length-1]),!(a>=l||null!=n.prop("selectionStart")&&n.prop("selectionStart")!==u.length)))return c=o&&"amex"===o.type?/^(\d{4}|\d{4}\s\d{6})$/:/(?:^|\s)(\d{4})$/,c.test(u)?(r.preventDefault(),setTimeout(function(){return n.val(u+" "+i)})):c.test(u+i)?(r.preventDefault(),setTimeout(function(){return n.val(u+i+" ")})):void 0},i=function(t){var r,n;if(r=e(t.currentTarget),n=r.val(),8===t.which&&(null==r.prop("selectionStart")||r.prop("selectionStart")===n.length))return/\d\s$/.test(n)?(t.preventDefault(),setTimeout(function(){return r.val(n.replace(/\d\s$/,""))})):/\s\d?$/.test(n)?(t.preventDefault(),setTimeout(function(){return r.val(n.replace(/\d$/,""))})):void 0},v=function(t){var r;return r=e(t.currentTarget),setTimeout(function(){var t;return t=r.val(),t=y(t),t=e.payment.formatExpiry(t),C(t,r)})},l=function(t){var r,n,o;if(n=String.fromCharCode(t.which),/^\d+$/.test(n))return r=e(t.currentTarget),o=r.val()+n,/^\d$/.test(o)&&"0"!==o&&"1"!==o?(t.preventDefault(),setTimeout(function(){return r.val("0"+o+" / ")})):/^\d\d$/.test(o)?(t.preventDefault(),setTimeout(function(){var e,t;return e=parseInt(o[0],10),t=parseInt(o[1],10),t>2&&0!==e?r.val("0"+e+" / "+t):r.val(""+o+" / ")})):void 0},u=function(t){var r,n,o;if(n=String.fromCharCode(t.which),/^\d+$/.test(n))return r=e(t.currentTarget),o=r.val(),/^\d\d$/.test(o)?r.val(""+o+" / "):void 0},s=function(t){var r,n,o;if(o=String.fromCharCode(t.which),"/"===o||" "===o)return r=e(t.currentTarget),n=r.val(),/^\d$/.test(n)&&"0"!==n?r.val("0"+n+" / "):void 0},a=function(t){var r,n;if(r=e(t.currentTarget),n=r.val(),8===t.which&&(null==r.prop("selectionStart")||r.prop("selectionStart")===n.length))return/\d\s\/\s$/.test(n)?(t.preventDefault(),setTimeout(function(){return r.val(n.replace(/\d\s\/\s$/,""))})):void 0},f=function(t){var r;return r=e(t.currentTarget),setTimeout(function(){var e;return e=r.val(),e=y(e),e=e.replace(/\D/g,"").slice(0,4),C(e,r)})},w=function(e){var t;return!(!e.metaKey&&!e.ctrlKey)||32!==e.which&&(0===e.which||(e.which<33||(t=String.fromCharCode(e.which),!!/[\d\s]/.test(t))))},_=function(r){var n,o,i,a;if(n=e(r.currentTarget),i=String.fromCharCode(r.which),/^\d+$/.test(i)&&!p(n))return a=(n.val()+i).replace(/\D/g,""),o=t(a),o?a.length<=o.length[o.length.length-1]:a.length<=16},b=function(t){var r,n,o;if(r=e(t.currentTarget),n=String.fromCharCode(t.which),/^\d+$/.test(n)&&!p(r))return o=r.val()+n,o=o.replace(/\D/g,""),!(o.length>6)&&void 0},g=function(t){var r,n,o;if(r=e(t.currentTarget),n=String.fromCharCode(t.which),/^\d+$/.test(n)&&!p(r))return o=r.val()+n,o.length<=4},x=function(t){var r,o,i,a,c;if(r=e(t.currentTarget),c=r.val(),a=e.payment.cardType(c)||"unknown",!r.hasClass(a))return o=function(){var e,t,r;for(r=[],e=0,t=n.length;e=0&&(r.luhn===!1||d(e))))},e.payment.validateCardExpiry=function(t,r){var n,o,i;return"object"==typeof t&&"month"in t&&(i=t,t=i.month,r=i.year),!(!t||!r)&&(t=e.trim(t),r=e.trim(r),!!/^\d+$/.test(t)&&(!!/^\d+$/.test(r)&&(1<=t&&t<=12&&(2===r.length&&(r=r<70?"20"+r:"19"+r),4===r.length&&(o=new Date(r,t),n=new Date,o.setMonth(o.getMonth()-1),o.setMonth(o.getMonth()+1,1),o>n)))))},e.payment.validateCardCVC=function(t,n){var o,i;return t=e.trim(t),!!/^\d+$/.test(t)&&(o=r(n),null!=o?(i=t.length,T.call(o.cvcLength,i)>=0):t.length>=3&&t.length<=4)},e.payment.cardType=function(e){var r;return e?(null!=(r=t(e))?r.type:void 0)||null:null},e.payment.formatCardNumber=function(r){var n,o,i,a;return r=r.replace(/\D/g,""),(n=t(r))?(i=n.length[n.length.length-1],r=r.slice(0,i),n.format.global?null!=(a=r.match(n.format))?a.join(" "):void 0:(o=n.format.exec(r),null!=o?(o.shift(),o=e.grep(o,function(e){return e}),o.join(" ")):void 0)):r},e.payment.formatExpiry=function(e){var t,r,n,o;return(r=e.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/))?(t=r[1]||"",n=r[2]||"",o=r[3]||"",o.length>0?n=" / ":" /"===n?(t=t.substring(0,1),n=""):2===t.length||n.length>0?n=" / ":1===t.length&&"0"!==t&&"1"!==t&&(t="0"+t,n=" / "),t+n+o):""}}).call(this)},{}],2:[function(e,t,r){function n(e){var t=this,e=e||{};a.publishableKey=t.stripe_key=e.key,t.form=e.form,t.cc_number=o.observable(null),t.cc_expiry=o.observable(null),t.cc_cvv=o.observable(null),t.error_cc_number=o.observable(null),t.error_cc_expiry=o.observable(null),t.error_cc_cvv=o.observable(null),t.stripe_token=o.observable(null),t.card_digits=o.observable(null),t.is_editing_card=o.observable(!1),t.show_card_form=o.computed(function(){return t.is_editing_card()||!t.card_digits()||t.cc_number()||t.cc_expiry()||t.cc_cvv()}),t.initialize_form(),t.error=o.observable(null),t.process_form=function(){var e=i.payment.cardExpiryVal(t.cc_expiry()),r={number:t.cc_number(),exp_month:e.month,exp_year:e.year,cvc:t.cc_cvv()};return t.error(null),t.error_cc_number(null),t.error_cc_expiry(null),t.error_cc_cvv(null),i.payment.validateCardNumber(r.number)?i.payment.validateCardExpiry(r.exp_month,r.exp_year)?i.payment.validateCardCVC(r.cvc)?void a.createToken(r,function(e,r){if(r.error)if("card_error"==r.error.type){var n={invalid_number:t.error_cc_number,incorrect_number:t.error_cc_number,expired_card:t.error_cc_number,card_declined:t.error_cc_number,invalid_expiry_month:t.error_cc_expiry,invalid_expiry_year:t.error_cc_expiry,invalid_cvc:t.error_cc_cvv,incorrect_cvc:t.error_cc_cvv},o=n[r.error.code]||t.error_cc_number;o(r.error.message)}else t.error_cc_number(r.error.message);else t.submit_form(r.card.last4,r.id)}):(t.error_cc_cvv("Invalid security code"),!1):(t.error_cc_expiry("Invalid expiration date"),!1):(t.error_cc_number("Invalid card number"),!1)},t.process_full_form=function(){return!t.show_card_form()||void t.process_form()}}var o=e("knockout"),i=(e("./../../../../../bower_components/jquery.payment/lib/jquery.payment.js"),e("jquery")),a=null;"undefined"!=typeof window&&"undefined"!=typeof window.Stripe&&(a=window.Stripe||{}),o.bindingHandlers.valueInit={init:function(e,t){var r=t();o.isWriteableObservable(r)&&r(e.value)}},n.prototype.submit_form=function(e,t){this.form.find("#id_card_digits").val(e),this.form.find("#id_stripe_token").val(t),this.form.submit()},n.prototype.initialize_form=function(){var e=i("input#id_cc_number"),t=i("input#id_cc_cvv"),r=i("input#id_cc_expiry");e.payment("formatCardNumber"),r.payment("formatCardExpiry"),t.payment("formatCardCVC"),e.trigger("keyup")},n.init=function(e,t){var r=new n(e),t=t||i("#payment-form")[0];return o.applyBindings(r,t),r},t.exports.PaymentView=n},{"./../../../../../bower_components/jquery.payment/lib/jquery.payment.js":1,jquery:"jquery",knockout:"knockout"}],"donate/donate":[function(e,t,r){function n(e){var t=this,e=e||{};t.constructor.call(t,e),t.dollars_select=i.observable(),t.dollars_input=i.observable(),t.dollars=i.computed(function(){var e;return e=t.dollars_select(),"custom"==e&&(e=t.dollars_input()),e}),t.logo_url=i.observable(),t.site_url=i.observable(),t.error_dollars=i.observable(),t.error_logo_url=i.observable(),t.error_site_url=i.observable(),i.computed(function(){var e=$("input#id_logo_url").closest("p"),r=$("input#id_site_url").closest("p");t.dollars()<400?(t.logo_url(null),t.site_url(null),e.hide(),r.hide()):(e.show(),r.show())}),t.urls_enabled=i.computed(function(){return t.dollars()>=400})}var o=(e("jquery"),e("readthedocs/payments/static-src/payments/js/base")),i=e("knockout");n.prototype=new o.PaymentView,n.init=function(e,t){var r=new n(e),t=t||$("#donate-payment")[0];return i.applyBindings(r,t),r},n.prototype.submit_form=function(e,t){this.form.find("#id_last_4_digits").val(e),this.form.find("#id_stripe_token").val(t),this.form.submit()},t.exports.DonateView=n},{jquery:"jquery",knockout:"knockout","readthedocs/payments/static-src/payments/js/base":2}]},{},[]); \ No newline at end of file diff --git a/readthedocs/donate/templates/donate/create.html b/readthedocs/donate/templates/donate/create.html deleted file mode 100644 index 189689cef75..00000000000 --- a/readthedocs/donate/templates/donate/create.html +++ /dev/null @@ -1,103 +0,0 @@ -{% extends "base.html" %} - -{% load i18n %} -{% load static %} - -{% block title %}{% trans "Sustainability" %}{% endblock %} - -{% block extra_links %} - -{% endblock %} - -{% block extra_scripts %} - - - - -{% endblock %} - - -{% block content %} -

Donate to Read the Docs

- -

- Contributions will be listed on our donation page, with your name and - photo (from Gravatar). You can choose to donate privately if you do not wish to be listed. - We do not store your credit card details, - payment is processed directly through Stripe. -

- - -{% endblock %} diff --git a/readthedocs/donate/templates/donate/ethicalads-success.html b/readthedocs/donate/templates/donate/ethicalads-success.html deleted file mode 100644 index caf2f7a7b3b..00000000000 --- a/readthedocs/donate/templates/donate/ethicalads-success.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base.html" %} - -{% load i18n %} -{% load static %} - -{% block title %}{% trans "Sponsorship" %}{% endblock %} - -{% block content %} -

Thanks for your support

- -

- We think that our Ethical Advertising campaign is a wonderful step forward for running ads on open source projects. - Thanks for your support. -

-{% endblock %} diff --git a/readthedocs/donate/templates/donate/ethicalads.html b/readthedocs/donate/templates/donate/ethicalads.html deleted file mode 100644 index 64cbe75f537..00000000000 --- a/readthedocs/donate/templates/donate/ethicalads.html +++ /dev/null @@ -1,92 +0,0 @@ -{% extends "base.html" %} - -{% load i18n %} -{% load static %} - -{% block title %}{% trans "Pay for your ad" %}{% endblock %} - -{% block extra_links %} - -{% endblock %} - -{% block extra_scripts %} - - - - -{% endblock %} - - -{% block content %} -

Pay for your Sponsorship

- -

- This form can be used to pay for your sponsorship of Read the Docs. - We think that our Ethical Advertising campaign is a wonderful step forward for running ads on open source projects. - You can pay for these ads below. -

- - -{% endblock %} diff --git a/readthedocs/donate/templates/donate/list.html b/readthedocs/donate/templates/donate/list.html deleted file mode 100644 index 0faf774f1ab..00000000000 --- a/readthedocs/donate/templates/donate/list.html +++ /dev/null @@ -1,136 +0,0 @@ -{% extends "base.html" %} - -{% load i18n %} -{% load gravatar %} - -{% block title %}{% trans "Sustainability" %}{% endblock %} - -{% block content %} - - {% include 'donate/progress.html' with evergreen=True %} - - -

About Read the Docs

- -

- Read the Docs has grown substantially - since its beginning as a weekend project. - Today, we: -

- - - -

- We host projects from all languages and programming communities. - Some examples of projects that we host include: -

- - - -

- Because of the growth and popularity of the service, the associated - technical and operational support demands a good deal of labor. - Where development can be sustained with community contributions, - consistent support for users, maintenance, and on-call procedures - require accountability and dedicated engineers. - These types of roles are hard to accomplish on a volunteer basis. -

- -

- To make sure these roles were filled, we decided to treat the project as - a job – we formed a company and dedicated ourselves full-time to - the project. - Now, we would like to make sure that the service is sustainable - and its costs are covered. -

- -

- We are asking for your contributions to ensure continued operational - and technical support for our community, as well as continued - development. -

- -

- Our primary goal is to cover the costs of supporting and maintaining - Read the Docs for the next three months. - We hope to account for the cost of at least one full-time engineer, - which, at the rate of $50/hour, would - cost $24,000 over the next three months. - Covering labor costs would allow us to spend time supporting the site - instead of seeking income elsewhere. -

- - - - -

Sponsors

- -

- Our servers are graciously sponsored by Rackspace, at a value of $2,000 each month. - They are our single largest sponsor, - and Read the Docs wouldn't be possible without their generous support. - You can view the full list of past sponsors in our documentation. -

- -

- If you are a company that shares our passion for documentation, - or if you want to support our mission, - contact us for more information on sponsoring Read the Docs. -

- - - -

Our Supporters

- -

- Below is a list of all the wonderful people who have supported our - vision. -

- - - {% if supporters|length > 75 %} - - {% endif %} - -

Thanks

- -

- This program is modeled after the fantastic Django Fellowship. - Thanks for the inspiration. -

- -{% endblock %} diff --git a/readthedocs/donate/templates/donate/progress.html b/readthedocs/donate/templates/donate/progress.html deleted file mode 100644 index 9dcd16cf436..00000000000 --- a/readthedocs/donate/templates/donate/progress.html +++ /dev/null @@ -1,68 +0,0 @@ -{% load i18n %} - - diff --git a/readthedocs/donate/templates/donate/promo_404.html b/readthedocs/donate/templates/donate/promo_404.html deleted file mode 100644 index d8fe59e0720..00000000000 --- a/readthedocs/donate/templates/donate/promo_404.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} -{% load core_tags %} -{% load static i18n %} - -{% block title %} - {% trans "Maze Found" %} -{% endblock %} - -{% block header-wrapper %} - {% include "error_header.html" %} -{% endblock %} - -{% block extra_links %} - -{% endblock %} - -{% block content %} -
- {% if suggestion %} -
-

You've found something that doesn't exist.

-

{{ suggestion.message }}

- {% ifequal suggestion.type 'top' %} -

- Go to the top of the documentation. -

- {% endifequal %} - {% ifequal suggestion.type 'list' %} - - {% endifequal %} -
- {% endif %} - -

-Page Not Found! -

- - - - - -

-Read the Docs is sponsored by Sentry, which gives developers the tools to keep calm when things catch fire. -

-
-{% endblock %} diff --git a/readthedocs/donate/templates/donate/promo_500.html b/readthedocs/donate/templates/donate/promo_500.html deleted file mode 100644 index 5520d325aed..00000000000 --- a/readthedocs/donate/templates/donate/promo_500.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends "base.html" %} -{% load i18n %} - - {% block title %} - {% trans "Server Error" %} - {% endblock %} - - {% block header-wrapper %} - {% include "error_header.html" %} - {% endblock %} - -{% block content %} -

-Server Error -

- -
-          .
-         ":"
-       ___:____     |"\/"|
-     ,'        `.    \  /
-     |  O        \___/  |
-   ~^~^~^~^~^~^~^~^~^~^~^~^~
-
-   Fail.  Check back in a bit!
-
-  
- - - - -{% if request.sentry.id %} - -{% endif %} - -

-Read the Docs is sponsored by Sentry, which gives developers the tools to keep calm when things catch fire. -

- -{% endblock %} diff --git a/readthedocs/donate/templates/donate/promo_detail.html b/readthedocs/donate/templates/donate/promo_detail.html deleted file mode 100644 index 027dd107cc2..00000000000 --- a/readthedocs/donate/templates/donate/promo_detail.html +++ /dev/null @@ -1,112 +0,0 @@ -{% extends "base.html" %} - -{% load i18n %} -{% load static %} - -{% block title %}{% trans "Promo Detail" %}{% endblock %} - -{% block extra_links %} - -{% endblock %} - -{% block content %} - -

Promo Results

- -{% if promos %} - -{% if promos|length > 1 %} -

-Total Clicks for all shown promos: {{ total_clicks }} -

-{% endif %} - -
-{% for promo in promos %} - -

-Results for {{ promo.name }} ({{ promo.analytics_id }}) over last {{ days }} days. -

- -
- -
- {% if promo.programming_language %} -
-
Filtered Language
-
{{ promo.programming_language }}
-
- {% endif %} - - {% if promo.geo_filters.count %} -
-
Filtered Geos
- {% for geo in promo.geo_filters.all %} -
- {{ geo.get_filter_type_display }}: {{ geo.countries.all|join:", " }} -
- {% endfor %} -
- {% endif %} - - {% if promo.sold_clicks %} -
-
Total Clicks Sold
-
- {{ promo.sold_clicks }} -
-
- {% endif %} -
- - -
-
- - - -
- -
- {{ promo.report_html_text|safe }} -
-
- -
- -
Promo Data
- - - - - - - - {% for day in promo.impressions.all|slice:days_slice %} - {% if day.views > 0 %} - - - - - - - {% endif %} - {% endfor %} - - - - - - -
Day (UTC)ViewsClicksCTR
{{ day.date }}{{ day.views }}{{ day.clicks }}{{ day.click_ratio }}%
Total (over all time) {{ promo.total_views }}{{ promo.total_clicks }}{{ promo.total_click_ratio }}%
- -{% endfor %} -
- -{% else %} - -No promos match this query. - -{% endif %} - -{% endblock %} diff --git a/readthedocs/donate/templates/donate/success.html b/readthedocs/donate/templates/donate/success.html deleted file mode 100644 index 0441ca1ea5e..00000000000 --- a/readthedocs/donate/templates/donate/success.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "base.html" %} - -{% load i18n %} -{% load static %} - -{% block title %}{% trans "Sustainability" %}{% endblock %} - -{% block content %} -

Thanks for your support

- -

- We appreciate your contribution greatly, thank you for showing your support! - Your help will go a long ways towards making the service more sustainable. -

- -

Help us get the word out

-

- You can help us get more contributions by spreading news of our campaign - and sharing the following link: -

-

- Help Support Read the Docs -

- - Tweet - - -{% endblock %} diff --git a/readthedocs/donate/tests.py b/readthedocs/donate/tests.py deleted file mode 100644 index 7faa7307109..00000000000 --- a/readthedocs/donate/tests.py +++ /dev/null @@ -1,273 +0,0 @@ -from __future__ import absolute_import - -import json -import mock - -from builtins import range -from django.contrib.auth.models import User -from django.test import TestCase -from django.core.urlresolvers import reverse -from django.core.cache import cache -from django.test.client import RequestFactory -from django_dynamic_fixture import get - -from ..core.middleware import FooterNoSessionMiddleware -from .models import SupporterPromo, GeoFilter, Country -from .constants import (CLICKS, VIEWS, OFFERS, - INCLUDE, EXCLUDE, READTHEDOCS_THEME) -from .signals import show_to_geo, get_promo, choose_promo, show_to_programming_language -from readthedocs.projects.models import Project - - -class PromoTests(TestCase): - - def setUp(self): - self.promo = get(SupporterPromo, - slug='promo-slug', - link='http://example.com', - image='http://media.example.com/img.png') - self.pip = get(Project, slug='pip', allow_promos=True) - - def test_clicks(self): - hash_key = 'random_hash' - cache.set(self.promo.cache_key(type=CLICKS, hash=hash_key), 0) - resp = self.client.get( - 'http://testserver/sustainability/click/%s/%s/' % (self.promo.id, hash_key)) - self.assertEqual(resp._headers['location'][1], 'http://example.com') - promo = SupporterPromo.objects.get(pk=self.promo.pk) - impression = promo.impressions.first() - self.assertEqual(impression.clicks, 1) - - def test_views(self): - cache.set(self.promo.cache_key(type=VIEWS, hash='random_hash'), 0) - resp = self.client.get( - 'http://testserver/sustainability/view/%s/random_hash/' % self.promo.id) - self.assertEqual(resp._headers['location'][1], 'http://media.example.com/img.png') - promo = SupporterPromo.objects.get(pk=self.promo.pk) - impression = promo.impressions.first() - self.assertEqual(impression.views, 1) - - def test_project_clicks(self): - cache.set(self.promo.cache_key(type=CLICKS, hash='random_hash'), 0) - cache.set(self.promo.cache_key(type='project', hash='random_hash'), self.pip.slug) - self.client.get('http://testserver/sustainability/click/%s/random_hash/' % self.promo.id) - promo = SupporterPromo.objects.get(pk=self.promo.pk) - impression = promo.project_impressions.first() - self.assertEqual(impression.clicks, 1) - - def test_stats(self): - for x in range(50): - self.promo.incr(OFFERS) - for x in range(20): - self.promo.incr(VIEWS) - for x in range(3): - self.promo.incr(CLICKS) - self.assertAlmostEqual(self.promo.view_ratio(), 40.0) - self.assertEqual(self.promo.click_ratio(), '15.000') - - def test_multiple_hash_usage(self): - cache.set(self.promo.cache_key(type=VIEWS, hash='random_hash'), 0) - self.client.get('http://testserver/sustainability/view/%s/random_hash/' % self.promo.id) - promo = SupporterPromo.objects.get(pk=self.promo.pk) - impression = promo.impressions.first() - self.assertEqual(impression.views, 1) - - # Don't increment again. - self.client.get('http://testserver/sustainability/view/%s/random_hash/' % self.promo.id) - promo = SupporterPromo.objects.get(pk=self.promo.pk) - impression = promo.impressions.first() - self.assertEqual(impression.views, 1) - - def test_invalid_id(self): - resp = self.client.get('http://testserver/sustainability/view/invalid/data/') - self.assertEqual(resp.status_code, 404) - - def test_invalid_hash(self): - cache.set(self.promo.cache_key(type=VIEWS, hash='valid_hash'), 0) - resp = self.client.get( - 'http://testserver/sustainability/view/%s/invalid_hash/' % self.promo.id) - promo = SupporterPromo.objects.get(pk=self.promo.pk) - self.assertEqual(promo.impressions.count(), 0) - self.assertEqual(resp._headers['location'][1], 'http://media.example.com/img.png') - - -class FilterTests(TestCase): - - def setUp(self): - us = Country.objects.all().filter(country='US').get() - ca = Country.objects.all().filter(country='CA').get() - mx = Country.objects.all().filter(country='MX').get() - az = Country.objects.all().filter(country='AZ').get() - - # Only show in US,CA - self.promo = get(SupporterPromo, - slug='promo-slug', - link='http://example.com', - live=True, - programming_language='py', - image='http://media.example.com/img.png' - ) - self.filter = get(GeoFilter, - promo=self.promo, - countries=[us, ca, mx], - filter_type=INCLUDE, - ) - - # Don't show in AZ - self.promo2 = get(SupporterPromo, - slug='promo2-slug', - link='http://example.com', - live=True, - programming_language='js', - image='http://media.example.com/img.png') - self.filter2 = get(GeoFilter, - promo=self.promo2, - countries=[az], - filter_type=EXCLUDE, - ) - - self.pip = get(Project, slug='pip', allow_promos=True, programming_language='py') - - def test_include(self): - # US view - ret = show_to_geo(self.promo, 'US') - self.assertTrue(ret) - - ret = show_to_geo(self.promo2, 'US') - self.assertTrue(ret) - - def test_exclude(self): - # Az -- don't show AZ ad - ret = show_to_geo(self.promo, 'AZ') - self.assertFalse(ret) - - ret = show_to_geo(self.promo2, 'AZ') - self.assertFalse(ret) - - def test_non_included_data(self): - # Random Country -- don't show "only US" ad - ret = show_to_geo(self.promo, 'FO') - self.assertFalse(ret) - - # Country FO is not excluded - ret2 = show_to_geo(self.promo2, 'FO') - self.assertTrue(ret2) - - def test_get_promo(self): - ret = get_promo('US', 'py', READTHEDOCS_THEME) - self.assertEqual(ret, self.promo) - - ret = get_promo('MX', 'py', READTHEDOCS_THEME) - self.assertEqual(ret, self.promo) - - ret = get_promo('FO', 'js', READTHEDOCS_THEME) - self.assertEqual(ret, self.promo2) - - ret = get_promo('AZ', 'js', READTHEDOCS_THEME) - self.assertEqual(ret, None) - - ret = get_promo('RANDOM', 'js', READTHEDOCS_THEME) - self.assertEqual(ret, self.promo2) - - def test_programming_language(self): - ret = show_to_programming_language(self.promo, 'py') - self.assertTrue(ret) - - ret = show_to_programming_language(self.promo, 'js') - self.assertFalse(ret) - - # This promo is JS only - ret = show_to_programming_language(self.promo2, 'py') - self.assertFalse(ret) - - ret = show_to_programming_language(self.promo2, 'js') - self.assertTrue(ret) - - -class ProbabilityTests(TestCase): - - def setUp(self): - # Only show in US,CA - self.promo = get(SupporterPromo, - slug='promo-slug', - link='http://example.com', - live=True, - image='http://media.example.com/img.png', - sold_impressions=1000, - ) - - # Don't show in AZ - self.promo2 = get(SupporterPromo, - slug='promo2-slug', - link='http://example.com', - live=True, - image='http://media.example.com/img.png', - sold_impressions=1000 * 1000, - ) - self.promo_list = [self.promo, self.promo2] - - def test_choose(self): - # US view - - promo_prob = self.promo.views_needed_today() - promo2_prob = self.promo2.views_needed_today() - total = promo_prob + promo2_prob - - with mock.patch('random.randint') as randint: - - randint.return_value = -1 - ret = choose_promo(self.promo_list) - self.assertEqual(ret, None) - - randint.return_value = 5 - ret = choose_promo(self.promo_list) - self.assertEqual(ret, self.promo) - - randint.return_value = promo_prob - 1 - ret = choose_promo(self.promo_list) - self.assertEqual(ret, self.promo) - - randint.return_value = promo_prob - ret = choose_promo(self.promo_list) - self.assertEqual(ret, self.promo) - - randint.return_value = promo_prob + 1 - ret = choose_promo(self.promo_list) - self.assertEqual(ret, self.promo2) - - randint.return_value = total - 1 - ret = choose_promo(self.promo_list) - self.assertEqual(ret, self.promo2) - - randint.return_value = total - ret = choose_promo(self.promo_list) - self.assertEqual(ret, self.promo2) - - randint.return_value = total + 1 - ret = choose_promo(self.promo_list) - self.assertEqual(ret, None) - - -class CookieTests(TestCase): - - def setUp(self): - self.promo = get(SupporterPromo, live=True) - - def test_no_cookie(self): - mid = FooterNoSessionMiddleware() - factory = RequestFactory() - - # Setup - cache.set(self.promo.cache_key(type=VIEWS, hash='random_hash'), 0) - request = factory.get( - 'http://testserver/sustainability/view/%s/random_hash/' % self.promo.id - ) - - # Null session here - mid.process_request(request) - self.assertEqual(request.session, {}) - - # Proper session here - home_request = factory.get('/') - mid.process_request(home_request) - self.assertTrue(home_request.session.TEST_COOKIE_NAME, 'testcookie') diff --git a/readthedocs/donate/urls.py b/readthedocs/donate/urls.py deleted file mode 100644 index 50fa8d9c0ec..00000000000 --- a/readthedocs/donate/urls.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Django URL patterns for the donate app.""" - -from __future__ import absolute_import -from django.conf.urls import url - -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.+)/$', 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\d+)/(?P.+)/$', view_proxy, name='donate_view_proxy'), - url(r'^click/(?P\d+)/(?P.+)/$', click_proxy, name='donate_click_proxy'), -] diff --git a/readthedocs/donate/utils.py b/readthedocs/donate/utils.py deleted file mode 100644 index bc1972ba95d..00000000000 --- a/readthedocs/donate/utils.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Support functions for donations.""" - -from __future__ import absolute_import -import pytz -import datetime - -from django.core.cache import cache - -from readthedocs.donate.constants import OFFERS, CLICKS, VIEWS - - -def get_ad_day(): - date = pytz.utc.localize(datetime.datetime.utcnow()) - day = datetime.datetime( - year=date.year, - month=date.month, - day=date.day, - tzinfo=pytz.utc, - ) - return day - - -def offer_promo(promo_obj, project=None): - """ - Do the book keeping required to track promo offers. - - This generated a hash as part of the return dict, - so that must be used throughout the processing pipeline in order to dedupe clicks. - """ - promo_dict = promo_obj.as_dict() - promo_obj.incr(OFFERS) - # Set validation cache - for promo_type in [VIEWS, CLICKS]: - cache.set( - promo_obj.cache_key(type=promo_type, hash=promo_dict['hash']), - 0, # Number of times used. Make this an int so we can detect multiple uses - 60 * 60 # hour - ) - - if project: - # Set project for hash key, so we can count it later. - promo_obj.incr(OFFERS, project=project) - cache.set( - promo_obj.cache_key(type='project', hash=promo_dict['hash']), - project.slug, - 60 * 60 # hour - ) - return promo_dict diff --git a/readthedocs/donate/views.py b/readthedocs/donate/views.py deleted file mode 100644 index b4860f1d0de..00000000000 --- a/readthedocs/donate/views.py +++ /dev/null @@ -1,204 +0,0 @@ -"""Donation views""" -# We use 'hash' heavily in the API here. -# pylint: disable=redefined-builtin - -from __future__ import absolute_import -import logging - -from django.views.generic import TemplateView -from django.core.urlresolvers import reverse -from django.utils.translation import ugettext_lazy as _ -from django.shortcuts import redirect, get_object_or_404, render_to_response -from django.template import RequestContext -from django.core.cache import cache -from django.http import Http404 - -from vanilla import CreateView, ListView - -from readthedocs.donate.utils import offer_promo -from readthedocs.payments.mixins import StripeMixin -from readthedocs.projects.models import Project -from readthedocs.redirects.utils import get_redirect_response - -from .models import Supporter, SupporterPromo -from .constants import CLICKS, VIEWS -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""" - - form_class = SupporterForm - success_message = _('Your contribution has been received') - template_name = 'donate/create.html' - - def get_success_url(self): - return reverse('donate_success') - - def get_initial(self): - return {'dollars': self.request.GET.get('dollars', 50)} - - def get_form(self, data=None, files=None, **kwargs): - kwargs['user'] = self.request.user - return super(DonateCreateView, self).get_form(data, files, **kwargs) - - -class DonateSuccessView(TemplateView): - template_name = 'donate/success.html' - - -class DonateListView(DonateProgressMixin, ListView): - - """Donation list and detail view""" - - template_name = 'donate/list.html' - model = Supporter - context_object_name = 'supporters' - - def get_queryset(self): - return (Supporter.objects - .filter(public=True) - .order_by('-dollars', '-pub_date')) - - 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 promo_slug[-1] == '*' and '-' 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): - """Track a click on a promotion and redirect to the link.""" - promo = get_object_or_404(SupporterPromo, pk=promo_id) - count = cache.get(promo.cache_key(type=CLICKS, hash=hash), None) - if count is None: - log.warning('Old or nonexistent hash tried on Click.') - elif count == 0: - promo.incr(CLICKS) - cache.incr(promo.cache_key(type=CLICKS, hash=hash)) - project_slug = cache.get( - promo.cache_key(type='project', hash=hash), - None - ) - if project_slug: - project = Project.objects.get(slug=project_slug) - promo.incr(CLICKS, project=project) - else: - agent = request.META.get('HTTP_USER_AGENT', 'Unknown') - log.warning( - 'Duplicate click logged. {count} total clicks tried. User Agent: [{agent}]'.format( - count=count, agent=agent - ) - ) - cache.incr(promo.cache_key(type=CLICKS, hash=hash)) - return redirect(promo.link) - - -def view_proxy(request, promo_id, hash): - """Track a view of a promotion and redirect to the image.""" - promo = get_object_or_404(SupporterPromo, pk=promo_id) - if not promo.image: - raise Http404('No image defined for this promo.') - count = cache.get(promo.cache_key(type=VIEWS, hash=hash), None) - if count is None: - log.warning('Old or nonexistent hash tried on View.') - elif count == 0: - promo.incr(VIEWS) - cache.incr(promo.cache_key(type=VIEWS, hash=hash)) - project_slug = cache.get( - promo.cache_key(type='project', hash=hash), - None - ) - if project_slug: - project = Project.objects.get(slug=project_slug) - promo.incr(VIEWS, project=project) - else: - agent = request.META.get('HTTP_USER_AGENT', 'Unknown') - log.warning( - 'Duplicate view logged. {count} total views tried. User Agent: [{agent}]'.format( - count=count, agent=agent - ) - ) - cache.incr(promo.cache_key(type=VIEWS, hash=hash)) - return redirect(promo.image) - - -def _add_promo_data(display_type): - promo_queryset = SupporterPromo.objects.filter(live=True, display_type=display_type) - promo_obj = promo_queryset.order_by('?').first() - if promo_obj: - promo_dict = offer_promo(promo_obj=promo_obj, project=None) - else: - promo_dict = None - return promo_dict - - -def promo_500(request, template_name='donate/promo_500.html', **__): - """A simple 500 handler so we get media""" - promo_dict = _add_promo_data(display_type='error') - r = render_to_response(template_name, - context_instance=RequestContext(request), - context={ - 'promo_data': promo_dict, - }) - r.status_code = 500 - return r - - -def promo_404(request, template_name='donate/promo_404.html', **__): - """A simple 404 handler so we get media""" - promo_dict = _add_promo_data(display_type='error') - response = get_redirect_response(request, path=request.get_full_path()) - if response: - return response - r = render_to_response(template_name, - context_instance=RequestContext(request), - context={ - 'promo_data': promo_dict, - }) - r.status_code = 404 - return r diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index a8f6fac172e..0dfa2c1e4a6 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -8,6 +8,12 @@ from readthedocs.core.settings import Settings +try: + import readthedocsext.donate # noqa + donate = True +except ImportError: + donate = False + djcelery.setup_loader() @@ -76,7 +82,6 @@ def INSTALLED_APPS(self): # noqa 'copyright', 'textclassifier', 'annoying', - 'django_countries', 'django_extensions', 'messages_extends', @@ -99,11 +104,11 @@ def INSTALLED_APPS(self): # noqa 'readthedocs.rtd_tests', 'readthedocs.restapi', 'readthedocs.gold', - 'readthedocs.donate', 'readthedocs.payments', 'readthedocs.notifications', 'readthedocs.integrations', + # allauth 'allauth', 'allauth.account', @@ -112,6 +117,9 @@ def INSTALLED_APPS(self): # noqa 'allauth.socialaccount.providers.bitbucket', 'allauth.socialaccount.providers.bitbucket_oauth2', ] + if donate: + apps.append('django_countries') + apps.append('readthedocsext.donate') return apps TEMPLATE_LOADERS = ( diff --git a/readthedocs/templates/homepage.html b/readthedocs/templates/homepage.html index a6928cd279b..e619659e5be 100644 --- a/readthedocs/templates/homepage.html +++ b/readthedocs/templates/homepage.html @@ -123,7 +123,6 @@

{% trans "Read the Docs is funded by the community" %}

Read the Docs is funded by readers like you. Help keep the site alive and well by supporting us with a Gold Subscription. - You can also make one-time donations on our sustainability page.

Hosting for the project is graciously provided by Rackspace. diff --git a/readthedocs/urls.py b/readthedocs/urls.py index a123524d1f2..afaec524ec3 100644 --- a/readthedocs/urls.py +++ b/readthedocs/urls.py @@ -44,6 +44,7 @@ url(r'^accounts/', include('readthedocs.profiles.urls.private')), url(r'^accounts/', include('allauth.urls')), url(r'^notifications/', include('readthedocs.notifications.urls')), + url(r'^accounts/gold/', include('readthedocs.gold.urls')), # For redirects url(r'^builds/', include('readthedocs.builds.urls')), # For testing the 404's with DEBUG on. @@ -83,11 +84,10 @@ groups = [basic_urls, rtd_urls, project_urls, api_urls, core_urls, i18n_urls, deprecated_urls] -if 'readthedocs.donate' in settings.INSTALLED_APPS: +if 'readthedocsext.donate' in settings.INSTALLED_APPS: # Include donation URL's groups.append([ - url(r'^sustainability/', include('readthedocs.donate.urls')), - url(r'^accounts/gold/', include('readthedocs.gold.urls')), + url(r'^sustainability/', include('readthedocsext.donate.urls')), ]) if not getattr(settings, 'USE_SUBDOMAIN', False) or settings.DEBUG: groups.insert(0, docs_urls) diff --git a/requirements/pip.txt b/requirements/pip.txt index 782dac3c8ea..caa6b917067 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -25,8 +25,6 @@ slumber==0.7.1 lxml==3.3.5 defusedxml==0.5.0 -django-countries==3.4.1 - # Basic tools redis==2.10.3 celery==3.1.23