Metadata-Version: 2.4
Name: django-thumbmark
Version: 0.3
Summary: A Django app to transparently identify users with the ThumbmarkJS library.
Author-email: Mark Ciecior <mark@markciecior.com>
License: MIT
Project-URL: Homepage, https://github.com/markciecior/django-thumbmark
Project-URL: Repository, https://github.com/markciecior/django-thumbmark
Project-URL: Issues, https://github.com/markciecior/django-thumbmark/issues
Project-URL: Changelog, https://github.com/markciecior/django-thumbmark/blob/main/CHANGELOG.md
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: django>=4.2
Dynamic: license-file

# django-thumbmark
&nbsp;[![License](https://img.shields.io/pypi/l/django-thumbmark.svg?label=License)](https://pypi.python.org/pypi/django-thumbmark)&nbsp;[![python versions](https://img.shields.io/pypi/pyversions/django-thumbmark.svg?label=python%20versions)](https://pypi.python.org/pypi/django-thumbmark)&nbsp;[![PyPI version](https://img.shields.io/pypi/v/django-thumbmark.svg?label=PyPI%20version)](https://pypi.python.org/pypi/django-thumbmark)

Django app to transparently identify users with the [ThumbmarkJS](https://github.com/thumbmarkjs/thumbmarkjs) library.

This app includes a decorator that mimics the built-in ```login_required``` decorator provided by Django.  Views using this decorator will have unidentified users transparently redirected through a page where the [ThumbmarkJS](https://github.com/thumbmarkjs/thumbmarkjs) library is used to generate a unique identifier for the client.  This unique identifier is then mapped to a Django user and associated with the ```HttpRequest``` object.

Common use cases include public forms that should be limited to one submission per user, without requring a username/password login.
* Survey Submissions
* Feedback Forms
* Anonymous Polls

## Usage

Add the included decorator to your function- or class-based view:

```python
from django.views.generic import TemplateView
from django.utils.decorators import method_decorator

from django_thumbmark.decorators import login_required_thumbmark

@login_required_thumbmark
def my_function_based_view(request):
    # ...
    print(request.user) # <-- The User attribute will be populated

### --- or --- ###

@method_decorator(login_required_thumbmark, name='dispatch')
class MyClassBasedView(TemplateView):
    # ...
    def get(self, request):
        print(request.user) # <-- The User attribute will be populated
```

## Standard Installation

Install from PyPi:

```python
python -m pip install django-thumbmark
```

Add `django_thumbmark` to your `INSTALLED_APPS`:

```python
INSTALLED_APPS = [
    # ...
    'django_thumbmark',
]
```

To use the app as written (without customization), `include` its URLs in your project's ```urls.py``` file, the same way you would for any other reusable Django app:

```python
# project/urls.py
from django.urls import include, path

urlpatterns = [
    # ...
    path("thumbmark/", include("django_thumbmark.urls")),
]

# project/views.py
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.views import View

from django_thumbmark.decorators import login_required_thumbmark

@login_required_thumbmark
def index(request):
    # Do the thing with your now logged-in user
    return HttpResponse(f"Hello, {request.user}")

### --- or, for a class-based view --- ###

@method_decorator(login_required_thumbmark, name="dispatch")
class IndexView(View):
    def get(self, request):
        # Do the thing with your now logged-in user
        return HttpResponse(f"Hello, {request.user}")
```

The path prefix (`"thumbmark/"` above) is up to you and doesn't need to match anything; `login_required_thumbmark` finds the `tm`/`tmlogin` views by namespace (see [Custom Namespace](#custom-namespace) below), not by path, so it works no matter which app or namespace your protected views live in.

## Installation with Customizations

To use the app with customization, overwrite the included `DjTmScriptView` as needed, then include *both* it and the stock `DjTmLoginView` from your own `urls.py` module. That module needs `app_name = "django_thumbmark"` (or a matching [`THUMBMARK_NAMESPACE`](#custom-namespace)) so `login_required_thumbmark` can still find it, and the URL names must still be `tm` and `tmlogin`.

```python
# myapp/views.py
from django_thumbmark.views import DjTmScriptView
from django.utils import timezone # ... as an example of customized usage

    # ...
    class MyDjTmScriptView(DjTmScriptView):
        def get_first_name(self, request, *args, **kwargs):
            return "CustomFirstName"

        def get_username(self, request, *args, **kwargs):
            tmid = kwargs["tmid"]
            path = request.path
            time = timezone.now().strftime("%Y-%m-%dT%H:%M:%S")
            return f"{tmid}-{path}-{time}"


# myapp/urls.py
from django.urls import path

from myapp import views
from django_thumbmark.views import DjTmLoginView

app_name = "django_thumbmark"  # must match THUMBMARK_NAMESPACE (default shown here)

urlpatterns = [
    path("tm/", views.MyDjTmScriptView.as_view(), name="tm"), # The URL name is "tm"
    path("login/", DjTmLoginView.as_view(), name="tmlogin"), # The URL name is "tmlogin"
]


# project/urls.py
from django.urls import include, path

urlpatterns = [
    # ...
    path("thumbmark/", include("myapp.urls")),
]

# project/views.py
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.views import View

from django_thumbmark.decorators import login_required_thumbmark

@login_required_thumbmark
def index(request):
    # Do the thing with your now logged-in user
    return HttpResponse(f"Hello, {request.user}")

### --- or, for a class-based view --- ###

@method_decorator(login_required_thumbmark, name="dispatch")
class IndexView(View):
    def get(self, request):
        # Do the thing with your now logged-in user
        return HttpResponse(f"Hello, {request.user}")
```

To modify the included HTML template, overwrite the ```django_thumbmark/login.html``` template within your app's templates directory.

```.
├── manage.py
├── project_root
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── requirements.txt
└── myapp
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── forms.py
    ├── migrations
    │   ├── 0001_initial.py
    ├── models.py
    ├── templates
    │   ├── django_thumbmark ## Create this directory
    │   │   └── login.html   ## Add this file
    │   └── myapp
    │       ├── index.html
    │       └── success.html
    ├── tests.py
    ├── urls.py
    └── views.py
```

The options with which this overwritten template file can be modified are shown in the Customization section below.

## Details

When a view uses this decorator
1) If the user is already authenticated, nothing happens.
2) If the user hasn't been authenticated they are redirected to a new login page named ```tmlogin```.  This login page contains the [ThumbmarkJS](https://github.com/thumbmarkjs/thumbmarkjs) script to generate a unique ID.
3) The unique ID is sent by Javascript via a POST request (with a CSRF token) to a second view named ```tm```.
4) The second view uses the unique ID to search for an existing User object.  A new User object is created if no object is found.
5) The user is logged in and associated with the request.
6) The user is redirected back to their original page.


