Full Code of guettli/django-htmx-fun for AI

main 07ac3ddd80cf cached
26 files
22.6 KB
6.1k tokens
29 symbols
1 requests
Download .txt
Repository: guettli/django-htmx-fun
Branch: main
Commit: 07ac3ddd80cf
Files: 26
Total size: 22.6 KB

Directory structure:
gitextract_v6ukcjns/

├── .gitignore
├── LICENSE
├── README.md
├── diary/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── models.py
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── conftest.py
│   │   └── test_views.py
│   ├── urls.py
│   ├── utils.py
│   └── views/
│       ├── __init__.py
│       ├── common.py
│       ├── note.py
│       └── start.py
├── mysite/
│   ├── __init__.py
│   ├── asgi.py
│   ├── manage.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── pytest.ini
└── setup.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
__pycache__
*.egg-info
*.sqlite3


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 Thomas Güttler

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.


================================================
FILE: README.md
================================================

# django-htmx-fun

A small Django application to advertise the fun [htmx](//htmx.org) can bring you.

It implements a Single-Page-Application for writing a diary.

The entries in the diary database get lazy loaded (endless scrolling) via [hx-trigger="revealed"](https://htmx.org/attributes/hx-trigger/)

# Why I love htmx?

If you look into the past then one thing is clear: stateless has won. Nobody starts a new project with [Corba](https://en.wikipedia.org/wiki/Common_Object_Request_Broker_Architecture)
these days. Stateless http is the winner.

I don't understand why JavaScript based frontend frameworks seem to be the only way for new projects.

I want the client/browser to be SSS (simple, stupid and stateless).

I need to validate my data on the server anyway. So why should I validate them on the client?

The Django Forms library has all you need to write database focused applications.

Sending HTML fragments over the wire keeps my application simple.

There is just one thing which is outdated (although it is still perfectly fine). The need
for a full page refresh after submitting a form.

I want html pages with several small forms and I want to load and submit each of them 
individually. This does not mean I want to write a Single-Page-Application. There
are more colors than black and white. 

For more about htmx see the homepage: [htmx.org](//htmx.org)

[HTMX: Frontend Revolution (Slides from DjangoCon 2021)](https://docs.google.com/presentation/d/12dgaBnUgl4cmEkiOhUJL5hsbGQ6hB5sslDuozmBjVUA/edit?usp=sharing)

Youtube video of the Talk [DjangoCon US 2021: HTMX, Frontend Revolution](https://www.youtube.com/watch?v=z0yPTv15Fjk)

## Install

If you want to install my small htmx demo application:

```
python3 -m venv django-htmx-fun-env
cd django-htmx-fun-env
. bin/activate
pip install -e git+https://github.com/guettli/django-htmx-fun.git#egg=django-htmx-fun
```

The source code is now in src/django-htmx-fun/

## Run Database Migrations

```
manage.py migrate
```

## Start Webserver
```
manage.py runserver
```

Diary: http://127.0.0.1:8000/

## Admin
```
manage.py createsuperuser

```
Admin: http://127.0.0.1:8000/admin

## No need for the POST/Redirect/GET pattern

If you are used to django's form handling, then you are used to the [POST/Redirect/GET Pattern](https://en.wikipedia.org/wiki/Post/Redirect/Get). This means after the client submitted a valid form, the server response has the http status 302 with a new location URL.

This is not needed if you submit a form via htmx and you just want to update one part of the whole page.

I use this pattern now:

Case 1: The http POST was successful, and data was changed. The server returns the status code 201 "Created". I use this even if data was changed, and not "created". I use this and not the usual 200 to simplify testing. I never ever want to confuse the http status of a successful htmx POST with the http status of an invalid traditional django http POST. The response contains the new HTML. No need for a redirect.

Case 2: The http POST was not successful, since the data in the form was not valid. Then my server code returns 422. 

Related question: [Which http status codes to use when processing http post?](https://stackoverflow.com/q/69773241/633961)

## Full Page (aka "client-side") Redirect

If you use htmx, then most http responses will contains html fragments which will get swapped into the current page.

But sometimes you want to do a traditional full page redirect. In the htmx docs it is called "client-side" redirect.

Then you need return a http response which has the http header "HX-Redirect" set to the URL of the new location. Docs: [Response Headers](https://htmx.org/docs/#response-headers)

A common mistake is the set the status code if this response to 302. But this will trigger a redirect inside htmx.

Here is some code to do a full page redirect with Django: [hx-target: swap html vs full page reload](https://stackoverflow.com/a/65569741/633961)

## Naming Pattern

Here is my personal naming pattern, which helps me to read the source more easily

**_page():** 

Function based view. 

`foo_page(request, ...)`. 

Returns a HttpResponse with a full page. 

URL: `/foo`

This servers only http GET. Updates (http POST) go to _hxpost URLs.

---

**_hx():**

Function based view.

`foo_hx(request, ...)`

This method should be called via HTTP-GET. Returns a HttpResponse which only contains a HTML fragment. 

URL: `/foo_hx`

---

**_hxpost():**

Function based view.

`foo_hxpost(request, ...)`

This methods should be called via HTTP-POST. Returns a HttpResponse which only 
contains a HTML fragment. 

URL: `/foo_hxpost`

It makes sense to use the [require_POST decorator](https://docs.djangoproject.com/en/dev/topics/http/decorators/#django.views.decorators.http.require_POST),if you have concerns that a GET request (where request.POST is empty) could accidently change data.

---

**_json():**

Function based view.

`foo_json(request, ...)`

This method returns a [JSONResponse](https://docs.djangoproject.com/en/dev/ref/request-response/#jsonresponse-objects).

URL: `/foo_json`

TODO: I am not happy with this yet, since you can't distinguish between a method
which returns a JSON data (dictionary), JSON string or JSONResponse.

---

**_html():**

Python method which returns a HTML SafeString. 

Usually created via [format_html()](https://docs.djangoproject.com/en/dev/ref/utils/#django.utils.html.format_html).

## Flat URL Namespace

Above naming pattern makes it very use to get to the corresponding code. 

Imagine you get a message from tool monitoring your servers. There is an exception at URL "/sunshine/123",
then I know the name of the method which handles this URL. The method is "sunshine_page()".

If you need several pages for a model, then you will not use "/sunshine/foo" and "/sunshine/bar", but instead "/sunshine_foo" and "/sunshine_bar".

## Opinionated Best Practices

I switched from Django class-based-views (CBV) to function-based-views (FBV). This simplifies things. 
One URL corresponds to one Python method. If an action requires two HTTP verbs (GET and POST), then I use **two URLs**. Posts
always go to hx-methods, not to URLs returning full pages.

I like it conditionless. I try to avoid to have too many "if" and "else".

I avoid to use `if request.method == 'POST'`. This means I don't handle different http verbs in one function based view. A function based view handles either GET xor a POST. URLs are cheap I create two URLs if I need a a readonly view and a view which does something.

I only use the http verbs GET and POST, although htmx can do http PUT, http PATCH, http DELETE, ...

I don't use the special http headers which get added by htmx. I avoid this (pseudo code): "if request is a htmx request, then ...".
Instead I create two endpoints: One which returns a full page, one which returns a fragment.

Goodbye formsets. I use several `<form>` tags in one page. This means I hardly use formsets. Some for the "prefix" of forms: Since
I don't put several Django form instances into one `<form>` tag, I don't need the prefix any more.


## Screenshot

![diary-django-htmx](docs/diary-django-htmx.png)

In devtools you can see the lazy loading of the endless scrolling

... All this is possible without writing a single line of JavaScript :-)


## Pull Requests are welcome

You have an idea how to improve this example? Great! Just do it, provide a pull request and I will merge it.

## Related

* [Güttli Django Tips](https://github.com/guettli/django-tips)
* [Güttli's opinionated Python Tips](https://github.com/guettli/python-tips)
* [Güttli working-out-loud](https://github.com/guettli/wol)
* [Güttli's Programming Guidelines (long)](https://github.com/guettli/programming-guidelines)



================================================
FILE: diary/__init__.py
================================================


================================================
FILE: diary/admin.py
================================================
from diary.models import Note
from django.contrib import admin


class NoteAdmin(admin.ModelAdmin):
    model = Note
    list_display = ['id', 'datetime', 'title', 'text']
    ordering = ['-datetime', '-id']


admin.site.register(Note, NoteAdmin)


================================================
FILE: diary/apps.py
================================================
from django.apps import AppConfig


class DiaryConfig(AppConfig):
    name = 'diary'


================================================
FILE: diary/migrations/0001_initial.py
================================================
# Generated by Django 3.1.5 on 2021-01-29 07:09

from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

    initial = True

    dependencies = []

    operations = [
        migrations.CreateModel(
            name='Note',
            fields=[
                (
                    'id',
                    models.AutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name='ID',
                    ),
                ),
                ('datetime', models.DateTimeField(default=django.utils.timezone.now)),
                ('title', models.CharField(default='', max_length=1024)),
                ('text', models.TextField(blank=True, default='')),
            ],
        ),
    ]


================================================
FILE: diary/migrations/__init__.py
================================================


================================================
FILE: diary/models.py
================================================
from django.db import models
from django.utils import timezone


class Note(models.Model):
    datetime = models.DateTimeField(default=timezone.now)
    title = models.CharField(max_length=1024, default='')
    text = models.TextField(default='', blank=True)

    def __str__(self):
        return 'Note {} {}'.format(self.datetime.strftime('%Y-%m-%d'), self.title)


================================================
FILE: diary/tests/__init__.py
================================================


================================================
FILE: diary/tests/conftest.py
================================================
import pytest
from diary.models import Note


@pytest.fixture()
def note(db):
    return Note.objects.create(title='My Title', text='my text')


@pytest.fixture()
def note2(note):
    return Note.objects.create(title='My second', text='my second text')


================================================
FILE: diary/tests/test_views.py
================================================
import pytest
from html_form_to_dict import html_form_to_dict

from diary.models import Note
from diary.utils import HttpResponseUnprocessableEntity, HttpResponseCreated
from diary.views.note import get_next_or_none


def test_get_next_or_none__last(note):
    assert get_next_or_none(note) is None


def test_get_next_or_none__next_exists(note2):
    assert get_next_or_none(note2).title == 'My Title'

@pytest.mark.django_db
def test_start_page__invalid(client):
    response = client.get('/')
    assert response.status_code == 200
    data = html_form_to_dict(response.content)
    assert list(data.keys()) == ['datetime', 'initial-datetime', 'title', 'text']
    data['text'] = 'my text'
    response = data.submit(client)
    assert response.status_code == HttpResponseUnprocessableEntity.status_code
    assert response.description == ('<ul class="errorlist"><li>title<ul class="errorlist"><li>This field is '
                                    'required.</li></ul></li></ul>')

@pytest.mark.django_db
def test_start_page__valid(client):
    response = client.get('/')
    assert response.status_code == 200
    data = html_form_to_dict(response.content)
    data['text'] = 'my text'
    data['title'] = 'my title'
    response = data.submit(client)
    assert isinstance(response, HttpResponseCreated)
    note = Note.objects.get(id=response.pk)
    assert note.title == 'my title'
    assert note.text == 'my text'


================================================
FILE: diary/urls.py
================================================
from diary.views.note import note_and_next_hx, create_note_hxpost
from diary.views.start import start_page
from django.urls import path

urlpatterns = [
    path('', start_page, name='start_page'),
    path('create_note_hxpost', create_note_hxpost, name='create_note_hxpost'),
    path('note_and_next_hx/<note_id>', note_and_next_hx, name='note_and_next_hx'),
]


================================================
FILE: diary/utils.py
================================================
from django.http import HttpResponse


class HttpResponseUnprocessableEntity(HttpResponse):
    description: str = ''

    def __init__(self, content=b'', description=None, **kwargs):
        assert description is not None, 'Please provide a description. For example form.errors'
        super().__init__(content, **kwargs)
        self.description = str(description)

    status_code = 422


class HttpResponseCreated(HttpResponse):
    pk: str = ''

    def __init__(self, content=b'', pk=None, **kwargs):
        assert pk is not None, 'Please provide a pk (primary key) if the object which got saved'
        super().__init__(content, **kwargs)
        self.pk = pk

    status_code = 201


================================================
FILE: diary/views/__init__.py
================================================


================================================
FILE: diary/views/common.py
================================================
from django.http import HttpResponse
from django.utils.html import format_html


def page(content):
    return HttpResponse(
        format_html(
            '''<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Diary</title>
    <style>
        input, textarea {{
            display: block;
        }}
    </style>
</head>

<body>
{content}
</body>

<script src="https://unpkg.com/htmx.org@1.4.1/dist/htmx.min.js" 
    integrity="sha384-1P2DfVFAJH2XsYTakfc6eNhTVtyio74YNA1tc6waJ0bO+IfaexSWXc2x62TgBcXe" 
    crossorigin="anonymous"></script>

</html>
''',
            content=content,
        )
    )


================================================
FILE: diary/views/note.py
================================================
from django.forms import ModelForm, Textarea
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.html import format_html
from django.views.decorators.http import require_POST

from diary.models import Note
from diary.utils import HttpResponseUnprocessableEntity, HttpResponseCreated


class NoteCreateForm(ModelForm):
    class Meta:
        fields = ['datetime', 'title', 'text']
        model = Note
        widgets = {
            'text': Textarea(attrs={'rows': 3}),
        }


def note_add_html():
    form = NoteCreateForm()
    return note_form_html(form)


def note_form_html(form):
    return format_html(
        '''
    <form hx-post="{url}" hx-swap="outerHTML">
     {form}
     <input type="submit">
    </form>''',
        url=reverse(create_note_hxpost),
        form=form,
    )


def note_html(note):
    return format_html(
        '''
    <h1>{date} {title}</h1>
    <p>{text}</p>
    ''',
        date=note.datetime.strftime('%d.%b'),
        title=note.title,
        text=note.text,
    )


def note_hx(request, note_id):
    note = get_object_or_404(Note, pk=note_id)
    return HttpResponse(note_html(note))


@require_POST
def create_note_hxpost(request):
    form = NoteCreateForm(request.POST)
    if form.is_valid():
        note = form.save()
        return HttpResponseCreated(format_html('{} {}', note_add_html(), note_html(note)), note.pk)
    return HttpResponseUnprocessableEntity(note_form_html(form), form.errors)


def note_and_next_html(note):
    next = get_next_or_none(note)
    if next:
        next_html = format_html(
            '<div hx-get="{}" hx-trigger="revealed" hx-swap="outerHTML">...</div>',
            reverse('note_and_next_hx', kwargs=dict(note_id=next.id)),
        )
    else:
        next_html = 'The End'
    return format_html(
        '{note_html} {next_html}', note_html=note_html(note), next_html=next_html
    )


def get_next_or_none(note):
    # SQLite does not store mircoseconds. Two entries added in one second can't be
    # distinguished with ".first()". Grrr ugly loop is needed.
    found = False
    for next in Note.objects.filter(datetime__lte=note.datetime).order_by(
            '-datetime', '-id'
    ):
        if found:
            return next
        if next == note:
            found = True


def note_and_next_hx(request, note_id):
    return HttpResponse(note_and_next_html(get_object_or_404(Note, pk=note_id)))


================================================
FILE: diary/views/start.py
================================================
from diary.models import Note
from diary.views.common import page
from diary.views.note import note_add_html, note_and_next_html
from django.utils.html import format_html


def start_page(request):
    return page(
        format_html(
            '''
     {note_add}
     {first_note}''',
            note_add=note_add_html(),
            first_note=first_note(),
        )
    )


def first_note():
    first = Note.objects.all().order_by('-datetime', '-id').first()
    if not first:
        return ''
    return note_and_next_html(first)


================================================
FILE: mysite/__init__.py
================================================


================================================
FILE: mysite/asgi.py
================================================
"""
ASGI config for mysite project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')

application = get_asgi_application()


================================================
FILE: mysite/manage.py
================================================
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()


================================================
FILE: mysite/settings.py
================================================
"""
Django settings for mysite project.

Generated by 'django-admin startproject' using Django 3.1.5.

For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '=)s+!bro%eng_*7pu#-b#ytioi8na&(d5l0!uw0ty@fdxm1b&q'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'diary',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'mysite.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'mysite.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'Europe/Berlin'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/

STATIC_URL = '/static/'

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'


================================================
FILE: mysite/urls.py
================================================
"""mysite URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('', include('diary.urls')),
    path('admin/', admin.site.urls),
]


================================================
FILE: mysite/wsgi.py
================================================
"""
WSGI config for mysite project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')

application = get_wsgi_application()


================================================
FILE: pytest.ini
================================================
[pytest]
DJANGO_SETTINGS_MODULE = mysite.settings
FAIL_INVALID_TEMPLATE_VARS = True
django_debug_mode = true


================================================
FILE: setup.py
================================================
import setuptools

with open("README.md", "r", encoding="utf-8") as fh:
    long_description = fh.read()

setuptools.setup(
    name="django-htmx-fun",
    version="0.0.1",
    author="Thomas Güttler",
    author_email="info.django-htmx-fun@thomas-guettler.de",
    description="A small Django application to advertise the fun htmx can bring you.",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://github.com/guettli/django-htmx-fun/",
    packages=setuptools.find_packages(),
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
    python_requires='>=3.6',
    install_requires=['Django', 'pytest-django', 'html_form_to_dict'],
    scripts=[
        'mysite/manage.py',
    ],
)
Download .txt
gitextract_v6ukcjns/

├── .gitignore
├── LICENSE
├── README.md
├── diary/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── models.py
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── conftest.py
│   │   └── test_views.py
│   ├── urls.py
│   ├── utils.py
│   └── views/
│       ├── __init__.py
│       ├── common.py
│       ├── note.py
│       └── start.py
├── mysite/
│   ├── __init__.py
│   ├── asgi.py
│   ├── manage.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── pytest.ini
└── setup.py
Download .txt
SYMBOL INDEX (29 symbols across 11 files)

FILE: diary/admin.py
  class NoteAdmin (line 5) | class NoteAdmin(admin.ModelAdmin):

FILE: diary/apps.py
  class DiaryConfig (line 4) | class DiaryConfig(AppConfig):

FILE: diary/migrations/0001_initial.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: diary/models.py
  class Note (line 5) | class Note(models.Model):
    method __str__ (line 10) | def __str__(self):

FILE: diary/tests/conftest.py
  function note (line 6) | def note(db):
  function note2 (line 11) | def note2(note):

FILE: diary/tests/test_views.py
  function test_get_next_or_none__last (line 9) | def test_get_next_or_none__last(note):
  function test_get_next_or_none__next_exists (line 13) | def test_get_next_or_none__next_exists(note2):
  function test_start_page__invalid (line 17) | def test_start_page__invalid(client):
  function test_start_page__valid (line 29) | def test_start_page__valid(client):

FILE: diary/utils.py
  class HttpResponseUnprocessableEntity (line 4) | class HttpResponseUnprocessableEntity(HttpResponse):
    method __init__ (line 7) | def __init__(self, content=b'', description=None, **kwargs):
  class HttpResponseCreated (line 15) | class HttpResponseCreated(HttpResponse):
    method __init__ (line 18) | def __init__(self, content=b'', pk=None, **kwargs):

FILE: diary/views/common.py
  function page (line 5) | def page(content):

FILE: diary/views/note.py
  class NoteCreateForm (line 12) | class NoteCreateForm(ModelForm):
    class Meta (line 13) | class Meta:
  function note_add_html (line 21) | def note_add_html():
  function note_form_html (line 26) | def note_form_html(form):
  function note_html (line 38) | def note_html(note):
  function note_hx (line 50) | def note_hx(request, note_id):
  function create_note_hxpost (line 56) | def create_note_hxpost(request):
  function note_and_next_html (line 64) | def note_and_next_html(note):
  function get_next_or_none (line 78) | def get_next_or_none(note):
  function note_and_next_hx (line 91) | def note_and_next_hx(request, note_id):

FILE: diary/views/start.py
  function start_page (line 7) | def start_page(request):
  function first_note (line 19) | def first_note():

FILE: mysite/manage.py
  function main (line 7) | def main():
Condensed preview — 26 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (25K chars).
[
  {
    "path": ".gitignore",
    "chars": 33,
    "preview": "__pycache__\n*.egg-info\n*.sqlite3\n"
  },
  {
    "path": "LICENSE",
    "chars": 1071,
    "preview": "MIT License\n\nCopyright (c) 2021 Thomas Güttler\n\nPermission is hereby granted, free of charge, to any person obtaining a "
  },
  {
    "path": "README.md",
    "chars": 7784,
    "preview": "\n# django-htmx-fun\n\nA small Django application to advertise the fun [htmx](//htmx.org) can bring you.\n\nIt implements a S"
  },
  {
    "path": "diary/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "diary/admin.py",
    "chars": 247,
    "preview": "from diary.models import Note\nfrom django.contrib import admin\n\n\nclass NoteAdmin(admin.ModelAdmin):\n    model = Note\n   "
  },
  {
    "path": "diary/apps.py",
    "chars": 85,
    "preview": "from django.apps import AppConfig\n\n\nclass DiaryConfig(AppConfig):\n    name = 'diary'\n"
  },
  {
    "path": "diary/migrations/0001_initial.py",
    "chars": 855,
    "preview": "# Generated by Django 3.1.5 on 2021-01-29 07:09\n\nfrom django.db import migrations, models\nimport django.utils.timezone\n\n"
  },
  {
    "path": "diary/migrations/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "diary/models.py",
    "chars": 366,
    "preview": "from django.db import models\nfrom django.utils import timezone\n\n\nclass Note(models.Model):\n    datetime = models.DateTim"
  },
  {
    "path": "diary/tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "diary/tests/conftest.py",
    "chars": 253,
    "preview": "import pytest\nfrom diary.models import Note\n\n\n@pytest.fixture()\ndef note(db):\n    return Note.objects.create(title='My T"
  },
  {
    "path": "diary/tests/test_views.py",
    "chars": 1425,
    "preview": "import pytest\nfrom html_form_to_dict import html_form_to_dict\n\nfrom diary.models import Note\nfrom diary.utils import Htt"
  },
  {
    "path": "diary/urls.py",
    "chars": 362,
    "preview": "from diary.views.note import note_and_next_hx, create_note_hxpost\nfrom diary.views.start import start_page\nfrom django.u"
  },
  {
    "path": "diary/utils.py",
    "chars": 693,
    "preview": "from django.http import HttpResponse\n\n\nclass HttpResponseUnprocessableEntity(HttpResponse):\n    description: str = ''\n\n "
  },
  {
    "path": "diary/views/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "diary/views/common.py",
    "chars": 693,
    "preview": "from django.http import HttpResponse\nfrom django.utils.html import format_html\n\n\ndef page(content):\n    return HttpRespo"
  },
  {
    "path": "diary/views/note.py",
    "chars": 2484,
    "preview": "from django.forms import ModelForm, Textarea\nfrom django.http import HttpResponse\nfrom django.shortcuts import get_objec"
  },
  {
    "path": "diary/views/start.py",
    "chars": 542,
    "preview": "from diary.models import Note\nfrom diary.views.common import page\nfrom diary.views.note import note_add_html, note_and_n"
  },
  {
    "path": "mysite/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "mysite/asgi.py",
    "chars": 389,
    "preview": "\"\"\"\nASGI config for mysite project.\n\nIt exposes the ASGI callable as a module-level variable named ``application``.\n\nFor"
  },
  {
    "path": "mysite/manage.py",
    "chars": 662,
    "preview": "#!/usr/bin/env python\n\"\"\"Django's command-line utility for administrative tasks.\"\"\"\nimport os\nimport sys\n\n\ndef main():\n "
  },
  {
    "path": "mysite/settings.py",
    "chars": 3090,
    "preview": "\"\"\"\nDjango settings for mysite project.\n\nGenerated by 'django-admin startproject' using Django 3.1.5.\n\nFor more informat"
  },
  {
    "path": "mysite/urls.py",
    "chars": 794,
    "preview": "\"\"\"mysite URL Configuration\n\nThe `urlpatterns` list routes URLs to views. For more information please see:\n    https://d"
  },
  {
    "path": "mysite/wsgi.py",
    "chars": 389,
    "preview": "\"\"\"\nWSGI config for mysite project.\n\nIt exposes the WSGI callable as a module-level variable named ``application``.\n\nFor"
  },
  {
    "path": "pytest.ini",
    "chars": 109,
    "preview": "[pytest]\nDJANGO_SETTINGS_MODULE = mysite.settings\nFAIL_INVALID_TEMPLATE_VARS = True\ndjango_debug_mode = true\n"
  },
  {
    "path": "setup.py",
    "chars": 854,
    "preview": "import setuptools\n\nwith open(\"README.md\", \"r\", encoding=\"utf-8\") as fh:\n    long_description = fh.read()\n\nsetuptools.set"
  }
]

About this extraction

This page contains the full source code of the guettli/django-htmx-fun GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 26 files (22.6 KB), approximately 6.1k tokens, and a symbol index with 29 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!