Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: withlogicco/django-prose
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 1.2.1
Choose a base ref
...
head repository: withlogicco/django-prose
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 1.2.2
Choose a head ref
Loading
72 changes: 49 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -12,29 +12,49 @@ Django Prose provides your Django applications with wonderful rich-text editing

## Getting started

To get started with Django Prose, first make sure to install it. We use and suggest using Poetry, although Pipenv and pip will work seamlessly as well

```console
poetry add django-prose
```

Then, add `prose` in Django's installed apps (example: [`example/example/settings.py`](https://github.com/withlogicco/django-prose/blob/9e24cc794eae6db48818dd15a483d106d6a99da0/example/example/settings.py#L46)):

```python
INSTALLED_APPS = [
# Django stock apps (e.g. 'django.contrib.admin')

'prose',

# your application's apps
]
```

Last, run migrations so you can use Django Prose's Document model:

```
python manage.py migrate prose
```
To get started with Django Prose, all you need to do is follow **just four steps**.

1. **Install `django-prose`**

We use and suggest using Poetry, although Pipenv and plain pip will work seamlessly as well

```console
poetry add django-prose
```

2. **Add to `INSTALLED_APPS`**
Add `prose` in your Django project's installed apps (example: [`example/example/settings.py`](https://github.com/withlogicco/django-prose/blob/9e24cc794eae6db48818dd15a483d106d6a99da0/example/example/settings.py#L46)):
```python
INSTALLED_APPS = [
# Django stock apps (e.g. 'django.contrib.admin')
'prose',
# your application's apps
]
```

3. **Run migrations**

This is required so you can use Django Prose's built-in Document model:
```console
python manage.py migrate prose
```

4. **Include URLs**

You need to edit the main `urls.py` file of your Django project and include `prose.urls`:
```python
urlpatterns = [
path('admin/', admin.site.urls),
# other urls ...
path("prose/", include("prose.urls")),
]
```

Now, you are ready to go 🚀.

