├── .gitignore ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── privatebeta ├── __init__.py ├── admin.py ├── forms.py ├── middleware.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_add_invited_field.py │ └── __init__.py ├── models.py ├── urls.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.egg-info 3 | .DS_store -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | Version 0.4.2 6 | ============= 7 | 8 | * `Sébastien Fievet `_ added the ability to use 9 | a custom form for the invite view. 10 | 11 | 12 | Version 0.4.1 13 | ============= 14 | 15 | * Added MANIFEST.in, updated setup.py to include migrations and rst files in 16 | source distribution. 17 | 18 | Version 0.4.0 19 | ============= 20 | 21 | * Added ``invited`` field to ``InviteRequest`` model. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Pragmatic Badger, LLC. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Pragmatic Badger, LLC. 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" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.rst 2 | include README.rst -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | django-privatebeta 3 | ================== 4 | 5 | This reusable Django applications provides two things useful to a site under 6 | private (closed) beta: 7 | 8 | * A form that allows users to enter their email address so that you can send 9 | them an invite or a site launch notification later. 10 | * A middleware that locks the site down for non-logged in users. If you 11 | control account creation this is a very effective way of limiting a site 12 | to beta testers only. 13 | 14 | Email invite form 15 | ================= 16 | 17 | To use the invite form, you first need to add ``privatebeta`` to 18 | ``INSTALLED_APPS`` in your settings file:: 19 | 20 | INSTALLED_APPS = ( 21 | ... 22 | 'privatebeta', 23 | ) 24 | 25 | You will also need to add :: 26 | 27 | urlpatterns = patterns('', 28 | ... 29 | (r'^invites/', include('privatebeta.urls')), 30 | ) 31 | 32 | You will also need to create two templates. The first is 33 | ``privatebeta/invite.html``:: 34 | 35 | {% extends 'base.html' %} 36 | 37 | {% block content %} 38 |

Enter your email address and we'll send you an invite soon

39 |
40 | {{ form }} 41 | {% csrf_token %} 42 | 43 |
44 | {% endblock %} 45 | 46 | When an email address is successfully entered, the user will be redirected to 47 | ``privatebeta/sent.html``:: 48 | 49 | {% extends 'base.html' %} 50 | 51 | {% block content %} 52 |

Thanks, we'll be in touch!

