wbwriter 2.2.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wbwriter might be problematic. Click here for more details.
- wbwriter/__init__.py +1 -0
- wbwriter/admin.py +142 -0
- wbwriter/apps.py +5 -0
- wbwriter/dynamic_preferences_registry.py +15 -0
- wbwriter/factories/__init__.py +13 -0
- wbwriter/factories/article.py +181 -0
- wbwriter/factories/meta_information.py +29 -0
- wbwriter/filters/__init__.py +2 -0
- wbwriter/filters/article.py +47 -0
- wbwriter/filters/metainformationinstance.py +24 -0
- wbwriter/migrations/0001_initial_squashed_squashed_0008_alter_article_author_alter_article_feedback_contact_and_more.py +653 -0
- wbwriter/migrations/0009_dependantarticle.py +41 -0
- wbwriter/migrations/0010_alter_article_options.py +20 -0
- wbwriter/migrations/0011_auto_20240103_0953.py +39 -0
- wbwriter/migrations/__init__.py +0 -0
- wbwriter/models/__init__.py +9 -0
- wbwriter/models/article.py +1179 -0
- wbwriter/models/article_type.py +59 -0
- wbwriter/models/block.py +24 -0
- wbwriter/models/block_parameter.py +19 -0
- wbwriter/models/in_editor_template.py +102 -0
- wbwriter/models/meta_information.py +87 -0
- wbwriter/models/mixins.py +9 -0
- wbwriter/models/publication_models.py +170 -0
- wbwriter/models/style.py +13 -0
- wbwriter/models/template.py +34 -0
- wbwriter/pdf_generator.py +172 -0
- wbwriter/publication_parser.py +258 -0
- wbwriter/serializers/__init__.py +28 -0
- wbwriter/serializers/article.py +359 -0
- wbwriter/serializers/article_type.py +14 -0
- wbwriter/serializers/in_editor_template.py +37 -0
- wbwriter/serializers/meta_information.py +67 -0
- wbwriter/serializers/publication.py +82 -0
- wbwriter/templatetags/__init__.py +0 -0
- wbwriter/templatetags/writer.py +72 -0
- wbwriter/tests/__init__.py +0 -0
- wbwriter/tests/conftest.py +32 -0
- wbwriter/tests/signals.py +23 -0
- wbwriter/tests/test_filter.py +58 -0
- wbwriter/tests/test_model.py +591 -0
- wbwriter/tests/test_writer.py +38 -0
- wbwriter/tests/tests.py +18 -0
- wbwriter/typings.py +23 -0
- wbwriter/urls.py +83 -0
- wbwriter/viewsets/__init__.py +22 -0
- wbwriter/viewsets/article.py +270 -0
- wbwriter/viewsets/article_type.py +49 -0
- wbwriter/viewsets/buttons.py +61 -0
- wbwriter/viewsets/display/__init__.py +6 -0
- wbwriter/viewsets/display/article.py +404 -0
- wbwriter/viewsets/display/article_type.py +27 -0
- wbwriter/viewsets/display/in_editor_template.py +39 -0
- wbwriter/viewsets/display/meta_information.py +37 -0
- wbwriter/viewsets/display/meta_information_instance.py +28 -0
- wbwriter/viewsets/display/publication.py +55 -0
- wbwriter/viewsets/endpoints/__init__.py +2 -0
- wbwriter/viewsets/endpoints/article.py +12 -0
- wbwriter/viewsets/endpoints/meta_information.py +14 -0
- wbwriter/viewsets/in_editor_template.py +68 -0
- wbwriter/viewsets/menu.py +42 -0
- wbwriter/viewsets/meta_information.py +51 -0
- wbwriter/viewsets/meta_information_instance.py +48 -0
- wbwriter/viewsets/publication.py +117 -0
- wbwriter/viewsets/titles/__init__.py +2 -0
- wbwriter/viewsets/titles/publication_title_config.py +18 -0
- wbwriter/viewsets/titles/reviewer_article_title_config.py +6 -0
- wbwriter-2.2.1.dist-info/METADATA +8 -0
- wbwriter-2.2.1.dist-info/RECORD +70 -0
- wbwriter-2.2.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from datetime import date
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from django.core.files.base import ContentFile
|
|
10
|
+
from django.template.loader import render_to_string
|
|
11
|
+
from PIL import Image, ImageOps
|
|
12
|
+
from slugify import slugify
|
|
13
|
+
from wbcore.contrib.tags.models import Tag
|
|
14
|
+
from wbwriter.pdf_generator import PdfGenerator
|
|
15
|
+
|
|
16
|
+
from .typings import ArticleDTO
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ParserValidationException(Exception):
|
|
20
|
+
def __init__(self, *args, errors=None, **kwargs): # real signature unknown
|
|
21
|
+
self.errors = errors
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PublicationParser(ABC): # pragma: no cover
|
|
25
|
+
def __init__(self, article: ArticleDTO, published_date: date):
|
|
26
|
+
self.article = article
|
|
27
|
+
self.published_date = published_date
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def template_path(self) -> str:
|
|
31
|
+
if hasattr(self, "TEMPLATE_PATH"):
|
|
32
|
+
return self.TEMPLATE_PATH
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"The attribute TEMPLATE_PATH needs to be define in the parser {self.__class__.__name__} in order to generate file"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def article_type(self) -> str:
|
|
39
|
+
if hasattr(self, "ARTICLE_TYPE"):
|
|
40
|
+
return self.ARTICLE_TYPE
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"The attribute ARTICLE_TYPE needs to be define in the parser {self.__class__.__name__} in order to generate content"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def default_section(self) -> str:
|
|
47
|
+
if hasattr(self, "DEFAULT_SECTION"):
|
|
48
|
+
return self.DEFAULT_SECTION
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"The attribute DEFAULT_SECTION needs to be define in the parser {self.__class__.__name__} in order to generate content"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def errors(self) -> list[str]:
|
|
55
|
+
if not hasattr(self, "_errors"):
|
|
56
|
+
raise AssertionError("You must call `.is_valid()` before accessing `.errors`.")
|
|
57
|
+
return self._errors
|
|
58
|
+
|
|
59
|
+
def _is_valid(self) -> bool:
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
def is_valid(self, raise_exception: bool = True) -> bool:
|
|
63
|
+
self._errors = []
|
|
64
|
+
is_valid = self._is_valid()
|
|
65
|
+
if self.errors and raise_exception:
|
|
66
|
+
raise ParserValidationException(errors=self.errors)
|
|
67
|
+
return is_valid
|
|
68
|
+
|
|
69
|
+
def _get_additional_information(self) -> dict[str, any]:
|
|
70
|
+
additional_information = {"type": self.article_type, "is-private": self.article.is_private}
|
|
71
|
+
|
|
72
|
+
for meta_info in self.article.meta_information:
|
|
73
|
+
additional_information[meta_info["meta_information__key"]] = meta_info["boolean_value"]
|
|
74
|
+
return additional_information
|
|
75
|
+
|
|
76
|
+
def get_file(self, context: dict[str, any] | None = None) -> BytesIO | None:
|
|
77
|
+
"""Parses the given object's data and returns the publications content
|
|
78
|
+
rendered as a PDF.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
context: The context data that should be used by the file. Optional. If not provided, it will be fetchted from get_file_context
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
Either None or ByteIO. The latter representing the PDF representation of the content.
|
|
87
|
+
"""
|
|
88
|
+
if not context:
|
|
89
|
+
context = self.get_file_context()
|
|
90
|
+
html = render_to_string(self.template_path, context)
|
|
91
|
+
|
|
92
|
+
if settings.DEBUG:
|
|
93
|
+
if not os.path.isdir("tmp"):
|
|
94
|
+
os.mkdir("tmp")
|
|
95
|
+
with open(f"tmp/{slugify(self.article.title)}.html", "w") as f:
|
|
96
|
+
f.write(html)
|
|
97
|
+
|
|
98
|
+
pdf_generator = PdfGenerator(
|
|
99
|
+
main_html=html,
|
|
100
|
+
custom_css=[],
|
|
101
|
+
side_margin=0,
|
|
102
|
+
extra_vertical_margin=0,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return pdf_generator.render_pdf()
|
|
106
|
+
|
|
107
|
+
def _get_images(self) -> tuple[None | tuple[str, ContentFile], None | tuple[str, ContentFile]]:
|
|
108
|
+
"""Derives the teaser image and its thumbnail form the given object.
|
|
109
|
+
|
|
110
|
+
On the first position there is a tuple that contains the filename and
|
|
111
|
+
the ContentFile for the teaser image, if it is not None.
|
|
112
|
+
|
|
113
|
+
On the second position there is a tuple that contains the filename and
|
|
114
|
+
the ContentFile for the thumbnail of the teaser image, if it is not
|
|
115
|
+
None.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
article: The data that should be published.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
Returns a tuple that may contain the data for a teaser image and
|
|
124
|
+
its thumbnail.
|
|
125
|
+
|
|
126
|
+
On the first position there is a tuple that contains the filename and
|
|
127
|
+
the ContentFile for the teaser image, if it is not None.
|
|
128
|
+
|
|
129
|
+
On the second position there is a tuple that contains the filename and
|
|
130
|
+
the ContentFile for the thumbnail of the teaser image, if it is not
|
|
131
|
+
None.
|
|
132
|
+
"""
|
|
133
|
+
teaser_image = self.article.teaser_image
|
|
134
|
+
if not teaser_image:
|
|
135
|
+
return (None, None)
|
|
136
|
+
|
|
137
|
+
basename_parts = os.path.basename(teaser_image.name).rsplit(".", 1)
|
|
138
|
+
if len(basename_parts) < 2:
|
|
139
|
+
teaser_image_basename = basename_parts[0]
|
|
140
|
+
teaser_image_extension = "png"
|
|
141
|
+
else:
|
|
142
|
+
teaser_image_basename = basename_parts[0]
|
|
143
|
+
teaser_image_extension = basename_parts[1]
|
|
144
|
+
|
|
145
|
+
if len(teaser_image_basename) > 100:
|
|
146
|
+
teaser_image_basename = teaser_image_basename.substring(0, 92)
|
|
147
|
+
if teaser_image_extension == "jpg": # NOTE: Image doesn't understand "jpg"
|
|
148
|
+
teaser_image_extension = "jpeg"
|
|
149
|
+
pil_image_obj = Image.open(self.article.teaser_image)
|
|
150
|
+
|
|
151
|
+
pil_image_obj = ImageOps.exif_transpose(
|
|
152
|
+
pil_image_obj
|
|
153
|
+
) # We need transpose the image accoording to the Exif data for mobile phone shots
|
|
154
|
+
|
|
155
|
+
pil_teaser_image_obj = pil_image_obj.copy()
|
|
156
|
+
pil_teaser_image_obj.thumbnail((1000, 1000))
|
|
157
|
+
teaser_image_tuple = None
|
|
158
|
+
f_teaser = BytesIO()
|
|
159
|
+
try:
|
|
160
|
+
pil_teaser_image_obj.save(f_teaser, format=teaser_image_extension)
|
|
161
|
+
teaser_image_tuple = (
|
|
162
|
+
f"{teaser_image_basename}.{teaser_image_extension}",
|
|
163
|
+
ContentFile(f_teaser.getvalue()),
|
|
164
|
+
)
|
|
165
|
+
finally:
|
|
166
|
+
f_teaser.close()
|
|
167
|
+
|
|
168
|
+
pil_thumbnail_image_obj = pil_image_obj.copy()
|
|
169
|
+
pil_thumbnail_image_obj.thumbnail((510, 510))
|
|
170
|
+
thumbnail_image_tuple = None
|
|
171
|
+
f_thumbnail = BytesIO()
|
|
172
|
+
try:
|
|
173
|
+
pil_thumbnail_image_obj.save(f_thumbnail, format=teaser_image_extension)
|
|
174
|
+
thumbnail_image_tuple = (
|
|
175
|
+
f"{teaser_image_basename}_thumbnail.{teaser_image_extension}",
|
|
176
|
+
ContentFile(f_thumbnail.getvalue()),
|
|
177
|
+
)
|
|
178
|
+
finally:
|
|
179
|
+
f_thumbnail.close()
|
|
180
|
+
|
|
181
|
+
if teaser_image_tuple and thumbnail_image_tuple:
|
|
182
|
+
return (teaser_image_tuple, thumbnail_image_tuple)
|
|
183
|
+
if teaser_image_tuple:
|
|
184
|
+
return (teaser_image_tuple, None)
|
|
185
|
+
if thumbnail_image_tuple:
|
|
186
|
+
return (None, thumbnail_image_tuple)
|
|
187
|
+
return (None, None)
|
|
188
|
+
|
|
189
|
+
def _get_tags(self) -> Iterable[tuple[str, str]]:
|
|
190
|
+
"""Derives the necessary tags from the object data..
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
article: The data that should be published.
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
A list of key, label tag
|
|
199
|
+
|
|
200
|
+
"""
|
|
201
|
+
for tag_id in self.article.tags:
|
|
202
|
+
tag = Tag.objects.get(id=tag_id)
|
|
203
|
+
yield (tag.slug, tag.title)
|
|
204
|
+
|
|
205
|
+
def _get_target(self) -> str:
|
|
206
|
+
"""Returns a string that is unique for the publication target.
|
|
207
|
+
|
|
208
|
+
Example:
|
|
209
|
+
|
|
210
|
+
"website" if the publication goes to a companies own website.
|
|
211
|
+
"""
|
|
212
|
+
return "website"
|
|
213
|
+
|
|
214
|
+
def get_publication_content(self) -> str:
|
|
215
|
+
return json.dumps({"content": self.article.content})
|
|
216
|
+
|
|
217
|
+
def get_file_context(self) -> dict[str, str]:
|
|
218
|
+
return self.article.to_dict()
|
|
219
|
+
|
|
220
|
+
def parse(self, publication) -> None:
|
|
221
|
+
"""Parses the data given in `article` and changes (but doesn't save) the
|
|
222
|
+
given publication instance accordingly.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
publication: An instance of the Publication model. This instance will
|
|
227
|
+
be changed based on the given data in `article`. The instance is not saved! You need to call the save method
|
|
228
|
+
yourself.
|
|
229
|
+
|
|
230
|
+
article: The data that should be parsed into a publication.
|
|
231
|
+
|
|
232
|
+
published_date: The date that the publication should display as the
|
|
233
|
+
publication date.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
if self.is_valid():
|
|
237
|
+
publication.additional_information = self._get_additional_information()
|
|
238
|
+
|
|
239
|
+
publication.content = self.get_publication_content()
|
|
240
|
+
|
|
241
|
+
file = self.get_file()
|
|
242
|
+
if file:
|
|
243
|
+
publication.content_file.save(f"{publication.slug}.pdf", BytesIO(file))
|
|
244
|
+
|
|
245
|
+
(teaser_image, thumbnail_image) = self._get_images()
|
|
246
|
+
if teaser_image and len(teaser_image) == 2:
|
|
247
|
+
publication.teaser_image.save(teaser_image[0], teaser_image[1])
|
|
248
|
+
if thumbnail_image and len(thumbnail_image) == 2:
|
|
249
|
+
publication.thumbnail_image.save(thumbnail_image[0], thumbnail_image[1])
|
|
250
|
+
|
|
251
|
+
publication.tags.clear()
|
|
252
|
+
for tag_key, tag_title in self._get_tags():
|
|
253
|
+
tag = Tag.objects.get_or_create(
|
|
254
|
+
slug=slugify(tag_key), content_type=None, defaults={"title": tag_title}
|
|
255
|
+
)[0]
|
|
256
|
+
publication.tags.add(tag)
|
|
257
|
+
|
|
258
|
+
publication.target = self._get_target()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .article import (
|
|
2
|
+
ArticleFullModelSerializer,
|
|
3
|
+
ArticleModelSerializer,
|
|
4
|
+
_get_plugin_configs,
|
|
5
|
+
can_administrate_article,
|
|
6
|
+
DependantArticleModelSerializer,
|
|
7
|
+
ArticleRepresentionSerializer,
|
|
8
|
+
)
|
|
9
|
+
from .article_type import (
|
|
10
|
+
ArticleTypeModelSerializer,
|
|
11
|
+
ArticleTypeRepresentationSerializer,
|
|
12
|
+
)
|
|
13
|
+
from .in_editor_template import (
|
|
14
|
+
InEditorTemplateModelSerializer,
|
|
15
|
+
InEditorTemplateRepresentationSerializer,
|
|
16
|
+
)
|
|
17
|
+
from .meta_information import (
|
|
18
|
+
MetaInformationInstanceModelSerializer,
|
|
19
|
+
MetaInformationInstanceRepresentationSerializer,
|
|
20
|
+
MetaInformationModelSerializer,
|
|
21
|
+
MetaInformationRepresentationSerializer,
|
|
22
|
+
)
|
|
23
|
+
from .publication import (
|
|
24
|
+
PublicationModelSerializer,
|
|
25
|
+
PublicationParserRepresentationSerializer,
|
|
26
|
+
PublicationParserSerializer,
|
|
27
|
+
PublicationRepresentationSerializer,
|
|
28
|
+
)
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import wbcore.serializers as wb_serializers
|
|
2
|
+
from rest_framework import serializers
|
|
3
|
+
from rest_framework.exceptions import ValidationError
|
|
4
|
+
from rest_framework.reverse import reverse, reverse_lazy
|
|
5
|
+
from wbcore.contrib.directory.models import Person
|
|
6
|
+
from wbcore.contrib.directory.serializers import (
|
|
7
|
+
InternalUserProfileRepresentationSerializer,
|
|
8
|
+
)
|
|
9
|
+
from wbcore.contrib.documents.models import Document
|
|
10
|
+
from wbcore.contrib.tags.serializers import TagSerializerMixin
|
|
11
|
+
from wbcore.serializers import (
|
|
12
|
+
HyperlinkField,
|
|
13
|
+
ModelSerializer,
|
|
14
|
+
PrimaryKeyRelatedField,
|
|
15
|
+
RepresentationSerializer,
|
|
16
|
+
TemplatedJSONTextEditor,
|
|
17
|
+
register_only_instance_resource,
|
|
18
|
+
register_resource,
|
|
19
|
+
)
|
|
20
|
+
from wbwriter.models import (
|
|
21
|
+
Article,
|
|
22
|
+
ArticleType,
|
|
23
|
+
DependantArticle,
|
|
24
|
+
can_administrate_article,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from .article_type import ArticleTypeRepresentationSerializer
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ArticleRepresentionSerializer(RepresentationSerializer):
|
|
31
|
+
class Meta:
|
|
32
|
+
model = Article
|
|
33
|
+
fields = (
|
|
34
|
+
"id",
|
|
35
|
+
"title",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DependantArticleModelSerializer(ModelSerializer):
|
|
40
|
+
_article = ArticleRepresentionSerializer(source="article")
|
|
41
|
+
article_url = HyperlinkField(reverse_name="wbwriter:article-detail", id_field_name="article_id")
|
|
42
|
+
_dependant_article = ArticleRepresentionSerializer(source="dependant_article")
|
|
43
|
+
dependant_article_url = HyperlinkField(
|
|
44
|
+
reverse_name="wbwriter:article-detail", id_field_name="dependant_article_id"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
class Meta:
|
|
48
|
+
model = DependantArticle
|
|
49
|
+
fields = (
|
|
50
|
+
"id",
|
|
51
|
+
"article",
|
|
52
|
+
"_article",
|
|
53
|
+
"article_url",
|
|
54
|
+
"dependant_article",
|
|
55
|
+
"_dependant_article",
|
|
56
|
+
"dependant_article_url",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ArticleModelSerializer(TagSerializerMixin, ModelSerializer):
|
|
61
|
+
"""Serializes a subset of the fields of of the Article model to minimize
|
|
62
|
+
workload on serializing lists of instances.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
author = PrimaryKeyRelatedField(
|
|
66
|
+
many=False, label="Author", queryset=lambda: Person.objects.filter_only_internal()
|
|
67
|
+
) # we lazy load queryset to know evaluate them at runtime
|
|
68
|
+
_author = InternalUserProfileRepresentationSerializer(source="author")
|
|
69
|
+
_qa_reviewer = InternalUserProfileRepresentationSerializer(source="qa_reviewer", many=False)
|
|
70
|
+
|
|
71
|
+
_type = ArticleTypeRepresentationSerializer(source="type")
|
|
72
|
+
|
|
73
|
+
@register_resource()
|
|
74
|
+
def generate(self, instance, request, user):
|
|
75
|
+
return {
|
|
76
|
+
"generate_pdf": reverse("wbwriter:article-generate-pdf", args=[instance.id], request=request),
|
|
77
|
+
"edit": reverse("wbwriter:article-edit", args=[instance.id], request=request),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@register_resource()
|
|
81
|
+
def metainformationinstance(self, instance, request, user):
|
|
82
|
+
return {
|
|
83
|
+
"metainformationinstance": f'{reverse("wbwriter:metainformationinstancearticle-list", args=[instance.id], request=request)}',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@register_resource()
|
|
87
|
+
def dependencies(self, instance, request, user):
|
|
88
|
+
return {
|
|
89
|
+
"dependantarticle-article": reverse(
|
|
90
|
+
viewname="wbwriter:dependantarticle-article-list", args=[instance.id], request=request
|
|
91
|
+
),
|
|
92
|
+
"usedarticle-article": reverse(
|
|
93
|
+
viewname="wbwriter:usedarticle-article-list", args=[instance.id], request=request
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@register_only_instance_resource()
|
|
98
|
+
def preview(self, instance, request, user, **kwargs):
|
|
99
|
+
if document := Document.get_for_object(instance).filter(system_key=instance.system_key).first():
|
|
100
|
+
return {"preview": document.file.url}
|
|
101
|
+
return dict()
|
|
102
|
+
|
|
103
|
+
@register_only_instance_resource()
|
|
104
|
+
def reroll_reviewers(self, instance, request, user, **kwargs):
|
|
105
|
+
if can_administrate_article(instance, user):
|
|
106
|
+
return {
|
|
107
|
+
"reroll_peer": reverse("wbwriter:article-reroll-peer", args=[instance.id], request=request),
|
|
108
|
+
"reroll_qa": reverse("wbwriter:article-reroll-qa", args=[instance.id], request=request),
|
|
109
|
+
"reroll_peer_and_qa": reverse(
|
|
110
|
+
"wbwriter:article-reroll-peer-and-qa", args=[instance.id], request=request
|
|
111
|
+
),
|
|
112
|
+
"assign_new_author": reverse(
|
|
113
|
+
"wbwriter:article-assign-new-author", args=[instance.id], request=request
|
|
114
|
+
),
|
|
115
|
+
}
|
|
116
|
+
else:
|
|
117
|
+
return {}
|
|
118
|
+
|
|
119
|
+
@register_only_instance_resource()
|
|
120
|
+
def publications(self, instance, request, user, view=None, **kwargs):
|
|
121
|
+
if view and instance.publications.exists():
|
|
122
|
+
return {
|
|
123
|
+
"publications": f"{reverse('wbwriter:publication-list', request=request)}?content_type={view.content_type.id}&object_id={instance.id}"
|
|
124
|
+
}
|
|
125
|
+
return {}
|
|
126
|
+
|
|
127
|
+
def create(self, validated_data):
|
|
128
|
+
if request := self.context.get("request"):
|
|
129
|
+
validated_data["author"] = request.user.profile
|
|
130
|
+
return super().create(validated_data)
|
|
131
|
+
|
|
132
|
+
def validate(self, data):
|
|
133
|
+
articles = Article.objects.filter(name=data.get("name"))
|
|
134
|
+
if self.instance is not None:
|
|
135
|
+
articles = articles.exclude(id=self.instance.id)
|
|
136
|
+
if data.get("title") in ["", None] and self.instance.title is None:
|
|
137
|
+
data["title"] = data.get("name")
|
|
138
|
+
|
|
139
|
+
if articles.exists():
|
|
140
|
+
raise ValidationError({"name": ["Name already exists."]})
|
|
141
|
+
|
|
142
|
+
return data
|
|
143
|
+
|
|
144
|
+
class Meta:
|
|
145
|
+
model = Article
|
|
146
|
+
fields = (
|
|
147
|
+
"id",
|
|
148
|
+
"name",
|
|
149
|
+
"title",
|
|
150
|
+
"slug",
|
|
151
|
+
"type",
|
|
152
|
+
"_type",
|
|
153
|
+
"status",
|
|
154
|
+
"teaser_image",
|
|
155
|
+
"created",
|
|
156
|
+
"modified",
|
|
157
|
+
"author",
|
|
158
|
+
"_author",
|
|
159
|
+
"qa_reviewer",
|
|
160
|
+
"_qa_reviewer",
|
|
161
|
+
"tags",
|
|
162
|
+
"_tags",
|
|
163
|
+
"_additional_resources",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _get_plugin_configs(request):
|
|
168
|
+
return {
|
|
169
|
+
"plugins": " ".join(
|
|
170
|
+
[
|
|
171
|
+
"advlist autolink lists link image charmap print preview anchor",
|
|
172
|
+
"searchreplace visualblocks fullscreen noneditable template",
|
|
173
|
+
"insertdatetime table advtable help wordcount code",
|
|
174
|
+
"pagebreak hr imagetools powerpaste lance flite",
|
|
175
|
+
]
|
|
176
|
+
),
|
|
177
|
+
"toolbar": " ".join(
|
|
178
|
+
[
|
|
179
|
+
"undo redo | formatselect | bold italic underline strikethrough backcolor | ",
|
|
180
|
+
"alignleft aligncenter alignright alignjustify | ",
|
|
181
|
+
"bullist numlist outdent indent | pastetext removeformat | help | code fullscreen | lance | ",
|
|
182
|
+
"flite-toggletracking flite-toggleshow | flite-acceptone flite-rejectone | flite-acceptall flite-rejectall",
|
|
183
|
+
]
|
|
184
|
+
),
|
|
185
|
+
"menu": {
|
|
186
|
+
"file": {
|
|
187
|
+
"title": "File",
|
|
188
|
+
"items": "newdocument restoredraft | preview | print",
|
|
189
|
+
},
|
|
190
|
+
"edit": {
|
|
191
|
+
"title": "Edit",
|
|
192
|
+
"items": "undo redo | cut copy paste | selectall | searchreplace",
|
|
193
|
+
},
|
|
194
|
+
"view": {
|
|
195
|
+
"title": "View",
|
|
196
|
+
"items": "visualaid visualchars visualblocks | spellchecker | preview fullscreen",
|
|
197
|
+
},
|
|
198
|
+
"insert": {
|
|
199
|
+
"title": "Insert",
|
|
200
|
+
"items": " | ".join(
|
|
201
|
+
[
|
|
202
|
+
"image link template codesample inserttable",
|
|
203
|
+
"charmap emoticons hr",
|
|
204
|
+
"pagebreak nonbreaking anchor toc",
|
|
205
|
+
"insertdatetime",
|
|
206
|
+
]
|
|
207
|
+
),
|
|
208
|
+
},
|
|
209
|
+
"format": {
|
|
210
|
+
"title": "Format",
|
|
211
|
+
"items": "bold italic underline strikethrough superscript subscript codeformat | formats blockformats"
|
|
212
|
+
+ " fontformats fontsizes align lineheight | forecolor backcolor | removeformat",
|
|
213
|
+
},
|
|
214
|
+
"tools": {
|
|
215
|
+
"title": "Tools",
|
|
216
|
+
"items": "code | spellchecker spellcheckerlanguage | wordcount",
|
|
217
|
+
},
|
|
218
|
+
"table": {
|
|
219
|
+
"title": "Table",
|
|
220
|
+
"items": "inserttable | cell row column | tableprops deletetable",
|
|
221
|
+
},
|
|
222
|
+
# "tc": {"title": "Comments", "items": "addcomment showcomments deleteallconversations"},
|
|
223
|
+
"help": {"title": "Help", "items": "help"},
|
|
224
|
+
},
|
|
225
|
+
"paste_as_text": True,
|
|
226
|
+
"powerpaste_word_import": "clean",
|
|
227
|
+
"powerpaste_googledocs_import": "clean",
|
|
228
|
+
"powerpaste_html_import": "clean",
|
|
229
|
+
"browser_spellcheck": True,
|
|
230
|
+
"atp": {
|
|
231
|
+
"templates": reverse_lazy("wbwriter:in-editor-template-list", request=request),
|
|
232
|
+
},
|
|
233
|
+
"content_style": "body{margin:0px 12px;}",
|
|
234
|
+
"deprecation_warnings": False,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class ArticleFullModelSerializer(ArticleModelSerializer):
|
|
239
|
+
"""Serializes the full set of the fields of of the Article model."""
|
|
240
|
+
|
|
241
|
+
author = wb_serializers.PrimaryKeyRelatedField(
|
|
242
|
+
many=False,
|
|
243
|
+
label="Author",
|
|
244
|
+
read_only=lambda view: view.instance
|
|
245
|
+
and (view.can_edit_article_author and view.instance.status == Article.Status.APPROVED),
|
|
246
|
+
queryset=lambda: Person.objects.filter_only_internal(),
|
|
247
|
+
default=wb_serializers.CurrentUserDefault("profile"),
|
|
248
|
+
)
|
|
249
|
+
name = wb_serializers.CharField(
|
|
250
|
+
max_length=1024,
|
|
251
|
+
label="Name",
|
|
252
|
+
help_text="A unique name to reference this article.",
|
|
253
|
+
read_only=lambda view: view.instance
|
|
254
|
+
and (view.can_edit_article_meta_data and view.instance.status == Article.Status.APPROVED),
|
|
255
|
+
)
|
|
256
|
+
title = wb_serializers.CharField(
|
|
257
|
+
max_length=1024,
|
|
258
|
+
label="Title",
|
|
259
|
+
required=False,
|
|
260
|
+
help_text="The title of the article that is going to be used when imported into other articles. "
|
|
261
|
+
+ "Defaults to the name of the article when not set.",
|
|
262
|
+
read_only=lambda view: view.instance
|
|
263
|
+
and (view.can_edit_article_meta_data and view.instance.status == Article.Status.APPROVED),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
teaser_image = wb_serializers.ImageField(
|
|
267
|
+
label="Teaser image",
|
|
268
|
+
required=False,
|
|
269
|
+
read_only=lambda view: view.instance
|
|
270
|
+
and (view.can_edit_article_content and view.instance.status == Article.Status.APPROVED),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
content = TemplatedJSONTextEditor(
|
|
274
|
+
templates="wbwriter:in-editor-template-list",
|
|
275
|
+
default_editor_config=_get_plugin_configs,
|
|
276
|
+
default="",
|
|
277
|
+
label="Content",
|
|
278
|
+
read_only=lambda view: view.instance
|
|
279
|
+
and (view.can_edit_article_content and view.instance.status == Article.Status.APPROVED),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
type = wb_serializers.PrimaryKeyRelatedField(
|
|
283
|
+
label="Type",
|
|
284
|
+
many=False,
|
|
285
|
+
queryset=ArticleType.objects.all(),
|
|
286
|
+
read_only=lambda view: view.instance
|
|
287
|
+
and (view.can_edit_article_type and view.instance.status == Article.Status.APPROVED),
|
|
288
|
+
)
|
|
289
|
+
_type = ArticleTypeRepresentationSerializer(source="type", many=False)
|
|
290
|
+
feedback_contact = wb_serializers.PrimaryKeyRelatedField(
|
|
291
|
+
label="Feedback Contact",
|
|
292
|
+
many=False,
|
|
293
|
+
required=False,
|
|
294
|
+
default=wb_serializers.DefaultAttributeFromObject(source="author.id"),
|
|
295
|
+
queryset=lambda: Person.objects.filter_only_internal(), # TODO: Filter out the people from the qa reviewer person group
|
|
296
|
+
)
|
|
297
|
+
_feedback_contact = InternalUserProfileRepresentationSerializer(source="feedback_contact", many=False)
|
|
298
|
+
reviewer = PrimaryKeyRelatedField(many=False, label="Reviewer", read_only=True)
|
|
299
|
+
_reviewer = InternalUserProfileRepresentationSerializer(source="reviewer", many=False)
|
|
300
|
+
|
|
301
|
+
peer_reviewer = PrimaryKeyRelatedField(
|
|
302
|
+
many=False,
|
|
303
|
+
label="Peer Reviewers",
|
|
304
|
+
read_only=lambda view: not view.can_administrate_article or not view.new_mode,
|
|
305
|
+
)
|
|
306
|
+
_peer_reviewer = InternalUserProfileRepresentationSerializer(source="peer_reviewer", many=False)
|
|
307
|
+
|
|
308
|
+
qa_reviewer = PrimaryKeyRelatedField(
|
|
309
|
+
many=False, label="QA Reviewer", read_only=lambda view: not view.can_administrate_article or not view.new_mode
|
|
310
|
+
)
|
|
311
|
+
_qa_reviewer = InternalUserProfileRepresentationSerializer(source="qa_reviewer", many=False)
|
|
312
|
+
|
|
313
|
+
def validate(self, data):
|
|
314
|
+
data = super().validate(data)
|
|
315
|
+
if "type" in data:
|
|
316
|
+
if (
|
|
317
|
+
self.instance
|
|
318
|
+
and data.get("type")
|
|
319
|
+
and data.get("type").slug != "ddq"
|
|
320
|
+
and (
|
|
321
|
+
("author" in data and not data.get("author"))
|
|
322
|
+
or ("author" not in data and self.instance.author is None)
|
|
323
|
+
)
|
|
324
|
+
):
|
|
325
|
+
raise serializers.ValidationError(
|
|
326
|
+
{"type": 'This must be set to "DDQ" as long as no author has been assigned!'}
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
return data
|
|
330
|
+
|
|
331
|
+
class Meta:
|
|
332
|
+
model = Article
|
|
333
|
+
fields = (
|
|
334
|
+
"id",
|
|
335
|
+
"name",
|
|
336
|
+
"title",
|
|
337
|
+
"slug",
|
|
338
|
+
"type",
|
|
339
|
+
"_type",
|
|
340
|
+
"created",
|
|
341
|
+
"modified",
|
|
342
|
+
"teaser_image",
|
|
343
|
+
"content",
|
|
344
|
+
"status",
|
|
345
|
+
"author",
|
|
346
|
+
"_author",
|
|
347
|
+
"feedback_contact",
|
|
348
|
+
"_feedback_contact",
|
|
349
|
+
"reviewer",
|
|
350
|
+
"_reviewer",
|
|
351
|
+
"peer_reviewer",
|
|
352
|
+
"_peer_reviewer",
|
|
353
|
+
"qa_reviewer",
|
|
354
|
+
"_qa_reviewer",
|
|
355
|
+
"is_private",
|
|
356
|
+
"tags",
|
|
357
|
+
"_tags",
|
|
358
|
+
"_additional_resources",
|
|
359
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from wbcore.serializers import ModelSerializer, RepresentationSerializer
|
|
2
|
+
from wbwriter.models import ArticleType
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ArticleTypeModelSerializer(ModelSerializer):
|
|
6
|
+
class Meta:
|
|
7
|
+
model = ArticleType
|
|
8
|
+
fields = ("id", "label", "peer_reviewers", "qa_reviewers")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ArticleTypeRepresentationSerializer(RepresentationSerializer):
|
|
12
|
+
class Meta:
|
|
13
|
+
model = ArticleType
|
|
14
|
+
fields = ("id", "label")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from wbcore.serializers import HyperlinkField, ModelSerializer, RepresentationSerializer
|
|
2
|
+
from wbwriter.models import InEditorTemplate
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class InEditorTemplateModelSerializer(ModelSerializer):
|
|
6
|
+
"""Serializes the complete InEditorTemplate model."""
|
|
7
|
+
|
|
8
|
+
class Meta:
|
|
9
|
+
model = InEditorTemplate
|
|
10
|
+
fields = (
|
|
11
|
+
"id",
|
|
12
|
+
"uuid",
|
|
13
|
+
"title",
|
|
14
|
+
"description",
|
|
15
|
+
"style",
|
|
16
|
+
"template",
|
|
17
|
+
"modified",
|
|
18
|
+
"is_stand_alone_template",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InEditorTemplateRepresentationSerializer(RepresentationSerializer):
|
|
23
|
+
"""Serializes the minimum number of fields of the InEditorTemplate model
|
|
24
|
+
that are needed to identify a template."""
|
|
25
|
+
|
|
26
|
+
_detail = HyperlinkField(reverse_name="wbwriter:in-editor-template-detail")
|
|
27
|
+
|
|
28
|
+
class Meta:
|
|
29
|
+
model = InEditorTemplate
|
|
30
|
+
fields = (
|
|
31
|
+
"id",
|
|
32
|
+
"uuid",
|
|
33
|
+
"title",
|
|
34
|
+
"description",
|
|
35
|
+
"is_stand_alone_template",
|
|
36
|
+
"_detail",
|
|
37
|
+
)
|