├── demo ├── demo │ ├── demo │ │ ├── __init__.py │ │ ├── wsgi.py │ │ ├── urls.py │ │ └── settings.py │ ├── articles │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── urls.py │ │ ├── views.py │ │ ├── models.py │ │ └── fixtures │ │ │ └── articles_article.json │ ├── templates │ │ ├── base.html │ │ ├── articles │ │ │ ├── article_list.html │ │ │ └── article_detail.html │ │ └── registration │ │ │ └── login.html │ └── manage.py └── requirements.txt ├── setup.cfg ├── regwall ├── tests │ ├── articles │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── __init__.py │ │ ├── urls.py │ │ ├── apps.py │ │ ├── views.py │ │ ├── models.py │ │ └── fixtures │ │ │ └── articles_article.json │ ├── urls.py │ ├── templates │ │ ├── base.html │ │ ├── articles │ │ │ └── article_detail.html │ │ └── registration │ │ │ └── login.html │ ├── __init__.py │ └── settings.py ├── __init__.py ├── templatetags │ ├── __init__.py │ └── regwall_tags.py ├── settings.py ├── templates │ └── regwall │ │ ├── history.html │ │ ├── login.html │ │ └── detail.html └── mixins.py ├── docs ├── _static │ └── img │ │ ├── regwall-login.png │ │ └── regwall-detail.png ├── Makefile ├── install.rst ├── tests.rst ├── make.bat ├── documentation.rst ├── settings.rst ├── index.rst ├── conf.py └── usage.rst ├── MANIFEST.in ├── .travis.yml ├── LICENSE ├── .gitignore ├── setup.py └── README.rst /demo/demo/demo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/demo/articles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | -------------------------------------------------------------------------------- /demo/demo/articles/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 -------------------------------------------------------------------------------- /regwall/tests/articles/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /regwall/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | -------------------------------------------------------------------------------- /regwall/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | -------------------------------------------------------------------------------- /regwall/tests/articles/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | -------------------------------------------------------------------------------- /demo/demo/articles/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /demo/demo/articles/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ArticlesConfig(AppConfig): 5 | name = 'articles' 6 | -------------------------------------------------------------------------------- /docs/_static/img/regwall-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardcornish/django-registrationwall/HEAD/docs/_static/img/regwall-login.png -------------------------------------------------------------------------------- /docs/_static/img/regwall-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardcornish/django-registrationwall/HEAD/docs/_static/img/regwall-detail.png -------------------------------------------------------------------------------- /regwall/tests/articles/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from .views import ArticleDetailView 4 | 5 | 6 | urlpatterns = [ 7 | url(r'^(?P[-\w]+)/$', ArticleDetailView.as_view(), name='article_detail'), 8 | ] 9 | -------------------------------------------------------------------------------- /regwall/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | 3 | 4 | urlpatterns = [ 5 | url(r'^accounts/', include('django.contrib.auth.urls')), 6 | url(r'^articles/', include('regwall.tests.articles.urls')), 7 | ] 8 | -------------------------------------------------------------------------------- /regwall/tests/articles/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | 7 | class ArticlesConfig(AppConfig): 8 | name = _('articles') 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include MANIFEST.in 3 | include README.rst 4 | recursive-include demo * 5 | recursive-include docs * 6 | recursive-include regwall/templates * 7 | recursive-include regwall/tests/articles/fixtures * 8 | recursive-include regwall/tests/templates * -------------------------------------------------------------------------------- /regwall/tests/articles/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import DetailView 2 | 3 | from regwall.mixins import RaiseRegWallMixin 4 | 5 | from .models import Article 6 | 7 | 8 | class ArticleDetailView(RaiseRegWallMixin, DetailView): 9 | model = Article 10 | -------------------------------------------------------------------------------- /demo/demo/articles/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from .views import ArticleDetailView, ArticleListView 4 | 5 | 6 | urlpatterns = [ 7 | url(r'^(?P[-\w]+)/$', ArticleDetailView.as_view(), name='article_detail'), 8 | url(r'^$', ArticleListView.as_view(), name='article_list'), 9 | ] 10 | -------------------------------------------------------------------------------- /demo/demo/articles/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import DetailView, ListView 2 | 3 | from regwall.mixins import RaiseRegWallMixin 4 | 5 | from .models import Article 6 | 7 | 8 | class ArticleDetailView(RaiseRegWallMixin, DetailView): 9 | model = Article 10 | 11 | 12 | class ArticleListView(ListView): 13 | model = Article 14 | -------------------------------------------------------------------------------- /regwall/settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.conf import settings 4 | 5 | 6 | REGWALL_LIMIT = getattr(settings, 'REGWALL_LIMIT', 10) 7 | 8 | REGWALL_EXPIRE = getattr(settings, 'REGWALL_EXPIRE', 30) 9 | 10 | REGWALL_SOCIAL = getattr(settings, 'REGWALL_SOCIAL', [ 11 | 'google', 12 | 'facebook', 13 | 'twitter', 14 | ]) 15 | -------------------------------------------------------------------------------- /demo/demo/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% load i18n %} 4 | {% get_current_language as LANGUAGE_CODE %} 5 | 6 | 7 | 8 | 9 | {% block title %}{% trans 'Django Registration Wall' %}{% endblock %} 10 | 11 | 12 | 13 | {% block content %} 14 | {% endblock %} 15 | 16 | 17 | -------------------------------------------------------------------------------- /regwall/tests/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% load i18n %} 4 | {% get_current_language as LANGUAGE_CODE %} 5 | 6 | 7 | 8 | 9 | {% block title %}{% trans 'Django Registration Wall' %}{% endblock %} 10 | 11 | 12 | 13 | {% block content %} 14 | {% endblock %} 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo/demo/demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /demo/demo/templates/articles/article_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | 6 | {% block title %}{% trans 'Articles' %} · {{ block.super }}{% endblock %} 7 | 8 | 9 | {% block content %} 10 | 11 |

{% trans 'Articles' %}

12 | 13 | {% if article_list %} 14 |
    15 | {% for article in article_list %} 16 |
  1. {{ article.headline }}
  2. 17 | {% endfor %} 18 |
19 | {% endif %} 20 | 21 | {% endblock %} -------------------------------------------------------------------------------- /regwall/templates/regwall/history.html: -------------------------------------------------------------------------------- 1 | {% load i18n regwall_tags %} 2 | 3 | {% get_regwall_attempts as regwall_attempts %} 4 | {% get_regwall_successes as regwall_successes %} 5 | {% get_regwall_limit as regwall_limit %} 6 | 7 | {% if regwall_attempts|length >= regwall_limit %} 8 |

{% trans 'You read these articles' %}

9 |
    10 | {% for article in regwall_successes %} 11 |
  1. {{ article.headline }}
  2. 12 | {% endfor %} 13 |
