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 11 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
40 changes: 36 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ 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**

```
```python
QUILL_CONFIGS = {
'default':{
'theme': 'snow',
Expand All @@ -80,11 +81,11 @@ QUILL_CONFIGS = {
]
}
}

}

```



## Usage

Add `QuillField` to the **Model class** you want to use
Expand Down Expand Up @@ -213,3 +214,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
```

86 changes: 55 additions & 31 deletions django_quill/fields.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,51 @@
import json

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.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)

self._committed = True

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 Down Expand Up @@ -78,7 +92,7 @@ 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
Expand All @@ -96,37 +110,25 @@ 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
return QuillFormJSONField

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)
quill.save(quill.data, save=False)
return quill

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

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 isinstance(value, Quill):
return value
if isinstance(value, FieldQuill):
Expand All @@ -136,9 +138,31 @@ def to_python(self, 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):
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)
16 changes: 15 additions & 1 deletion django_quill/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,26 @@

__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

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):
return QuillFormJSONField(*args, **kwargs)
16 changes: 9 additions & 7 deletions django_quill/quill.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ def __str__(self):


class Quill:
def __init__(self, json_string):
def __init__(self, data):
assert isinstance(data, dict), (
"Quill expects dictionary as data but got %s(%s)." % (type(data), 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)
6 changes: 5 additions & 1 deletion django_quill/static/django_quill/django_quill.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
div.form-row.field-content div.django-quill-widget-container {
div.form-row.field-content, div.django-quill-widget-container {
display: inline-block;
}

.ql-editor{
min-height:350px;
}
73 changes: 61 additions & 12 deletions django_quill/static/django_quill/django_quill.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,66 @@
class QuillWrapper {
constructor(targetDivId, targetInputId, quillOptions) {
this.targetDiv = document.getElementById(targetDivId);
if (!this.targetDiv) throw 'Target div(' + targetDivId + ') id was invalid';
constructor(targetDivId, targetInputId, uploadURL, quillOptions) {
this.targetDiv = document.getElementById(targetDivId);
if (!this.targetDiv) throw 'Target div(' + targetDivId + ') id was invalid';

this.targetInput = document.getElementById(targetInputId);
if (!this.targetInput) throw 'Target Input id was invalid';
this.targetInput = document.getElementById(targetInputId);
if (!this.targetInput) throw 'Target Input id was invalid';

this.quill = new Quill('#' + targetDivId, quillOptions);
this.quill.on('text-change', () => {
var delta = JSON.stringify(this.quill.getContents());
var html = this.targetDiv.getElementsByClassName('ql-editor')[0].innerHTML;
var data = {delta: delta, html: html};
this.targetInput.value = JSON.stringify(data);
});
if (quillOptions.useInlineStyleAttributes) {
// https://quilljs.com/guides/how-to-customize-quill/#class-vs-inline
Quill.register(Quill.import('attributors/style/align'), true);
}

if (uploadURL) {
// https://www.npmjs.com/package/quill-image-uploader
Quill.register("modules/imageUploader", ImageUploader);

var imageUploaderModule = {
upload: file => {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append("image", file);

fetch(
uploadURL, {
method: "POST",
body: formData,
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
}
)
.then(response => response.json())
.then(result => {
console.log(result);
resolve(result.image_url);
})
.catch(error => {
reject("Upload failed");
alert("Uploading failed");
console.error("Error:", error);
});
});
}
}

}

this.quill = new Quill('#' + targetDivId, {
...quillOptions,
modules: {
...quillOptions.modules,
imageUploader: imageUploaderModule
}
});
this.quill.on('text-change', () => {
var delta = JSON.stringify(this.quill.getContents());
var html = this.targetDiv.getElementsByClassName('ql-editor')[0].innerHTML;
var data = {
delta: delta,
html: html
};
this.targetInput.value = JSON.stringify(data);
});
}
}
3 changes: 3 additions & 0 deletions django_quill/templates/django_quill/media.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
<script src="//cdn.quilljs.com/1.3.6/quill.min.js"></script>

<!-- Custom -->
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/quill.imageUploader.min.css"/>
<script src="https://unpkg.com/[email protected]/dist/quill.imageUploader.min.js"></script>

<link rel="stylesheet" href="{% static 'django_quill/django_quill.css' %}">
<script src="{% static 'django_quill/django_quill.js' %}"></script>
23 changes: 15 additions & 8 deletions django_quill/templates/django_quill/widget.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
{% load static %}
<div class="vLargeTextField django-quill-widget-container">
<div id="quill-{{ id }}" class="django-quill-widget" data-config="{{ config }}" data-type="django-quill"></div>
<input id="quill-input-{{ id }}" name="{{ name }}" type="hidden">
<div id="quill-{{ widget.attrs.id }}" class="django-quill-widget" data-config="{{ widget.config }}" data-type="django-quill"></div>
<input id="quill-input-{{ widget.attrs.id }}" name="{{ widget.name }}" type="hidden">
{% url 'quill-editor-upload' as upload_url %}
{% if not upload_url %}
{% url '' as upload_url %}
{% endif %}
<script>
(function () {
var wrapper = new QuillWrapper('quill-{{ id }}', 'quill-input-{{ id }}', JSON.parse('{{ config|safe }}'));
{% if quill and quill.delta %}
var contents = JSON.parse('{{ quill.delta|safe|escapejs }}');
var wrapper = new QuillWrapper('quill-{{ widget.attrs.id }}', 'quill-input-{{ widget.attrs.id }}', '{{upload_url}}', JSON.parse('{{ widget.config|safe }}'));
try {
var value = JSON.parse('{{ widget.value|safe|escapejs }}');
var contents = JSON.parse(value.delta);
wrapper.quill.setContents(contents);
{% elif value %}
}
// When a parsing error occurs, the contents are regarded as HTML and the contents of the editor are filled.
catch (e) {
wrapper.quill.clipboard.dangerouslyPasteHTML(0, '{{ value|safe }}')
{% endif %}
})();
}
})();
</script>
</div>
Loading