From: Radek Czajka Date: Tue, 26 Mar 2013 11:54:40 +0000 (+0100) Subject: Basic crowdfunding. X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/8d1372958ce4a205f11fcdd44cf6e9cac8429a69 Basic crowdfunding. Make all timing stuff tz-aware. --- diff --git a/apps/funding/__init__.py b/apps/funding/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/funding/admin.py b/apps/funding/admin.py new file mode 100755 index 000000000..132c86304 --- /dev/null +++ b/apps/funding/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import Offer, Perk, Funding + +admin.site.register(Offer) +admin.site.register(Perk) +admin.site.register(Funding) diff --git a/apps/funding/migrations/0001_initial.py b/apps/funding/migrations/0001_initial.py new file mode 100644 index 000000000..924ef7728 --- /dev/null +++ b/apps/funding/migrations/0001_initial.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Offer' + db.create_table('funding_offer', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('author', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('slug', self.gf('django.db.models.fields.SlugField')(max_length=50)), + ('book_url', self.gf('django.db.models.fields.URLField')(max_length=200, blank=True)), + ('redakcja_url', self.gf('django.db.models.fields.URLField')(max_length=200, blank=True)), + ('target', self.gf('django.db.models.fields.DecimalField')(max_digits=10, decimal_places=2)), + ('start', self.gf('django.db.models.fields.DateField')()), + ('end', self.gf('django.db.models.fields.DateField')()), + )) + db.send_create_signal('funding', ['Offer']) + + # Adding model 'Perk' + db.create_table('funding_perk', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('offer', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['funding.Offer'], null=True)), + ('price', self.gf('django.db.models.fields.DecimalField')(max_digits=10, decimal_places=2)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('description', self.gf('django.db.models.fields.TextField')(blank=True)), + )) + db.send_create_signal('funding', ['Perk']) + + # Adding model 'Funding' + db.create_table('funding_funding', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('offer', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['funding.Offer'])), + ('name', self.gf('django.db.models.fields.CharField')(max_length=127)), + ('email', self.gf('django.db.models.fields.EmailField')(max_length=75)), + ('amount', self.gf('django.db.models.fields.DecimalField')(max_digits=10, decimal_places=2)), + ('payed_at', self.gf('django.db.models.fields.DateTimeField')()), + )) + db.send_create_signal('funding', ['Funding']) + + # Adding M2M table for field perks on 'Funding' + db.create_table('funding_funding_perks', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('funding', models.ForeignKey(orm['funding.funding'], null=False)), + ('perk', models.ForeignKey(orm['funding.perk'], null=False)) + )) + db.create_unique('funding_funding_perks', ['funding_id', 'perk_id']) + + + def backwards(self, orm): + # Deleting model 'Offer' + db.delete_table('funding_offer') + + # Deleting model 'Perk' + db.delete_table('funding_perk') + + # Deleting model 'Funding' + db.delete_table('funding_funding') + + # Removing M2M table for field perks on 'Funding' + db.delete_table('funding_funding_perks') + + + models = { + 'funding.funding': { + 'Meta': {'ordering': "['-payed_at']", 'object_name': 'Funding'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '2'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '127'}), + 'offer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['funding.Offer']"}), + 'payed_at': ('django.db.models.fields.DateTimeField', [], {}), + 'perks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['funding.Perk']", 'symmetrical': 'False'}) + }, + 'funding.offer': { + 'Meta': {'ordering': "['-end']", 'object_name': 'Offer'}, + 'author': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'book_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'end': ('django.db.models.fields.DateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'redakcja_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'start': ('django.db.models.fields.DateField', [], {}), + 'target': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '2'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'funding.perk': { + 'Meta': {'ordering': "['-price']", 'object_name': 'Perk'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'offer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['funding.Offer']", 'null': 'True'}), + 'price': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '2'}) + } + } + + complete_apps = ['funding'] \ No newline at end of file diff --git a/apps/funding/migrations/__init__.py b/apps/funding/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/funding/models.py b/apps/funding/models.py new file mode 100644 index 000000000..a088a07b7 --- /dev/null +++ b/apps/funding/models.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django.core.urlresolvers import reverse +from django.db import models +from django.utils.translation import ugettext_lazy as _, ugettext as __ +from datetime import date, datetime + + +class Offer(models.Model): + author = models.CharField(_('author'), max_length=255) + title = models.CharField(_('title'), max_length=255) + slug = models.SlugField(_('slug')) + book_url = models.URLField(_('book URL'), blank=True) + redakcja_url = models.URLField(_('redakcja URL'), blank=True) + target = models.DecimalField(_('target'), decimal_places=2, max_digits=10) + start = models.DateField(_('start')) + end = models.DateField(_('end')) + + class Meta: + verbose_name = _('offer') + verbose_name_plural = _('offers') + ordering = ['-end'] + + def __unicode__(self): + return u"%s – %s" % (self.author, self.title) + + def get_absolute_url(self): + return reverse('funding_offer', args=[self.slug]) + + @classmethod + def current(cls): + today = date.today() + objects = cls.objects.filter(start__lte=today, end__gte=today) + try: + return objects[0] + except IndexError: + return None + + @classmethod + def public(cls): + today = date.today() + return cls.objects.filter(start__lte=today) + + def get_perks(self, amount=None): + perks = Perk.objects.filter( + models.Q(offer=self) | models.Q(offer=None) + ) + if amount is not None: + perks = perks.filter(price__lte=amount) + return perks + + def fund(self, name, email, amount): + funding = self.funding_set.create( + name=name, email=email, amount=amount, + payed_at=datetime.now()) + funding.perks = self.get_perks(amount) + return funding + + def sum(self): + return self.funding_set.aggregate(s=models.Sum('amount'))['s'] or 0 + + def state(self): + if self.sum() >= self.target: + return 'win' + elif self.start <= date.today() <= self.end: + return 'running' + else: + return 'lose' + + +class Perk(models.Model): + offer = models.ForeignKey(Offer, verbose_name=_('offer'), null=True, blank=True) + price = models.DecimalField(_('price'), decimal_places=2, max_digits=10) + name = models.CharField(_('name'), max_length=255) + description = models.TextField(_('description'), blank=True) + + class Meta: + verbose_name = _('perk') + verbose_name_plural = _('perks') + ordering = ['-price'] + + def __unicode__(self): + return "%s (%s%s)" % (self.name, self.price, u" for %s" % self.offer if self.offer else "") + + +class Funding(models.Model): + offer = models.ForeignKey(Offer, verbose_name=_('offer')) + name = models.CharField(_('name'), max_length=127) + email = models.EmailField(_('email')) + amount = models.DecimalField(_('amount'), decimal_places=2, max_digits=10) + payed_at = models.DateTimeField(_('payed_at')) + perks = models.ManyToManyField(Perk, verbose_name=_('perks'), blank=True) + + class Meta: + verbose_name = _('funding') + verbose_name_plural = _('fundings') + ordering = ['-payed_at'] + + def __unicode__(self): + return "%s payed %s for %s" % (self.name, self.amount, self.offer) diff --git a/apps/funding/static/funding/.sass-cache/9a22640771b7b9d791c15ce50176b00a63a474df/funding.scssc b/apps/funding/static/funding/.sass-cache/9a22640771b7b9d791c15ce50176b00a63a474df/funding.scssc new file mode 100644 index 000000000..eea6a038f Binary files /dev/null and b/apps/funding/static/funding/.sass-cache/9a22640771b7b9d791c15ce50176b00a63a474df/funding.scssc differ diff --git a/apps/funding/static/funding/funding.css b/apps/funding/static/funding/funding.css new file mode 100755 index 000000000..7ed53e140 --- /dev/null +++ b/apps/funding/static/funding/funding.css @@ -0,0 +1,17 @@ +.standalone-funding { + border: 1px solid black; } + +.funding { + font-size: 1.5em; + padding: 0; + padding: .5em 1em; + background-image: url(/static/img/green-pixel.png); + background-repeat: repeat-y; } + .funding a { + color: black; } + +.funding-plus { + background: rgba(13, 126, 133, 0.2); } + +.funding-minus { + background: rgba(255, 0, 0, 0.2); } diff --git a/apps/funding/static/funding/funding.scss b/apps/funding/static/funding/funding.scss new file mode 100755 index 000000000..6268f48ac --- /dev/null +++ b/apps/funding/static/funding/funding.scss @@ -0,0 +1,23 @@ +.standalone-funding { + border: 1px solid black; +} + +.funding { + font-size: 1.5em; + padding: 0; + padding: .5em 1em; + background-image: url(/static/img/green-pixel.png); + background-repeat: repeat-y; + + a { + color: black; + } +} + +.funding-plus { + background: fade-out(#0D7E85, .8); +} + +.funding-minus { + background: fade-out(red, .8); +} diff --git a/apps/funding/templates/funding/offer_detail.html b/apps/funding/templates/funding/offer_detail.html new file mode 100755 index 000000000..c7aa6392a --- /dev/null +++ b/apps/funding/templates/funding/offer_detail.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% load funding_tags %} + +{% block titleextra %}{{ object }}{% endblock %} + +{% block body %} + +

{{ object }}

+
+ +{% funding object add_class="standalone-funding" %} + +{% if object.state == 'running' %} + + +

Wpłać:

+
+ {% for perk in object.get_perks %} +
+ {% endfor %} +
+ zł + + + +
+
+{% endif %} +
+ + +

Wpłaty:

+ +
+ + + +
+ +{% endblock %} diff --git a/apps/funding/templates/funding/offer_list.html b/apps/funding/templates/funding/offer_list.html new file mode 100755 index 000000000..926d1d3e2 --- /dev/null +++ b/apps/funding/templates/funding/offer_list.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% load funding_tags %} + +{% block titleextra %}Funding{% endblock %} + +{% block body %} +

