Skip to content

Commit 0f00dc4

Browse files
authored
Add a Positron bootstrap. (#3114)
1 parent 98ef5b5 commit 0f00dc4

23 files changed

+764
-63
lines changed

.github/workflows/ci.yml

+52
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ jobs:
6363
- "gtk"
6464
- "iOS"
6565
- "toga"
66+
- "positron"
6667
- "travertino"
6768
- "textual"
6869
- "web"
@@ -433,3 +434,54 @@ jobs:
433434
# - name: Setup tmate session
434435
# uses: mxschmitt/action-tmate@v3
435436
# if: failure()
437+
438+
bootstraps:
439+
name: "Bootstrap"
440+
needs: [ package ]
441+
runs-on: "ubuntu-24.04"
442+
strategy:
443+
fail-fast: false
444+
matrix:
445+
bootstrap:
446+
- "Positron (Django)"
447+
- "Positron (Static)"
448+
- "Positron (Site-specific)"
449+
include:
450+
- bootstrap: "Positron (Django)"
451+
new-options: '-Q "bootstrap=Toga Positron (Django server)"'
452+
453+
- bootstrap: "Positron (Static)"
454+
new-options: '-Q "bootstrap=Toga Positron (Static server)"'
455+
456+
- bootstrap: "Positron (Site-specific)"
457+
new-options: '-Q "bootstrap=Toga Positron (Site-specific browser)" -Q "site_url=https://github.com/"'
458+
459+
steps:
460+
- name: Checkout
461+
uses: actions/[email protected]
462+
with:
463+
fetch-depth: 0
464+
465+
- name: Set up Python
466+
uses: actions/[email protected]
467+
with:
468+
python-version: "3.12"
469+
470+
- name: Get Packages
471+
uses: actions/[email protected]
472+
with:
473+
pattern: ${{ format('{0}-*', needs.package.outputs.artifact-basename) }}
474+
merge-multiple: true
475+
path: dist
476+
477+
- name: Test Bootstrap
478+
run: |
479+
mkdir bootstrap-test
480+
cd bootstrap-test
481+
python3 -m venv venv
482+
source venv/bin/activate
483+
pip install -U pip
484+
pip install ../dist/toga_positron-*.whl
485+
echo
486+
echo "===== Create Briefcase project with ${{ matrix.bootstrap }} bootstrap ====="
487+
briefcase new --no-input -Q "formal_name=Hello Bootstrap" ${{ matrix.new-options }}

.github/workflows/publish.yml

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jobs:
2323
- "toga_demo"
2424
- "toga_dummy"
2525
- "toga_gtk"
26+
- "toga_positron"
2627
- "toga_textual"
2728
- "toga_iOS"
2829
- "toga_web"

.github/workflows/release.yml

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ jobs:
6767
- "toga_dummy"
6868
- "toga_gtk"
6969
- "toga_iOS"
70+
- "toga_positron"
7071
- "toga_textual"
7172
- "toga_web"
7273
- "toga_winforms"

changes/3114.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A Briefcase bootstrap for generating Positron apps (i.e., apps that are a web view in a native wrapper) was added.

examples/positron-django/README.rst

+19-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ To set up a development environment::
1818

1919
To run Django management commands::
2020

21-
PYTHONPATH=src python src/webapp/manage.py
21+
(venv) PYTHONPATH=src python src/manage.py
2222

2323
To run in development mode::
2424

@@ -27,3 +27,21 @@ To run in development mode::
2727
To run as a packaged app::
2828

2929
(venv) $ briefcase run
30+
31+
The Django app will run on a SQLite3 database, stored in the user's data directory (the
32+
location of this directory is platform specific). This database file will be created if
33+
it doesn't exist, and migrations will be run on every app start.
34+
35+
If you need to start the database with some initial content (e.g., an initial user
36+
login) you can use ``manage.py`` to create an initial database file. If there is a
37+
``db.sqlite3`` in the ``src/positron/resources`` folder when the app starts, and the
38+
user doesn't already have a ``db.sqlit3`` file in their app data folder, the initial
39+
database file will be copied into the user's data folder as a starting point.
40+
41+
To create an initial database, use ``manage.py`` - e.g.,:
42+
43+
(venv) PYTHONPATH=src python src/manage.py migrate
44+
(venv) PYTHONPATH=src python src/manage.py createsuperuser
45+
46+
This will create an initial ``db.sqlite3`` file with a superuser account. All users of
47+
the app will have this superuser account in their database.

