Skip to content

Commit 0481f81

Browse files
committed
Add example blog app
1 parent b76f5e8 commit 0481f81

File tree

24 files changed

+837
-55
lines changed

24 files changed

+837
-55
lines changed

django_cf/d1_api/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212

1313
class DatabaseWrapper(SQLiteDatabaseWrapper):
14-
vendor = "sqlite"
14+
vendor = "cloudflare_d1"
1515
display_name = "D1"
1616

1717
Database = Database

django_cf/d1_binding/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212

1313
class DatabaseWrapper(SQLiteDatabaseWrapper):
14-
vendor = "sqlite"
14+
vendor = "cloudflare_d1"
1515
display_name = "D1"
1616

1717
Database = Database

django_cf/d1_binding/database.py

Lines changed: 50 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
1-
import json
2-
import http.client
3-
import datetime
4-
import re
1+
import sqlparse
52

63
from django.db import DatabaseError, Error, DataError, OperationalError, \
74
IntegrityError, InternalError, ProgrammingError, NotSupportedError, InterfaceError
8-
from django.conf import settings
9-
from django.utils import timezone
105

116
class D1Result:
127
lastrowid = None
@@ -121,81 +116,68 @@ def process_query(self, query, params=None):
121116
def run_query(self, query, params=None):
122117
proc_query, params = self.process_query(query, params)
123118

124-
# print(query)
125-
# print(params)
126-
127119
cf_workers = self.import_from_javascript("cloudflare:workers")
128-
# print(dir(cf_workers.env))
129120
db = getattr(cf_workers.env, self.binding)
130121

131122
if params:
132123
stmt = db.prepare(proc_query).bind(*params);
133124
else:
134125
stmt = db.prepare(proc_query);
135126

127+
read_only = is_read_only_query(proc_query)
128+
136129
try:
137-
resp = self.run_sync(stmt.all())
130+
if read_only:
131+
resp = self.run_sync(stmt.raw())
132+
else:
133+
resp = self.run_sync(stmt.all())
138134
except:
139135
from js import Error
140136
Error.stackTraceLimit = 1e10
141137
raise Error(Error.new().stack)
142138

143-
results = self._convert_results(resp.results.to_py())
144-
145-
# print(results)
146-
# print(f'rowsRead: {resp.meta.rows_read}')
147-
# print(f'rowsWritten: {resp.meta.rows_written}')
148-
# print('---')
139+
# this is a hack, because D1 Raw method (required for reading rows) doesn't return metadata
140+
if read_only:
141+
results = self._convert_results_list(resp.to_py())
142+
rows_read = len(results)
143+
rows_written = 0
144+
else:
145+
results = self._convert_results_dict(resp.results.to_py())
146+
rows_read = resp.meta.rows_read
147+
rows_written = resp.meta.rows_written
149148

150149
return results, {
151-
"rows_read": resp.meta.rows_read,
152-
"rows_written": resp.meta.rows_written,
150+
"rows_read": rows_read,
151+
"rows_written": rows_written,
153152
}
154153

155-
def _convert_results(self, data):
156-
"""
157-
Convert any datetime strings in the result set to actual timezone-aware datetime objects.
158-
"""
159-
# print('before')
160-
# print(data)
154+
def _convert_results_dict(self, data):
161155
result = []
162156

163157
for row in data:
164158
row_items = ()
165159
for k, v in row.items():
166-
if isinstance(v, str):
167-
v = self._parse_datetime(v)
168160
row_items += (v,)
169161

170162
result.append(row_items)
171163

172-
# print('after')
173-
# print(result)
164+
return result
165+
166+
def _convert_results_list(self, data):
167+
result = []
168+
169+
for row in data:
170+
row_items = ()
171+
for v in row:
172+
row_items += (v,)
173+
174+
result.append(row_items)
175+
174176
return result
175177

176178
query = None
177179
params = None
178180