14 | {% endif %} -------------------------------------------------------------------------------- /regwall/templates/regwall/login.html: -------------------------------------------------------------------------------- 1 | {% load regwall_tags %} 2 | 3 | {% get_regwall_attempts as regwall_attempts %} 4 | {% get_regwall_successes as regwall_successes %} 5 | {% get_regwall_limit as regwall_limit %} 6 | {% get_regwall_expire as regwall_expire %} 7 | 8 | {% if regwall_attempts|length >= regwall_limit %} 9 |

You read {{ regwall_successes|length }} of your {{ regwall_limit }} free article{{ regwall_limit|pluralize }} for {{ regwall_expire }} day{{ regwall_expire|pluralize }}. Log in or register to read unlimited articles.

10 | {% endif %} -------------------------------------------------------------------------------- /regwall/templates/regwall/detail.html: -------------------------------------------------------------------------------- 1 | {% load regwall_tags %} 2 | 3 | {% get_regwall_attempts as regwall_attempts %} 4 | {% get_regwall_successes as regwall_successes %} 5 | {% get_regwall_limit as regwall_limit %} 6 | {% get_regwall_expire as regwall_expire %} 7 | 8 | {% if regwall_successes|length > 0 %} 9 |

You read {{ regwall_successes|length }} of your {{ regwall_limit }} free article{{ regwall_limit|pluralize }} for {{ regwall_expire }} day{{ regwall_expire|pluralize }}. Log in or register to read unlimited articles.

