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,59 @@
1
+ from django.db import models
2
+ from slugify import slugify
3
+ from wbcore.models import WBModel
4
+
5
+
6
+ class ArticleType(WBModel):
7
+ """An arbitrary type for articles.
8
+
9
+ The type of an article helps filtering out undesired categories of
10
+ articles.
11
+ Furthermore, having the article type in a separate model allows adding,
12
+ removing, and changing types during runtime.
13
+
14
+ Attributes
15
+ ----------
16
+ label : models.CharField
17
+ The unique label for this type.
18
+ This is meant to be displayed to users.
19
+ slug : models.SlugField
20
+ A slugified version fo the label.
21
+ This is used for searching ArticleTypes.
22
+ parsers : models.ManyToManyField
23
+ One or more optional parsers. The parser are used to convert the
24
+ `content` of an article and its associated in-editor template(s)
25
+ to something that can be published.
26
+ """
27
+
28
+ label = models.CharField(max_length=255, unique=True)
29
+ slug = models.SlugField(max_length=255, null=True, blank=True, unique=True)
30
+ parsers = models.ManyToManyField("wbwriter.PublicationParser", related_name="publication_parsers", blank=True)
31
+ peer_reviewers = models.ManyToManyField("directory.Person", related_name="article_type_peer_reviewers")
32
+ qa_reviewers = models.ManyToManyField("directory.Person", related_name="article_type_qa_reviewers")
33
+
34
+ @classmethod
35
+ def get_endpoint_basename(self):
36
+ return "wbwriter:articletyperepresentation"
37
+
38
+ @classmethod
39
+ def get_representation_endpoint(cls):
40
+ return "wbwriter:articletyperepresentation-list"
41
+
42
+ @classmethod
43
+ def get_representation_value_key(cls):
44
+ return "id"
45
+
46
+ @classmethod
47
+ def get_representation_label_key(cls):
48
+ return "{{label}}"
49
+
50
+ def __str__(self):
51
+ return self.label
52
+
53
+ def save(self, *args, **kwargs):
54
+ self.slug = slugify(self.label)
55
+ super().save(*args, **kwargs)
56
+
57
+ class Meta:
58
+ verbose_name = "Article Type"
59
+ verbose_name_plural = "Article Types"
@@ -0,0 +1,24 @@
1
+ from django.db import models
2
+ from django.template import Context
3
+ from django.template import Template as DjangoTemplate
4
+ from slugify import slugify
5
+
6
+
7
+ class Block(models.Model):
8
+ title = models.CharField(max_length=512)
9
+ key = models.CharField(max_length=512, null=True, blank=True)
10
+ html = models.TextField(default="")
11
+
12
+ def __str__(self):
13
+ return self.title
14
+
15
+ def save(self, *args, **kwargs):
16
+ self.key = slugify(self.title)
17
+ super().save(*args, **kwargs)
18
+
19
+ def parse_parameters(self, parameters):
20
+ for index, parameter in enumerate(self.parameters.all().order_by("order")):
21
+ yield parameter.title, parameter.parse_parameter(parameters[index])
22
+
23
+ def render(self, parameters):
24
+ return DjangoTemplate(self.html).render(Context(dict(self.parse_parameters(parameters))))
@@ -0,0 +1,19 @@
1
+ import json
2
+
3
+ from django.db import models
4
+
5
+
6
+ class BlockParameter(models.Model):
7
+ block = models.ForeignKey(to="wbwriter.Block", related_name="parameters", on_delete=models.CASCADE)
8
+
9
+ order = models.PositiveIntegerField()
10
+ title = models.CharField(max_length=255)
11
+ many = models.BooleanField(default=False)
12
+
13
+ def __str__(self):
14
+ return f"{self.block.title}: {self.title}"
15
+
16
+ def parse_parameter(self, parameter):
17
+ if self.many:
18
+ return json.loads(parameter)
19
+ return parameter
@@ -0,0 +1,102 @@
1
+ from django.db import models
2
+ from wbcore.models import WBModel
3
+
4
+
5
+ class InEditorTemplate(WBModel):
6
+ """A template that the user can choose and configure in the context of
7
+ the WYSIWYG editor.
8
+
9
+ In-Editor templates define CSS styles that need to be applied in
10
+ combination with the HTML template, and a number of other
11
+ In-Editor templates that may be nested within the given In-Editor
12
+ template.
13
+
14
+ Note that templates can not allow themself to be nested inside them.
15
+
16
+ Attributes
17
+ ----------
18
+ uuid : models.CharField
19
+ A unique identifier. The `uuid` must pass as a valid CSS class and
20
+ must less than 256 characters long.
21
+
22
+ The regular expression for valid CSS classes: "-?[_a-zA-Z]+[_a-zA-Z0-9-]*"
23
+
24
+ See section 4.1.3, second paragraph on
25
+ https://www.w3.org/TR/CSS21/syndata.html#characters
26
+ title : models.CharField
27
+ A human readable title.
28
+ description : models.TextField
29
+ A descriptive text that outlines the use case and possible
30
+ configurations of the template.
31
+ style : models.TextField, optional
32
+ The CSS that is required to properly display the HTML template.
33
+ template : models.TextField
34
+ The HTML that makes up the template.
35
+
36
+ TODO: WRITE ABOUT SCHEMA AND CONFIGURATION OPTIONS.
37
+ is_stand_alone_template : models.BooleanField, default=True
38
+ Signifies whether this template can be used without a surrounding
39
+ template (i.e. as root node of the article).
40
+ """
41
+
42
+ uuid = models.CharField(max_length=255, unique=True)
43
+ title = models.CharField(
44
+ verbose_name="Title",
45
+ max_length=255,
46
+ help_text="The title should be unique but doesn't need to be.",
47
+ )
48
+ description = models.TextField(
49
+ verbose_name="Description",
50
+ default="",
51
+ help_text="A brief text that describes the use case for this template.",
52
+ )
53
+ style = models.TextField(
54
+ verbose_name="Template CSS",
55
+ default="",
56
+ blank=True,
57
+ null=True,
58
+ help_text="The CSS that styles the templates HTML.",
59
+ )
60
+ template = models.TextField(
61
+ verbose_name="Template HTML",
62
+ default="",
63
+ help_text="The HTML code of the template.",
64
+ )
65
+ # configuration_field_definitions = models.JSONField(
66
+ # verbose_name="Configuration Field Definitions",
67
+ # help_text="Maps configuration IDs to field definitions for rendering a form.",
68
+ # blank=True,
69
+ # null=True,
70
+ # )
71
+ # configuration_form_layout = models.JSONField(
72
+ # verbose_name="Configuration Form Layout",
73
+ # help_text="The field layout of the configuration form.",
74
+ # blank=True,
75
+ # null=True,
76
+ # )
77
+ modified = models.DateTimeField(
78
+ verbose_name="Last modification date and time",
79
+ auto_now=True,
80
+ help_text="The last time this template has been edited.",
81
+ )
82
+
83
+ is_stand_alone_template = models.BooleanField(default=True)
84
+
85
+ def __str__(self):
86
+ return self.title
87
+
88
+ @classmethod
89
+ def get_endpoint_basename(self):
90
+ return "wbwriter:in-editor-template"
91
+
92
+ @classmethod
93
+ def get_representation_endpoint(cls):
94
+ return "wbwriter:in-editor-template-list"
95
+
96
+ @classmethod
97
+ def get_representation_value_key(cls):
98
+ return "id"
99
+
100
+ @classmethod
101
+ def get_representation_label_key(cls):
102
+ return "{{ title }}"
@@ -0,0 +1,87 @@
1
+ from django.db import models
2
+ from django.dispatch import receiver
3
+ from wbcore.models import WBModel
4
+
5
+
6
+ class MetaInformation(WBModel):
7
+ class MetaInformationType(models.TextChoices):
8
+ # NOTE: This is just one value for now. However, this structure allows us to add more data type in the future.
9
+ BOOLEAN = "BOOLEAN", "Boolean"
10
+
11
+ article_type = models.ManyToManyField(to="wbwriter.ArticleType", related_name="meta_information")
12
+ name = models.CharField(max_length=255, null=False, blank=False, unique=True)
13
+ key = models.CharField(max_length=255, null=False, blank=False, unique=True)
14
+ meta_information_type = models.CharField(
15
+ max_length=24, choices=MetaInformationType.choices, default=MetaInformationType.BOOLEAN
16
+ )
17
+ boolean_default = models.BooleanField(null=True, blank=True)
18
+
19
+ def __str__(self) -> str:
20
+ return f"{self.name} ({self.key})"
21
+
22
+ class Meta:
23
+ verbose_name = "Meta Information"
24
+ verbose_name_plural = "Meta Information"
25
+
26
+ @classmethod
27
+ def get_endpoint_basename(self):
28
+ return "wbwriter:metainformation"
29
+
30
+ @classmethod
31
+ def get_representation_endpoint(cls):
32
+ return "wbwriter:metainformation-list"
33
+
34
+ @classmethod
35
+ def get_representation_value_key(cls):
36
+ return "id"
37
+
38
+ @classmethod
39
+ def get_representation_label_key(cls):
40
+ return "{{name}} ({{key}})"
41
+
42
+
43
+ class MetaInformationInstance(WBModel):
44
+ article = models.ForeignKey(to="wbwriter.Article", related_name="meta_information", on_delete=models.CASCADE)
45
+
46
+ meta_information = models.ForeignKey(
47
+ to="wbwriter.MetaInformation", related_name="instances", on_delete=models.CASCADE
48
+ )
49
+ boolean_value = models.BooleanField(null=True, blank=True)
50
+
51
+ def __str__(self) -> str:
52
+ return f"{self.meta_information} / {self.article}: {self.boolean_value}"
53
+
54
+ class Meta:
55
+ verbose_name = "Meta Information Instance"
56
+ verbose_name_plural = "Meta Information Instances"
57
+
58
+ constraints = [
59
+ models.UniqueConstraint(fields=["meta_information", "article"], name="unique_meta_information_article")
60
+ ]
61
+
62
+ @classmethod
63
+ def get_endpoint_basename(self):
64
+ return "wbwriter:metainformationinstance"
65
+
66
+ @classmethod
67
+ def get_representation_endpoint(cls):
68
+ return "wbwriter:metainformationinstance-list"
69
+
70
+ @classmethod
71
+ def get_representation_value_key(cls):
72
+ return "id"
73
+
74
+ @classmethod
75
+ def get_representation_label_key(cls):
76
+ return "{{meta_information.name}}"
77
+
78
+
79
+ @receiver(models.signals.post_save, sender="wbwriter.Article")
80
+ def create_meta_information_instances(sender, instance, created, **kwargs):
81
+ if created and instance.type:
82
+ for meta_information in instance.type.meta_information.all():
83
+ MetaInformationInstance.objects.get_or_create(
84
+ meta_information=meta_information,
85
+ article=instance,
86
+ defaults={"boolean_value": meta_information.boolean_default},
87
+ )
@@ -0,0 +1,9 @@
1
+ from django.db import models
2
+
3
+
4
+ class PublishableMixin(models.Model):
5
+ def get_publication_metadata(self) -> dict[str, str]:
6
+ raise NotImplementedError("You must implement get_publication_metadata")
7
+
8
+ class Meta:
9
+ abstract = True
@@ -0,0 +1,170 @@
1
+ import datetime
2
+ from importlib import import_module
3
+
4
+ from django.contrib.contenttypes.fields import GenericForeignKey
5
+ from django.contrib.contenttypes.models import ContentType
6
+ from django.db import models
7
+ from django.utils.functional import cached_property
8
+ from rest_framework.reverse import reverse
9
+ from slugify import slugify
10
+ from wbcore.contrib.tags.models import TagModelMixin
11
+
12
+
13
+ class Publication(TagModelMixin, models.Model):
14
+ """A publication of anything.
15
+
16
+ A publication stores content prepared for a specific platform or use
17
+ case. For instance, it stores an HTML string for publishing something on
18
+ web.
19
+ """
20
+
21
+ class Meta:
22
+ verbose_name = "Publication"
23
+ verbose_name_plural = "Publications"
24
+
25
+ title = models.CharField(max_length=1024)
26
+
27
+ slug = models.CharField(max_length=1024, blank=True)
28
+
29
+ target = models.CharField(max_length=256)
30
+
31
+ teaser_image = models.ImageField(blank=True, null=True, upload_to="writer/publication/teasers")
32
+ thumbnail_image = models.ImageField(blank=True, null=True, upload_to="writer/publication/thumbnails")
33
+
34
+ author = models.ForeignKey(
35
+ "directory.Person",
36
+ related_name="publication",
37
+ on_delete=models.PROTECT,
38
+ blank=True,
39
+ null=True,
40
+ )
41
+
42
+ created = models.DateField(
43
+ verbose_name="Creation Date",
44
+ auto_now_add=True,
45
+ help_text="The date on which this has been created.",
46
+ )
47
+
48
+ modified = models.DateTimeField(
49
+ verbose_name="Last Modification Datetime",
50
+ auto_now=True,
51
+ help_text="The date and time on which this has been modified last.",
52
+ )
53
+
54
+ description = models.TextField(default="", blank=True)
55
+
56
+ content = models.TextField(default="")
57
+
58
+ content_file = models.FileField(
59
+ max_length=256,
60
+ upload_to="writer/publication/content_files",
61
+ blank=True,
62
+ null=True,
63
+ )
64
+
65
+ parser = models.ForeignKey(
66
+ "wbwriter.PublicationParser",
67
+ related_name="parsed_publication",
68
+ on_delete=models.PROTECT,
69
+ )
70
+
71
+ additional_information = models.JSONField(default=dict, null=True, blank=True)
72
+
73
+ content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
74
+ object_id = models.PositiveIntegerField()
75
+ content_object = GenericForeignKey("content_type", "object_id")
76
+
77
+ @classmethod
78
+ def create_or_update_from_parser_and_object(cls, parser, generic_object):
79
+ if hasattr(generic_object, "_build_dto") and callable(generic_object._build_dto):
80
+ ctype = ContentType.objects.get_for_model(generic_object)
81
+ pub, created = cls.objects.get_or_create(parser=parser, content_type=ctype, object_id=generic_object.id)
82
+ publ_metadata = generic_object.get_publication_metadata()
83
+ for k, v in publ_metadata.items():
84
+ setattr(pub, k, v)
85
+ pub.parser.parser_class(
86
+ generic_object._build_dto(), datetime.date.today() if created else pub.created
87
+ ).parse(pub)
88
+ pub.save()
89
+
90
+ @classmethod
91
+ def get_endpoint_basename(self):
92
+ return "wbwriter:publication"
93
+
94
+ @classmethod
95
+ def get_representation_endpoint(cls):
96
+ return "wbwriter:publicationrepresentation-list"
97
+
98
+ def save(self, *args, **kwargs):
99
+ self.slug = slugify(self.title)
100
+ super().save(*args, **kwargs)
101
+
102
+ def get_tag_detail_endpoint(self):
103
+ return reverse("wbwriter:publication-detail", [self.id])
104
+
105
+ def get_tag_representation(self):
106
+ return self.title
107
+
108
+
109
+ class PublicationParser(models.Model):
110
+ """A parser meant to be used to parse something into a publication.
111
+
112
+ Attributes
113
+ ----------
114
+ title : models.CharField
115
+ The unique title for this parser.
116
+
117
+ parser_path : models.CharField
118
+ A dotted path to the parser file relatoive to the projects root
119
+ (ROOT/projects/).
120
+
121
+ created : models.DateField
122
+ The date on which this publication has been created.
123
+
124
+ content : models.TextField
125
+ The content of this publication in text form.
126
+
127
+ content_file : models.FileField
128
+ An optional file attachment, that represents the publications content.
129
+ """
130
+
131
+ class Meta:
132
+ verbose_name = "PublicationParser"
133
+ verbose_name_plural = "PublicationParsers"
134
+
135
+ title = models.CharField(
136
+ max_length=1024,
137
+ unique=True,
138
+ )
139
+ slug = models.CharField(max_length=1024, blank=True)
140
+ parser_path = models.CharField(
141
+ max_length=1024,
142
+ unique=True,
143
+ )
144
+
145
+ @cached_property
146
+ def parser_class(self):
147
+ return import_module(self.parser_path).Parser
148
+
149
+ @classmethod
150
+ def get_endpoint_basename(cls):
151
+ return "wbwriter:publicationparser"
152
+
153
+ @classmethod
154
+ def get_representation_endpoint(cls):
155
+ return "wbwriter:publicationparserrepresentation-list"
156
+
157
+ @classmethod
158
+ def get_representation_value_key(cls):
159
+ return "id"
160
+
161
+ @classmethod
162
+ def get_representation_label_key(cls):
163
+ return "{{title}}"
164
+
165
+ def save(self, *args, **kwargs):
166
+ self.slug = slugify(self.title)
167
+ super().save(*args, **kwargs)
168
+
169
+ def __str__(self):
170
+ return f"{self.title}"
@@ -0,0 +1,13 @@
1
+ from django.db import models
2
+
3
+
4
+ class Style(models.Model):
5
+ """
6
+ A style is a valid CSS construct.
7
+ """
8
+
9
+ title = models.CharField(max_length=255, unique=True)
10
+ style = models.TextField(default="")
11
+
12
+ def __str__(self):
13
+ return self.title
@@ -0,0 +1,34 @@
1
+ from django.db import models
2
+
3
+
4
+ class Template(models.Model):
5
+ """
6
+ A template consists of n styles and a template which holds a {{content}} templatetag where the content is rendered
7
+ into.
8
+ """
9
+
10
+ title = models.CharField(max_length=255, unique=True)
11
+ template = models.TextField(default="")
12
+
13
+ header_template = models.ForeignKey(
14
+ "Template",
15
+ null=True,
16
+ blank=True,
17
+ related_name="header_templates",
18
+ on_delete=models.SET_NULL,
19
+ )
20
+ footer_template = models.ForeignKey(
21
+ "Template",
22
+ null=True,
23
+ blank=True,
24
+ related_name="footer_templates",
25
+ on_delete=models.SET_NULL,
26
+ )
27
+
28
+ styles = models.ManyToManyField(to="wbwriter.Style", related_name="templates", blank=True)
29
+
30
+ side_margin = models.FloatField(default=2.5)
31
+ extra_vertical_margin = models.FloatField(default=10)
32
+
33
+ def __str__(self):
34
+ return self.title
@@ -0,0 +1,172 @@
1
+ import base64
2
+
3
+ from weasyprint import CSS, HTML
4
+ from weasyprint.text.fonts import FontConfiguration
5
+
6
+
7
+ class PdfGenerator:
8
+ """
9
+ Generate a PDF out of a rendered template, with the possibility to integrate nicely
10
+ a header and a footer if provided.
11
+
12
+ Notes:
13
+ ------
14
+ - When Weasyprint renders an html into a PDF, it goes though several intermediate steps.
15
+ Here, in this class, we deal mostly with a box representation: 1 `Document` have 1 `Page`
16
+ or more, each `Page` 1 `Box` or more. Each box can contain other box. Hence the recursive
17
+ method `get_element` for example.
18
+ For more, see:
19
+ https://weasyprint.readthedocs.io/en/stable/hacking.html#dive-into-the-source
20
+ https://weasyprint.readthedocs.io/en/stable/hacking.html#formatting-structure
21
+ - Warning: the logic of this class relies heavily on the internal Weasyprint API. This
22
+ snippet was written at the time of the release 47, it might break in the future.
23
+ - This generator draws its inspiration and, also a bit of its implementation, from this
24
+ discussion in the library github issues: https://github.com/Kozea/WeasyPrint/issues/92
25
+ """
26
+
27
+ OVERLAY_LAYOUT = "@page {size: A4 portrait; margin: 0;}"
28
+
29
+ def __init__(
30
+ self,
31
+ main_html,
32
+ header_html=None,
33
+ footer_html=None,
34
+ custom_css=[],
35
+ base_url=None,
36
+ side_margin=2,
37
+ extra_vertical_margin=30,
38
+ ):
39
+ """
40
+ Parameters
41
+ ----------
42
+ main_html: str
43
+ An HTML file (most of the time a template rendered into a string) which represents
44
+ the core of the PDF to generate.
45
+ header_html: str
46
+ An optional header html.
47
+ footer_html: str
48
+ An optional footer html.
49
+ custom_css: list
50
+ An optional list of css objects. (see weasyprint.CSS)
51
+ base_url: str
52
+ An absolute url to the page which serves as a reference to Weasyprint to fetch assets,
53
+ required to get our media.
54
+ side_margin: int, interpreted in cm, by default 2cm
55
+ The margin to apply on the core of the rendered PDF (i.e. main_html).
56
+ extra_vertical_margin: int, interpreted in pixel, by default 30 pixels
57
+ An extra margin to apply between the main content and header and the footer.
58
+ The goal is to avoid having the content of `main_html` touching the header or the
59
+ footer.
60
+ """
61
+ self.main_html = main_html
62
+ self.header_html = header_html
63
+ self.footer_html = footer_html
64
+ self.custom_css = custom_css
65
+ self.base_url = base_url
66
+ self.side_margin = side_margin
67
+ self.extra_vertical_margin = extra_vertical_margin
68
+
69
+ def _compute_overlay_element(self, element: str):
70
+ """
71
+ Parameters
72
+ ----------
73
+ element: str
74
+ Either 'header' or 'footer'
75
+
76
+ Returns
77
+ -------
78
+ element_body: BlockBox
79
+ A Weasyprint pre-rendered representation of an html element
80
+ element_height: float
81
+ The height of this element, which will be then translated in a html height
82
+ """
83
+ html = HTML(
84
+ string=getattr(self, f"{element}_html"),
85
+ base_url=self.base_url,
86
+ )
87
+ element_doc = html.render(stylesheets=[CSS(string=self.OVERLAY_LAYOUT)] + self.custom_css)
88
+ element_page = element_doc.pages[0]
89
+ element_body = PdfGenerator.get_element(element_page._page_box.all_children(), "body")
90
+ element_body = element_body.copy_with_children(element_body.all_children())
91
+ element_html = PdfGenerator.get_element(element_page._page_box.all_children(), element)
92
+
93
+ if element == "header":
94
+ element_height = element_html.height
95
+ if element == "footer":
96
+ element_height = element_page.height - element_html.position_y
97
+
98
+ return element_body, element_height
99
+
100
+ def _apply_overlay_on_main(self, main_doc, header_body=None, footer_body=None):
101
+ """
102
+ Insert the header and the footer in the main document.
103
+
104
+ Parameters
105
+ ----------
106
+ main_doc: Document
107
+ The top level representation for a PDF page in Weasyprint.
108
+ header_body: BlockBox
109
+ A representation for an html element in Weasyprint.
110
+ footer_body: BlockBox
111
+ A representation for an html element in Weasyprint.
112
+ """
113
+ for page in main_doc.pages:
114
+ page_body = PdfGenerator.get_element(page._page_box.all_children(), "body")
115
+
116
+ if header_body:
117
+ page_body.children += header_body.all_children()
118
+ if footer_body:
119
+ page_body.children += footer_body.all_children()
120
+
121
+ def render_pdf(self):
122
+ """
123
+ Returns
124
+ -------
125
+ pdf: a bytes sequence
126
+ The rendered PDF.
127
+ """
128
+ if self.header_html:
129
+ header_body, header_height = self._compute_overlay_element("header")
130
+ else:
131
+ header_body, header_height = None, 0
132
+ if self.footer_html:
133
+ footer_body, footer_height = self._compute_overlay_element("footer")
134
+ else:
135
+ footer_body, footer_height = None, 0
136
+
137
+ margins = "{header_size}px {side_margin} {footer_size}px {side_margin}".format(
138
+ header_size=header_height + self.extra_vertical_margin,
139
+ footer_size=footer_height + self.extra_vertical_margin,
140
+ side_margin=f"{self.side_margin}cm",
141
+ )
142
+ content_print_layout = "@page {size: A4 portrait; margin: %s;}" % margins
143
+
144
+ html = HTML(
145
+ string=self.main_html,
146
+ base_url=self.base_url,
147
+ )
148
+ font_config = FontConfiguration()
149
+ main_doc = html.render(
150
+ stylesheets=[CSS(string=content_print_layout)] + self.custom_css,
151
+ font_config=font_config,
152
+ )
153
+
154
+ if self.header_html or self.footer_html:
155
+ self._apply_overlay_on_main(main_doc, header_body, footer_body)
156
+ pdf = main_doc.write_pdf()
157
+ encoded = base64.b64encode(pdf)
158
+ encoded = encoded.decode("utf-8")
159
+ return pdf
160
+
161
+ @staticmethod
162
+ def get_element(boxes, element):
163
+ """
164
+ Given a set of boxes representing the elements of a PDF page in a DOM-like way, find the
165
+ box which is named `element`.
166
+
167
+ Look at the notes of the class for more details on Weasyprint insides.
168
+ """
169
+ for box in boxes:
170
+ if box.element_tag == element:
171
+ return box
172
+ return PdfGenerator.get_element(box.all_children(), element)