[
  {
    "path": ".gitignore",
    "content": "__pycache__\n*.egg-info\n*.sqlite3\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Thomas Güttler\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "\n# django-htmx-fun\n\nA small Django application to advertise the fun [htmx](//htmx.org) can bring you.\n\nIt implements a Single-Page-Application for writing a diary.\n\nThe entries in the diary database get lazy loaded (endless scrolling) via [hx-trigger=\"revealed\"](https://htmx.org/attributes/hx-trigger/)\n\n# Why I love htmx?\n\nIf 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)\nthese days. Stateless http is the winner.\n\nI don't understand why JavaScript based frontend frameworks seem to be the only way for new projects.\n\nI want the client/browser to be SSS (simple, stupid and stateless).\n\nI need to validate my data on the server anyway. So why should I validate them on the client?\n\nThe Django Forms library has all you need to write database focused applications.\n\nSending HTML fragments over the wire keeps my application simple.\n\nThere is just one thing which is outdated (although it is still perfectly fine). The need\nfor a full page refresh after submitting a form.\n\nI want html pages with several small forms and I want to load and submit each of them \nindividually. This does not mean I want to write a Single-Page-Application. There\nare more colors than black and white. \n\nFor more about htmx see the homepage: [htmx.org](//htmx.org)\n\n[HTMX: Frontend Revolution (Slides from DjangoCon 2021)](https://docs.google.com/presentation/d/12dgaBnUgl4cmEkiOhUJL5hsbGQ6hB5sslDuozmBjVUA/edit?usp=sharing)\n\nYoutube video of the Talk [DjangoCon US 2021: HTMX, Frontend Revolution](https://www.youtube.com/watch?v=z0yPTv15Fjk)\n\n## Install\n\nIf you want to install my small htmx demo application:\n\n```\npython3 -m venv django-htmx-fun-env\ncd django-htmx-fun-env\n. bin/activate\npip install -e git+https://github.com/guettli/django-htmx-fun.git#egg=django-htmx-fun\n```\n\nThe source code is now in src/django-htmx-fun/\n\n## Run Database Migrations\n\n```\nmanage.py migrate\n```\n\n## Start Webserver\n```\nmanage.py runserver\n```\n\nDiary: http://127.0.0.1:8000/\n\n## Admin\n```\nmanage.py createsuperuser\n\n```\nAdmin: http://127.0.0.1:8000/admin\n\n## No need for the POST/Redirect/GET pattern\n\nIf 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.\n\nThis is not needed if you submit a form via htmx and you just want to update one part of the whole page.\n\nI use this pattern now:\n\nCase 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.\n\nCase 2: The http POST was not successful, since the data in the form was not valid. Then my server code returns 422. \n\nRelated question: [Which http status codes to use when processing http post?](https://stackoverflow.com/q/69773241/633961)\n\n## Full Page (aka \"client-side\") Redirect\n\nIf you use htmx, then most http responses will contains html fragments which will get swapped into the current page.\n\nBut sometimes you want to do a traditional full page redirect. In the htmx docs it is called \"client-side\" redirect.\n\nThen 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)\n\nA common mistake is the set the status code if this response to 302. But this will trigger a redirect inside htmx.\n\nHere 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)\n\n## Naming Pattern\n\nHere is my personal naming pattern, which helps me to read the source more easily\n\n**_page():** \n\nFunction based view. \n\n`foo_page(request, ...)`. \n\nReturns a HttpResponse with a full page. \n\nURL: `/foo`\n\nThis servers only http GET. Updates (http POST) go to _hxpost URLs.\n\n---\n\n**_hx():**\n\nFunction based view.\n\n`foo_hx(request, ...)`\n\nThis method should be called via HTTP-GET. Returns a HttpResponse which only contains a HTML fragment. \n\nURL: `/foo_hx`\n\n---\n\n**_hxpost():**\n\nFunction based view.\n\n`foo_hxpost(request, ...)`\n\nThis methods should be called via HTTP-POST. Returns a HttpResponse which only \ncontains a HTML fragment. \n\nURL: `/foo_hxpost`\n\nIt 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.\n\n---\n\n**_json():**\n\nFunction based view.\n\n`foo_json(request, ...)`\n\nThis method returns a [JSONResponse](https://docs.djangoproject.com/en/dev/ref/request-response/#jsonresponse-objects).\n\nURL: `/foo_json`\n\nTODO: I am not happy with this yet, since you can't distinguish between a method\nwhich returns a JSON data (dictionary), JSON string or JSONResponse.\n\n---\n\n**_html():**\n\nPython method which returns a HTML SafeString. \n\nUsually created via [format_html()](https://docs.djangoproject.com/en/dev/ref/utils/#django.utils.html.format_html).\n\n## Flat URL Namespace\n\nAbove naming pattern makes it very use to get to the corresponding code. \n\nImagine you get a message from tool monitoring your servers. There is an exception at URL \"/sunshine/123\",\nthen I know the name of the method which handles this URL. The method is \"sunshine_page()\".\n\nIf you need several pages for a model, then you will not use \"/sunshine/foo\" and \"/sunshine/bar\", but instead \"/sunshine_foo\" and \"/sunshine_bar\".\n\n## Opinionated Best Practices\n\nI switched from Django class-based-views (CBV) to function-based-views (FBV). This simplifies things. \nOne URL corresponds to one Python method. If an action requires two HTTP verbs (GET and POST), then I use **two URLs**. Posts\nalways go to hx-methods, not to URLs returning full pages.\n\nI like it conditionless. I try to avoid to have too many \"if\" and \"else\".\n\nI 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.\n\nI only use the http verbs GET and POST, although htmx can do http PUT, http PATCH, http DELETE, ...\n\nI don't use the special http headers which get added by htmx. I avoid this (pseudo code): \"if request is a htmx request, then ...\".\nInstead I create two endpoints: One which returns a full page, one which returns a fragment.\n\nGoodbye formsets. I use several `<form>` tags in one page. This means I hardly use formsets. Some for the \"prefix\" of forms: Since\nI don't put several Django form instances into one `<form>` tag, I don't need the prefix any more.\n\n\n## Screenshot\n\n![diary-django-htmx](docs/diary-django-htmx.png)\n\nIn devtools you can see the lazy loading of the endless scrolling\n\n... All this is possible without writing a single line of JavaScript :-)\n\n\n## Pull Requests are welcome\n\nYou have an idea how to improve this example? Great! Just do it, provide a pull request and I will merge it.\n\n## Related\n\n* [Güttli Django Tips](https://github.com/guettli/django-tips)\n* [Güttli's opinionated Python Tips](https://github.com/guettli/python-tips)\n* [Güttli working-out-loud](https://github.com/guettli/wol)\n* [Güttli's Programming Guidelines (long)](https://github.com/guettli/programming-guidelines)\n\n"
  },
  {
    "path": "diary/__init__.py",
    "content": ""
  },
  {
    "path": "diary/admin.py",
    "content": "from diary.models import Note\nfrom django.contrib import admin\n\n\nclass NoteAdmin(admin.ModelAdmin):\n    model = Note\n    list_display = ['id', 'datetime', 'title', 'text']\n    ordering = ['-datetime', '-id']\n\n\nadmin.site.register(Note, NoteAdmin)\n"
  },
  {
    "path": "diary/apps.py",
    "content": "from django.apps import AppConfig\n\n\nclass DiaryConfig(AppConfig):\n    name = 'diary'\n"
  },
  {
    "path": "diary/migrations/0001_initial.py",
    "content": "# 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\nclass Migration(migrations.Migration):\n\n    initial = True\n\n    dependencies = []\n\n    operations = [\n        migrations.CreateModel(\n            name='Note',\n            fields=[\n                (\n                    'id',\n                    models.AutoField(\n                        auto_created=True,\n                        primary_key=True,\n                        serialize=False,\n                        verbose_name='ID',\n                    ),\n                ),\n                ('datetime', models.DateTimeField(default=django.utils.timezone.now)),\n                ('title', models.CharField(default='', max_length=1024)),\n                ('text', models.TextField(blank=True, default='')),\n            ],\n        ),\n    ]\n"
  },
  {
    "path": "diary/migrations/__init__.py",
    "content": ""
  },
  {
    "path": "diary/models.py",
    "content": "from django.db import models\nfrom django.utils import timezone\n\n\nclass Note(models.Model):\n    datetime = models.DateTimeField(default=timezone.now)\n    title = models.CharField(max_length=1024, default='')\n    text = models.TextField(default='', blank=True)\n\n    def __str__(self):\n        return 'Note {} {}'.format(self.datetime.strftime('%Y-%m-%d'), self.title)\n"
  },
  {
    "path": "diary/tests/__init__.py",
    "content": ""
  },
  {
    "path": "diary/tests/conftest.py",
    "content": "import pytest\nfrom diary.models import Note\n\n\n@pytest.fixture()\ndef note(db):\n    return Note.objects.create(title='My Title', text='my text')\n\n\n@pytest.fixture()\ndef note2(note):\n    return Note.objects.create(title='My second', text='my second text')\n"
  },
  {
    "path": "diary/tests/test_views.py",
    "content": "import pytest\nfrom html_form_to_dict import html_form_to_dict\n\nfrom diary.models import Note\nfrom diary.utils import HttpResponseUnprocessableEntity, HttpResponseCreated\nfrom diary.views.note import get_next_or_none\n\n\ndef test_get_next_or_none__last(note):\n    assert get_next_or_none(note) is None\n\n\ndef test_get_next_or_none__next_exists(note2):\n    assert get_next_or_none(note2).title == 'My Title'\n\n@pytest.mark.django_db\ndef test_start_page__invalid(client):\n    response = client.get('/')\n    assert response.status_code == 200\n    data = html_form_to_dict(response.content)\n    assert list(data.keys()) == ['datetime', 'initial-datetime', 'title', 'text']\n    data['text'] = 'my text'\n    response = data.submit(client)\n    assert response.status_code == HttpResponseUnprocessableEntity.status_code\n    assert response.description == ('<ul class=\"errorlist\"><li>title<ul class=\"errorlist\"><li>This field is '\n                                    'required.</li></ul></li></ul>')\n\n@pytest.mark.django_db\ndef test_start_page__valid(client):\n    response = client.get('/')\n    assert response.status_code == 200\n    data = html_form_to_dict(response.content)\n    data['text'] = 'my text'\n    data['title'] = 'my title'\n    response = data.submit(client)\n    assert isinstance(response, HttpResponseCreated)\n    note = Note.objects.get(id=response.pk)\n    assert note.title == 'my title'\n    assert note.text == 'my text'\n"
  },
  {
    "path": "diary/urls.py",
    "content": "from diary.views.note import note_and_next_hx, create_note_hxpost\nfrom diary.views.start import start_page\nfrom django.urls import path\n\nurlpatterns = [\n    path('', start_page, name='start_page'),\n    path('create_note_hxpost', create_note_hxpost, name='create_note_hxpost'),\n    path('note_and_next_hx/<note_id>', note_and_next_hx, name='note_and_next_hx'),\n]\n"
  },
  {
    "path": "diary/utils.py",
    "content": "from django.http import HttpResponse\n\n\nclass HttpResponseUnprocessableEntity(HttpResponse):\n    description: str = ''\n\n    def __init__(self, content=b'', description=None, **kwargs):\n        assert description is not None, 'Please provide a description. For example form.errors'\n        super().__init__(content, **kwargs)\n        self.description = str(description)\n\n    status_code = 422\n\n\nclass HttpResponseCreated(HttpResponse):\n    pk: str = ''\n\n    def __init__(self, content=b'', pk=None, **kwargs):\n        assert pk is not None, 'Please provide a pk (primary key) if the object which got saved'\n        super().__init__(content, **kwargs)\n        self.pk = pk\n\n    status_code = 201\n"
  },
  {
    "path": "diary/views/__init__.py",
    "content": ""
  },
  {
    "path": "diary/views/common.py",
    "content": "from django.http import HttpResponse\nfrom django.utils.html import format_html\n\n\ndef page(content):\n    return HttpResponse(\n        format_html(\n            '''<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>Diary</title>\n    <style>\n        input, textarea {{\n            display: block;\n        }}\n    </style>\n</head>\n\n<body>\n{content}\n</body>\n\n<script src=\"https://unpkg.com/htmx.org@1.4.1/dist/htmx.min.js\" \n    integrity=\"sha384-1P2DfVFAJH2XsYTakfc6eNhTVtyio74YNA1tc6waJ0bO+IfaexSWXc2x62TgBcXe\" \n    crossorigin=\"anonymous\"></script>\n\n</html>\n''',\n            content=content,\n        )\n    )\n"
  },
  {
    "path": "diary/views/note.py",
    "content": "from django.forms import ModelForm, Textarea\nfrom django.http import HttpResponse\nfrom django.shortcuts import get_object_or_404\nfrom django.urls import reverse\nfrom django.utils.html import format_html\nfrom django.views.decorators.http import require_POST\n\nfrom diary.models import Note\nfrom diary.utils import HttpResponseUnprocessableEntity, HttpResponseCreated\n\n\nclass NoteCreateForm(ModelForm):\n    class Meta:\n        fields = ['datetime', 'title', 'text']\n        model = Note\n        widgets = {\n            'text': Textarea(attrs={'rows': 3}),\n        }\n\n\ndef note_add_html():\n    form = NoteCreateForm()\n    return note_form_html(form)\n\n\ndef note_form_html(form):\n    return format_html(\n        '''\n    <form hx-post=\"{url}\" hx-swap=\"outerHTML\">\n     {form}\n     <input type=\"submit\">\n    </form>''',\n        url=reverse(create_note_hxpost),\n        form=form,\n    )\n\n\ndef note_html(note):\n    return format_html(\n        '''\n    <h1>{date} {title}</h1>\n    <p>{text}</p>\n    ''',\n        date=note.datetime.strftime('%d.%b'),\n        title=note.title,\n        text=note.text,\n    )\n\n\ndef note_hx(request, note_id):\n    note = get_object_or_404(Note, pk=note_id)\n    return HttpResponse(note_html(note))\n\n\n@require_POST\ndef create_note_hxpost(request):\n    form = NoteCreateForm(request.POST)\n    if form.is_valid():\n        note = form.save()\n        return HttpResponseCreated(format_html('{} {}', note_add_html(), note_html(note)), note.pk)\n    return HttpResponseUnprocessableEntity(note_form_html(form), form.errors)\n\n\ndef note_and_next_html(note):\n    next = get_next_or_none(note)\n    if next:\n        next_html = format_html(\n            '<div hx-get=\"{}\" hx-trigger=\"revealed\" hx-swap=\"outerHTML\">...</div>',\n            reverse('note_and_next_hx', kwargs=dict(note_id=next.id)),\n        )\n    else:\n        next_html = 'The End'\n    return format_html(\n        '{note_html} {next_html}', note_html=note_html(note), next_html=next_html\n    )\n\n\ndef get_next_or_none(note):\n    # SQLite does not store mircoseconds. Two entries added in one second can't be\n    # distinguished with \".first()\". Grrr ugly loop is needed.\n    found = False\n    for next in Note.objects.filter(datetime__lte=note.datetime).order_by(\n            '-datetime', '-id'\n    ):\n        if found:\n            return next\n        if next == note:\n            found = True\n\n\ndef note_and_next_hx(request, note_id):\n    return HttpResponse(note_and_next_html(get_object_or_404(Note, pk=note_id)))\n"
  },
  {
    "path": "diary/views/start.py",
    "content": "from diary.models import Note\nfrom diary.views.common import page\nfrom diary.views.note import note_add_html, note_and_next_html\nfrom django.utils.html import format_html\n\n\ndef start_page(request):\n    return page(\n        format_html(\n            '''\n     {note_add}\n     {first_note}''',\n            note_add=note_add_html(),\n            first_note=first_note(),\n        )\n    )\n\n\ndef first_note():\n    first = Note.objects.all().order_by('-datetime', '-id').first()\n    if not first:\n        return ''\n    return note_and_next_html(first)\n"
  },
  {
    "path": "mysite/__init__.py",
    "content": ""
  },
  {
    "path": "mysite/asgi.py",
    "content": "\"\"\"\nASGI config for mysite project.\n\nIt exposes the ASGI callable as a module-level variable named ``application``.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/3.1/howto/deployment/asgi/\n\"\"\"\n\nimport os\n\nfrom django.core.asgi import get_asgi_application\n\nos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')\n\napplication = get_asgi_application()\n"
  },
  {
    "path": "mysite/manage.py",
    "content": "#!/usr/bin/env python\n\"\"\"Django's command-line utility for administrative tasks.\"\"\"\nimport os\nimport sys\n\n\ndef main():\n    \"\"\"Run administrative tasks.\"\"\"\n    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')\n    try:\n        from django.core.management import execute_from_command_line\n    except ImportError as exc:\n        raise ImportError(\n            \"Couldn't import Django. Are you sure it's installed and \"\n            \"available on your PYTHONPATH environment variable? Did you \"\n            \"forget to activate a virtual environment?\"\n        ) from exc\n    execute_from_command_line(sys.argv)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "mysite/settings.py",
    "content": "\"\"\"\nDjango settings for mysite project.\n\nGenerated by 'django-admin startproject' using Django 3.1.5.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/3.1/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/3.1/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = '=)s+!bro%eng_*7pu#-b#ytioi8na&(d5l0!uw0ty@fdxm1b&q'\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = True\n\nALLOWED_HOSTS = []\n\n\n# Application definition\n\nINSTALLED_APPS = [\n    'django.contrib.admin',\n    'django.contrib.auth',\n    'django.contrib.contenttypes',\n    'django.contrib.sessions',\n    'django.contrib.messages',\n    'django.contrib.staticfiles',\n    'diary',\n]\n\nMIDDLEWARE = [\n    'django.middleware.security.SecurityMiddleware',\n    'django.contrib.sessions.middleware.SessionMiddleware',\n    'django.middleware.common.CommonMiddleware',\n    'django.contrib.auth.middleware.AuthenticationMiddleware',\n    'django.contrib.messages.middleware.MessageMiddleware',\n    'django.middleware.clickjacking.XFrameOptionsMiddleware',\n]\n\nROOT_URLCONF = 'mysite.urls'\n\nTEMPLATES = [\n    {\n        'BACKEND': 'django.template.backends.django.DjangoTemplates',\n        'DIRS': [],\n        'APP_DIRS': True,\n        'OPTIONS': {\n            'context_processors': [\n                'django.template.context_processors.debug',\n                'django.template.context_processors.request',\n                'django.contrib.auth.context_processors.auth',\n                'django.contrib.messages.context_processors.messages',\n            ],\n        },\n    },\n]\n\nWSGI_APPLICATION = 'mysite.wsgi.application'\n\n\n# Database\n# https://docs.djangoproject.com/en/3.1/ref/settings/#databases\n\nDATABASES = {\n    'default': {\n        'ENGINE': 'django.db.backends.sqlite3',\n        'NAME': BASE_DIR / 'db.sqlite3',\n    }\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n    {\n        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',\n    },\n    {\n        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',\n    },\n    {\n        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',\n    },\n    {\n        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',\n    },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/3.1/topics/i18n/\n\nLANGUAGE_CODE = 'en-us'\n\nTIME_ZONE = 'Europe/Berlin'\n\nUSE_I18N = True\n\nUSE_L10N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/3.1/howto/static-files/\n\nSTATIC_URL = '/static/'\n\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n"
  },
  {
    "path": "mysite/urls.py",
    "content": "\"\"\"mysite URL Configuration\n\nThe `urlpatterns` list routes URLs to views. For more information please see:\n    https://docs.djangoproject.com/en/3.1/topics/http/urls/\nExamples:\nFunction views\n    1. Add an import:  from my_app import views\n    2. Add a URL to urlpatterns:  path('', views.home, name='home')\nClass-based views\n    1. Add an import:  from other_app.views import Home\n    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')\nIncluding another URLconf\n    1. Import the include() function: from django.urls import include, path\n    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))\n\"\"\"\nfrom django.contrib import admin\nfrom django.urls import path, include\n\nurlpatterns = [\n    path('', include('diary.urls')),\n    path('admin/', admin.site.urls),\n]\n"
  },
  {
    "path": "mysite/wsgi.py",
    "content": "\"\"\"\nWSGI config for mysite project.\n\nIt exposes the WSGI callable as a module-level variable named ``application``.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/\n\"\"\"\n\nimport os\n\nfrom django.core.wsgi import get_wsgi_application\n\nos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')\n\napplication = get_wsgi_application()\n"
  },
  {
    "path": "pytest.ini",
    "content": "[pytest]\nDJANGO_SETTINGS_MODULE = mysite.settings\nFAIL_INVALID_TEMPLATE_VARS = True\ndjango_debug_mode = true\n"
  },
  {
    "path": "setup.py",
    "content": "import setuptools\n\nwith open(\"README.md\", \"r\", encoding=\"utf-8\") as fh:\n    long_description = fh.read()\n\nsetuptools.setup(\n    name=\"django-htmx-fun\",\n    version=\"0.0.1\",\n    author=\"Thomas Güttler\",\n    author_email=\"info.django-htmx-fun@thomas-guettler.de\",\n    description=\"A small Django application to advertise the fun htmx can bring you.\",\n    long_description=long_description,\n    long_description_content_type=\"text/markdown\",\n    url=\"https://github.com/guettli/django-htmx-fun/\",\n    packages=setuptools.find_packages(),\n    classifiers=[\n        \"Programming Language :: Python :: 3\",\n        \"License :: OSI Approved :: MIT License\",\n        \"Operating System :: OS Independent\",\n    ],\n    python_requires='>=3.6',\n    install_requires=['Django', 'pytest-django', 'html_form_to_dict'],\n    scripts=[\n        'mysite/manage.py',\n    ],\n)\n"
  }
]