Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
julianwachholz committed Oct 23, 2021
0 parents commit b4d77fe
Show file tree
Hide file tree
Showing 30 changed files with 994 additions and 0 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Code Lint

on:
push:
paths:
- "**.py"

jobs:
lint:
name: Python Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: "3.10"
- name: Run flake8
uses: julianwachholz/flake8-action@v2
with:
checkName: "Python Lint"
plugins: flake8-black
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.py[co]
*.sqlite3
poetry.lock
/dist/
19 changes: 19 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) 2021 Julian Wachholz

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
177 changes: 177 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# django-guest-user

A Django app that allows visitors to interact with your site as a guest user
without requiring registration.

Largely inspired by [django-lazysignup](https://github.com/danfairs/django-lazysignup) and rewritten for Django 3 and Python 3.6 and up.

## Installation

Install the package with your favorite package manager from PyPI:

```
pip install django-guest-user
```

Add the app to your `INSTALLED_APPS` and `AUTHENTICATION_BACKENDS`:

```python
INSTALLED_APPS = [
...
"django_guest_user",
]

AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"guest_user.backends.GuestBackend",
]
```

Add the patterns to your URL config:

```python
urlpatterns = [
...
path("convert/", include("guest_user.urls")),
]
```

Don't forget to run migrations:

```
python manage.py migrate
```

## How to use

Guest users are not created for every unauthenticated request.
Instead, use the `@allow_guest_user` decorator on a view to enable
that view to be accessed by a temporary guest user.

```python
from guest_user.decorators import allow_guest_user

@allow_guest_user
def my_view(request):
# Will always be either a registered a guest user.
username = request.user.username
return HttpResponse(f"Hello, {username}!")
```

## API

### `@guest_user.decorators.allow_guest_user`

View decorator that will create a temporary guest user in the event
that the decorated view is accessed by an unauthenticated visitor.

Takes no arguments.

### `@guest_user.decorators.guest_user_required(redirect_field_name="next", login_url=None)`

View decorator that redirects to a given URL if the accessing user is
anonymous or already authenticated.

Arguments:

- `redirect_field_name`: URL query parameter to use to link back in the case of a redirect to the login url. Defaults to `django.contrib.auth.REDIRECT_FIELD_NAME` ("next").
- `login_url`: URL to redirect to if the user is not authenticated. Defaults to the `LOGIN_URL` setting.

### `@guest_user.decorators.regular_user_required(redirect_field_name="next", login_url=None)`

Decorator that will not allow guest users to access the view.
Will redirect to the conversion page to allow a guest user to fully register.

Arguments:

- `redirect_field_name`: URL query parameter to use to link back in the case of a redirect to the login url. Defaults to `django.contrib.auth.REDIRECT_FIELD_NAME` ("next").
- `login_url`: URL to redirect to if the user is a guest. Defaults to `"guest_user_convert"`.

### `guest_user.functions.get_guest_model()`

The guest user model is swappable. This function will return the currently configured model class.

### `guest_user.functions.is_guest_user(user)`

Check wether the given user instance is a temporary guest.

### `guest_user.signals.converted`

Signal that is dispatched when a guest user is converted to a regular user.

### Template tag `is_guest_user`

A filter to use in templates to check if the user object is a guest.

```
{% load guest_user %}
{% if user|is_guest_user %}
Hello guest.
{% endif %}
```

## Settings

Various settings are provided to allow customization of the guest user behavior.

### `GUEST_USER_ENABLED`

`bool`. If `False`, the `@allow_guest_user` decorator will not create guest users.
Defaults to `True`.

### `GUEST_USER_MODEL`

`str`. The swappable model identifier to use as the guest model.
Defaults to `"guest_user.Guest"`.

### `GUEST_USER_NAME_GENERATOR`

`str`. Import path to a function that will generate a username for a guest user.
Defaults to `"guest_user.functions.generate_uuid_username"`.

Included with the package are two alternatives:

`"guest_user.functions.generate_numbered_username"`: Will create a random four digit
number prefixed by `GUEST_USER_NAME_PREFIX`.

`"guest_user.functions.generate_friendly_username"`: Creates a friendly and easy to remember username by combining an adjective, noun and number. Requires `random_username` to be installed.

### `GUEST_USER_NAME_PREFIX`

`str`. A prefix to use with the `generate_numbered_username` generator.
Defaults to `"Guest"`.

### `GUEST_USER_CONVERT_FORM`

`str`. Import path for the guest conversion form.
Must implement `get_credentials` to be passed to Django's `authenticate` function.
Defaults to `"guest_user.forms.UserCreationForm"`.

### `GUEST_USER_CONVERT_PREFILL_USERNAME`

`bool`. Set the generated username as initial value on the conversion form.
Defaults to `False`.

### `GUEST_USER_CONVERT_URL`

`str`. URL name for the convert view.
Defaults to `"guest_user_convert"`.

### `GUEST_USER_CONVERT_REDIRECT_URL`

`str`. URL name to redirect to after conversion, unless a redirect parameter was provided.
Defaults to `"guest_user_convert_success"`.

### `GUEST_USER_BLOCKED_USER_AGENTS`

`list[str]`. Web crawlers and other user agents to block from becoming guest users.
The list will be combined into a regular expression.
Default includes a number of well known bots and spiders.

## Status

This project is currently untested. But thanks to [previous work](https://github.com/danfairs/django-lazysignup) it is largely functional.

I decided to rewrite the project since the original project hasn't seen any
larger updates for a few years now and the code base was written a long time ago.
Empty file added guest_user/__init__.py
Empty file.
8 changes: 8 additions & 0 deletions guest_user/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.contrib import admin

from .models import Guest


@admin.register(Guest)
class GuestAdmin(admin.ModelAdmin):
list_display = ["user", "created_at"]
6 changes: 6 additions & 0 deletions guest_user/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class GuestUserConfig(AppConfig):
name = "guest_user"
verbose_name = "Guest User"
22 changes: 22 additions & 0 deletions guest_user/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend


class GuestBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
"""Authenticate with username only."""
UserModel = get_user_model()

try:
return UserModel.objects.get(**{UserModel.USERNAME_FIELD: username})
except UserModel.DoesNotExist:
return None

def get_user(self, user_id):
UserModel = get_user_model()
try:
user = UserModel._default_manager.get(pk=user_id)
# user.backend = "guest_user.backends.GuestBackend"
except UserModel.DoesNotExist:
return None
return user
79 changes: 79 additions & 0 deletions guest_user/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from functools import wraps

from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
from django.contrib.auth.decorators import user_passes_test

from . import settings
from .functions import get_guest_model, is_guest_user


def allow_guest_user(function=None):
"""
Allow anonymous users to access the view by creating a guest user.
"""

def wrapped(request, *args, **kwargs):
assert hasattr(
request, "session"
), "Please add 'django.contrib.sessions' to INSTALLED_APPS."

if settings.ENABLED and request.user.is_anonymous:
user_agent = request.META.get("HTTP_USER_AGENT", "")

if not settings.BLOCKED_USER_AGENTS.match(user_agent):
Guest = get_guest_model()
user = Guest.objects.create_guest_user()
# request.user = None
user = authenticate(username=user.username)
assert user, (
"Guest authentication failed. Do you have "
"'guest_user.backends.GuestBackend' in AUTHENTICATION_BACKENDS?"
)
login(request, user)

return function(request, *args, **kwargs)

return wraps(function)(wrapped)


def guest_user_required(
function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None
):
"""
Current user must be a temporary guest.
Other visitors will be redirected to `login_url` or a redirect parameter given in the URL.
"""
actual_decorator = user_passes_test(
is_guest_user,
login_url=login_url,
redirect_field_name=redirect_field_name,
)

if function:
return actual_decorator(function)
return actual_decorator


def regular_user_required(
function=None, redirect_field_name=REDIRECT_FIELD_NAME, convert_url=None
):
"""
Current user must not be a temporary guest.
Guest users will be redirected to the convert page.
"""
if convert_url is None:
convert_url = settings.CONVERT_URL

actual_decorator = user_passes_test(
lambda u: u.is_authenticated and not is_guest_user(u),
login_url=convert_url,
redirect_field_name=redirect_field_name,
)
if function:
return actual_decorator(function)
return actual_decorator
2 changes: 2 additions & 0 deletions guest_user/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class NotGuestError(TypeError):
"""Raised when an operation is attempted on a non-lazy user"""
9 changes: 9 additions & 0 deletions guest_user/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm


class UserCreationForm(BaseUserCreationForm):
def get_credentials(self):
return {
"username": self.cleaned_data["username"],
"password": self.cleaned_data["password1"],
}
Loading

0 comments on commit b4d77fe

Please sign in to comment.