examples/positron-django/pyproject.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ author_email = "[email protected]"
1414
formal_name = "Positron"
1515
description = "Electron, but in Python"
1616
icon = "src/positron/resources/positron"
17-
sources = ["src/positron", "src/webapp"]
17+
sources = ["src/positron"]
1818
requires = [
1919
"../../travertino",
2020
"../../core",
21-
"django~=4.1",
21+
"django~=5.1",
2222
]
2323

2424

examples/positron-django/src/webapp/manage.py examples/positron-django/src/manage.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
def main():
88
"""Run administrative tasks."""
9-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webapp.settings")
9+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "positron.settings")
1010
try:
1111
from django.core.management import execute_from_command_line
1212
except ImportError as exc:

examples/positron-django/src/positron/app.py

+32-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import asyncio
12
import os
3+
import shutil
24
import socketserver
3-
from threading import Event, Thread
5+
from threading import Thread
46
from wsgiref.simple_server import WSGIServer
57

68
import django
9+
from django.core import management as django_manage
710
from django.core.handlers.wsgi import WSGIHandler
811
from django.core.servers.basehttp import WSGIRequestHandler
912

@@ -16,19 +19,36 @@ class ThreadedWSGIServer(socketserver.ThreadingMixIn, WSGIServer):
1619

1720
class Positron(toga.App):
1821
def web_server(self):
22+
print("Configuring settings...")
23+
os.environ["DJANGO_SETTINGS_MODULE"] = "positron.settings"
24+
django.setup(set_prefix=False)
25+
26+
self.paths.data.mkdir(exist_ok=True)
27+
user_db = self.paths.data / "db.sqlite3"
28+
if user_db.exists():
29+
print("User already has a database.")
30+
else:
31+
template_db = self.paths.app / "resources" / "db.sqlite3"
32+
if template_db.exists():
33+
print("Copying initial database...")
34+
shutil.copy(template_db, user_db)
35+
else:
36+
print("No initial database.")
37+
38+
print("Applying database migrations...")
39+
django_manage.call_command("migrate")
40+
1941
print("Starting server...")
2042
# Use port 0 to let the server select an available port.
2143
self._httpd = ThreadedWSGIServer(("127.0.0.1", 0), WSGIRequestHandler)
2244
self._httpd.daemon_threads = True
2345

24-
os.environ["DJANGO_SETTINGS_MODULE"] = "webapp.settings"
25-
django.setup(set_prefix=False)
2646
wsgi_handler = WSGIHandler()
2747
self._httpd.set_app(wsgi_handler)
2848

2949
# The server is now listening, but connections will block until
3050
# serve_forever is run.
31-
self.server_exists.set()
51+
self.loop.call_soon_threadsafe(self.server_exists.set_result, "ready")
3252
self._httpd.serve_forever()
3353

3454
def cleanup(self, app, **kwargs):
@@ -37,7 +57,7 @@ def cleanup(self, app, **kwargs):
3757
return True
3858

3959
def startup(self):
40-
self.server_exists = Event()
60+
self.server_exists = asyncio.Future()
4161

4262
self.web_view = toga.WebView()
4363

@@ -46,12 +66,15 @@ def startup(self):
4666

4767
self.on_exit = self.cleanup
4868

49-
self.server_exists.wait()
50-
host, port = self._httpd.socket.getsockname()
51-
self.web_view.url = f"http://{host}:{port}/"
52-
5369
self.main_window = toga.MainWindow()
5470
self.main_window.content = self.web_view
71+
72+
async def on_running(self):
73+
await self.server_exists
74+
75+
host, port = self._httpd.socket.getsockname()
76+
self.web_view.url = f"http://{host}:{port}/admin"
77+
5578
self.main_window.show()
5679

5780

examples/positron-django/src/webapp/settings.py examples/positron-django/src/positron/settings.py