Funding!

+ + + +{% endblock %} diff --git a/apps/funding/templates/funding/tags/funding.html b/apps/funding/templates/funding/tags/funding.html new file mode 100755 index 000000000..7d72c3161 --- /dev/null +++ b/apps/funding/templates/funding/tags/funding.html @@ -0,0 +1,11 @@ +{% load time_tags %} +
+ {% if link %}{% endif %} + {{ offer }}; + zebraliśmy {{ offer.sum }} z {{ offer.target }}{% if offer.state = 'running' %}; + do końca: + {% endif %} + {% if link %}{% endif %} +
diff --git a/apps/funding/templates/funding/wlfund.html b/apps/funding/templates/funding/wlfund.html new file mode 100755 index 000000000..eb33257a5 --- /dev/null +++ b/apps/funding/templates/funding/wlfund.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block titleextra %}Fundusz Wolnych Lektur{% endblock %} + +{% block body %} + +

Fundusz Wolnych Lektur

+

Suma: {{ amount }}

+ + + + +{% endblock %} diff --git a/apps/funding/templatetags/__init__.py b/apps/funding/templatetags/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/apps/funding/templatetags/funding_tags.py b/apps/funding/templatetags/funding_tags.py new file mode 100755 index 000000000..c533672b5 --- /dev/null +++ b/apps/funding/templatetags/funding_tags.py @@ -0,0 +1,17 @@ +from django import template +from ..models import Offer + +register = template.Library() + + +@register.inclusion_tag("funding/tags/funding.html") +def funding(offer=None, link=False, add_class=""): + if offer is None: + offer = Offer.current() + + return { + 'offer': offer, + 'percentage': 100 * offer.sum() / offer.target, + 'link': link, + 'add_class': add_class, + } diff --git a/apps/funding/tests.py b/apps/funding/tests.py new file mode 100644 index 000000000..c9438a878 --- /dev/null +++ b/apps/funding/tests.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django.test import TestCase +from .models import Offer, Perk, Funding + + +class FundTest(TestCase): + def setUp(self): + self.offer1 = Offer.objects.create( + author='author1', title='title1', slug='slug1', + target=100, start='2013-03-01', end='2013-03-31') + + def test_perks(self): + perk = Perk.objects.create(price=20, name='Perk 20') + perk1 = Perk.objects.create(offer=self.offer1, price=50, name='Perk 50') + offer2 = Offer.objects.create( + author='author2', title='title2', slug='slug2', + target=100, start='2013-02-01', end='2013-02-20') + perk2 = Perk.objects.create(offer=offer2, price=1, name='Perk 1') + + self.assertEqual( + set(self.offer1.fund('Tester', 'test@example.com', 10).perks.all()), + set()) + self.assertEqual( + set(self.offer1.fund('Tester', 'test@example.com', 70).perks.all()), + set([perk, perk1])) diff --git a/apps/funding/urls.py b/apps/funding/urls.py new file mode 100755 index 000000000..c712b5e08 --- /dev/null +++ b/apps/funding/urls.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django.conf.urls import patterns, url +from django.views.generic import DetailView, ListView, FormView + +from .models import Offer +from .views import WLFundView + + +urlpatterns = patterns('', + url(r'^$', ListView.as_view(queryset=Offer.public()), name='funding'), + url(r'lektura/^(?P[^/]+)/$', DetailView.as_view(queryset=Offer.public()), name='funding_offer'), + url(r'fundusz/$', WLFundView.as_view(), name='funding_wlfund'), +) diff --git a/apps/funding/views.py b/apps/funding/views.py new file mode 100644 index 000000000..dab996a75 --- /dev/null +++ b/apps/funding/views.py @@ -0,0 +1,39 @@ +# Create your views here. +from django.views.generic import TemplateView +from .models import Offer + + +def mix(*streams): + substreams = [] + for stream, read_date in streams: + iterstream = iter(stream) + try: + item = next(iterstream) + except StopIteration: + pass + else: + substreams.append([read_date(item), item, iterstream, read_date]) + while substreams: + i, substream = max(enumerate(substreams), key=lambda x: x[0]) + yield substream[1] + try: + item = next(substream[2]) + except StopIteration: + del substreams[i] + else: + substream[0:2] = [substream[3](item), item] + + +class WLFundView(TemplateView): + template_name = "funding/wlfund.html" + + def get_context_data(self): + ctx = super(WLFundView, self).get_context_data() + offers = [o for o in Offer.objects.all() if o.state() == 'lose' and o.sum()] + amount = sum(o.sum() for o in offers) + print offers + + #offers = (o for o in Offer.objects.all() if o.state() == 'lose' and o.sum()) + ctx['amount'] = amount + ctx['log'] = mix((offers, lambda x: x.end)) + return ctx diff --git a/apps/oai/handlers.py b/apps/oai/handlers.py index 59e599c98..eabb59c9f 100644 --- a/apps/oai/handlers.py +++ b/apps/oai/handlers.py @@ -12,6 +12,7 @@ from lxml.etree import ElementTree from django.db.models import Q from django.conf import settings from django.contrib.sites.models import Site +from django.utils import timezone WL_DC_READER_XPATH = '(.|*)/rdf:RDF/rdf:Description/%s/text()' @@ -60,7 +61,7 @@ class Catalogue(common.ResumptionOAIPMH): self.oai_id = "oai:" + Site.objects.get_current().domain + ":%s" # earliest change - year_zero = datetime(1990, 1, 1, 0, 0, 0) + year_zero = timezone.make_aware(datetime(1990, 1, 1, 0, 0, 0), timezone.utc) try: earliest_change = \ diff --git a/apps/pdcounter/static/pdcounter/pdcounter.js b/apps/pdcounter/static/pdcounter/pdcounter.js index 0352076b8..661fc9e14 100755 --- a/apps/pdcounter/static/pdcounter/pdcounter.js +++ b/apps/pdcounter/static/pdcounter/pdcounter.js @@ -1,8 +1,7 @@ (function($) { $(function() { - - $('#countdown').each(function() { + $('.countdown').each(function() { var $this = $(this); var serverTime = function() { @@ -24,13 +23,19 @@ $.countdown.setDefaults($.countdown.regional['']); } - var d = new Date($this.attr('data-year'), 0, 1); - function re() {location.reload()}; - $this.countdown({until: d, format: 'ydHMS', serverSync: serverTime, - onExpiry: re, alwaysExpire: true}); - + var options = { + until: new Date($this.attr('data-until')), + format: 'ydHMS', + serverSync: serverTime, + onExpiry: function(){location.reload()}, // TODO: no reload + }; + if ($this.hasClass('inline')) { + options.layout = '{dn} {dl} {hnn}{sep}{mnn}{sep}{snn}'; + } + + $this.countdown(options); }); }); -})(jQuery); \ No newline at end of file +})(jQuery); diff --git a/apps/pdcounter/templates/pdcounter/author_detail.html b/apps/pdcounter/templates/pdcounter/author_detail.html index 3810a4578..352b2050d 100644 --- a/apps/pdcounter/templates/pdcounter/author_detail.html +++ b/apps/pdcounter/templates/pdcounter/author_detail.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load i18n %} +{% load time_tags %} {% block titleextra %}{{ author.name }}{% endblock %} @@ -40,7 +41,7 @@ {% else %}