10 | {% endif %} -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | - "3.5" 5 | - "3.4" 6 | - "2.7" 7 | env: 8 | - DJANGO_VERSION=1.9.13 9 | - DJANGO_VERSION=1.10.7 10 | - DJANGO_VERSION=1.11 11 | matrix: 12 | exclude: 13 | - python: "3.6" 14 | env: DJANGO_VERSION=1.9.13 15 | - python: "3.6" 16 | env: DJANGO_VERSION=1.10.7 17 | before_install: 18 | - export DJANGO_SETTINGS_MODULE="regwall.tests.settings" 19 | install: 20 | - pip install -q Django==$DJANGO_VERSION 21 | - python setup.py -q install 22 | - django-admin migrate 23 | script: 24 | - django-admin test regwall.tests -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = django-registrationwall 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | Install 4 | ******* 5 | 6 | Install with the `pip `_ package manager. 7 | 8 | .. code-block:: bash 9 | 10 | $ mkvirtualenv myvenv -p python3 11 | $ pip install django 12 | $ pip install django-registrationwall 13 | 14 | After `creating a project `_, add ``regwall`` to ``INSTALLED_APPS`` in ``settings.py``. 15 | 16 | .. code-block:: python 17 | 18 | INSTALLED_APPS = [ 19 | # ... 20 | 'regwall', 21 | ] 22 | 23 | Remember to update your ``requirements.txt`` file. In your project directory: 24 | 25 | .. code-block:: bash 26 | 27 | $ pip freeze > requirements.txt 28 | -------------------------------------------------------------------------------- /docs/tests.rst: -------------------------------------------------------------------------------- 1 | .. _tests: 2 | 3 | Tests 4 | ***** 5 | 6 | `Continuous integration test results `_ are available online. 7 | 8 | However, you can also test the source code. 9 | 10 | .. code-block:: bash 11 | 12 | $ workon myvenv 13 | $ django-admin test regwall.tests --settings="regwall.tests.settings" 14 | 15 | Creating test database for alias 'default'... 16 | .. 17 | ---------------------------------------------------------------------- 18 | Ran 2 tests in 0.229s 19 | 20 | OK 21 | Destroying test database for alias 'default'... 22 | 23 | A bundled settings file allows you to test the code without even creating a Django project. 24 | -------------------------------------------------------------------------------- /regwall/templatetags/regwall_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from .. import mixins 4 | from .. import settings 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.simple_tag(takes_context=True) 10 | def get_regwall_attempts(context): 11 | request = context['request'] 12 | return request.session['regwall']['attempts'] 13 | 14 | 15 | @register.simple_tag(takes_context=True) 16 | def get_regwall_successes(context): 17 | request = context['request'] 18 | return request.session['regwall']['successes'] 19 | 20 | 21 | @register.simple_tag 22 | def get_regwall_limit(): 23 | return settings.REGWALL_LIMIT 24 | 25 | 26 | @register.simple_tag 27 | def get_regwall_expire(): 28 | return settings.REGWALL_EXPIRE 29 | -------------------------------------------------------------------------------- /demo/demo/templates/articles/article_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | 6 | {% block title %}{{ article.headline }} · {{ block.super }}{% endblock %} 7 | 8 | 9 | {% block content %} 10 | 11 | {% include 'regwall/detail.html' %} 12 | 13 |

{{ article.headline }}

14 | 15 |

{{ article.body }}

16 | 17 | {% if article.get_previous_by_pub_date or article.get_next_by_pub_date %} 18 | 26 | {% endif %} 27 | 28 | {% endblock %} -------------------------------------------------------------------------------- /demo/demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /regwall/tests/templates/articles/article_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | 6 | {% block title %}{{ article.headline }} · {{ block.super }}{% endblock %} 7 | 8 | 9 | {% block content %} 10 | 11 | {% include 'regwall/detail.html' %} 12 | 13 |

{{ article.headline }}

14 | 15 |

{{ article.body }}

16 | 17 | {% if article.get_previous_by_pub_date or article.get_next_by_pub_date %} 18 | 26 | {% endif %} 27 | 28 | {% endblock %} -------------------------------------------------------------------------------- /demo/demo/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | 6 | {% block title %}{% trans 'Log in' %} · {{ block.super }}{% endblock %} 7 | 8 | 9 | {% block content %} 10 | 11 | {% include 'regwall/login.html' %} 12 | 13 |

{% trans 'Log in' %}

14 | 15 | {% if form.errors %} 16 |
17 |
    18 | {% for error in form.non_field_errors %} 19 |
  • {{ error }}
  • 20 | {% endfor %} 21 |
22 |
23 | {% endif %} 24 | 25 |
26 | {% csrf_token %} 27 | {{ form.as_p }} 28 | 29 |
30 | 31 |

Forgot your password?

32 | 33 |

Register for an account.

34 | 35 | {% include 'regwall/history.html' %} 36 | 37 | {% endblock %} -------------------------------------------------------------------------------- /regwall/tests/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | 6 | {% block title %}{% trans 'Log in' %} · {{ block.super }}{% endblock %} 7 | 8 | 9 | {% block content %} 10 | 11 | {% include 'regwall/login.html' %} 12 | 13 |

{% trans 'Log in' %}

14 | 15 | {% if form.errors %} 16 |
17 |
    18 | {% for error in form.non_field_errors %} 19 |
  • {{ error }}
  • 20 | {% endfor %} 21 |
22 |
23 | {% endif %} 24 | 25 |
26 | {% csrf_token %} 27 | {{ form.as_p }} 28 | 29 |
30 | 31 |

Forgot your password?

32 | 33 |

Register for an account.

34 | 35 | {% include 'regwall/history.html' %} 36 | 37 | {% endblock %} -------------------------------------------------------------------------------- /demo/demo/articles/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.core.urlresolvers import reverse 4 | from django.db import models 5 | from django.utils import timezone 6 | from django.utils.encoding import python_2_unicode_compatible 7 | from django.utils.translation import ugettext_lazy as _ 8 | 9 | 10 | @python_2_unicode_compatible 11 | class Article(models.Model): 12 | headline = models.CharField(_("Headline"), max_length=255) 13 | slug = models.SlugField(_("Slug"), max_length=255, unique=True) 14 | body = models.TextField(_("Body"), ) 15 | pub_date = models.DateTimeField(_("Pub date"), default=timezone.now) 16 | 17 | class Meta: 18 | ordering = ["-pub_date"] 19 | verbose_name = _("article") 20 | verbose_name_plural = _("articles") 21 | 22 | def __str__(self): 23 | return self.headline 24 | 25 | def get_absolute_url(self): 26 | return reverse("article_detail", args=[str(self.slug)]) 27 | -------------------------------------------------------------------------------- /regwall/tests/articles/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.core.urlresolvers import reverse 4 | from django.db import models 5 | from django.utils import timezone 6 | from django.utils.encoding import python_2_unicode_compatible 7 | from django.utils.translation import ugettext_lazy as _ 8 | 9 | 10 | @python_2_unicode_compatible 11 | class Article(models.Model): 12 | headline = models.CharField(_("Headline"), max_length=255) 13 | slug = models.SlugField(_("Slug"), max_length=255, unique=True) 14 | body = models.TextField(_("Body"), ) 15 | pub_date = models.DateTimeField(_("Pub date"), default=timezone.now) 16 | 17 | class Meta: 18 | ordering = ["-pub_date"] 19 | verbose_name = _("article") 20 | verbose_name_plural = _("articles") 21 | 22 | def __str__(self): 23 | return self.headline 24 | 25 | def get_absolute_url(self): 26 | return reverse("article_detail", args=[str(self.slug)]) 27 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=django-registrationwall 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /demo/demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | """demo URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.10/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | 19 | 20 | urlpatterns = [ 21 | url(r'^admin/', admin.site.urls), 22 | url(r'^articles/', include('articles.urls')), 23 | url(r'^accounts/', include('django.contrib.auth.urls')), 24 | ] 25 | -------------------------------------------------------------------------------- /regwall/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.core.urlresolvers import reverse 4 | from django.test import TestCase, Client 5 | 6 | from .articles.models import Article 7 | 8 | 9 | class RegWallTestCase(TestCase): 10 | """Registration Wall test cases.""" 11 | 12 | fixtures = ['articles_article.json'] 13 | 14 | def setUp(self): 15 | self.client = Client() 16 | 17 | def test_regwall(self): 18 | for article in Article.objects.all()[:10]: 19 | response = self.client.get(reverse('article_detail', args=[article.slug])) 20 | self.assertEqual(response.request['PATH_INFO'], '/articles/study-finds-majority-accidental-heroin-overdoses-could-be-prevented-less-heroin/') 21 | 22 | def test_regwall_raise(self): 23 | for article in Article.objects.all(): 24 | response = self.client.get(reverse('article_detail', args=[article.slug])) 25 | self.assertRedirects(response, '/accounts/login/?next=/articles/guy-wearing-thumb-drive-around-neck-wonders-if-you-tried-hard-reboot/') 26 | -------------------------------------------------------------------------------- /demo/demo/articles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.7 on 2017-04-06 19:07 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Article', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('headline', models.CharField(max_length=255, verbose_name='Headline')), 22 | ('slug', models.SlugField(max_length=255, unique=True, verbose_name='Slug')), 23 | ('body', models.TextField(verbose_name='Body')), 24 | ('pub_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Pub date')), 25 | ], 26 | options={ 27 | 'verbose_name_plural': 'articles', 28 | 'ordering': ['-pub_date'], 29 | 'verbose_name': 'article', 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /regwall/tests/articles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-04-14 17:20 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Article', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('headline', models.CharField(max_length=255, verbose_name='Headline')), 22 | ('slug', models.SlugField(max_length=255, unique=True, verbose_name='Slug')), 23 | ('body', models.TextField(verbose_name='Body')), 24 | ('pub_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Pub date')), 25 | ], 26 | options={ 27 | 'verbose_name': 'article', 28 | 'ordering': ['-pub_date'], 29 | 'verbose_name_plural': 'articles', 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /docs/documentation.rst: -------------------------------------------------------------------------------- 1 | .. _documentation: 2 | 3 | Documentation 4 | ************* 5 | 6 | `Full documentation `_ is available online. 7 | 8 | However, you can also build the documentation from source. Enter your `virtual environment `_. 9 | 10 | .. code-block:: bash 11 | 12 | $ workon myvenv 13 | 14 | Clone the code repository. 15 | 16 | .. code-block:: bash 17 | 18 | $ git clone git@github.com:richardcornish/django-registrationwall.git 19 | $ cd django-registrationwall/ 20 | 21 | Install `Sphinx `_, |sphinx-autobuild|_, and |sphinx_rtd_theme|_. 22 | 23 | .. |sphinx-autobuild| replace:: ``sphinx-autobuild`` 24 | .. _sphinx-autobuild: https://pypi.python.org/pypi/sphinx-autobuild 25 | 26 | .. |sphinx_rtd_theme| replace:: ``sphinx_rtd_theme`` 27 | .. _sphinx_rtd_theme: https://pypi.python.org/pypi/sphinx_rtd_theme 28 | 29 | .. code-block:: bash 30 | 31 | $ pip install sphinx sphinx-autobuild sphinx_rtd_theme 32 | 33 | Create an HTML build. 34 | 35 | .. code-block:: bash 36 | 37 | $ (cd docs/ && make html) 38 | 39 | Or use ``sphinx-autobuild`` to watch for live changes. 40 | 41 | .. code-block:: bash 42 | 43 | $ sphinx-autobuild docs/ docs/_build_html 44 | 45 | Open `127.0.0.1:8000 `_. 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Richard Cornish 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Apple 2 | *.DS_Store 3 | 4 | # Database 5 | db.sqlite3 6 | 7 | # https://github.com/github/gitignore/blob/master/Python.gitignore 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | env/ 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *,cover 54 | .hypothesis/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | docs/_build_html/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # IPython Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # dotenv 88 | .env 89 | 90 | # virtualenv 91 | .venv/ 92 | venv/ 93 | ENV/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | 98 | # Rope project settings 99 | .ropeproject -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import os 4 | 5 | from setuptools import find_packages, setup 6 | 7 | 8 | setup( 9 | name='django-registrationwall', 10 | version='0.1.5', 11 | description='A Django mixin application to raise a registration or paywall', 12 | long_description=open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'README.rst')).read(), 13 | author='Richard Cornish', 14 | author_email='rich@richardcornish.com', 15 | url='https://github.com/richardcornish/django-registrationwall', 16 | license='BSD', 17 | zip_safe=False, 18 | include_package_data=True, 19 | packages=find_packages(exclude=('tests',)), 20 | install_requires=[ 21 | 'tldextract' 22 | ], 23 | test_suite='regwall.tests', 24 | classifiers=[ 25 | 'Development Status :: 5 - Production/Stable', 26 | 'Environment :: Web Environment', 27 | 'Framework :: Django', 28 | 'Framework :: Django :: 1.9', 29 | 'Framework :: Django :: 1.10', 30 | 'Framework :: Django :: 1.11', 31 | 'Intended Audience :: Developers', 32 | 'License :: OSI Approved :: BSD License', 33 | 'Operating System :: OS Independent', 34 | 'Programming Language :: Python', 35 | 'Programming Language :: Python :: 2', 36 | 'Programming Language :: Python :: 2.7', 37 | 'Programming Language :: Python :: 3', 38 | 'Programming Language :: Python :: 3.3', 39 | 'Programming Language :: Python :: 3.4', 40 | 'Programming Language :: Python :: 3.5', 41 | 'Programming Language :: Python :: 3.6', 42 | 'Topic :: Internet :: WWW/HTTP', 43 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 44 | ], 45 | ) 46 | -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | .. _settings: 2 | 3 | Settings 4 | ******** 5 | 6 | The mixin tag offers three settings. By default, they are: 7 | 8 | .. code-block:: python 9 | 10 | REGWALL_LIMIT = 10 11 | 12 | REGWALL_EXPIRE = 30 13 | 14 | REGWALL_SOCIAL = [ 15 | 'google', 16 | 'facebook', 17 | 'twitter', 18 | ] 19 | 20 | ``REGWALL_LIMIT`` 21 | ================= 22 | 23 | An integer indicating the number of resources to display before the registration wall appears. 24 | 25 | The mixin displays ``10`` resources by default. 26 | 27 | ``REGWALL_EXPIRE`` 28 | ================== 29 | 30 | An integer indicating the number of days before the consumed resources count is reset to zero. 31 | 32 | The mixin resets after ``30`` days by default. 33 | 34 | ``REGWALL_SOCIAL`` 35 | ================== 36 | 37 | A list of strings of domains whose referral does not increment the consumed resources count. In other words, visitors coming from these domains are not penalized. Previously, the app used a rudimentary method of domain checking with the |urlparse|_/|urllib_parse|_ modules. Because URLs vary so widely in construction, the app now uses the `tldextract `_ package to accurately extract the domain. Therefore, this setting should contain only `domains `_ and not `top-level domains `_, e.g. ``['google', 'facebook', 'twitter']`` and *not* ``['google.com', 'facebook.com', 'twitter.com']``. 38 | 39 | The mixin allows referrals from Google, Facebook, and Twitter by default. 40 | 41 | .. |urlparse| replace:: ``urlparse`` 42 | .. _urlparse: https://docs.python.org/2/library/urlparse.html 43 | 44 | .. |urllib_parse| replace:: ``urllib.parse`` 45 | .. _urllib_parse: https://docs.python.org/3/library/urllib.parse.html -------------------------------------------------------------------------------- /regwall/tests/settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import os 4 | 5 | 6 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 7 | 8 | SECRET_KEY = 'fake-key' 9 | 10 | INSTALLED_APPS = [ 11 | 'django.contrib.auth', 12 | 'django.contrib.contenttypes', 13 | 'django.contrib.sessions', 14 | 'regwall', 15 | 'regwall.tests.articles', 16 | ] 17 | 18 | MIDDLEWARE_CLASSES = [ 19 | 'django.middleware.security.SecurityMiddleware', 20 | 'django.contrib.sessions.middleware.SessionMiddleware', 21 | 'django.middleware.common.CommonMiddleware', 22 | 'django.middleware.csrf.CsrfViewMiddleware', 23 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 24 | 'django.contrib.messages.middleware.MessageMiddleware', 25 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 26 | ] 27 | 28 | DATABASES = { 29 | 'default': { 30 | 'ENGINE': 'django.db.backends.sqlite3', 31 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 32 | 'TEST': { 33 | 'NAME': None, 34 | }, 35 | } 36 | } 37 | 38 | TEMPLATES = [ 39 | { 40 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 41 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 42 | 'APP_DIRS': True, 43 | 'OPTIONS': { 44 | 'context_processors': [ 45 | 'django.template.context_processors.debug', 46 | 'django.template.context_processors.request', 47 | 'django.contrib.auth.context_processors.auth', 48 | 'django.contrib.messages.context_processors.messages', 49 | ], 50 | }, 51 | } 52 | ] 53 | 54 | TIME_ZONE = 'UTC' 55 | 56 | USE_TZ = True 57 | 58 | ROOT_URLCONF = 'regwall.tests.urls' 59 | 60 | import django 61 | if hasattr(django, 'setup'): # < Django 1.9 62 | django.setup() 63 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Registration Wall 2 | ************************ 3 | 4 | |PyPI version|_ |Build status|_ 5 | 6 | .. |PyPI version| image:: 7 | https://badge.fury.io/py/django-registrationwall.svg 8 | .. _PyPI version: https://pypi.python.org/pypi/django-registrationwall 9 | 10 | .. |Build status| image:: 11 | https://travis-ci.org/richardcornish/django-registrationwall.svg?branch=master 12 | .. _Build status: https://travis-ci.org/richardcornish/django-registrationwall 13 | 14 | .. image:: https://raw.githubusercontent.com/richardcornish/django-registrationwall/master/docs/_static/img/regwall-detail.png 15 | 16 | **Django Registration Wall** is a `Django `_ `mixin `_ `application `_ that limits an `anonymous user `_'s access to content, after which the user is redirected to the `login URL `_. The behavior is modeled after the common `paywall `_ scenario. 17 | 18 | Fake news articles credit goes to The Onion. 19 | 20 | * `Package distribution `_ 21 | * `Code repository `_ 22 | * `Documentation `_ 23 | * `Tests `_ 24 | 25 | Install 26 | ======= 27 | 28 | .. code-block:: bash 29 | 30 | $ pip install django-registrationwall 31 | 32 | Add to ``settings.py``. 33 | 34 | .. code-block:: python 35 | 36 | INSTALLED_APPS = [ 37 | # ... 38 | 'regwall', 39 | ] 40 | 41 | Add to one of your ``views.py``. 42 | 43 | .. code-block:: python 44 | 45 | from django.views.generic import DetailView 46 | 47 | from regwall.mixins import RaiseRegWallMixin 48 | 49 | from .models import Article 50 | 51 | 52 | class ArticleDetailView(RaiseRegWallMixin, DetailView): 53 | model = Article 54 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. _index: 2 | .. module:: regwall 3 | 4 | Django Registration Wall 5 | ************************ 6 | 7 | |PyPI version|_ |Build status|_ 8 | 9 | .. |PyPI version| image:: 10 | https://badge.fury.io/py/django-registrationwall.svg 11 | .. _PyPI version: https://pypi.python.org/pypi/django-registrationwall 12 | 13 | .. |Build status| image:: 14 | https://travis-ci.org/richardcornish/django-registrationwall.svg?branch=master 15 | .. _Build status: https://travis-ci.org/richardcornish/django-registrationwall 16 | 17 | .. image:: _static/img/regwall-detail.png 18 | 19 | **Django Registration Wall** is a `Django `_ `mixin `_ `application `_ that limits an `anonymous user `_'s access to content, after which the user is redirected to the `login URL `_. The behavior is modeled after the common `paywall `_ scenario. 20 | 21 | Fake news articles credit goes to The Onion. 22 | 23 | * `Package distribution `_ 24 | * `Code repository `_ 25 | * `Documentation `_ 26 | * `Tests `_ 27 | 28 | Install 29 | ======= 30 | 31 | .. code-block:: bash 32 | 33 | $ pip install django-registrationwall 34 | 35 | Add to ``settings.py``. 36 | 37 | .. code-block:: python 38 | 39 | INSTALLED_APPS = [ 40 | # ... 41 | 'regwall', 42 | ] 43 | 44 | Add to one of your ``views.py``. 45 | 46 | .. code-block:: python 47 | 48 | from django.views.generic import DetailView 49 | 50 | from regwall.mixins import RaiseRegWallMixin 51 | 52 | from .models import Article 53 | 54 | 55 | class ArticleDetailView(RaiseRegWallMixin, DetailView): 56 | model = Article 57 | 58 | Contents 59 | ======== 60 | 61 | .. toctree:: 62 | :maxdepth: 2 63 | 64 | install 65 | usage 66 | settings 67 | documentation 68 | tests 69 | 70 | 71 | Indices and tables 72 | ================== 73 | 74 | * :ref:`genindex` 75 | * :ref:`modindex` 76 | * :ref:`search` 77 | -------------------------------------------------------------------------------- /regwall/mixins.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | try: 4 | from django.contrib.auth.mixins import AccessMixin 5 | except ImportError: 6 | raise ImportError('Django Registration Wall requires Django 1.9 or greater.') 7 | 8 | import tldextract 9 | 10 | from . import settings 11 | 12 | 13 | class RaiseRegWallMixin(AccessMixin): 14 | """View mixin that increments an anonymous user's article count.""" 15 | 16 | def get_or_create_regwall_list(self, list_name): 17 | try: 18 | regwall = self.request.session['regwall'] 19 | except KeyError: 20 | seconds = 60 * 60 * 24 * settings.REGWALL_EXPIRE 21 | self.request.session.set_expiry(seconds) 22 | self.request.session['regwall'] = {} 23 | regwall = self.request.session['regwall'] 24 | try: 25 | return regwall[list_name] 26 | except KeyError: 27 | regwall[list_name] = [] 28 | self.request.session.modified = True 29 | return regwall[list_name] 30 | 31 | def increment_regwall_list(self, regwall_list): 32 | obj = self.get_object() 33 | regwall_list.append({ 34 | 'app_label': obj._meta.app_label, 35 | 'id': obj.id, 36 | 'headline': obj.headline or obj.title or obj.name or '', 37 | 'url': obj.get_absolute_url(), 38 | }) 39 | self.request.session.modified = True 40 | 41 | def is_under_limit(self, regwall_list): 42 | return len(regwall_list) <= settings.REGWALL_LIMIT 43 | 44 | @property 45 | def is_authenticated(self): 46 | try: 47 | return self.request.user.is_authenticated() 48 | except TypeError: 49 | return self.request.user.is_authenticated 50 | 51 | @property 52 | def is_social(self): 53 | try: 54 | referer = self.request.META['HTTP_REFERER'] 55 | except KeyError: 56 | return False 57 | ext = tldextract.extract(referer) 58 | return ext.domain in settings.REGWALL_SOCIAL 59 | 60 | @property 61 | def has_visited(self): 62 | successes_list = self.get_or_create_regwall_list('successes') 63 | value_list = [article[key] for article in successes_list for key in article] 64 | obj = self.get_object() 65 | return obj.get_absolute_url() in value_list 66 | 67 | def dispatch(self, request, *args, **kwargs): 68 | attempts_list = self.get_or_create_regwall_list('attempts') 69 | successes_list = self.get_or_create_regwall_list('successes') 70 | if not self.is_authenticated and not self.is_social and not self.has_visited: 71 | self.increment_regwall_list(attempts_list) 72 | if self.is_under_limit(attempts_list): 73 | self.increment_regwall_list(successes_list) 74 | else: 75 | return self.handle_no_permission() 76 | return super(RaiseRegWallMixin, self).dispatch(request, *args, **kwargs) 77 | -------------------------------------------------------------------------------- /demo/demo/demo/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '$x9lugky&5*@a_h=+v^cq)eb$-&vy@vvfu6luajyjuf8@k#(*$' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'regwall', 41 | 'articles', 42 | ] 43 | 44 | MIDDLEWARE_CLASSES = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | MIDDLEWARE = list(MIDDLEWARE_CLASSES) 55 | 56 | ROOT_URLCONF = 'demo.urls' 57 | 58 | TEMPLATES = [ 59 | { 60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 61 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 62 | 'APP_DIRS': True, 63 | 'OPTIONS': { 64 | 'context_processors': [ 65 | 'django.template.context_processors.debug', 66 | 'django.template.context_processors.request', 67 | 'django.contrib.auth.context_processors.auth', 68 | 'django.contrib.messages.context_processors.messages', 69 | ], 70 | }, 71 | }, 72 | ] 73 | 74 | WSGI_APPLICATION = 'demo.wsgi.application' 75 | 76 | 77 | # Database 78 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 79 | 80 | DATABASES = { 81 | 'default': { 82 | 'ENGINE': 'django.db.backends.sqlite3', 83 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 84 | } 85 | } 86 | 87 | 88 | # Password validation 89 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 90 | 91 | AUTH_PASSWORD_VALIDATORS = [ 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 103 | }, 104 | ] 105 | 106 | 107 | # Internationalization 108 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 109 | 110 | LANGUAGE_CODE = 'en-us' 111 | 112 | TIME_ZONE = 'UTC' 113 | 114 | USE_I18N = True 115 | 116 | USE_L10N = True 117 | 118 | USE_TZ = True 119 | 120 | 121 | # Static files (CSS, JavaScript, Images) 122 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 123 | 124 | STATIC_URL = '/static/' 125 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # django-registrationwall documentation build configuration file, created by 5 | # sphinx-quickstart on Thu Apr 13 10:25:33 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = 'django-registrationwall' 50 | copyright = '2017, Richard Cornish' 51 | author = 'Richard Cornish' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = '0.1' 59 | # The full version, including alpha/beta/rc tags. 60 | release = '0.1' 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This patterns also effect to html_static_path and html_extra_path 72 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 73 | 74 | # The name of the Pygments (syntax highlighting) style to use. 75 | pygments_style = 'sphinx' 76 | 77 | # If true, `todo` and `todoList` produce output, else they produce nothing. 78 | todo_include_todos = False 79 | 80 | 81 | # -- Options for HTML output ---------------------------------------------- 82 | 83 | # The theme to use for HTML and HTML Help pages. See the documentation for 84 | # a list of builtin themes. 85 | # 86 | # html_theme = 'alabaster' 87 | 88 | import sphinx_rtd_theme 89 | html_theme = "sphinx_rtd_theme" 90 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 91 | 92 | # Theme options are theme-specific and customize the look and feel of a theme 93 | # further. For a list of options available for each theme, see the 94 | # documentation. 95 | # 96 | # html_theme_options = {} 97 | 98 | # Add any paths that contain custom static files (such as style sheets) here, 99 | # relative to this directory. They are copied after the builtin static files, 100 | # so a file named "default.css" will overwrite the builtin "default.css". 101 | html_static_path = ['_static'] 102 | 103 | 104 | # -- Options for HTMLHelp output ------------------------------------------ 105 | 106 | # Output file base name for HTML help builder. 107 | htmlhelp_basename = 'django-registrationwalldoc' 108 | 109 | 110 | # -- Options for LaTeX output --------------------------------------------- 111 | 112 | latex_elements = { 113 | # The paper size ('letterpaper' or 'a4paper'). 114 | # 115 | # 'papersize': 'letterpaper', 116 | 117 | # The font size ('10pt', '11pt' or '12pt'). 118 | # 119 | # 'pointsize': '10pt', 120 | 121 | # Additional stuff for the LaTeX preamble. 122 | # 123 | # 'preamble': '', 124 | 125 | # Latex figure (float) alignment 126 | # 127 | # 'figure_align': 'htbp', 128 | } 129 | 130 | # Grouping the document tree into LaTeX files. List of tuples 131 | # (source start file, target name, title, 132 | # author, documentclass [howto, manual, or own class]). 133 | latex_documents = [ 134 | (master_doc, 'django-registrationwall.tex', 'django-registrationwall Documentation', 135 | 'Richard Cornish', 'manual'), 136 | ] 137 | 138 | 139 | # -- Options for manual page output --------------------------------------- 140 | 141 | # One entry per manual page. List of tuples 142 | # (source start file, name, description, authors, manual section). 143 | man_pages = [ 144 | (master_doc, 'django-registrationwall', 'django-registrationwall Documentation', 145 | [author], 1) 146 | ] 147 | 148 | 149 | # -- Options for Texinfo output ------------------------------------------- 150 | 151 | # Grouping the document tree into Texinfo files. List of tuples 152 | # (source start file, target name, title, author, 153 | # dir menu entry, description, category) 154 | texinfo_documents = [ 155 | (master_doc, 'django-registrationwall', 'django-registrationwall Documentation', 156 | author, 'django-registrationwall', 'One line description of project.', 157 | 'Miscellaneous'), 158 | ] 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /demo/demo/articles/fixtures/articles_article.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "articles.article", 4 | "pk": 1, 5 | "fields": { 6 | "headline": "Guy wearing thumb drive around neck wonders if you tried hard reboot", 7 | "slug": "guy-wearing-thumb-drive-around-neck-wonders-if-you-tried-hard-reboot", 8 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 9 | "pub_date": "2015-12-22T05:04:33Z" 10 | } 11 | }, 12 | { 13 | "model": "articles.article", 14 | "pk": 2, 15 | "fields": { 16 | "headline": "Study finds majority of accidental heroin overdoses could be prevented with less heroin", 17 | "slug": "study-finds-majority-accidental-heroin-overdoses-could-be-prevented-less-heroin", 18 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 19 | "pub_date": "2015-12-22T05:04:49Z" 20 | } 21 | }, 22 | { 23 | "model": "articles.article", 24 | "pk": 3, 25 | "fields": { 26 | "headline": "EPA urges nation to develop new air source", 27 | "slug": "epa-urges-nation-develop-new-air-source", 28 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 29 | "pub_date": "2015-12-22T05:04:59Z" 30 | } 31 | }, 32 | { 33 | "model": "articles.article", 34 | "pk": 4, 35 | "fields": { 36 | "headline": "Astronomers discover previously unknown cluster of nothingness in deep space", 37 | "slug": "astronomers-discover-previously-unknown-cluster-nothingness-deep-space", 38 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 39 | "pub_date": "2015-12-22T05:06:05Z" 40 | } 41 | }, 42 | { 43 | "model": "articles.article", 44 | "pk": 5, 45 | "fields": { 46 | "headline": "Elderly woman relieved to know she's tackled last technological advancement of lifetime", 47 | "slug": "elderly-woman-relieved-know-shes-tackled-last-technological-advancement-lifetime", 48 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 49 | "pub_date": "2015-12-22T05:06:11Z" 50 | } 51 | }, 52 | { 53 | "model": "articles.article", 54 | "pk": 6, 55 | "fields": { 56 | "headline": "'Seek funding' step added to scientific method", 57 | "slug": "seek-funding-step-added-scientific-method", 58 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 59 | "pub_date": "2015-12-22T05:06:21Z" 60 | } 61 | }, 62 | { 63 | "model": "articles.article", 64 | "pk": 7, 65 | "fields": { 66 | "headline": "Nation figured everything would run on some kind of cubes of blue energy by now", 67 | "slug": "nation-figured-everything-would-run-some-kind-cubes-blue-energy-now", 68 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 69 | "pub_date": "2015-12-22T05:06:31Z" 70 | } 71 | }, 72 | { 73 | "model": "articles.article", 74 | "pk": 8, 75 | "fields": { 76 | "headline": "New study finds box still world's most popular container", 77 | "slug": "new-study-finds-box-still-worlds-most-popular-container", 78 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 79 | "pub_date": "2015-12-22T05:06:40Z" 80 | } 81 | }, 82 | { 83 | "model": "articles.article", 84 | "pk": 9, 85 | "fields": { 86 | "headline": "Archaeologists discover ancient femur that could make mouthwatering broth", 87 | "slug": "archaeologists-discover-ancient-femur-could-make-mouthwatering-broth", 88 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 89 | "pub_date": "2015-12-22T05:06:50Z" 90 | } 91 | }, 92 | { 93 | "model": "articles.article", 94 | "pk": 10, 95 | "fields": { 96 | "headline": "Groundbreaking study finds gratification can be deliberately postponed", 97 | "slug": "groundbreaking-study-finds-gratification-can-be-deliberately-postponed", 98 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 99 | "pub_date": "2015-12-22T05:07:00Z" 100 | } 101 | }, 102 | { 103 | "model": "articles.article", 104 | "pk": 11, 105 | "fields": { 106 | "headline": "Jeff Bezos assures Amazon employees that HR working 100 hours a week to address their complaints", 107 | "slug": "jeff-bezos-assures-amazon-employees-hr-working-100-hours-week-address-their-complaints", 108 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 109 | "pub_date": "2015-12-22T05:07:09Z" 110 | } 111 | } 112 | ] 113 | -------------------------------------------------------------------------------- /regwall/tests/articles/fixtures/articles_article.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "articles.article", 4 | "pk": 1, 5 | "fields": { 6 | "headline": "Guy wearing thumb drive around neck wonders if you tried hard reboot", 7 | "slug": "guy-wearing-thumb-drive-around-neck-wonders-if-you-tried-hard-reboot", 8 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 9 | "pub_date": "2015-12-22T05:04:33Z" 10 | } 11 | }, 12 | { 13 | "model": "articles.article", 14 | "pk": 2, 15 | "fields": { 16 | "headline": "Study finds majority of accidental heroin overdoses could be prevented with less heroin", 17 | "slug": "study-finds-majority-accidental-heroin-overdoses-could-be-prevented-less-heroin", 18 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 19 | "pub_date": "2015-12-22T05:04:49Z" 20 | } 21 | }, 22 | { 23 | "model": "articles.article", 24 | "pk": 3, 25 | "fields": { 26 | "headline": "EPA urges nation to develop new air source", 27 | "slug": "epa-urges-nation-develop-new-air-source", 28 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 29 | "pub_date": "2015-12-22T05:04:59Z" 30 | } 31 | }, 32 | { 33 | "model": "articles.article", 34 | "pk": 4, 35 | "fields": { 36 | "headline": "Astronomers discover previously unknown cluster of nothingness in deep space", 37 | "slug": "astronomers-discover-previously-unknown-cluster-nothingness-deep-space", 38 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 39 | "pub_date": "2015-12-22T05:06:05Z" 40 | } 41 | }, 42 | { 43 | "model": "articles.article", 44 | "pk": 5, 45 | "fields": { 46 | "headline": "Elderly woman relieved to know she's tackled last technological advancement of lifetime", 47 | "slug": "elderly-woman-relieved-know-shes-tackled-last-technological-advancement-lifetime", 48 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 49 | "pub_date": "2015-12-22T05:06:11Z" 50 | } 51 | }, 52 | { 53 | "model": "articles.article", 54 | "pk": 6, 55 | "fields": { 56 | "headline": "'Seek funding' step added to scientific method", 57 | "slug": "seek-funding-step-added-scientific-method", 58 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 59 | "pub_date": "2015-12-22T05:06:21Z" 60 | } 61 | }, 62 | { 63 | "model": "articles.article", 64 | "pk": 7, 65 | "fields": { 66 | "headline": "Nation figured everything would run on some kind of cubes of blue energy by now", 67 | "slug": "nation-figured-everything-would-run-some-kind-cubes-blue-energy-now", 68 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 69 | "pub_date": "2015-12-22T05:06:31Z" 70 | } 71 | }, 72 | { 73 | "model": "articles.article", 74 | "pk": 8, 75 | "fields": { 76 | "headline": "New study finds box still world's most popular container", 77 | "slug": "new-study-finds-box-still-worlds-most-popular-container", 78 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 79 | "pub_date": "2015-12-22T05:06:40Z" 80 | } 81 | }, 82 | { 83 | "model": "articles.article", 84 | "pk": 9, 85 | "fields": { 86 | "headline": "Archaeologists discover ancient femur that could make mouthwatering broth", 87 | "slug": "archaeologists-discover-ancient-femur-could-make-mouthwatering-broth", 88 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 89 | "pub_date": "2015-12-22T05:06:50Z" 90 | } 91 | }, 92 | { 93 | "model": "articles.article", 94 | "pk": 10, 95 | "fields": { 96 | "headline": "Groundbreaking study finds gratification can be deliberately postponed", 97 | "slug": "groundbreaking-study-finds-gratification-can-be-deliberately-postponed", 98 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 99 | "pub_date": "2015-12-22T05:07:00Z" 100 | } 101 | }, 102 | { 103 | "model": "articles.article", 104 | "pk": 11, 105 | "fields": { 106 | "headline": "Jeff Bezos assures Amazon employees that HR working 100 hours a week to address their complaints", 107 | "slug": "jeff-bezos-assures-amazon-employees-hr-working-100-hours-week-address-their-complaints", 108 | "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et dictum eros. Mauris in ultricies nunc. Vivamus condimentum magna accumsan massa tempus, in aliquet neque consectetur.", 109 | "pub_date": "2015-12-22T05:07:09Z" 110 | } 111 | } 112 | ] 113 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | 3 | Usage 4 | ***** 5 | 6 | Views 7 | ===== 8 | 9 | The app is almost entirely a `mixin `_ that subclasses Django's |AccessMixin|_. Import the mixin and subclass it in a ``DetailView`` or a view that uses ``SingleObjectMixin``. 10 | 11 | .. code-block:: python 12 | 13 | from django.views.generic import DetailView 14 | 15 | from regwall.mixins import RaiseRegWallMixin 16 | 17 | from .models import Article 18 | 19 | 20 | class ArticleDetailView(RaiseRegWallMixin, DetailView): 21 | model = Article 22 | 23 | .. |AccessMixin| replace:: ``AccessMixin`` 24 | .. _AccessMixin: https://docs.djangoproject.com/en/1.11/topics/auth/default/#django.contrib.auth.mixins.AccessMixin 25 | 26 | On each request, the mixin increments the number of consumed resources and checks its count against the ``REGWALL_LIMIT`` setting. Resources contain information about each view's main object, which means the mixin expects to be added to views that focus on a single object such as |DetailView|_, although technically any view that incorporates |SingleObjectMixin|_ is valid. 27 | 28 | .. |DetailView| replace:: ``DetailView`` 29 | .. _DetailView: https://docs.djangoproject.com/en/1.11/ref/class-based-views/generic-display/#detailview 30 | 31 | .. |SingleObjectMixin| replace:: ``SingleObjectMixin`` 32 | .. _SingleObjectMixin: https://docs.djangoproject.com/en/1.11/ref/class-based-views/mixins-single-object/#singleobjectmixin 33 | 34 | The app stores the visited resources into the browser `session `_, whose session ID is stored in a cookie in the user's web browser. The app does not employ more sophisticated user tracking such as IP detection and storage. 35 | 36 | Template tags 37 | ============= 38 | 39 | The bulk of the app's logic is in the mixin, but `template tags `_ allow for display of the list of resources consumed, the limit, and the days of expiration. 40 | 41 | Load the template tags. 42 | 43 | .. code-block:: django 44 | 45 | {% load regwall_tags %} 46 | 47 | Assign any of the tags' output to a variable with the ``as`` syntax. 48 | 49 | .. code-block:: django 50 | 51 | {% load regwall_tags %} 52 | 53 | {% get_regwall_limit as regwall_limit %} 54 | {% get_regwall_expire as regwall_expire %} 55 | {% get_regwall_attempts as regwall_attempts %} 56 | {% get_regwall_successes as regwall_successes %} 57 | 58 | Use the assigned variables as you like. 59 | 60 | ``{% get_regwall_limit %}`` 61 | --------------------------- 62 | 63 | Gets the number of resources a user can consume before the registration wall is raised. 64 | 65 | .. code-block:: django 66 | 67 | {% load regwall_tags %} 68 | 69 | {% get_regwall_limit as regwall_limit %} 70 | 71 |