+23-28
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
1-
"""Django settings for webapp project.
1+
"""
2+
Django settings for positron project.
23
3-
Generated by 'django-admin startproject' using Django 4.1.1.
4+
Generated by "django-admin startproject" using Django 5.1.5.
45
5-
For more information on this file, see:
6-
- https://docs.djangoproject.com/en/4.1/topics/settings/
6+
For more information on this file, see
7+
https://docs.djangoproject.com/en/5.1/topics/settings/
78
8-
For the full list of settings and their values, see:
9-
- https://docs.djangoproject.com/en/4.1/ref/settings/
9+
For the full list of settings and their values, see
10+
https://docs.djangoproject.com/en/5.1/ref/settings/
1011
"""
1112

1213
from pathlib import Path
1314

14-
# Build paths inside the project like this: BASE_DIR / 'subdir'.
15-
BASE_DIR = Path(__file__).resolve().parent.parent
16-
15+
from toga import App as TogaApp
1716

18-
# Quick-start development settings - unsuitable for production
19-
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
17+
BASE_PATH = Path(__file__).parent / "resources"
2018

21-
# SECURITY WARNING: keep the secret key used in production secret!
22-
SECRET_KEY = "django-insecure-mcl=_9h9=1h)*%pbt8%*!n724ik0@v25b-=s0*v0bazgrnepyl"
23-
24-
# SECURITY WARNING: don't run with debug turned on in production!
19+
# A Positron app is only ever serving to itself, so a lot of the usual advice about
20+
# Django best practices in production don't apply. The secret key doesn't need to be
21+
# *that* secret; and running in debug mode (with staticfiles) is fine.
22+
SECRET_KEY = "django-insecure-%vgal2@#0@feqe3jz@1d+f95c*@)2f9n^v9@#%&po5+ct7plwz"
2523
DEBUG = True
2624

2725
ALLOWED_HOSTS = []
@@ -48,7 +46,7 @@
4846
"django.middleware.clickjacking.XFrameOptionsMiddleware",
4947
]
5048

51-
ROOT_URLCONF = "webapp.urls"
49+
ROOT_URLCONF = "positron.urls"
5250

5351
TEMPLATES = [
5452
{
@@ -66,29 +64,25 @@
6664
},
6765
]
6866

69-
WSGI_APPLICATION = "webapp.wsgi.application"
67+
WSGI_APPLICATION = "positron.wsgi.application"
7068

7169

7270
# Database
73-
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
71+
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
7472

7573
DATABASES = {
7674
"default": {
7775
"ENGINE": "django.db.backends.sqlite3",
78-
"NAME": BASE_DIR / "db.sqlite3",
76+
"NAME": (TogaApp.app.paths.data if TogaApp.app else BASE_PATH) / "db.sqlite3",
7977
}
8078
}
8179

82-
8380
# Password validation
84-
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
81+
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
8582

8683
AUTH_PASSWORD_VALIDATORS = [
8784
{
88-
"NAME": (
89-
"django.contrib.auth.password_validation."
90-
"UserAttributeSimilarityValidator"
91-
),
85+
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa: E501
9286
},
9387
{
9488
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
@@ -103,7 +97,7 @@
10397

10498

10599
# Internationalization
106-
# https://docs.djangoproject.com/en/4.1/topics/i18n/
100+
# https://docs.djangoproject.com/en/5.1/topics/i18n/
107101

108102
LANGUAGE_CODE = "en-us"
109103

@@ -115,11 +109,12 @@
115109

116110

117111
# Static files (CSS, JavaScript, Images)
118-
# https://docs.djangoproject.com/en/4.1/howto/static-files/
112+
# https://docs.djangoproject.com/en/5.1/howto/static-files/
119113

120114
STATIC_URL = "static/"
115+
STATIC_ROOT = BASE_PATH / "static"
121116

122117
# Default primary key field type
123-
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
118+
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
124119

125120
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django.contrib import admin
2+
from django.contrib.staticfiles import views as staticfiles
3+
from django.urls import path, re_path
4+
5+
urlpatterns = [
6+
path("admin/", admin.site.urls),
7+
re_path(r"^static/(?P<path>.*)$", staticfiles.serve),
8+
]
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
"""WSGI config for mysite project.
1+
"""
2+
WSGI config for positron project.
23
34
It exposes the WSGI callable as a module-level variable named ``application``.
45
56
For more information on this file, see
6-
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
7+
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
78
"""
89

910
import os
1011

1112
from django.core.wsgi import get_wsgi_application
1213

13-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
14+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "positron.settings")
1415

1516
application = get_wsgi_application()

examples/positron-django/src/webapp/asgi.py

-15
This file was deleted.

0 commit comments

Comments
 (0)