Merge pull request #2770 from rtfd/promo-filter-updates
Add a bit more color to the promo display.tox-dependencies
commit
817fe496e1
|
@ -45,7 +45,7 @@ class SupporterPromoAdmin(admin.ModelAdmin):
|
|||
model = SupporterPromo
|
||||
save_as = True
|
||||
prepopulated_fields = {'analytics_id': ('name',)}
|
||||
list_display = ('name', 'live', 'click_ratio', 'sold_impressions',
|
||||
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')
|
||||
|
|
|
@ -22,3 +22,13 @@ IMPRESSION_TYPES = (
|
|||
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'),
|
||||
)
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.12 on 2017-04-04 13:48
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.12 on 2017-04-12 13:05
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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'),
|
||||
),
|
||||
]
|
|
@ -8,7 +8,7 @@ from django_countries.fields import CountryField
|
|||
|
||||
from readthedocs.donate.utils import get_ad_day
|
||||
from readthedocs.donate.constants import (
|
||||
DISPLAY_CHOICES, FILTER_CHOICES, IMPRESSION_TYPES
|
||||
DISPLAY_CHOICES, FILTER_CHOICES, IMPRESSION_TYPES, THEMES, READTHEDOCS_THEME
|
||||
)
|
||||
from readthedocs.projects.models import Project
|
||||
from readthedocs.projects.constants import PROGRAMMING_LANGUAGES
|
||||
|
@ -48,11 +48,15 @@ class SupporterPromo(models.Model):
|
|||
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=1000)
|
||||
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)
|
||||
live = models.BooleanField(_('Live'), default=False)
|
||||
|
||||
class Meta:
|
||||
|
@ -148,6 +152,15 @@ class SupporterPromo(models.Model):
|
|||
(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 fruad protection!
|
||||
"""
|
||||
return self.text.replace('<a>', "<a href='%s'>" % self.link)
|
||||
|
||||
|
||||
class BaseImpression(models.Model):
|
||||
date = models.DateField(_('Date'))
|
||||
|
@ -172,24 +185,28 @@ class BaseImpression(models.Model):
|
|||
def click_ratio(self):
|
||||
if self.views == 0:
|
||||
return 0 # Don't divide by 0
|
||||
return float(
|
||||
return '%.3f' % float(
|
||||
float(self.clicks) / float(self.views) * 100
|
||||
)
|
||||
|
||||
|
||||
class PromoImpressions(BaseImpression):
|
||||
"""Track stats around how successful this promo has been.
|
||||
"""
|
||||
Track stats around how successful this promo has been.
|
||||
|
||||
Indexed one per promo per day."""
|
||||
Indexed one per promo per day.
|
||||
"""
|
||||
|
||||
promo = models.ForeignKey(SupporterPromo, related_name='impressions',
|
||||
blank=True, null=True)
|
||||
|
||||
|
||||
class ProjectImpressions(BaseImpression):
|
||||
"""Track stats for a specific project and promo.
|
||||
"""
|
||||
Track stats for a specific project and promo.
|
||||
|
||||
Indexed one per project per promo per day"""
|
||||
Indexed one per project per promo per day
|
||||
"""
|
||||
|
||||
promo = models.ForeignKey(SupporterPromo, related_name='project_impressions',
|
||||
blank=True, null=True)
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
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
|
||||
|
@ -9,6 +13,8 @@ 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:
|
||||
|
@ -82,7 +88,7 @@ def choose_promo(promo_list):
|
|||
return None
|
||||
|
||||
|
||||
def get_promo(country_code, programming_language, gold_project=False, gold_user=False):
|
||||
def get_promo(country_code, programming_language, theme, gold_project=False, gold_user=False):
|
||||
"""
|
||||
Get a proper promo.
|
||||
|
||||
|
@ -97,15 +103,18 @@ def get_promo(country_code, programming_language, gold_project=False, gold_user=
|
|||
promo_queryset = SupporterPromo.objects.filter(live=True, display_type='doc')
|
||||
|
||||
filtered_promos = []
|
||||
for obj in promo_queryset:
|
||||
for promo in promo_queryset:
|
||||
# Break out if we aren't meant to show to this language
|
||||
if obj.programming_language and not show_to_programming_language(obj, programming_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(obj, country_code):
|
||||
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(obj)
|
||||
filtered_promos.append(promo)
|
||||
|
||||
promo_obj = choose_promo(filtered_promos)
|
||||
|
||||
|
@ -140,6 +149,7 @@ def attach_promo_data(sender, **kwargs):
|
|||
resp_data = kwargs['resp_data']
|
||||
|
||||
project = context['project']
|
||||
theme = context['theme']
|
||||
|
||||
# Bail out early if promo's are disabled.
|
||||
use_promo = getattr(settings, 'USE_PROMOS', True)
|
||||
|
@ -181,6 +191,7 @@ def attach_promo_data(sender, **kwargs):
|
|||
promo_obj = get_promo(
|
||||
country_code=country_code,
|
||||
programming_language=project.programming_language,
|
||||
theme=theme,
|
||||
gold_project=gold_project,
|
||||
gold_user=gold_user,
|
||||
)
|
||||
|
@ -195,3 +206,25 @@ def attach_promo_data(sender, **kwargs):
|
|||
|
||||
# Set promo object on return JSON
|
||||
resp_data['promo'] = show_promo
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
"""
|
||||
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)
|
||||
except (AttributeError, redis.exceptions.ConnectionError):
|
||||
log.warning('Redis theme indexing error: %s', exc_info=True)
|
||||
|
|
|
@ -3,4 +3,13 @@
|
|||
margin-right: auto;
|
||||
width: 900px;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.promo {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.filters dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
|
@ -5,31 +5,76 @@
|
|||
|
||||
{% block title %}{% trans "Promo Detail" %}{% endblock %}
|
||||
|
||||
{% block extra_links %}
|
||||
<link rel="stylesheet" href="{% static 'donate/css/donate.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1> Promo Results </h1>
|
||||
|
||||
{% if promos %}
|
||||
|
||||
{% if promos|length > 1 %}
|
||||
<p>
|
||||
Total Clicks for all shown promos: {{ total_clicks }}
|
||||
<strong>Total Clicks for all shown promos</strong>: {{ total_clicks }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div id="promo_detail">
|
||||
{% 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 class="example">
|
||||
|
||||
<div class="filters">
|
||||
{% if promo.programming_language %}
|
||||
<dl>
|
||||
<dt>Filtered Language</dt>
|
||||
<dd>{{ promo.programming_language }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
|
||||
{% if promo.geo_filters.count %}
|
||||
<dl>
|
||||
<dt>Filtered Geos</dt>
|
||||
{% for geo in promo.geo_filters.all %}
|
||||
<dd>
|
||||
{{ geo.get_filter_type_display }}: {{ geo.countries.all|join:", " }}
|
||||
</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
{% endif %}
|
||||
|
||||
{% if promo.sold_clicks %}
|
||||
<dl>
|
||||
<dt>Total Clicks Sold</dt>
|
||||
<dd>
|
||||
{{ promo.sold_clicks }}
|
||||
</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="promo">
|
||||
<div id="promo_image">
|
||||
<a href="{{ promo.link }}">
|
||||
<img width=120 height=90 src="{{ promo.image }}">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="promo_text">
|
||||
{{ promo.report_html_text|safe }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<h5>Promo Data</h5>
|
||||
<table>
|
||||
<tr>
|
||||
<th><strong>Day (UTC)</strong></th>
|
||||
|
@ -56,6 +101,7 @@ Results for {{ promo.name }} ({{ promo.analytics_id }}) over last {{ days }} day
|
|||
</table>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ class PromoDetailView(TemplateView):
|
|||
|
||||
if promo_slug == 'live' and self.request.user.is_staff:
|
||||
promos = SupporterPromo.objects.filter(live=True)
|
||||
elif '*' in promo_slug:
|
||||
elif promo_slug[-1] == '*' and '-' in promo_slug:
|
||||
promos = SupporterPromo.objects.filter(
|
||||
analytics_id__contains=promo_slug.replace('*', '')
|
||||
)
|
||||
|
@ -124,8 +124,14 @@ def click_proxy(request, promo_id, hash):
|
|||
project = Project.objects.get(slug=project_slug)
|
||||
promo.incr(CLICKS, project=project)
|
||||
else:
|
||||
log.warning('Duplicate click logged. {count} total clicks tried.'.format(count=count))
|
||||
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))
|
||||
raise Http404('Invalid click. This has been logged.')
|
||||
return redirect(promo.link)
|
||||
|
||||
|
||||
|
@ -147,12 +153,18 @@ def view_proxy(request, promo_id, hash):
|
|||
project = Project.objects.get(slug=project_slug)
|
||||
promo.incr(VIEWS, project=project)
|
||||
else:
|
||||
log.warning('Duplicate view logged. {count} total clicks tried.'.format(count=count))
|
||||
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))
|
||||
raise Http404('Invalid click. This has been logged.')
|
||||
return redirect(promo.image)
|
||||
|
||||
|
||||
def add_promo_data(display_type):
|
||||
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:
|
||||
|
@ -164,7 +176,7 @@ def add_promo_data(display_type):
|
|||
|
||||
def promo_500(request, template_name='donate/promo_500.html', **kwargs):
|
||||
"""A simple 500 handler so we get media"""
|
||||
promo_dict = add_promo_data(display_type='error')
|
||||
promo_dict = _add_promo_data(display_type='error')
|
||||
r = render_to_response(template_name,
|
||||
context_instance=RequestContext(request),
|
||||
context={
|
||||
|
@ -176,7 +188,7 @@ def promo_500(request, template_name='donate/promo_500.html', **kwargs):
|
|||
|
||||
def promo_404(request, template_name='donate/promo_404.html', **kwargs):
|
||||
"""A simple 404 handler so we get media"""
|
||||
promo_dict = add_promo_data(display_type='error')
|
||||
promo_dict = _add_promo_data(display_type='error')
|
||||
response = get_redirect_response(request, path=request.get_full_path())
|
||||
if response:
|
||||
return response
|
||||
|
|
|
@ -105,6 +105,7 @@ def footer_html(request):
|
|||
'github_edit_url': version.get_github_url(docroot, page_slug, source_suffix, 'edit'),
|
||||
'github_view_url': version.get_github_url(docroot, page_slug, source_suffix, 'view'),
|
||||
'bitbucket_url': version.get_bitbucket_url(docroot, page_slug, source_suffix),
|
||||
'theme': theme,
|
||||
}
|
||||
|
||||
request_context = RequestContext(request, context)
|
||||
|
@ -116,7 +117,8 @@ def footer_html(request):
|
|||
'version_supported': version.supported,
|
||||
}
|
||||
|
||||
# Allow folks to hook onto the footer response for various information usage.
|
||||
# Allow folks to hook onto the footer response for various information collection,
|
||||
# or to modify the resp_data.
|
||||
footer_response.send(sender=None, request=request, context=context, resp_data=resp_data)
|
||||
|
||||
return Response(resp_data)
|
||||
|
|
Loading…
Reference in New Issue