Skip to content

Commit b4d77fe

Browse files
Initial commit
0 parents  commit b4d77fe

30 files changed

+994
-0
lines changed

.github/workflows/lint.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Code Lint
2+
3+
on:
4+
push:
5+
paths:
6+
- "**.py"
7+
8+
jobs:
9+
lint:
10+
name: Python Lint
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v2
14+
- uses: actions/setup-python@v2
15+
with:
16+
python-version: "3.10"
17+
- name: Run flake8
18+
uses: julianwachholz/flake8-action@v2
19+
with:
20+
checkName: "Python Lint"
21+
plugins: flake8-black
22+
env:
23+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*.py[co]
2+
*.sqlite3
3+
poetry.lock
4+
/dist/

LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2021 Julian Wachholz
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.

README.md

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# django-guest-user
2+
3+
A Django app that allows visitors to interact with your site as a guest user
4+
without requiring registration.
5+
6+
Largely inspired by [django-lazysignup](https://github.com/danfairs/django-lazysignup) and rewritten for Django 3 and Python 3.6 and up.
7+
8+
## Installation
9+
10+
Install the package with your favorite package manager from PyPI:
11+
12+
```
13+
pip install django-guest-user
14+
```
15+
16+
Add the app to your `INSTALLED_APPS` and `AUTHENTICATION_BACKENDS`:
17+
18+
```python
19+
INSTALLED_APPS = [
20+
...
21+
"django_guest_user",
22+
]
23+
24+
AUTHENTICATION_BACKENDS = [
25+
"django.contrib.auth.backends.ModelBackend",
26+
"guest_user.backends.GuestBackend",
27+
]
28+
```
29+
30+
Add the patterns to your URL config:
31+
32+
```python
33+
urlpatterns = [
34+
...
35+
path("convert/", include("guest_user.urls")),
36+
]
37+
```
38+
39+
Don't forget to run migrations:
40+
41+
```
42+
python manage.py migrate
43+
```
44+
45+
## How to use
46+
47+
Guest users are not created for every unauthenticated request.
48+
Instead, use the `@allow_guest_user` decorator on a view to enable
49+
that view to be accessed by a temporary guest user.
50+
51+
```python
52+
from guest_user.decorators import allow_guest_user
53+
54+
@allow_guest_user
55+
def my_view(request):
56+
# Will always be either a registered a guest user.
57+
username = request.user.username
58+
return HttpResponse(f"Hello, {username}!")
59+
```
60+
61+
## API
62+
63+
### `@guest_user.decorators.allow_guest_user`
64+
65+
View decorator that will create a temporary guest user in the event
66+
that the decorated view is accessed by an unauthenticated visitor.
67+
68+
Takes no arguments.
69+
70+
### `@guest_user.decorators.guest_user_required(redirect_field_name="next", login_url=None)`
71+
72+
View decorator that redirects to a given URL if the accessing user is
73+
anonymous or already authenticated.
74+
75+
Arguments:
76+
77+
- `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").
78+
- `login_url`: URL to redirect to if the user is not authenticated. Defaults to the `LOGIN_URL` setting.
79+
80+
### `@guest_user.decorators.regular_user_required(redirect_field_name="next", login_url=None)`
81+
82+
Decorator that will not allow guest users to access the view.
83+
Will redirect to the conversion page to allow a guest user to fully register.
84+
85+
Arguments:
86+
87+
- `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").
88+
- `login_url`: URL to redirect to if the user is a guest. Defaults to `"guest_user_convert"`.
89+
90+
### `guest_user.functions.get_guest_model()`
91+
92+
The guest user model is swappable. This function will return the currently configured model class.
93+
94+
### `guest_user.functions.is_guest_user(user)`
95+
96+
Check wether the given user instance is a temporary guest.
97+
98+
### `guest_user.signals.converted`
99+
100+
Signal that is dispatched when a guest user is converted to a regular user.
101+
102+
### Template tag `is_guest_user`
103+
104+
A filter to use in templates to check if the user object is a guest.
105+
106+
```
107+
{% load guest_user %}
108+
109+
{% if user|is_guest_user %}
110+
Hello guest.
111+
{% endif %}
112+
```
113+
114+
## Settings
115+
116+
Various settings are provided to allow customization of the guest user behavior.
117+
118+
### `GUEST_USER_ENABLED`
119+
120+
`bool`. If `False`, the `@allow_guest_user` decorator will not create guest users.
121+
Defaults to `True`.
122+
123+
### `GUEST_USER_MODEL`
124+
125+
`str`. The swappable model identifier to use as the guest model.
126+
Defaults to `"guest_user.Guest"`.
127+
128+
### `GUEST_USER_NAME_GENERATOR`
129+
130+
`str`. Import path to a function that will generate a username for a guest user.
131+
Defaults to `"guest_user.functions.generate_uuid_username"`.
132+
133+
Included with the package are two alternatives:
134+
135+
`"guest_user.functions.generate_numbered_username"`: Will create a random four digit
136+
number prefixed by `GUEST_USER_NAME_PREFIX`.
137+
138+
`"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.
139+
140+
### `GUEST_USER_NAME_PREFIX`
141+
142+
`str`. A prefix to use with the `generate_numbered_username` generator.
143+
Defaults to `"Guest"`.
144+
145+
### `GUEST_USER_CONVERT_FORM`
146+
147+
`str`. Import path for the guest conversion form.
148+
Must implement `get_credentials` to be passed to Django's `authenticate` function.
149+
Defaults to `"guest_user.forms.UserCreationForm"`.
150+
151+
### `GUEST_USER_CONVERT_PREFILL_USERNAME`
152+
153+
`bool`. Set the generated username as initial value on the conversion form.
154+
Defaults to `False`.
155+
156+
### `GUEST_USER_CONVERT_URL`
157+
158+
`str`. URL name for the convert view.
159+
Defaults to `"guest_user_convert"`.
160+
161+
### `GUEST_USER_CONVERT_REDIRECT_URL`
162+
163+
`str`. URL name to redirect to after conversion, unless a redirect parameter was provided.
164+
Defaults to `"guest_user_convert_success"`.
165+
166+
### `GUEST_USER_BLOCKED_USER_AGENTS`
167+
168+
`list[str]`. Web crawlers and other user agents to block from becoming guest users.
169+
The list will be combined into a regular expression.
170+
Default includes a number of well known bots and spiders.
171+
172+
## Status
173+
174+
This project is currently untested. But thanks to [previous work](https://github.com/danfairs/django-lazysignup) it is largely functional.
175+
176+
I decided to rewrite the project since the original project hasn't seen any
177+
larger updates for a few years now and the code base was written a long time ago.

guest_user/__init__.py

Whitespace-only changes.

guest_user/admin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django.contrib import admin
2+
3+
from .models import Guest
4+
5+
6+
@admin.register(Guest)
7+
class GuestAdmin(admin.ModelAdmin):
8+
list_display = ["user", "created_at"]

guest_user/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class GuestUserConfig(AppConfig):
5+
name = "guest_user"
6+
verbose_name = "Guest User"

guest_user/backends.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.contrib.auth import get_user_model
2+
from django.contrib.auth.backends import ModelBackend
3+
4+
5+
class GuestBackend(ModelBackend):
6+
def authenticate(self, request, username=None, password=None, **kwargs):
7+
"""Authenticate with username only."""
8+
UserModel = get_user_model()
9+
10+
try:
11+
return UserModel.objects.get(**{UserModel.USERNAME_FIELD: username})
12+
except UserModel.DoesNotExist:
13+
return None
14+
15+
def get_user(self, user_id):
16+
UserModel = get_user_model()
17+
try:
18+
user = UserModel._default_manager.get(pk=user_id)
19+
# user.backend = "guest_user.backends.GuestBackend"
20+
except UserModel.DoesNotExist:
21+
return None
22+
return user

guest_user/decorators.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from functools import wraps
2+
3+
from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
4+
from django.contrib.auth.decorators import user_passes_test
5+
6+
from . import settings
7+
from .functions import get_guest_model, is_guest_user
8+
9+
10+
def allow_guest_user(function=None):
11+
"""
12+
Allow anonymous users to access the view by creating a guest user.
13+
14+
"""
15+
16+
def wrapped(request, *args, **kwargs):
17+
assert hasattr(
18+
request, "session"
19+
), "Please add 'django.contrib.sessions' to INSTALLED_APPS."
20+
21+
if settings.ENABLED and request.user.is_anonymous:
22+
user_agent = request.META.get("HTTP_USER_AGENT", "")
23+
24+
if not settings.BLOCKED_USER_AGENTS.match(user_agent):
25+
Guest = get_guest_model()
26+
user = Guest.objects.create_guest_user()
27+
# request.user = None
28+
user = authenticate(username=user.username)
29+
assert user, (
30+
"Guest authentication failed. Do you have "
31+
"'guest_user.backends.GuestBackend' in AUTHENTICATION_BACKENDS?"
32+
)
33+
login(request, user)
34+
35+
return function(request, *args, **kwargs)
36+
37+
return wraps(function)(wrapped)
38+
39+
40+
def guest_user_required(
41+
function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None
42+
):
43+
"""
44+
Current user must be a temporary guest.
45+
46+
Other visitors will be redirected to `login_url` or a redirect parameter given in the URL.
47+
48+
"""
49+
actual_decorator = user_passes_test(
50+
is_guest_user,
51+
login_url=login_url,
52+
redirect_field_name=redirect_field_name,
53+
)
54+
55+
if function:
56+
return actual_decorator(function)
57+
return actual_decorator
58+
59+
60+
def regular_user_required(
61+
function=None, redirect_field_name=REDIRECT_FIELD_NAME, convert_url=None
62+
):
63+
"""
64+
Current user must not be a temporary guest.
65+
66+
Guest users will be redirected to the convert page.
67+
68+
"""
69+
if convert_url is None:
70+
convert_url = settings.CONVERT_URL
71+
72+
actual_decorator = user_passes_test(
73+
lambda u: u.is_authenticated and not is_guest_user(u),
74+
login_url=convert_url,
75+
redirect_field_name=redirect_field_name,
76+
)
77+
if function:
78+
return actual_decorator(function)
79+
return actual_decorator

guest_user/exceptions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class NotGuestError(TypeError):
2+
"""Raised when an operation is attempted on a non-lazy user"""

0 commit comments

Comments
 (0)