From 09d2896f082bddfe21735e973bc723e45b239fa7 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Wed, 9 Jan 2013 09:40:13 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 + LICENSE | 27 +++++ MANIFEST.in | 8 ++ README.rst | 3 + feincms_banners/__init__.py | 2 + feincms_banners/admin.py | 16 +++ feincms_banners/contents.py | 47 ++++++++ feincms_banners/migrations/__init__.py | 0 feincms_banners/models.py | 108 ++++++++++++++++++ .../templates/content/banner/box.html | 9 ++ .../templates/content/banner/default.html | 1 + .../templates/content/banner/leaderboard.html | 12 ++ .../templates/content/banner/skyscraper.html | 22 ++++ feincms_banners/urls.py | 7 ++ feincms_banners/views.py | 19 +++ requirements.txt | 1 + setup.py | 30 +++++ setuplib.py | 58 ++++++++++ 18 files changed, 372 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 feincms_banners/__init__.py create mode 100644 feincms_banners/admin.py create mode 100644 feincms_banners/contents.py create mode 100644 feincms_banners/migrations/__init__.py create mode 100644 feincms_banners/models.py create mode 100644 feincms_banners/templates/content/banner/box.html create mode 100644 feincms_banners/templates/content/banner/default.html create mode 100644 feincms_banners/templates/content/banner/leaderboard.html create mode 100644 feincms_banners/templates/content/banner/skyscraper.html create mode 100644 feincms_banners/urls.py create mode 100644 feincms_banners/views.py create mode 100644 requirements.txt create mode 100755 setup.py create mode 100644 setuplib.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7fdea58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +*.egg-info diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..df797cb --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013, FEINHEIT GmbH and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of FEINHEIT GmbH nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..60098f9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +include LICENSE +include MANIFEST.in +include README.rst +include requirements.txt +include setuplib.py +recursive-include feincms_banners/static * +recursive-include feincms_banners/locale * +recursive-include feincms_banners/templates * diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..f20199b --- /dev/null +++ b/README.rst @@ -0,0 +1,3 @@ +=========================================================================== +feincms-banners -- a simple banner system with views and clicks for FeinCMS +=========================================================================== diff --git a/feincms_banners/__init__.py b/feincms_banners/__init__.py new file mode 100644 index 0000000..c6d9b06 --- /dev/null +++ b/feincms_banners/__init__.py @@ -0,0 +1,2 @@ +VERSION = (1, 0, 0) +__version__ = '.'.join(map(str, VERSION)) \ No newline at end of file diff --git a/feincms_banners/admin.py b/feincms_banners/admin.py new file mode 100644 index 0000000..e8cbf97 --- /dev/null +++ b/feincms_banners/admin.py @@ -0,0 +1,16 @@ +from django.contrib import admin + +from feincms_banners import models + + +admin.site.register(models.Banner, + list_display=('name', 'is_active', 'type', 'url', 'active_from', + 'active_until', 'embeds', 'impressions', 'click_count'), + list_filter=('is_active', 'type'), + raw_id_fields=('mediafile',), + search_fields=('name', 'url', 'code'), + ) +admin.site.register(models.Click, + list_display=('timestamp', 'banner', 'ip', 'user_agent', 'referrer'), + search_fields=('banner__name', 'user_agent', 'referrer'), + ) diff --git a/feincms_banners/contents.py b/feincms_banners/contents.py new file mode 100644 index 0000000..c7e3c5e --- /dev/null +++ b/feincms_banners/contents.py @@ -0,0 +1,47 @@ +from django.db import models +from django.template.loader import render_to_string +from django.utils.translation import ugettext_lazy as _ + +from feincms_banners.models import Banner + +from feincms.admin.item_editor import FeinCMSInline + + +class BannerContentInline(FeinCMSInline): + raw_id_fields = ('specific',) + + +class BannerContent(models.Model): + feincms_item_editor_inline = BannerContentInline + + is_section_aware = True + + specific = models.ForeignKey(Banner, verbose_name=_('specific'), + blank=True, null=True, help_text=_('If you leave this empty, a random banner will be selected.'), + limit_choices_to={'is_active': True}) + type = models.CharField(_('type'), max_length=20, choices=Banner.TYPE_CHOICES) + + class Meta: + abstract = True + verbose_name = _('banner') + verbose_name = _('banners') + + def render(self, **kwargs): + if self.specific: + if self.specific.is_active: + banner = self.specific + type = banner.type + else: + return u'' + else: + try: + banner = Banner.objects.active().filter( + type=self.type).select_related('mediafile').order_by('?')[0] + type = self.type + except IndexError: + return u'' + + return render_to_string([ + 'content/banner/%s.html' % type, + 'content/banner/default.html', + ], {'content': self, 'banner': banner}) diff --git a/feincms_banners/migrations/__init__.py b/feincms_banners/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/feincms_banners/models.py b/feincms_banners/models.py new file mode 100644 index 0000000..1db64d2 --- /dev/null +++ b/feincms_banners/models.py @@ -0,0 +1,108 @@ +from random import choice + +from django.db import models +from django.db.models import Q +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + +from feincms.module.medialibrary.fields import MediaFileForeignKey +from feincms.module.medialibrary.models import MediaFile + + +def generate_key(): + return ''.join([ + choice('abcdefghijklmnopqrstuvwxyz0123456789-_') for i in range(40)]) + + +class BannerManager(models.Manager): + def active(self): + return self.filter( + Q(is_active=True, active_from__lte=timezone.now()) + & ( + Q(active_until__isnull=True) + | Q( + active_until__isnull=False, + active_until__gte=timezone.now() + ) + ) + ) + + +class Banner(models.Model): + SKYSCRAPER = 'skyscraper' + LEADERBOARD = 'leaderboard' + BOX = 'box' + + TYPE_CHOICES = ( + (SKYSCRAPER, _('skyscraper')), + (LEADERBOARD, _('leaderboard')), + (BOX, _('box')), + ) + + is_active = models.BooleanField(_('is active'), default=True) + name = models.CharField(_('name'), max_length=100, + help_text=_('Only for internal use, will not be shown on the website.' + )) + mediafile = MediaFileForeignKey(MediaFile, verbose_name=_('media file')) + url = models.URLField(_('URL'), verify_exists=False) + type = models.CharField(_('type'), max_length=20, choices=TYPE_CHOICES) + code = models.CharField(_('code'), max_length=40, default=generate_key, + unique=True) + + active_from = models.DateTimeField(_('active from'), default=timezone.now) + active_until = models.DateTimeField(_('active until'), + blank=True, null=True) + + embeds = models.PositiveIntegerField(_('embeds'), default=0, + editable=False, + help_text=_('How many times has this banner been embdedded on a' + ' website?') + impressions = models.PositiveIntegerField(_('impressions'), default=0, + editable=False, + help_text=_('How many times has an impression been registered using' + ' a Javascript callback, verifying that it actually was a' + ' browser? (Too low because of network issues and deactivated' + ' Javascript support in some browsers.)')) + + objects = BannerManager() + + class Meta: + ordering= ['-active_from'] + verbose_name = _('banner') + verbose_name_plural = _('banners') + + def __unicode__(self): + return self.name + + @models.permalink + def get_absolute_url(self): + return ('banner_click', (), {'code': self.code}) + + @models.permalink + def impression_url(self): + return ('banner_impression', (), {'code': self.code}) + + def click(self, request): + self.clicks.create( + ip=request.META.get('REMOTE_ADDR'), + user_agent=request.META.get('HTTP_USER_AGENT', ''), + referrer=request.META.get('HTTP_REFERER', ''), + ) + + def click_count(self): + return self.clicks.count() + click_count.short_description = _('click count') + + +class Click(models.Model): + banner = models.ForeignKey(Banner, verbose_name=_('banner'), + related_name='clicks') + timestamp = models.DateTimeField(_('timestamp'), default=timezone.now) + ip = models.IPAddressField(_('IP'), blank=True, null=True) + user_agent = models.TextField(_('user agent'), blank=True, default='') + referrer = models.TextField(_('referrer'), blank=True, default='') + + class Meta: + orering = ['-timestamp'] + verbose_name = _('click') + verbose_name_plural = _('clicks') diff --git a/feincms_banners/templates/content/banner/box.html b/feincms_banners/templates/content/banner/box.html new file mode 100644 index 0000000..54272ab --- /dev/null +++ b/feincms_banners/templates/content/banner/box.html @@ -0,0 +1,9 @@ + + + diff --git a/feincms_banners/templates/content/banner/default.html b/feincms_banners/templates/content/banner/default.html new file mode 100644 index 0000000..a590460 --- /dev/null +++ b/feincms_banners/templates/content/banner/default.html @@ -0,0 +1 @@ + diff --git a/feincms_banners/templates/content/banner/leaderboard.html b/feincms_banners/templates/content/banner/leaderboard.html new file mode 100644 index 0000000..a83c07d --- /dev/null +++ b/feincms_banners/templates/content/banner/leaderboard.html @@ -0,0 +1,12 @@ +
+ + + +
+ diff --git a/feincms_banners/templates/content/banner/skyscraper.html b/feincms_banners/templates/content/banner/skyscraper.html new file mode 100644 index 0000000..d98209a --- /dev/null +++ b/feincms_banners/templates/content/banner/skyscraper.html @@ -0,0 +1,22 @@ +
+
Anzeige
+
+{% if banner.mediafile.type == 'swf' %} + + + + +{% else %} + + + +{% endif %} +
+
+ diff --git a/feincms_banners/urls.py b/feincms_banners/urls.py new file mode 100644 index 0000000..6c9f39b --- /dev/null +++ b/feincms_banners/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls.defaults import url, patterns, include + + +urlpatterns = patterns('feincms_banners.views', + url(r'^b/c/(?P[^/]+)/$', 'click', name='banner_click'), + url(r'^b/i/(?P[^/]+)/$', 'impression', name='banner_impression'), +) diff --git a/feincms_banners/views.py b/feincms_banners/views.py new file mode 100644 index 0000000..8a9492b --- /dev/null +++ b/feincms_banners/views.py @@ -0,0 +1,19 @@ +from django.db.models import F +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404 + +from feincms_banners.models import Banner + + +def click(request, code): + banner = get_object_or_404(Banner, code=code) + banner.click(request) + return HttpResponseRedirect(banner.url) + + +def impression(request, code): + if Banner.objects.filter(code=code).update( + impressions=F('impressions') + 1 + ): + return HttpResponse('+1') + return HttpResponse('?') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2f37ece --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +feincms>=1.6 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..f885a5f --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +from distutils.core import setup +import os +import setuplib + +packages, package_data = setuplib.find_packages('feincms_banners') + +setup(name='feincms-banners', + version=__import__('feincms_banners').__version__, + description='A simple banner system with views and clicks for FeinCMS.', + long_description=open(os.path.join(os.path.dirname(__file__), 'README.rst')).read(), + author='Matthias Kestenholz', + author_email='mk@feinheit.ch', + url='https://github.com/matthiask/feincms-banners/', + license='BSD License', + platforms=['OS Independent'], + packages=packages, + package_data=package_data, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Web Environment', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + ], +) diff --git a/setuplib.py b/setuplib.py new file mode 100644 index 0000000..42f6f4e --- /dev/null +++ b/setuplib.py @@ -0,0 +1,58 @@ +import os + + +__all__ = ['find_files'] + + +def fullsplit(path, result=None): + """ + Split a pathname into components (the opposite of os.path.join) in a + platform-neutral way. + """ + if result is None: + result = [] + head, tail = os.path.split(path) + if head == '': + return [tail] + result + if head == path: + return result + return fullsplit(head, [tail] + result) + + +def find_packages(package_dir): + """ + Returns a tuple consisting of a ``packages`` list and a ``package_data`` + dictionary suitable for passing on to ``distutils.core.setup`` + + Requires the folder name containing the package files; ``find_files`` + assumes that ``setup.py`` is located in the same folder as the folder + containing those files. + + Code lifted from Django's ``setup.py``, with improvements by PSyton. + """ + + # Compile the list of packages available, because distutils doesn't have + # an easy way to do this. + packages = [] + package_data = {} + root_dir = os.path.dirname(__file__) + if root_dir != '': + os.chdir(root_dir) + + for dirpath, dirnames, filenames in sorted(os.walk(package_dir)): + # Ignore dirnames that start with '.' + for i, dirname in enumerate(dirnames): + if dirname.startswith('.'): del dirnames[i] + if '__init__.py' in filenames: + packages.append('.'.join(fullsplit(dirpath))) + elif filenames: + cur_pack = packages[0] # Assign all data files to the toplevel package + if cur_pack not in package_data: + package_data[cur_pack] = [] + package_dir = os.path.join(*cur_pack.split(".")) + dir_relpath = os.path.relpath(dirpath, package_dir) + for f in filenames: + package_data[cur_pack].append(os.path.join(dir_relpath, f)) + + return packages, package_data +