The limit is {{ regwall_limit }} articles.

72 | 73 | ``{% get_regwall_expire %}`` 74 | ---------------------------- 75 | 76 | Gets the number of days until the consumed resources count is reset to zero. 77 | 78 | .. code-block:: django 79 | 80 | {% load regwall_tags %} 81 | 82 | {% get_regwall_limit as regwall_limit %} 83 | {% get_regwall_expire as regwall_expire %} 84 | 85 |

The limit is {{ regwall_limit }} articles for {{ regwall_expire }} days.

86 | 87 | ``{% get_regwall_attempts %}`` 88 | ------------------------------ 89 | 90 | Gets a list of the attempted consumed resources. The mixin logs each attempt a user makes for a request, which is not necessarily the same as a successful request. Each item of the list is a dictionary, which contains the ``app_label``, ``id``, ``headline``, and ``url`` of a single resource. You will probably use the ``length`` filter on ``get_regwall_attempts`` to get the number of attempted consumed resources. 91 | 92 | .. code-block:: django 93 | 94 | {% get_regwall_attempts as regwall_attempts %} 95 | 96 |

You tried to read {{ regwall_attempts|length }} free articles.

97 | 98 | Use ``get_regwall_attempts`` to check against the result of ``get_regwall_limit``. 99 | 100 | .. code-block:: django 101 | 102 | {% get_regwall_limit as regwall_limit %} 103 | {% get_regwall_expire as regwall_expire %} 104 | {% get_regwall_attempts as regwall_attempts %} 105 | 106 | {% if regwall_attempts|length >= regwall_limit %} 107 |