@@ -135,6 +155,12 @@ For this reason Django Prose is using [Bleach](https://bleach.readthedocs.io/en/

![Django Prose Document in Django Admin](./docs/django-admin-prose-document.png)

## Video tutorial

If you are more of a video person, you can watch our video showcasing how to **Create a blog using Django Prose** on YouTube

[![Watch the video](https://img.youtube.com/vi/uICZjUpAbhQ/hqdefault.jpg)](https://youtu.be/uICZjUpAbhQ)

## Real world use cases
- **Remote Work Café**: Used to edit location pagess, like [Amsterdam | Remote Work Café](https://remotework.cafe/locations/amsterdam/)
- In production by multiple clients of [LOGIC](https://withlogic.co), from small companies to the public sector.
1 change: 0 additions & 1 deletion example/blog/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@


class Migration(migrations.Migration):

initial = True

dependencies = [
1 change: 0 additions & 1 deletion example/blog/migrations/0002_article_excerpt.py
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@


class Migration(migrations.Migration):

dependencies = [
("blog", "0001_initial"),
]
Original file line number Diff line number Diff line change
@@ -4,7 +4,6 @@


class Migration(migrations.Migration):

dependencies = [
("blog", "0002_article_excerpt"),
]
18 changes: 18 additions & 0 deletions example/blog/migrations/0004_alter_article_excerpt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.1.7 on 2023-03-16 10:08

from django.db import migrations
import prose.fields


class Migration(migrations.Migration):
dependencies = [
("blog", "0003_rename_content_article_body"),
]

operations = [
migrations.AlterField(
model_name="article",
name="excerpt",
field=prose.fields.RichTextField(blank=True, null=True),
),
]
2 changes: 1 addition & 1 deletion example/blog/models.py
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ class Article(models.Model):
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
excerpt = RichTextField(blank=True)
excerpt = RichTextField(blank=True, null=True)
body = models.OneToOneField(Document, on_delete=models.CASCADE)

def __str__(self):
4 changes: 3 additions & 1 deletion example/blog/templates/blog/index.html
Original file line number Diff line number Diff line change
@@ -8,7 +8,9 @@
{% for article in articles %}
<li>
<h2>{{ article.title }}</h2>
<p>{{ article.excerpt | safe }}</p>
{% if article.excerpt %}
<p>{{ article.excerpt | safe }}</p>
{% endif %}
<a href="{% url 'blog_article' article.pk %}">Read more</a>
</li>
{% empty %}
65 changes: 64 additions & 1 deletion example/blog/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,66 @@
from django.test import TestCase
from django.contrib.auth.models import User

# Create your tests here.
# internal imports
from blog.models import Article
from prose.models import Document


class ArticleModelTests(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.su = User.objects.create_superuser(
"admin", "fake@adminmail.com", "superpass"
)
assert User.objects.count() == 1

def create_article(self, title: str, excerpt: str | None, body: str) -> Article:
"""Create a Article with the given informations."""
body_document = Document.objects.create(content=body)
return Article.objects.create(
title=title, author=self.su, excerpt=excerpt, body=body_document
)

def test_article_is_in_db_with_none(self):
"""Article is in database and excerpt field is None."""
excerpt = None
article = self.create_article(title="Test", excerpt=excerpt, body="Test body.")
assert isinstance(article, Article)
assert article.excerpt == excerpt
article_in_db = Article.objects.all()[0]
assert Article.objects.count() == 1
self.assertQuerysetEqual([article_in_db], [article])

def test_article_is_in_db_with_empty_string(self):
"""Article is in database and excerpt field is empty string => ''."""
excerpt = ""
article = self.create_article(title="Test", excerpt=excerpt, body="Test body")
assert isinstance(article, Article)
assert article.excerpt == excerpt
article_in_db = Article.objects.all()[0]
assert Article.objects.count() == 1
self.assertQuerysetEqual([article_in_db], [article])

def test_article_is_in_db_with_string(self):
"""Article is in database and excerpt field is string => 'string'."""
excerpt = "string"
article = self.create_article(title="Test", excerpt=excerpt, body="Test body")
assert isinstance(article, Article)
assert article.excerpt == excerpt
article_in_db = Article.objects.all()[0]
assert Article.objects.count() == 1
self.assertQuerysetEqual([article_in_db], [article])

def test_article_is_in_db_correctly(self):
"""We test whether it is possible to have a string, empty string or None in RichTextField. RichTextField is an excerpt field in the Article model."""
excerpt_list = ["string", "", None]
for nr, excerpt in enumerate(excerpt_list, start=1):
title = f"Test {nr}"
body = f"Test body {nr}"
article = self.create_article(title=title, excerpt=excerpt, body=body)
assert isinstance(article, Article)
assert article.excerpt == excerpt
article_in_db = Article.objects.get(title=title)
assert Article.objects.count() == nr
self.assertQuerysetEqual([article_in_db], [article])
66 changes: 46 additions & 20 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions prose/apps.py
Original file line number Diff line number Diff line change
@@ -2,5 +2,6 @@


class ProseConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "prose"
label = "prose"
3 changes: 3 additions & 0 deletions prose/fields.py
Original file line number Diff line number Diff line change
@@ -50,6 +50,9 @@ def formfield(self, **kwargs):

def pre_save(self, model_instance, add):
raw_html = getattr(model_instance, self.attname)
if not raw_html:
return raw_html

sanitized_html = bleach.clean(
raw_html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES
)
1 change: 0 additions & 1 deletion prose/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@


class Migration(migrations.Migration):

initial = True

dependencies = []
19 changes: 19 additions & 0 deletions prose/migrations/0002_alter_document_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2 on 2023-04-18 10:39

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("prose", "0001_initial"),
]

operations = [
migrations.AlterField(
model_name="document",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
]
4 changes: 4 additions & 0 deletions prose/static/prose/editor.css
Original file line number Diff line number Diff line change
@@ -15,3 +15,7 @@ form .aligned .django-prose-editor-container trix-editor ul li {
form .aligned .django-prose-editor-container trix-editor ul li {
list-style-type: initial;
}

.django-prose-editor-container trix-toolbar .trix-button {
background: #fff !important;
}
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "django-prose"
version = "1.2.0"
version = "1.2.2"
description = "Wonderful rich text for Django"
authors = ["Paris Kasidiaris <paris@withlogic.co>"]
license = "MIT"
@@ -16,7 +16,7 @@ django = ">=3.2"
bleach = ">=4.0"

[tool.poetry.dev-dependencies]
black = "^22.8.0"
black = "^23.3.0"

[build-system]
requires = ["poetry>=0.12"]