## Customization

### New User First/Last Names

By default, if a new User object is created, the first and last names are set to "Test" and "User," respectively.  These can be modified by overwriting the ```DjTmScriptView.get_first_name()``` and ```DjTmScriptView.get_last_name()``` functions.

### New User Username

By default, if a new User object is created, the username is set to the unique ID generated by [ThumbmarkJS](https://github.com/thumbmarkjs/thumbmarkjs).  This can be modified by overwriting the ```DjTmScriptView.get_username()``` function.

### New User Object

By default, Django's ```get_or_create()``` function is used to find/create a User object directly, which is then logged in and associated with the request.  To modify how that user is discovered and/or created, you can overwrite the ```DjTmScriptView.get_user_object_()``` function.

### Custom Namespace

`login_required_thumbmark` and `DjTmLoginView` locate the `tm`/`tmlogin` views by URL namespace, not by path — this is what lets protected views live anywhere in your project regardless of which app or namespace they're in. By default that namespace is `django_thumbmark`, matching the `app_name` set in `django_thumbmark/urls.py`. If you `include()` those URLs under a different namespace (or provide your own `urls.py` with a different `app_name`, per the customization example above), set:

```python
# settings.py
THUMBMARK_NAMESPACE = "my-custom-namespace"
```

### JavaScript Disabled Output

By default, if the browser doesn't have JavaScript enabled, a static text string will be shown to the user instructing them to enable Javascript to use this site.  This text can be changed by overwriting the ```django_thumbprint/login.html``` template and modifying this block:

```html
{% block no-js-message %}Please, pretty please, won't you enable JavaScript?{% endblock %}
```

### ThumbmarkJS Source

By default the [ThumbmarkJS](https://github.com/thumbmarkjs/thumbmarkjs) library is loaded from cdn.jsdelivr.net, pinned to a specific released version.  This can be changed (to reference a static, self-hosted copy, or a newer/older pinned version) by overwriting the ```django_thumbprint/login.html``` template and modifying this block:

```html
{% block js-source %}<script src="https://cdn.jsdelivr.net/npm/@thumbmarkjs/thumbmarkjs@1.10.0/dist/thumbmark.umd.js"></script>{% endblock %}
```

> [!IMPORTANT]
> The URL is pinned to a specific ThumbmarkJS release on purpose. Loading an unpinned/`@latest` URL means an upstream release can change behavior (or break the API used in `js-script` below) without warning. If you override this block, pin a version here too.

### ThumbmarkJS Script

By default the [ThumbmarkJS](https://github.com/thumbmarkjs/thumbmarkjs) script is run using the `new ThumbmarkJS.Thumbmark().get()` pattern documented in that project's README.  The contents of that script can be changed by overwriting the ```django_thumbprint/login.html template``` and modifying this block:

```html
{% block js-script %}<script>console.log('This is my new script')</script>{% endblock %}
```

> [!IMPORTANT]
> Take extreme caution when modifying this template block to ensure dynamic values are properly escaped.

## Versioning

This package uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
TL;DR you are safe to use [compatible release version specifier](https://packaging.python.org/en/latest/specifications/version-specifiers/#compatible-release) `~=MAJOR.MINOR` in your `pyproject.toml` or `requirements.txt`.

## Release process

```python
python -m pip install build
python -m pip install twine
python -m build
python -m twine upload dist/*
```