You read all of your {{ regwall_limit }} articles for {{ regwall_expire }} days.

108 | {% endif %} 109 | 110 | ``{% get_regwall_successes %}`` 111 | ------------------------------- 112 | 113 | Similar to ``get_regwall_attempts``, but ``get_regwall_successes`` gets a list of the resources that were successful delivered to the user. 114 | 115 | .. code-block:: django 116 | 117 | {% load regwall_tags %} 118 | 119 | {% get_regwall_limit as regwall_limit %} 120 | {% get_regwall_expire as regwall_expire %} 121 | {% get_regwall_attempts as regwall_attempts %} 122 | {% get_regwall_successes as regwall_successes %} 123 | 124 | {% if regwall_attempts|length >= regwall_limit %} 125 |

You read all {{ regwall_successes|length }} of your {{ regwall_limit }} articles for {{ regwall_expire }} days.

126 |
    127 | {% for article in regwall_successes %} 128 |
  1. {{ article.headline }}
  2. 129 | {% endfor %} 130 |
131 | {% endif %} 132 | 133 | Note that because different models can use different conventions for what constitutes a "headline," the template tag checks against these model attributes in this order: ``headline``, ``title``, ``name``, and finally empty string. 134 | 135 | Includes 136 | ======== 137 | 138 | To ease the creation of probable messages displayed to users, use (or be inspired by) the app's template `includes `_ in the ``regwall`` directory. 139 | 140 | ``regwall/detail.html`` 141 | ----------------------- 142 | 143 | Usage in a template, intended for a "detail" template whose view probably uses a ``DetailView`` of your own creation: 144 | 145 | .. code-block:: django 146 | 147 | {% include 'regwall/detail.html' %} 148 | 149 | The result: 150 | 151 | .. code-block:: django 152 | 153 | {% load regwall_tags %} 154 | 155 | {% get_regwall_attempts as regwall_attempts %} 156 | {% get_regwall_successes as regwall_successes %} 157 | {% get_regwall_limit as regwall_limit %} 158 | {% get_regwall_expire as regwall_expire %} 159 | 160 | {% if regwall_successes|length > 0 %} 161 |