179-
def _parse_datetime(self, value):
180-
"""
181-
Parse the string value to a timezone-aware datetime object, if applicable.
182-
Handles both datetime strings with and without milliseconds.
183-
Uses Django's timezone utilities for proper conversion.
184-
"""
185-
datetime_formats = ["%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%d %H:%M:%S"]
186-
187-
for dt_format in datetime_formats:
188-
try:
189-
naive_dt = datetime.datetime.strptime(value, dt_format)
190-
# If Django is using timezones, convert to an aware datetime object
191-
if timezone.is_naive(naive_dt):
192-
return timezone.make_aware(naive_dt, timezone.get_default_timezone())
193-
return naive_dt
194-
except (ValueError, TypeError):
195-
continue # Try the next format if parsing fails
196-
197-
return value # If it's not a datetime string, return the original value
198-
199181
def execute(self, query, params=None):
200182
if params:
201183
newParams = []
@@ -252,3 +234,22 @@ def fetchmany(self, size=1):
252234

253235
def close(self):
254236
return
237+
238+
239+
def is_read_only_query(query: str) -> bool:
240+
parsed = sqlparse.parse(query.strip())
241+
242+
if not parsed:
243+
return False # Invalid or empty query
244+
245+
# Get the first statement
246+
statement = parsed[0]
247+
248+
# Check if the statement is a SELECT query
249+
if statement.get_type().upper() == "SELECT":
250+
return True
251+
252+
# List of modifying query types
253+
modifying_types = {"INSERT", "UPDATE", "DELETE", "CREATE", "ALTER", "DROP", "REPLACE"}
254+
255+
return statement.get_type().upper() not in modifying_types

django_cf/do_binding/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212

1313
class DatabaseWrapper(SQLiteDatabaseWrapper):
14-
vendor = "sqlite"
14+
vendor = "cloudflare_durable_objects"
1515
display_name = "DO"
1616

1717
Database = Database

templates/d1/src/app/settings.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
'django.contrib.sessions',
3838
'django.contrib.messages',
3939
'django.contrib.staticfiles',
40+
41+
'blog'
4042
]
4143

4244
MIDDLEWARE = [
@@ -54,7 +56,9 @@
5456
TEMPLATES = [
5557
{
5658
'BACKEND': 'django.template.backends.django.DjangoTemplates',
57-
'DIRS': [],
59+
'DIRS': [
60+
BASE_DIR.joinpath('templates')
61+
],
5862
'APP_DIRS': True,
5963
'OPTIONS': {
6064
'context_processors': [

templates/d1/src/app/urls.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from django.contrib.auth import get_user_model
1919
from django.core.management import call_command
2020
from django.http import JsonResponse
21-
from django.urls import path
21+
from django.urls import path, include
2222
from django.contrib.auth.decorators import user_passes_test
2323

2424
def is_superuser(user):
@@ -44,10 +44,13 @@ def run_migrations_view(request):
4444
call_command("migrate")
4545
return JsonResponse({"status": "success", "message": "Migrations applied."})
4646
except Exception as e:
47+
raise e
4748
return JsonResponse({"status": "error", "message": str(e)}, status=500)
4849

4950

5051
urlpatterns = [
52+
path('', include('blog.urls')),
53+
5154
path('admin/', admin.site.urls),
5255
# Management endpoints - secure these appropriately for your application
5356
path('__create_admin__/', create_admin_view, name='create_admin'),

templates/d1/src/blog/__init__.py

Whitespace-only changes.

templates/d1/src/blog/admin.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from django.contrib import admin
2+
from .models import Post
3+
4+
admin.site.register(Post)

templates/d1/src/blog/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 BlogConfig(AppConfig):
5+
default_auto_field = 'django.db.models.BigAutoField'
6+
name = 'blog'
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Generated by Django 5.1.2 on 2025-07-06 13:30
2+
3+
import django.utils.timezone
4+
from django.db import migrations, models
5+
6+
7+
def insert_dummy_blog_post(apps, schema_editor):
8+
Post = apps.get_model('blog', 'Post')
9+
10+
# Insert the record
11+
Post.objects.create(
12+
title='Example Post',
13+
content="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
14+
)
15+
16+
17+
class Migration(migrations.Migration):
18+
19+
initial = True
20+
21+
dependencies = [
22+
]
23+
24+
operations = [
25+
migrations.CreateModel(
26+
name='Post',
27+
fields=[
28+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
29+
('title', models.CharField(max_length=200)),
30+
('content', models.TextField()),
31+
('pub_date', models.DateTimeField(default=django.utils.timezone.now)),
32+
],
33+
),
34+
migrations.RunPython(
35+
code=insert_dummy_blog_post,
36+
),
37+
]

0 commit comments

Comments
 (0)