53 | {% endblock %} 54 | 55 | The above templates assume a standard Django template structure with a 56 | ``base.html`` template and a ``content`` block. 57 | 58 | The included views take two optional keyword arguments for flexibility.: 59 | 60 | ``template_name`` 61 | The name of the tempalte to render. Optional, overrides the default 62 | template. 63 | 64 | ``extra_context`` 65 | A dictionary to add to the context of the view. Keys will become 66 | variable names and values will be accessible via those variables. 67 | Optional. 68 | 69 | Closed beta middleware 70 | ====================== 71 | 72 | If you would also like to prevent non-logged-in users from viewing your site, 73 | you can make use of ``privatebeta.middleware.PrivateBetaMiddleware``. This 74 | middleware redirects all views to a specified location if a user is not logged in. 75 | 76 | To use the middleware, add it to ``MIDDLEWARE_CLASSSES`` in your settings file:: 77 | 78 | MIDDLEWARE_CLASSES = ( 79 | ... 80 | 'privatebeta.middleware.PrivateBetaMiddleware', 81 | ) 82 | 83 | There are a few settings that influence behavior of the middleware: 84 | 85 | ``PRIVATEBETA_NEVER_ALLOW_VIEWS`` 86 | A list of full view names that should *never* be displayed. This 87 | list is checked before the others so that this middleware exhibits 88 | deny then allow behavior. 89 | 90 | ``PRIVATEBETA_ALWAYS_ALLOW_VIEWS`` 91 | A list of full view names that should always pass through. 92 | 93 | ``PRIVATEBETA_ALWAYS_ALLOW_MODULES`` 94 | A list of modules that should always pass through. All 95 | views in ``django.contrib.auth.views``, ``django.views.static`` 96 | and ``privatebeta.views`` will pass through unless they are 97 | explicitly prohibited in ``PRIVATEBETA_NEVER_ALLOW_VIEWS`` 98 | 99 | ``PRIVATEBETA_REDIRECT_URL`` 100 | The URL to redirect to. Can be relative or absolute. 101 | 102 | Similar projects 103 | ================ 104 | 105 | * Pinax includes a `private beta project`_ that shows how to dynamically enable 106 | or disable account creation in your `urlconf`_ in concert with a setting. It 107 | also includes a `signup code`_ app that allows you to create beta codes with 108 | a limited number of uses. 109 | * `django-invitation`_ is a reusable application designed to allow existing beta 110 | users to invite new users to the site for a viral beta. It builds on top of 111 | `django-registration`_ and can require an invite before a user is allowed to 112 | create an account. 113 | * `django-invite`_ is a lighter weight reusable application designed to restrict 114 | logins via an invite system. 115 | 116 | Upgrading from 0.3 117 | ================== 118 | 119 | This application uses `South`_ (0.6.2 or greater) to manage schema migrations. 120 | If you don't already have South installed, you will need to do so: 121 | 122 | * Add south to your INSTALLED_APPS. 123 | * Run manage.py syncdb to install South. 124 | 125 | Once South is installed, you will have to "fake" the schema for the 0.3 126 | release, then migrate to 0.4.0: 127 | 128 | * Run manage.py migrate --fake privatebeta 0001. 129 | * Run manage.py migrate --list. 130 | * Verify that 0001_initial has been applied to privatebeta and 002_add_invited_field has not. 131 | * Run manage.py migrate privatebeta 132 | 133 | Compatibility 134 | ============= 135 | 136 | This application is known to work with Django 1.0.X, Django 1.1.X & Django 1.2.X. 137 | 138 | .. _private beta project: http://github.com/pinax/pinax/tree/master/pinax/projects/private_beta_project/ 139 | .. _urlconf: http://github.com/pinax/pinax/blob/master/pinax/projects/private_beta_project/urls.py 140 | .. _signup code: http://github.com/pinax/pinax/tree/master/pinax/apps/signup_codes/ 141 | .. _django-invitation: http://bitbucket.org/david/django-invitation/overview/ 142 | .. _django-registration: http://bitbucket.org/ubernostrum/django-registration/ 143 | .. _django-invite: http://bitbucket.org/lorien/django-invite/ 144 | .. _South: http://south.aeracode.org/ 145 | -------------------------------------------------------------------------------- /privatebeta/__init__.py: -------------------------------------------------------------------------------- 1 | # Let the wookie pass. -------------------------------------------------------------------------------- /privatebeta/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from privatebeta.models import InviteRequest 3 | 4 | class InviteRequestAdmin(admin.ModelAdmin): 5 | date_hierarchy = 'created' 6 | list_display = ('email', 'created', 'invited',) 7 | list_filter = ('created', 'invited',) 8 | 9 | admin.site.register(InviteRequest, InviteRequestAdmin) 10 | -------------------------------------------------------------------------------- /privatebeta/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from privatebeta.models import InviteRequest 3 | 4 | class InviteRequestForm(forms.ModelForm): 5 | class Meta: 6 | model = InviteRequest 7 | fields = ['email'] 8 | -------------------------------------------------------------------------------- /privatebeta/middleware.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import HttpResponseRedirect 3 | 4 | class PrivateBetaMiddleware(object): 5 | """ 6 | Add this to your ``MIDDLEWARE_CLASSES`` make all views except for 7 | those in the account application require that a user be logged in. 8 | This can be a quick and easy way to restrict views on your site, 9 | particularly if you remove the ability to create accounts. 10 | 11 | **Settings:** 12 | 13 | ``PRIVATEBETA_ENABLE_BETA`` 14 | Whether or not the beta middleware should be used. If set to `False` 15 | the PrivateBetaMiddleware middleware will be ignored and the request 16 | will be returned. This is useful if you want to disable privatebeta 17 | on a development machine. Default is `True`. 18 | 19 | ``PRIVATEBETA_NEVER_ALLOW_VIEWS`` 20 | A list of full view names that should *never* be displayed. This 21 | list is checked before the others so that this middleware exhibits 22 | deny then allow behavior. 23 | 24 | ``PRIVATEBETA_ALWAYS_ALLOW_VIEWS`` 25 | A list of full view names that should always pass through. 26 | 27 | ``PRIVATEBETA_ALWAYS_ALLOW_MODULES`` 28 | A list of modules that should always pass through. All 29 | views in ``django.contrib.auth.views``, ``django.views.static`` 30 | and ``privatebeta.views`` will pass through unless they are 31 | explicitly prohibited in ``PRIVATEBETA_NEVER_ALLOW_VIEWS`` 32 | 33 | ``PRIVATEBETA_REDIRECT_URL`` 34 | The URL to redirect to. Can be relative or absolute. 35 | """ 36 | 37 | def __init__(self): 38 | self.enable_beta = getattr(settings, 'PRIVATEBETA_ENABLE_BETA', True) 39 | self.never_allow_views = getattr(settings, 'PRIVATEBETA_NEVER_ALLOW_VIEWS', []) 40 | self.always_allow_views = getattr(settings, 'PRIVATEBETA_ALWAYS_ALLOW_VIEWS', []) 41 | self.always_allow_modules = getattr(settings, 'PRIVATEBETA_ALWAYS_ALLOW_MODULES', []) 42 | self.redirect_url = getattr(settings, 'PRIVATEBETA_REDIRECT_URL', '/invite/') 43 | 44 | def process_view(self, request, view_func, view_args, view_kwargs): 45 | if request.user.is_authenticated() or not self.enable_beta: 46 | # User is logged in, no need to check anything else. 47 | return 48 | whitelisted_modules = ['django.contrib.auth.views', 'django.views.static', 'privatebeta.views'] 49 | if self.always_allow_modules: 50 | whitelisted_modules += self.always_allow_modules 51 | 52 | full_view_name = '%s.%s' % (view_func.__module__, view_func.__name__) 53 | 54 | if full_view_name in self.never_allow_views: 55 | return HttpResponseRedirect(self.redirect_url) 56 | 57 | if full_view_name in self.always_allow_views: 58 | return 59 | if '%s' % view_func.__module__ in whitelisted_modules: 60 | return 61 | else: 62 | return HttpResponseRedirect(self.redirect_url) 63 | -------------------------------------------------------------------------------- /privatebeta/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | 2 | from south.db import db 3 | from django.db import models 4 | from privatebeta.models import * 5 | 6 | class Migration: 7 | 8 | def forwards(self, orm): 9 | 10 | # Adding model 'InviteRequest' 11 | db.create_table('privatebeta_inviterequest', ( 12 | ('id', orm['privatebeta.InviteRequest:id']), 13 | ('email', orm['privatebeta.InviteRequest:email']), 14 | ('created', orm['privatebeta.InviteRequest:created']), 15 | )) 16 | db.send_create_signal('privatebeta', ['InviteRequest']) 17 | 18 | 19 | 20 | def backwards(self, orm): 21 | 22 | # Deleting model 'InviteRequest' 23 | db.delete_table('privatebeta_inviterequest') 24 | 25 | 26 | 27 | models = { 28 | 'privatebeta.inviterequest': { 29 | 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 30 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'unique': 'True'}), 31 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 32 | } 33 | } 34 | 35 | complete_apps = ['privatebeta'] 36 | -------------------------------------------------------------------------------- /privatebeta/migrations/0002_add_invited_field.py: -------------------------------------------------------------------------------- 1 | 2 | from south.db import db 3 | from django.db import models 4 | from privatebeta.models import * 5 | 6 | class Migration: 7 | 8 | def forwards(self, orm): 9 | 10 | # Adding field 'InviteRequest.invited' 11 | db.add_column('privatebeta_inviterequest', 'invited', orm['privatebeta.inviterequest:invited']) 12 | 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'InviteRequest.invited' 18 | db.delete_column('privatebeta_inviterequest', 'invited') 19 | 20 | 21 | 22 | models = { 23 | 'privatebeta.inviterequest': { 24 | 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 25 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'unique': 'True'}), 26 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 27 | 'invited': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}) 28 | } 29 | } 30 | 31 | complete_apps = ['privatebeta'] 32 | -------------------------------------------------------------------------------- /privatebeta/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pragmaticbadger/django-privatebeta/60d74466826444cd73f68c0e7578fe7de6b467f9/privatebeta/migrations/__init__.py -------------------------------------------------------------------------------- /privatebeta/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.db import models 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | class InviteRequest(models.Model): 6 | email = models.EmailField(_('Email address'), unique=True) 7 | created = models.DateTimeField(_('Created'), default=datetime.datetime.now) 8 | invited = models.BooleanField(_('Invited'), default=False) 9 | 10 | def __unicode__(self): 11 | return _('Invite for %(email)s') % {'email': self.email} 12 | -------------------------------------------------------------------------------- /privatebeta/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns('', 4 | url(r'^$', 'privatebeta.views.invite', name='privatebeta_invite'), 5 | url(r'^sent/$', 'privatebeta.views.sent', name='privatebeta_sent'), 6 | ) 7 | -------------------------------------------------------------------------------- /privatebeta/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic.simple import direct_to_template 2 | from django.core.urlresolvers import reverse 3 | from django.http import HttpResponseRedirect 4 | from django.shortcuts import render_to_response 5 | from django.template import RequestContext 6 | from privatebeta.forms import InviteRequestForm 7 | 8 | def invite(request, form_class=InviteRequestForm, template_name="privatebeta/invite.html", extra_context=None): 9 | """ 10 | Allow a user to request an invite at a later date by entering their email address. 11 | 12 | **Arguments:** 13 | 14 | ``template_name`` 15 | The name of the tempalte to render. Optional, defaults to 16 | privatebeta/invite.html. 17 | 18 | ``extra_context`` 19 | A dictionary to add to the context of the view. Keys will become 20 | variable names and values will be accessible via those variables. 21 | Optional. 22 | 23 | **Context:** 24 | 25 | The context will contain an ``InviteRequestForm`` that represents a 26 | :model:`invitemelater.InviteRequest` accessible via the variable ``form``. 27 | If ``extra_context`` is provided, those variables will also be accessible. 28 | 29 | **Template:** 30 | 31 | :template:`privatebeta/invite.html` or the template name specified by 32 | ``template_name``. 33 | """ 34 | form = form_class(request.POST or None) 35 | if form.is_valid(): 36 | form.save() 37 | return HttpResponseRedirect(reverse('privatebeta_sent')) 38 | 39 | context = {'form': form} 40 | 41 | if extra_context is not None: 42 | context.update(extra_context) 43 | 44 | return render_to_response(template_name, context, 45 | context_instance=RequestContext(request)) 46 | 47 | def sent(request, template_name="privatebeta/sent.html", extra_context=None): 48 | """ 49 | Display a message to the user after the invite request is completed 50 | successfully. 51 | 52 | **Arguments:** 53 | 54 | ``template_name`` 55 | The name of the tempalte to render. Optional, defaults to 56 | privatebeta/sent.html. 57 | 58 | ``extra_context`` 59 | A dictionary to add to the context of the view. Keys will become 60 | variable names and values will be accessible via those variables. 61 | Optional. 62 | 63 | **Context:** 64 | 65 | There will be nothing in the context unless a dictionary is passed to 66 | ``extra_context``. 67 | 68 | **Template:** 69 | 70 | :template:`privatebeta/sent.html` or the template name specified by 71 | ``template_name``. 72 | """ 73 | return direct_to_template(request, template=template_name, extra_context=extra_context) 74 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup(name='privatebeta', 6 | version='0.4.2', 7 | description='A reusable application for collecting email addresses for later invitations and to restrict access to a site under private beta.', 8 | author='Pragmatic Badger, LLC.', 9 | author_email='info@pragmaticbadger.com', 10 | url='http://github.com/pragmaticbadger/django-privatebeta/', 11 | packages=['privatebeta', 'privatebeta.migrations'], 12 | package_dir={'privatebeta': 'privatebeta'}, 13 | ) 14 | --------------------------------------------------------------------------------