You read {{ regwall_successes|length }} of your {{ regwall_limit }} free article{{ regwall_limit|pluralize }} for {{ regwall_expire }} day{{ regwall_expire|pluralize }}. Log in or register to read unlimited articles.

162 | {% endif %} 163 | 164 | ``regwall/login.html`` 165 | ---------------------- 166 | 167 | Usage in a template, intended for ``registration/login.html``: 168 | 169 | .. code-block:: django 170 | 171 | {% include 'regwall/login.html' %} 172 | 173 | The result: 174 | 175 | .. code-block:: django 176 | 177 | {% load regwall_tags %} 178 | 179 | {% get_regwall_attempts as regwall_attempts %} 180 | {% get_regwall_successes as regwall_successes %} 181 | {% get_regwall_limit as regwall_limit %} 182 | {% get_regwall_expire as regwall_expire %} 183 | 184 | {% if regwall_attempts|length >= regwall_limit %} 185 |

You read {{ regwall_successes|length }} of your {{ regwall_limit }} free article{{ regwall_limit|pluralize }} for {{ regwall_expire }} day{{ regwall_expire|pluralize }}. Log in or register to read unlimited articles.

186 | {% endif %} 187 | 188 | ``regwall/history.html`` 189 | ------------------------ 190 | 191 | Usage in a template, intended for ``registration/login.html``: 192 | 193 | .. code-block:: django 194 | 195 | {% include 'regwall/history.html' %} 196 | 197 | The result: 198 | 199 | .. code-block:: django 200 | 201 | {% load i18n regwall_tags %} 202 | 203 | {% get_regwall_attempts as regwall_attempts %} 204 | {% get_regwall_successes as regwall_successes %} 205 | {% get_regwall_limit as regwall_limit %} 206 | 207 | {% if regwall_attempts|length >= regwall_limit %} 208 |

{% trans 'You read these articles' %}

209 |
    210 | {% for article in regwall_successes %} 211 |
  1. {{ article.headline }}
  2. 212 | {% endfor %} 213 |
214 | {% endif %} 215 | 216 | Demo 217 | ==== 218 | 219 | The repo contains a sample Django project that shows how a typical intergration might occur with the template tags and includes. A fixture with sample data is also included to quickly test. 220 | 221 | .. code-block:: django 222 | 223 | $ mkvirtualenv -p python3 demo 224 | (demo)$ git clone git@github.com:richardcornish/django-registrationwall.git 225 | (demo)$ cd django-registrationwall/demo/ 226 | (demo)$ pip install -r requirements.txt 227 | (demo)$ cd demo/ 228 | (demo)$ python manage.py migrate 229 | (demo)$ python manage.py loaddata articles_article.json 230 | (demo)$ python manage.py runserver 231 | 232 | Open `http://127.0.0.1:8000/articles/ `_. 233 | --------------------------------------------------------------------------------