Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add QuillJSONField #36

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9962c1b
Update README.md
LeeHanYeong Jul 14, 2020
be1352a
Bug fixes (#20)
michaldyczko Aug 4, 2020
4d85604
Deletion of dynamically generated form processing logic, bug fix
LeeHanYeong Aug 4, 2020
e1288e2
Docs update
LeeHanYeong Aug 4, 2020
b6f26c1
Update widgets.py
Pierox57 Aug 13, 2020
d602b0d
Merge pull request #23 from Pierox57/patch-1
LeeHanYeong Aug 13, 2020
2c6fe47
bump to 0.1.15
LeeHanYeong Aug 13, 2020
76e6e72
Update setup.py classifiers
LeeHanYeong Aug 20, 2020
fe8ef8a
fix admin form style issues
gokselcoban Feb 8, 2021
3533ed9
add image uploader
gokselcoban Feb 8, 2021
a362dca
rewrite QuillField to inherit JSONField (#27)
Feb 9, 2021
1ee92ab
Bump to Quill Version 1.3.7 (#34)
cdesch Feb 9, 2021
b7092cb
bump to 0.1.18
LeeHanYeong Feb 9, 2021
3e838d3
add QuillJSONField
gokselcoban Feb 9, 2021
4e11071
remove print
gokselcoban Feb 9, 2021
e4d1746
fix inconsistent quill versions (#37)
gokselcoban Feb 10, 2021
f867a96
bump to 0.1.19
LeeHanYeong Feb 10, 2021
dd66ea7
improve image uploader config
gokselcoban Feb 10, 2021
25df5c2
add image uploads docs
gokselcoban Feb 10, 2021
fabe28f
minor documentation fix
gokselcoban Feb 10, 2021
5452487
Merge branch 'master' of github.com:LeeHanYeong/django-quill-editor i…
gokselcoban Feb 10, 2021
4cbf08c
Merge branch 'image-uploader' of github.com:cobang/django-quill-edito…
gokselcoban Feb 10, 2021
b7ae2ea
remove save methods
gokselcoban Feb 10, 2021
d0fef9d
add deprecation warnings
gokselcoban Feb 10, 2021
0541e4e
update documentations
gokselcoban Feb 10, 2021
e7db4b8
Return value itself that holds the data on post requests
KaratasFurkan May 4, 2021
84a9f95
Merge pull request #2 from KaratasFurkan/fix-empty-value-on-form-error
gokselcoban May 4, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 96 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,21 @@ python app/manage.py runserver
Documentation for **django-quill-editor** is located at [https://django-quill-editor.readthedocs.io/](https://django-quill-editor.readthedocs.io/)


## Change toolbar menus`

## Change toolbar configs

Add `QUILL_CONFIGS` to the **settings.py**

```
If you want to use inline style attributes (`style="text-align: center;"`) instead of class (`class="ql-align-center"`)
, set `useInlineStyleAttributes` to `True`.
It changes the settings only for `align` now. You can check the related
[Quill Docs](https://quilljs.com/guides/how-to-customize-quill/#class-vs-inline).

```python
QUILL_CONFIGS = {
'default':{
'theme': 'snow',
'useInlineStyleAttributes': True,
'modules': {
'syntax': True,
'toolbar': [
Expand All @@ -77,25 +84,73 @@ QUILL_CONFIGS = {
],
['code-block', 'link'],
['clean'],
]
],
'imageUploader': {
'uploadURL': '/admin/quill/upload/', # You can also use an absolute URL (https://example.com/3rd-party/uploader/)
'addCSRFTokenHeader': True,
}
}
}

}
```

## Image uploads

If you want to upload images instead of storing encoded images in your database. You need to add `imageUploader` module
to your configuration. If you set a `uploadURL` for this modules, it registers
[quill-image-uploader](https://www.npmjs.com/package/quill-image-uploader) to Quill.
You can add a view to upload images to your storage service. Response of the view must contain `image_url` field.

```python
# urls.py
from django.urls import path
from .views import EditorImageUploadAPIView

urlpatterns = [
...
path('admin/quill/upload/', EditorImageUploadAPIView.as_view(), name='quill-editor-upload'),
...
]
```

```python
# You don't have to use Django Rest Framework. This is just an example.
from rest_framework import status
from rest_framework.generics import CreateAPIView
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response

from .serializers import EditorImageSerializer


class EditorImageUploadAPIView(CreateAPIView):
serializer_class = EditorImageSerializer
permission_classes = (IsAdminUser,)

def post(self, request, *args, **kwargs):
# image_url = handle image upload
return Response({'image_url': "https://xxx.s3.amazonaws.com/xxx/x.png"}, status=status.HTTP_200_OK)
```

```json
{
"image_url": "https://xxx.s3.amazonaws.com/xxx/x.png"
}
```

## Usage

Add `QuillField` to the **Model class** you want to use
Add `QuillTextField` or `QuillJSONField` to the **Model class** you want to use.

```python
# models.py
from django.db import models
from django_quill.fields import QuillField
from django_quill.fields import QuillField, QuillTextField, QuillJSONField

class QuillPost(models.Model):
content = QuillField()
content = QuillField() # Deprecated. It is same with QuillTextField.
content = QuillTextField()
content = QuillJSONField()
```


Expand All @@ -119,7 +174,7 @@ class QuillPostAdmin(admin.ModelAdmin):

### 2. Form

- Add `QuillFormField` to the **Form class** you want to use
- Add `QuillFormJSONField` to the **Form class** you want to use

- There are two ways to add CSS and JS files to a template.

Expand All @@ -145,10 +200,10 @@ class QuillPostAdmin(admin.ModelAdmin):
```python
# forms.py
from django import forms
from django_quill.forms import QuillFormField
from django_quill.forms import QuillFormJSONField

class QuillFieldForm(forms.Form):
content = QuillFormField()
content = QuillFormJSONField()
```

```python
Expand Down Expand Up @@ -213,3 +268,34 @@ def model_form(request):
As an open source project, we welcome contributions.
The code lives on [GitHub](https://github.com/LeeHanYeong/django-quill-editor)



## Distribution (for owners)

### PyPI Release

```shell
poetry install # Install PyPI distribution packages
python deploy.py
```



### Sphinx docs

```shell
brew install sphinx-doc # macOS
```

#### Local

```
cd docs
make html
# ...
# The HTML pages are in _build/html.

cd _build/html
python -m http.server 3001
```

103 changes: 57 additions & 46 deletions django_quill/fields.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,50 @@
import json
import warnings

from django.db import models

from .forms import QuillFormField
from .quill import Quill
from .forms import QuillFormJSONField
from .quill import Quill, QuillParseError

__all__ = (
'FieldQuill',
'QuillDescriptor',
'QuillField',
'QuillTextField',
'QuillJSONField'
)


class FieldQuill:
def __init__(self, instance, field, json_string):
def __init__(self, instance, field, data):
self.instance = instance
self.field = field
self.json_string = json_string
self._committed = True
self.data = data or dict(delta="", html="")

assert isinstance(self.data, (str, dict)), (
"FieldQuill expects dictionary or string as data but got %s(%s)." % (type(data), data)
)
if isinstance(self.data, str):
try:
self.data = json.loads(data)
except json.JSONDecodeError:
raise QuillParseError(data)

def __eq__(self, other):
if hasattr(other, 'json_string'):
return self.json_string == other.json_string
return self.json_string == other
if hasattr(other, 'data'):
return self.data == other.data
return self.data == other

def __hash__(self):
return hash(self.json_string)
return hash(self.data)

def _require_quill(self):
if not self:
raise ValueError("The '%s' attribute has no Quill JSON String associated with it." % self.field.name)

def _get_quill(self):
self._require_quill()
self._quill = Quill(self.json_string)
self._quill = Quill(self.data)
return self._quill

def _set_quill(self, quill):
Expand All @@ -52,12 +65,6 @@ def delta(self):
self._require_quill()
return self.quill.delta

def save(self, json_string, save=True):
setattr(self.instance, self.field.name, json_string)
self._committed = True
if save:
self.instance.save()


class QuillDescriptor:
def __init__(self, field):
Expand All @@ -78,9 +85,8 @@ def __get__(self, instance, cls=None):
instance.__dict__[self.field.name] = attr

elif isinstance(quill, Quill) and not isinstance(quill, FieldQuill):
quill_copy = self.field.attr_class(instance, self.field, quill.json_string)
quill_copy = self.field.attr_class(instance, self.field, quill.data)
quill_copy.quill = quill
quill_copy._committed = False
instance.__dict__[self.field.name] = quill_copy

elif isinstance(quill, FieldQuill) and not hasattr(quill, 'field'):
Expand All @@ -96,49 +102,54 @@ def __set__(self, instance, value):
instance.__dict__[self.field.name] = value


class QuillField(models.TextField):
class QuillFieldMixin:
attr_class = FieldQuill
descriptor_class = QuillDescriptor

def formfield(self, **kwargs):
kwargs.update({'form_class': QuillFormField})
kwargs.update({'form_class': self._get_form_class()})
return super().formfield(**kwargs)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

@staticmethod
def _get_form_class():
return QuillFormField

def pre_save(self, model_instance, add):
quill = super().pre_save(model_instance, add)
if quill and not quill._committed:
quill.save(quill.json_string, save=False)
return quill

def from_db_value(self, value, expression, connection):
return self.to_python(value)
return QuillFormJSONField

def to_python(self, value):
"""
Expect a JSON string with 'delta' and 'html' keys
ex) b'{"delta": "...", "html": "..."}'
:param value: JSON string with 'delta' and 'html' keys
:return: Quill's 'Delta' JSON String
"""
if value is None:
return value
if isinstance(value, Quill):
return value
if isinstance(value, FieldQuill):
return value.quill
if value is None or isinstance(value, str):
return value
return Quill(value)

def get_prep_value(self, value):
value = super().get_prep_value(value)
if value is None:
if value is None or isinstance(value, str):
return value
if isinstance(value, Quill):
return value.json_string
return value
if isinstance(value, (Quill, FieldQuill)):
value = value.data

return json.dumps(value, cls=getattr(self, 'encoder', None))

def value_to_string(self, obj):
value = self.value_from_object(obj)
return self.get_prep_value(value)


class QuillTextField(QuillFieldMixin, models.TextField):
pass


def QuillField(*args, **kwargs):
warnings.warn('QuillField is deprecated in favor of QuillTextField', stacklevel=2)
return QuillTextField(*args, **kwargs)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason this returns a text field instead of a json field?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current users of the package, imports and uses QuillField which is based on text field. They may don't want to change the existing approach. It returns the text field for backward capability.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair point. i would just add a bit to the readme explaining the different fields so new users know what to use



class QuillJSONField(QuillFieldMixin, models.JSONField):

def from_db_value(self, value, expression, connection):
return self.to_python(value)

def validate(self, value, model_instance):
value = self.get_prep_value(value)
super(QuillJSONField, self).validate(value, model_instance)
20 changes: 19 additions & 1 deletion django_quill/forms.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
import warnings

from django import forms
from .widgets import QuillWidget

__all__ = (
'QuillFormField',
'QuillFormJSONField'
)


class QuillFormField(forms.fields.CharField):
class QuillFormJSONField(forms.JSONField):
def __init__(self, *args, **kwargs):
kwargs.update({
'widget': QuillWidget(),
})
super().__init__(*args, **kwargs)

def prepare_value(self, value):
if hasattr(value, "data"):
return value.data
return value

def has_changed(self, initial, data):
if hasattr(initial, 'data'):
initial = initial.data
return super(QuillFormJSONField, self).has_changed(initial, data)


def QuillFormField(*args, **kwargs):
warnings.warn('QuillFormField is deprecated in favor of QuillFormJSONField', stacklevel=2)
return QuillFormJSONField(*args, **kwargs)
23 changes: 15 additions & 8 deletions django_quill/quill.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import json
from json import JSONDecodeError

__all__ = (
'QuillParseError',
Expand All @@ -16,11 +15,19 @@ def __str__(self):


class Quill:
def __init__(self, json_string):
def __init__(self, data):
assert isinstance(data, (str, dict)), (
"Quill expects dictionary or string as data but got %s(%s)." % (type(data), data)
)
if isinstance(data, str):
try:
data = json.loads(data)
except json.JSONDecodeError:
raise QuillParseError(data)

self.data = data
try:
self.json_string = json_string
json_data = json.loads(json_string)
self.delta = json_data['delta']
self.html = json_data['html']
except (JSONDecodeError, KeyError, TypeError):
raise QuillParseError(json_string)
self.delta = data['delta']
self.html = data['html']
except (KeyError, TypeError):
raise QuillParseError(data)
Loading