{% trans "This author's works will become part of public domain and will be allowed to be published without restrictions in" %}

-
+

{% trans "Find out why Internet libraries can't publish this author's works." %}

{% endif %} diff --git a/apps/pdcounter/templates/pdcounter/book_stub_detail.html b/apps/pdcounter/templates/pdcounter/book_stub_detail.html index f76edd17a..03ba8fa16 100644 --- a/apps/pdcounter/templates/pdcounter/book_stub_detail.html +++ b/apps/pdcounter/templates/pdcounter/book_stub_detail.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load i18n %} +{% load time_tags %} {% block titleextra %}{{ book.title }}{% endblock %} @@ -17,7 +18,7 @@ {% else %} {% if book.pd %}

{% trans "This work will become part of public domain and will be allowed to be published without restrictions in" %}

-
+

{% trans "Find out why Internet libraries can't publish this work." %}

{% else %}

{% trans "This work is copyrighted." %} diff --git a/apps/pdcounter/templatetags/__init__.py b/apps/pdcounter/templatetags/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/apps/pdcounter/templatetags/time_tags.py b/apps/pdcounter/templatetags/time_tags.py new file mode 100755 index 000000000..1d4f23a93 --- /dev/null +++ b/apps/pdcounter/templatetags/time_tags.py @@ -0,0 +1,21 @@ +import datetime +import pytz +from django.conf import settings +from django import template +from django.utils import timezone + + +register = template.Library() + +@register.filter +def local_to_utc(localtime): + if isinstance(localtime, datetime.date): + localtime = datetime.datetime.combine(localtime, datetime.time(0,0)) + return timezone.utc.normalize( + pytz.timezone(settings.TIME_ZONE).localize(localtime) + ) + + +@register.filter +def utc_for_js(dt): + return dt.strftime('%Y/%m/%d %H:%M:%S UTC') diff --git a/apps/pdcounter/views.py b/apps/pdcounter/views.py index b07ee11e9..8fb1b13fa 100644 --- a/apps/pdcounter/views.py +++ b/apps/pdcounter/views.py @@ -2,8 +2,7 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -#from datetime import datetime - +from datetime import datetime from django.template import RequestContext from django.shortcuts import render_to_response, get_object_or_404 from pdcounter import models @@ -12,7 +11,7 @@ from suggest.forms import PublishingSuggestForm def book_stub_detail(request, slug): book = get_object_or_404(models.BookStub, slug=slug) - pd_counter = book.pd + pd_counter = datetime(book.pd, 1, 1) form = PublishingSuggestForm( initial={"books": u"%s — %s, \n" % (book.author, book.title)}) @@ -23,7 +22,7 @@ def book_stub_detail(request, slug): def author_detail(request, slug): author = get_object_or_404(models.Author, slug=slug) - pd_counter = author.goes_to_pd() + pd_counter = datetime(author.goes_to_pd(), 1, 1) form = PublishingSuggestForm(initial={"books": author.name + ", \n"}) diff --git a/apps/wolnelektury_core/static/css/header.css b/apps/wolnelektury_core/static/css/header.css index 633e8bde8..f1bd0df94 100755 --- a/apps/wolnelektury_core/static/css/header.css +++ b/apps/wolnelektury_core/static/css/header.css @@ -25,6 +25,9 @@ color: #989898; } +#logo a, #logo img { + display: block; +} #tagline { margin-left: 1.5em; diff --git a/apps/wolnelektury_core/static/img/green-pixel.png b/apps/wolnelektury_core/static/img/green-pixel.png new file mode 100644 index 000000000..edb512d11 Binary files /dev/null and b/apps/wolnelektury_core/static/img/green-pixel.png differ diff --git a/apps/wolnelektury_core/templates/superbase.html b/apps/wolnelektury_core/templates/superbase.html index 95220b6b6..a54cb4bca 100644 --- a/apps/wolnelektury_core/templates/superbase.html +++ b/apps/wolnelektury_core/templates/superbase.html @@ -2,7 +2,7 @@ {% load cache compressed i18n %} {% load static from staticfiles %} - {% load catalogue_tags reporting_stats sponsor_tags %} + {% load catalogue_tags funding_tags reporting_stats sponsor_tags %} @@ -121,9 +121,8 @@ -

- + {% funding link=1 %}