Skip to content

Commit 4e28d24

Browse files
authored
Add django example using DO backend and R2 (#101)
1 parent 9a15b61 commit 4e28d24

20 files changed

Lines changed: 500 additions & 0 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Need to deploy your Worker to Cloudflare? Python Workers are in open beta and ha
3737
- [**`image-redraw/`**](image-redraw) — an example that combines [FastAPI](https://fastapi.tiangolo.com/), [R2](https://developers.cloudflare.com/r2/), [Queues](https://developers.cloudflare.com/queues/), [Workflows](https://developers.cloudflare.com/workflows/) and [Workers AI](https://developers.cloudflare.com/workers-ai/) to redraw uploaded images.
3838
- [**`django/`**](django) — runs a naive Django WSGI application directly on Python Workers.
3939
- [**`django-todo-d1/`**](django-todo-d1) — uses Django with D1 for a basic TODO application.
40+
- [**`django-markdown-r2/`**](django-markdown-r2) — a server-rendered Django blog using Durable Object SQLite and R2 for media storage.
4041
- [**`fastapi-todo/`**](fastapi-todo) — implements the [Todo-Backend](https://todobackend.com) spec with FastAPI (ASGI) and D1.
4142
- [**`flask-todo/`**](flask-todo) — implements the same [Todo-Backend](https://todobackend.com) API with [Flask](https://flask.palletsprojects.com/) (WSGI) and D1.
4243

django-markdown-r2/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Django Markdown Blog + Durable Objects + R2
2+
3+
[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/python-workers-examples/tree/main/django-markdown-r2)
4+
5+
A blog built with Django on Cloudflare Python Workers, using Durable Objects for database storage and R2 for image storage.
6+
7+
## Local setup
8+
9+
Install [uv](https://docs.astral.sh/uv/getting-started/installation/#standalone-installer), then install the project dependencies and start the Worker:
10+
11+
```sh
12+
uv sync
13+
uv run pywrangler dev
14+
```
15+
16+
Open http://localhost:8787/. Wrangler provisions the configured Durable Object locally and simulates the `IMAGES` R2 binding.
17+
18+
## Remote setup and deployment
19+
20+
Create an R2 bucket, update the `IMAGES` bucket name in `wrangler.jsonc`, and deploy:
21+
22+
```sh
23+
uv run pywrangler deploy
24+
```

django-markdown-r2/package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "django-markdown-r2-worker",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"deploy": "uv run pywrangler deploy",
7+
"dev": "uv run pywrangler dev",
8+
"start": "uv run pywrangler dev"
9+
},
10+
"devDependencies": {
11+
"wrangler": "^4.114.0"
12+
}
13+
}

django-markdown-r2/pyproject.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[project]
2+
name = "django-markdown-r2-worker"
3+
version = "0.1.0"
4+
description = "Server-rendered Django blog backed by Durable Object SQLite and R2"
5+
readme = "README.md"
6+
requires-python = ">=3.13"
7+
dependencies = [
8+
"django",
9+
"django-cf>=0.2.16",
10+
"markdown-it-py==4.2.0",
11+
]
12+
13+
[dependency-groups]
14+
dev = [
15+
"workers-py",
16+
"workers-runtime-sdk",
17+
]

django-markdown-r2/src/articles/__init__.py

Whitespace-only changes.
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 ArticlesConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "articles"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from django import forms
2+
3+
from .models import Article
4+
5+
6+
class ArticleForm(forms.ModelForm):
7+
class Meta:
8+
model = Article
9+
fields = ["title", "body", "image"]
10+
widgets = {
11+
"body": forms.Textarea(attrs={"rows": 16}),
12+
"image": forms.ClearableFileInput(
13+
attrs={"accept": "image/gif,image/jpeg,image/png,image/webp"}
14+
),
15+
}
16+
help_texts = {"image": "PNG, JPEG, GIF, or WebP."}
17+
18+
19+
class ArticleEditForm(ArticleForm):
20+
class Meta(ArticleForm.Meta):
21+
fields = ["title", "body"]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import uuid
2+
3+
from django.core.validators import FileExtensionValidator
4+
from django.db import models
5+
from django.utils.text import slugify
6+
7+
8+
def generate_article_id() -> str:
9+
return str(uuid.uuid4())
10+
11+
12+
class Article(models.Model):
13+
id = models.CharField(primary_key=True, max_length=36, default=generate_article_id)
14+
title = models.CharField(max_length=200)
15+
slug = models.SlugField(max_length=100, unique=True)
16+
body = models.TextField(max_length=20_000)
17+
image = models.FileField(
18+
upload_to="articles",
19+
blank=True,
20+
validators=[FileExtensionValidator(["gif", "jpeg", "jpg", "png", "webp"])],
21+
)
22+
created_at = models.DateTimeField(auto_now_add=True)
23+
updated_at = models.DateTimeField(auto_now=True)
24+
25+
class Meta:
26+
db_table = "articles"
27+
ordering = ["-created_at"]
28+
29+
def save(self, *args, **kwargs):
30+
if not self.slug:
31+
base = (slugify(self.title) or "article")[:100].rstrip("-")
32+
candidate = base
33+
suffix_number = 2
34+
while type(self).objects.filter(slug=candidate).exists():
35+
suffix = f"-{suffix_number}"
36+
candidate = f"{base[: 100 - len(suffix)].rstrip('-')}{suffix}"
37+
suffix_number += 1
38+
self.slug = candidate
39+
return super().save(*args, **kwargs)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{% extends "articles/base.html" %}
2+
3+
{% block title %}{{ article.title }} | Knowledge base{% endblock %}
4+
5+
{% block content %}
6+
<article aria-labelledby="article-title">
7+
<header>
8+
<hgroup>
9+
<h1 id="article-title">{{ article.title }}</h1>
10+
<p>
11+
<small>
12+
Published <time datetime="{{ created_iso }}">{{ created_date }}</time>
13+
&middot; Updated <time datetime="{{ updated_iso }}">{{ updated_date }}</time>
14+
</small>
15+
</p>
16+
</hgroup>
17+
<nav aria-label="Article actions">
18+
<ul>
19+
<li><a href="{% url 'article-list' %}">Back to articles</a></li>
20+
<li><a href="{% url 'article-edit' slug=article.slug %}">Edit article</a></li>
21+
</ul>
22+
</nav>
23+
</header>
24+
{% if article.image %}
25+
<figure>
26+
<img src="{{ article.image.url }}" alt="Illustration for {{ article.title }}">
27+
</figure>
28+
{% endif %}
29+
<section aria-label="Article content">
30+
{{ rendered_body }}
31+
</section>
32+
</article>
33+
{% endblock %}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{% extends "articles/base.html" %}
2+
3+
{% block title %}{% if is_edit %}Edit {{ article.title }}{% else %}New article{% endif %} | Knowledge base{% endblock %}
4+
5+
{% block content %}
6+
<section aria-labelledby="article-form-heading">
7+
<header>
8+
<h1 id="article-form-heading">{% if is_edit %}Edit article{% else %}New article{% endif %}</h1>
9+
<p>{% if is_edit %}Refine this entry and save the updated reference for readers.{% else %}Add a clear, useful entry to the knowledge base.{% endif %}</p>
10+
</header>
11+
<form method="post"{% if not is_edit %} enctype="multipart/form-data"{% endif %}>
12+
{% csrf_token %}
13+
{% if form.non_field_errors %}
14+
<div role="alert">
15+
{{ form.non_field_errors }}
16+
</div>
17+
{% endif %}
18+
<fieldset>
19+
<legend>Article details</legend>
20+
{% for field in form %}
21+
{% if field.is_hidden %}
22+
{{ field }}
23+
{% if field.errors %}
24+
<div role="alert">{{ field.errors }}</div>
25+
{% endif %}
26+
{% else %}
27+
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
28+
{{ field }}
29+
{% if field.help_text %}
30+
<small>{{ field.help_text }}</small>
31+
{% endif %}
32+
{% if field.errors %}
33+
<div role="alert">{{ field.errors }}</div>
34+
{% endif %}
35+
{% endif %}
36+
{% endfor %}
37+
</fieldset>
38+
<footer>
39+
<button type="submit">{% if is_edit %}Save changes{% else %}Create article{% endif %}</button>
40+
<a href="{% if is_edit %}{% url 'article-detail' slug=article.slug %}{% else %}{% url 'article-list' %}{% endif %}">Cancel</a>
41+
</footer>
42+
</form>
43+
</section>
44+
{% endblock %}

0 commit comments

Comments
 (0)