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.

Files changed (70) hide show
  1. wbwriter/__init__.py +1 -0
  2. wbwriter/admin.py +142 -0
  3. wbwriter/apps.py +5 -0
  4. wbwriter/dynamic_preferences_registry.py +15 -0
  5. wbwriter/factories/__init__.py +13 -0
  6. wbwriter/factories/article.py +181 -0
  7. wbwriter/factories/meta_information.py +29 -0
  8. wbwriter/filters/__init__.py +2 -0
  9. wbwriter/filters/article.py +47 -0
  10. wbwriter/filters/metainformationinstance.py +24 -0
  11. wbwriter/migrations/0001_initial_squashed_squashed_0008_alter_article_author_alter_article_feedback_contact_and_more.py +653 -0
  12. wbwriter/migrations/0009_dependantarticle.py +41 -0
  13. wbwriter/migrations/0010_alter_article_options.py +20 -0
  14. wbwriter/migrations/0011_auto_20240103_0953.py +39 -0
  15. wbwriter/migrations/__init__.py +0 -0
  16. wbwriter/models/__init__.py +9 -0
  17. wbwriter/models/article.py +1179 -0
  18. wbwriter/models/article_type.py +59 -0
  19. wbwriter/models/block.py +24 -0
  20. wbwriter/models/block_parameter.py +19 -0
  21. wbwriter/models/in_editor_template.py +102 -0
  22. wbwriter/models/meta_information.py +87 -0
  23. wbwriter/models/mixins.py +9 -0
  24. wbwriter/models/publication_models.py +170 -0
  25. wbwriter/models/style.py +13 -0
  26. wbwriter/models/template.py +34 -0
  27. wbwriter/pdf_generator.py +172 -0
  28. wbwriter/publication_parser.py +258 -0
  29. wbwriter/serializers/__init__.py +28 -0
  30. wbwriter/serializers/article.py +359 -0
  31. wbwriter/serializers/article_type.py +14 -0
  32. wbwriter/serializers/in_editor_template.py +37 -0
  33. wbwriter/serializers/meta_information.py +67 -0
  34. wbwriter/serializers/publication.py +82 -0
  35. wbwriter/templatetags/__init__.py +0 -0
  36. wbwriter/templatetags/writer.py +72 -0
  37. wbwriter/tests/__init__.py +0 -0
  38. wbwriter/tests/conftest.py +32 -0
  39. wbwriter/tests/signals.py +23 -0
  40. wbwriter/tests/test_filter.py +58 -0
  41. wbwriter/tests/test_model.py +591 -0
  42. wbwriter/tests/test_writer.py +38 -0
  43. wbwriter/tests/tests.py +18 -0
  44. wbwriter/typings.py +23 -0
  45. wbwriter/urls.py +83 -0
  46. wbwriter/viewsets/__init__.py +22 -0
  47. wbwriter/viewsets/article.py +270 -0
  48. wbwriter/viewsets/article_type.py +49 -0
  49. wbwriter/viewsets/buttons.py +61 -0
  50. wbwriter/viewsets/display/__init__.py +6 -0
  51. wbwriter/viewsets/display/article.py +404 -0
  52. wbwriter/viewsets/display/article_type.py +27 -0
  53. wbwriter/viewsets/display/in_editor_template.py +39 -0
  54. wbwriter/viewsets/display/meta_information.py +37 -0
  55. wbwriter/viewsets/display/meta_information_instance.py +28 -0
  56. wbwriter/viewsets/display/publication.py +55 -0
  57. wbwriter/viewsets/endpoints/__init__.py +2 -0
  58. wbwriter/viewsets/endpoints/article.py +12 -0
  59. wbwriter/viewsets/endpoints/meta_information.py +14 -0
  60. wbwriter/viewsets/in_editor_template.py +68 -0
  61. wbwriter/viewsets/menu.py +42 -0
  62. wbwriter/viewsets/meta_information.py +51 -0
  63. wbwriter/viewsets/meta_information_instance.py +48 -0
  64. wbwriter/viewsets/publication.py +117 -0
  65. wbwriter/viewsets/titles/__init__.py +2 -0
  66. wbwriter/viewsets/titles/publication_title_config.py +18 -0
  67. wbwriter/viewsets/titles/reviewer_article_title_config.py +6 -0
  68. wbwriter-2.2.1.dist-info/METADATA +8 -0
  69. wbwriter-2.2.1.dist-info/RECORD +70 -0
  70. 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
+ )