django-camomilla-cms 6.1.4__tar.gz → 6.2.0__tar.gz

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.
Files changed (149) hide show
  1. {django_camomilla_cms-6.1.4/django_camomilla_cms.egg-info → django_camomilla_cms-6.2.0}/PKG-INFO +1 -1
  2. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/__init__.py +1 -1
  3. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/managers/pages.py +36 -5
  4. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/models/mixins/__init__.py +1 -0
  5. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/models/page.py +3 -2
  6. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/mixins/translation.py +2 -3
  7. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/settings.py +12 -1
  8. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/templates/defaults/base.html +2 -0
  9. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/templates/defaults/parts/langswitch.html +7 -0
  10. django_camomilla_cms-6.2.0/camomilla/theme/__init__.py +1 -0
  11. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/theme/admin/pages.py +1 -1
  12. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/translation.py +1 -0
  13. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/utils/translation.py +7 -8
  14. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/articles.py +0 -2
  15. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/contents.py +0 -2
  16. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/decorators.py +23 -3
  17. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/medias.py +0 -3
  18. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/menus.py +0 -2
  19. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/pages.py +10 -5
  20. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/tags.py +0 -2
  21. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/users.py +1 -2
  22. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0/django_camomilla_cms.egg-info}/PKG-INFO +1 -1
  23. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/django_camomilla_cms.egg-info/SOURCES.txt +5 -0
  24. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/setup.py +1 -1
  25. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_admin_page_form.py +2 -2
  26. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_camomilla_filters.py +1 -1
  27. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_model_api_permissions.py +6 -0
  28. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_pages.py +18 -0
  29. django_camomilla_cms-6.2.0/tests/test_parsers.py +62 -0
  30. django_camomilla_cms-6.2.0/tests/test_sitemap.py +16 -0
  31. django_camomilla_cms-6.2.0/tests/test_utils_getters.py +41 -0
  32. django_camomilla_cms-6.2.0/tests/test_utils_normalization.py +37 -0
  33. django_camomilla_cms-6.2.0/tests/test_utils_setters.py +62 -0
  34. django_camomilla_cms-6.1.4/camomilla/theme/__init__.py +0 -1
  35. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/LICENSE +0 -0
  36. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/MANIFEST.in +0 -0
  37. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/README.md +0 -0
  38. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/apps.py +0 -0
  39. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/authentication.py +0 -0
  40. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/context_processors.py +0 -0
  41. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/contrib/__init__.py +0 -0
  42. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/contrib/modeltranslation/__init__.py +0 -0
  43. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/contrib/modeltranslation/hvad_migration.py +0 -0
  44. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/defaults.py +0 -0
  45. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/dynamic_pages_urls.py +0 -0
  46. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/exceptions.py +0 -0
  47. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/fields/__init__.py +0 -0
  48. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/fields/json.py +0 -0
  49. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/management/__init__.py +0 -0
  50. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/management/commands/__init__.py +0 -0
  51. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/management/commands/regenerate_thumbnails.py +0 -0
  52. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/managers/__init__.py +0 -0
  53. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/model_api.py +0 -0
  54. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/models/__init__.py +0 -0
  55. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/models/article.py +0 -0
  56. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/models/content.py +0 -0
  57. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/models/media.py +0 -0
  58. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/models/menu.py +0 -0
  59. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/openapi/__init__.py +0 -0
  60. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/openapi/schema.py +0 -0
  61. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/parsers.py +0 -0
  62. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/permissions.py +0 -0
  63. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/redirects.py +0 -0
  64. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/__init__.py +0 -0
  65. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/article.py +0 -0
  66. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/base/__init__.py +0 -0
  67. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/content_type.py +0 -0
  68. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/fields/__init__.py +0 -0
  69. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/fields/file.py +0 -0
  70. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/fields/related.py +0 -0
  71. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/media.py +0 -0
  72. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/menu.py +0 -0
  73. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/mixins/__init__.py +0 -0
  74. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/mixins/fields.py +0 -0
  75. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/mixins/filter_fields.py +0 -0
  76. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/mixins/json.py +0 -0
  77. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/mixins/language.py +0 -0
  78. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/mixins/nesting.py +0 -0
  79. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/mixins/optimize.py +0 -0
  80. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/mixins/ordering.py +0 -0
  81. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/mixins/page.py +0 -0
  82. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/page.py +0 -0
  83. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/user.py +0 -0
  84. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/utils.py +0 -0
  85. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/serializers/validators.py +0 -0
  86. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/sitemap.py +0 -0
  87. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/storages/__init__.py +0 -0
  88. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/storages/default.py +0 -0
  89. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/storages/optimize.py +0 -0
  90. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/storages/overwrite.py +0 -0
  91. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/templates/admin/camomilla/page/change_form.html +0 -0
  92. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/templates/defaults/articles/default.html +0 -0
  93. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/templates/defaults/pages/default.html +0 -0
  94. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/templates/defaults/parts/menu.html +0 -0
  95. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/templates/defaults/widgets/media_select_multiple.html +0 -0
  96. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/templates_context/__init__.py +0 -0
  97. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/templates_context/autodiscover.py +0 -0
  98. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/templates_context/rendering.py +0 -0
  99. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/templatetags/__init__.py +0 -0
  100. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/templatetags/camomilla_filters.py +0 -0
  101. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/templatetags/menus.py +0 -0
  102. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/templatetags/model_extras.py +0 -0
  103. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/theme/admin/__init__.py +0 -0
  104. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/theme/admin/translations.py +0 -0
  105. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/theme/apps.py +0 -0
  106. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/theme/static/admin/css/responsive.css +0 -0
  107. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/theme/static/admin/img/favicon.ico +0 -0
  108. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/theme/static/admin/img/logo.svg +0 -0
  109. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/theme/templates/admin/base.html +0 -0
  110. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/theme/templates/rosetta/base.html +0 -0
  111. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/urls.py +0 -0
  112. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/utils/__init__.py +0 -0
  113. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/utils/getters.py +0 -0
  114. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/utils/normalization.py +0 -0
  115. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/utils/query_parser.py +0 -0
  116. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/utils/seo.py +0 -0
  117. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/utils/setters.py +0 -0
  118. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/utils/templates.py +0 -0
  119. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/__init__.py +0 -0
  120. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/base/__init__.py +0 -0
  121. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/languages.py +0 -0
  122. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/mixins/__init__.py +0 -0
  123. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/mixins/bulk_actions.py +0 -0
  124. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/mixins/language.py +0 -0
  125. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/mixins/optimize.py +0 -0
  126. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/mixins/ordering.py +0 -0
  127. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/mixins/pagination.py +0 -0
  128. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/camomilla/views/mixins/permissions.py +0 -0
  129. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/django_camomilla_cms.egg-info/dependency_links.txt +0 -0
  130. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/django_camomilla_cms.egg-info/requires.txt +0 -0
  131. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/django_camomilla_cms.egg-info/top_level.txt +0 -0
  132. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/pyproject.toml +0 -0
  133. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/setup.cfg +0 -0
  134. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/__init__.py +0 -0
  135. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/fixtures/__init__.py +0 -0
  136. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_api.py +0 -0
  137. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_media.py +0 -0
  138. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_menu.py +0 -0
  139. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_model_api.py +0 -0
  140. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_model_api_register.py +0 -0
  141. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_models.py +0 -0
  142. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_page_meta.py +0 -0
  143. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_page_relation_api.py +0 -0
  144. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_query_parser.py +0 -0
  145. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_templates_context.py +0 -0
  146. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/test_utils.py +0 -0
  147. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/utils/__init__.py +0 -0
  148. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/utils/api.py +0 -0
  149. {django_camomilla_cms-6.1.4 → django_camomilla_cms-6.2.0}/tests/utils/media.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-camomilla-cms
3
- Version: 6.1.4
3
+ Version: 6.2.0
4
4
  Summary: Django powered cms
5
5
  Author-email: Lotrèk <dimmitutto@lotrek.it>
6
6
  License: MIT
@@ -1,4 +1,4 @@
1
- __version__ = "6.1.4"
1
+ __version__ = "6.2.0"
2
2
 
3
3
 
4
4
  def get_core_apps():
@@ -42,13 +42,44 @@ class PageQuerySet(QuerySet):
42
42
 
43
43
 
44
44
  class UrlNodeManager(models.Manager):
45
+
46
+ def get_reverse_pages_relations(self):
47
+ """
48
+ Get all reverse relations coming from AbstractPages models.
49
+ This is used to annotate the UrlNode with the related page fields.
50
+ """
51
+ from camomilla.models.page import AbstractPage
52
+
53
+ relations = []
54
+
55
+ for field in self.model._meta.get_fields():
56
+ if not (hasattr(field, "related_model") and field.one_to_one):
57
+ continue
58
+
59
+ if not issubclass(field.related_model, AbstractPage):
60
+ continue
61
+
62
+ if field.remote_field.name != "url_node":
63
+ continue
64
+
65
+ related_name = field.get_accessor_name()
66
+ relations.append(
67
+ {
68
+ "name": related_name,
69
+ "model": field.related_model,
70
+ "field_name": field.remote_field.name,
71
+ "field": field,
72
+ }
73
+ )
74
+ return relations
75
+
45
76
  @property
46
77
  def related_names(self):
47
- self._related_names = getattr(
48
- self,
49
- "_related_names",
50
- super().get_queryset().values_list("related_name", flat=True).distinct(),
51
- )
78
+ self._related_names = getattr(self, "_related_names", None)
79
+ if self._related_names is None:
80
+ self._related_names = list(
81
+ set([rel["name"] for rel in self.get_reverse_pages_relations()])
82
+ )
52
83
  return self._related_names
53
84
 
54
85
  def _annotate_fields(
@@ -16,6 +16,7 @@ class SeoMixin(models.Model):
16
16
  on_delete=models.SET_NULL,
17
17
  related_name="%(app_label)s_%(class)s_og_images",
18
18
  )
19
+ keywords = models.JSONField(default=list)
19
20
 
20
21
  class Meta:
21
22
  abstract = True
@@ -99,7 +99,7 @@ class UrlNode(models.Model):
99
99
  LANG_PERMALINK_FIELDS = (
100
100
  [
101
101
  build_localized_fieldname("permalink", lang)
102
- for lang in settings.AVAILABLE_LANGUAGES
102
+ for lang in settings.LANGUAGE_CODES
103
103
  ]
104
104
  if settings.ENABLE_TRANSLATIONS
105
105
  else ["permalink"]
@@ -319,7 +319,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
319
319
  @property
320
320
  def breadcrumbs(self) -> Sequence[dict]:
321
321
  breadcrumb = {
322
- "permalink": self.permalink,
322
+ "permalink": self.routerlink,
323
323
  "title": self.breadcrumbs_title or self.title or "",
324
324
  }
325
325
  if self.parent:
@@ -481,6 +481,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
481
481
  )
482
482
  if preview:
483
483
  permalinks = {k: f"{v}?preview=true" for k, v in permalinks.items()}
484
+ permalinks.pop(get_language(), None)
484
485
  return permalinks
485
486
 
486
487
  class Meta:
@@ -1,6 +1,5 @@
1
1
  from functools import cached_property
2
2
  from typing import Iterable, List
3
- from modeltranslation import settings as mt_settings
4
3
  from modeltranslation.translator import NotRegistered, translator
5
4
  from modeltranslation.utils import build_localized_fieldname
6
5
  from rest_framework import serializers
@@ -8,7 +7,7 @@ from rest_framework.exceptions import ValidationError
8
7
  from camomilla.utils.getters import pointed_getter
9
8
  from camomilla.utils.translation import is_translatable
10
9
  from camomilla.utils.translation import nest_to_plain, plain_to_nest
11
- from camomilla.settings import API_TRANSLATION_ACCESSOR
10
+ from camomilla.settings import API_TRANSLATION_ACCESSOR, LANGUAGE_CODES
12
11
 
13
12
 
14
13
  class TranslationsMixin(serializers.ModelSerializer):
@@ -94,7 +93,7 @@ class RemoveTranslationsMixin(serializers.ModelSerializer):
94
93
  included_translations = []
95
94
 
96
95
  field_names = super().get_default_field_names(declared_fields, model_info)
97
- for lang in mt_settings.AVAILABLE_LANGUAGES:
96
+ for lang in LANGUAGE_CODES:
98
97
  if lang not in included_translations:
99
98
  for field in self.translation_fields:
100
99
  localized_fieldname = build_localized_fieldname(field, lang)
@@ -105,6 +105,10 @@ INTEGRATIONS_ASTRO_URL = pointed_getter(
105
105
  django_settings, "CAMOMILLA.INTEGRATIONS.ASTRO.URL", ""
106
106
  )
107
107
 
108
+ PAGE_ROUTER_CACHE = pointed_getter(
109
+ django_settings, "CAMOMILLA.API.PAGES.ROUTER_CACHE", 60 * 15
110
+ )
111
+
108
112
  DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG)
109
113
 
110
114
  # camomilla settings example
@@ -133,6 +137,13 @@ DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG
133
137
  # "URL": "http://localhost:4321"
134
138
  # }
135
139
  # }
136
- # "API": {"NESTING_DEPTH": 10, "TRANSLATION_ACCESSOR": "translations", "PAGES": {"DEFAULT_SERIALIZER": "camomilla.serializers.page.RouteSerializer"}},
140
+ # "API": {
141
+ # "NESTING_DEPTH": 10,
142
+ # "TRANSLATION_ACCESSOR": "translations",
143
+ # "PAGES": {
144
+ # "DEFAULT_SERIALIZER": "camomilla.serializers.page.RouteSerializer"
145
+ # },
146
+ # "ROUTER_CACHE": 60 * 15
147
+ # },
137
148
  # "DEBUG": False
138
149
  # }
@@ -21,6 +21,8 @@
21
21
  justify-content: center;
22
22
  font-family: sans-serif;
23
23
  margin: 0;
24
+ width: 100vw;
25
+ height: 100vh;
24
26
  }
25
27
 
26
28
  .letter {
@@ -36,6 +36,13 @@
36
36
  {% get_language_info_list for LANGUAGES as languages %}
37
37
  {% get_language_info for LANGUAGE_CODE as current_lang %}
38
38
 
39
+ <div class="language-switch--btn" data-lang="{{current_lang.code}}"
40
+ {% if current_lang.code != LANGUAGE_CODE %}
41
+ onclick="submitLanguageWithoutRedirect('{{current_lang.code}}');"
42
+ {% endif %}
43
+ >
44
+ <a>{{ current_lang.name_translated }} {% if current_lang.code == LANGUAGE_CODE %} 👾 {% endif %}</a>
45
+ </div>
39
46
  {% for lang_code, redirect in page.alternate_urls.items %}
40
47
  <div class="language-switch--btn" data-lang="{{lang_code}}"
41
48
  {% if current_lang.code != lang_code %}
@@ -0,0 +1 @@
1
+ __version__ = "6.2.0"
@@ -29,7 +29,7 @@ class AbstractPageModelForm(
29
29
  super().__init__(*args, **kwargs)
30
30
  templates = [(t, t) for t in get_templates(request)]
31
31
  templates.insert(0, ("", "---------"))
32
- self.fields["template"] = forms.ChoiceField(choices=templates)
32
+ self.fields["template"] = forms.ChoiceField(choices=templates, required=False)
33
33
 
34
34
  def get_initial_for_field(self, field, field_name):
35
35
  if field_name in UrlNode.LANG_PERMALINK_FIELDS:
@@ -12,6 +12,7 @@ class SeoMixinTranslationOptions(TranslationOptions):
12
12
  "og_type",
13
13
  "og_url",
14
14
  "canonical",
15
+ "keywords",
15
16
  )
16
17
 
17
18
 
@@ -3,13 +3,12 @@ from typing import Any, Sequence, Iterator, Union, List
3
3
 
4
4
  from django.db.models import Model, Q
5
5
  from django.utils.translation.trans_real import activate, get_language
6
- from modeltranslation.settings import AVAILABLE_LANGUAGES, DEFAULT_LANGUAGE
7
6
  from modeltranslation.utils import build_localized_fieldname
8
- from camomilla.settings import BASE_URL
7
+ from camomilla.settings import BASE_URL, DEFAULT_LANGUAGE, LANGUAGE_CODES
9
8
  from django.http import QueryDict
10
9
 
11
10
 
12
- def activate_languages(languages: Sequence[str] = AVAILABLE_LANGUAGES) -> Iterator[str]:
11
+ def activate_languages(languages: Sequence[str] = LANGUAGE_CODES) -> Iterator[str]:
13
12
  old = get_language()
14
13
  for language in languages:
15
14
  activate(language)
@@ -37,7 +36,7 @@ def url_lang_decompose(url):
37
36
  if BASE_URL and url.startswith(BASE_URL):
38
37
  url = url[len(BASE_URL) :]
39
38
  data = {"url": url, "permalink": url, "language": DEFAULT_LANGUAGE}
40
- result = re.match(rf"^/?({'|'.join(AVAILABLE_LANGUAGES)})?/(.*)", url) # noqa: W605
39
+ result = re.match(rf"^/?({'|'.join(LANGUAGE_CODES)})?/(.*)", url) # noqa: W605
41
40
  groups = result and result.groups()
42
41
  if groups and len(groups) == 2:
43
42
  data["language"] = groups[0]
@@ -48,14 +47,14 @@ def url_lang_decompose(url):
48
47
  def get_field_translations(instance: Model, field_name: str, *args, **kwargs):
49
48
  return {
50
49
  lang: get_nofallbacks(instance, field_name, language=lang, *args, **kwargs)
51
- for lang in AVAILABLE_LANGUAGES
50
+ for lang in LANGUAGE_CODES
52
51
  }
53
52
 
54
53
 
55
54
  def lang_fallback_query(**kwargs):
56
55
  current_lang = get_language()
57
56
  query = Q()
58
- for lang in AVAILABLE_LANGUAGES:
57
+ for lang in LANGUAGE_CODES:
59
58
  query |= Q(**{f"{key}_{lang}": value for key, value in kwargs.items()})
60
59
  if current_lang:
61
60
  query = query & Q(
@@ -77,7 +76,7 @@ def plain_to_nest(data, fields, accessor="translations"):
77
76
  into a dictionary with nested translations fields (es. {"translations": {"en": {"title": "Hello"}}}).
78
77
  """
79
78
  trans_data = {}
80
- for lang in AVAILABLE_LANGUAGES:
79
+ for lang in LANGUAGE_CODES:
81
80
  lang_data = {}
82
81
  for field in fields:
83
82
  trans_field_name = build_localized_fieldname(field, lang)
@@ -101,7 +100,7 @@ def nest_to_plain(
101
100
  if isinstance(data, QueryDict):
102
101
  data = data.dict()
103
102
  translations = data.pop(accessor, {})
104
- for lang in AVAILABLE_LANGUAGES:
103
+ for lang in LANGUAGE_CODES:
105
104
  nest_trans = translations.pop(lang, {})
106
105
  for k in fields:
107
106
  data.pop(k, None) # this removes all trans field without lang
@@ -1,5 +1,4 @@
1
1
  from camomilla.models import Article
2
- from camomilla.permissions import CamomillaBasePermissions
3
2
  from camomilla.serializers import ArticleSerializer
4
3
  from camomilla.views.base import BaseModelViewset
5
4
  from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
@@ -8,6 +7,5 @@ from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
8
7
  class ArticleViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
9
8
  queryset = Article.objects.all()
10
9
  serializer_class = ArticleSerializer
11
- permission_classes = (CamomillaBasePermissions,)
12
10
  search_fields = ["title", "identifier", "content", "permalink"]
13
11
  model = Article
@@ -4,7 +4,6 @@ from django.http import JsonResponse
4
4
  from rest_framework.decorators import action
5
5
 
6
6
  from camomilla.models import Content
7
- from camomilla.permissions import CamomillaBasePermissions
8
7
  from camomilla.serializers import ContentSerializer
9
8
  from camomilla.views.base import BaseModelViewset
10
9
  from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
@@ -14,7 +13,6 @@ class ContentViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
14
13
  queryset = Content.objects.all()
15
14
  serializer_class = ContentSerializer
16
15
  model = Content
17
- permission_classes = (CamomillaBasePermissions,)
18
16
 
19
17
  @action(detail=True, methods=["get", "patch"])
20
18
  def djsuperadmin(self, request, pk):
@@ -1,6 +1,7 @@
1
1
  import functools
2
2
  from django.utils.translation import activate
3
- from django.conf import settings
3
+ from django.views.decorators.cache import cache_page
4
+ from camomilla.settings import LANGUAGE_CODES, DEFAULT_LANGUAGE
4
5
 
5
6
 
6
7
  def active_lang(*args, **kwargs):
@@ -11,12 +12,12 @@ def active_lang(*args, **kwargs):
11
12
  request = args[0].request
12
13
  else:
13
14
  request = args[0] if len(args) else kwargs.get("request", None)
14
- lang = settings.LANGUAGE_CODE
15
+ lang = DEFAULT_LANGUAGE
15
16
  if request and hasattr(request, "GET"):
16
17
  lang = request.GET.get("lang", request.GET.get("language", lang))
17
18
  if request and hasattr(request, "data"):
18
19
  lang = request.data.pop("lang", request.data.pop("language", lang))
19
- if lang and lang in [lng[0] for lng in settings.LANGUAGES]:
20
+ if lang and lang in LANGUAGE_CODES:
20
21
  activate(lang)
21
22
  request.LANGUAGE_CODE = lang
22
23
  return func(*args, **kwargs)
@@ -24,3 +25,22 @@ def active_lang(*args, **kwargs):
24
25
  return wrapped_func
25
26
 
26
27
  return decorator
28
+
29
+
30
+ def staff_excluded_cache(timing=None):
31
+ def decorator(func):
32
+ if timing is None:
33
+ return func # No caching applied
34
+
35
+ @functools.wraps(func)
36
+ def wrapped_func(*args, **kwargs):
37
+ request = args[0] if len(args) else kwargs.get("request", None)
38
+ if request and hasattr(request, "user"):
39
+ user = request.user
40
+ if user.is_authenticated and user.is_staff:
41
+ return func(*args, **kwargs)
42
+ return cache_page(timing)(func)(*args, **kwargs)
43
+
44
+ return wrapped_func
45
+
46
+ return decorator
@@ -3,7 +3,6 @@ from rest_framework.response import Response
3
3
 
4
4
  from camomilla.models import Media, MediaFolder
5
5
  from camomilla.parsers import MultipartJsonParser
6
- from camomilla.permissions import CamomillaBasePermissions
7
6
  from camomilla.serializers import (
8
7
  MediaFolderSerializer,
9
8
  MediaListSerializer,
@@ -33,7 +32,6 @@ class MediaFolderViewSet(
33
32
  ):
34
33
  model = MediaFolder
35
34
  serializer_class = MediaFolderSerializer
36
- permission_classes = (CamomillaBasePermissions,)
37
35
  items_per_page = 18
38
36
  search_fields = ["title", "alt_text", "file"]
39
37
 
@@ -85,6 +83,5 @@ class MediaViewSet(
85
83
  ):
86
84
  queryset = Media.objects.all()
87
85
  serializer_class = MediaSerializer
88
- permission_classes = (CamomillaBasePermissions,)
89
86
  model = Media
90
87
  parser_classes = [MultipartJsonParser, JSONParser]
@@ -7,7 +7,6 @@ from rest_framework.response import Response
7
7
 
8
8
  from camomilla.models import AbstractPage, Menu
9
9
  from camomilla.models.page import UrlNode
10
- from camomilla.permissions import CamomillaBasePermissions
11
10
  from camomilla.serializers import ContentTypeSerializer, MenuSerializer
12
11
  from camomilla.serializers.page import UrlNodeSerializer
13
12
  from camomilla.views.base import BaseModelViewset
@@ -17,7 +16,6 @@ from camomilla.views.decorators import active_lang
17
16
  class MenuViewSet(BaseModelViewset):
18
17
  queryset = Menu.objects.all()
19
18
  serializer_class = MenuSerializer
20
- permission_classes = (CamomillaBasePermissions,)
21
19
  model = Menu
22
20
 
23
21
  lookup_field = "key"
@@ -1,26 +1,27 @@
1
1
  from camomilla.models import Page
2
2
  from camomilla.models.page import UrlNode, UrlRedirect
3
- from camomilla.permissions import CamomillaBasePermissions
4
3
  from camomilla.serializers import PageSerializer
5
4
  from camomilla.serializers.page import RouteSerializer
5
+ from camomilla.utils.translation import url_lang_decompose
6
6
  from camomilla.views.base import BaseModelViewset
7
- from camomilla.views.decorators import active_lang
7
+ from camomilla.views.decorators import staff_excluded_cache
8
8
  from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
9
9
  from rest_framework.decorators import api_view, permission_classes
10
10
  from rest_framework.response import Response
11
11
  from rest_framework import permissions
12
+ from django.utils.translation.trans_real import activate as activate_language
12
13
  from django.shortcuts import get_object_or_404
14
+ from camomilla.settings import PAGE_ROUTER_CACHE
13
15
 
14
16
 
15
17
  class PageViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
16
18
  queryset = Page.objects.all()
17
19
  serializer_class = PageSerializer
18
- permission_classes = (CamomillaBasePermissions,)
19
20
  model = Page
20
21
 
21
22
 
22
- @active_lang()
23
23
  @api_view(["GET"])
24
+ @staff_excluded_cache(PAGE_ROUTER_CACHE)
24
25
  @permission_classes(
25
26
  [
26
27
  permissions.AllowAny,
@@ -31,5 +32,9 @@ def pages_router(request, permalink=""):
31
32
  if redirect:
32
33
  redirect = redirect.redirect()
33
34
  return Response({"redirect": redirect.url, "status": redirect.status_code})
34
- node: UrlNode = get_object_or_404(UrlNode, permalink=f"/{permalink}")
35
+ url_decomposition = url_lang_decompose(permalink)
36
+ if not url_decomposition["permalink"].startswith("/"):
37
+ url_decomposition["permalink"] = f"/{url_decomposition['permalink']}"
38
+ activate_language(url_decomposition["language"])
39
+ node: UrlNode = get_object_or_404(UrlNode, permalink=url_decomposition["permalink"])
35
40
  return Response(RouteSerializer(node, context={"request": request}).data)
@@ -1,5 +1,4 @@
1
1
  from camomilla.models import Tag
2
- from camomilla.permissions import CamomillaBasePermissions
3
2
  from camomilla.serializers import TagSerializer
4
3
  from camomilla.views.base import BaseModelViewset
5
4
  from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
@@ -8,5 +7,4 @@ from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
8
7
  class TagViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
9
8
  queryset = Tag.objects.all()
10
9
  serializer_class = TagSerializer
11
- permission_classes = (CamomillaBasePermissions,)
12
10
  model = Tag
@@ -8,7 +8,7 @@ from rest_framework.permissions import IsAuthenticated
8
8
  from rest_framework.response import Response
9
9
  from rest_framework.views import APIView
10
10
 
11
- from camomilla.permissions import CamomillaBasePermissions, CamomillaSuperUser, ReadOnly
11
+ from camomilla.permissions import CamomillaSuperUser, ReadOnly
12
12
  from camomilla.serializers import (
13
13
  PermissionSerializer,
14
14
  UserProfileSerializer,
@@ -30,7 +30,6 @@ class UserViewSet(BaseModelViewset):
30
30
  queryset = get_user_model().objects.all()
31
31
  serializer_class = UserSerializer
32
32
  model = get_user_model()
33
- permission_classes = (CamomillaSuperUser | CamomillaBasePermissions,)
34
33
 
35
34
  @action(detail=False, methods=["get", "put"], permission_classes=(IsAuthenticated,))
36
35
  def current(self, request):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-camomilla-cms
3
- Version: 6.1.4
3
+ Version: 6.2.0
4
4
  Summary: Django powered cms
5
5
  Author-email: Lotrèk <dimmitutto@lotrek.it>
6
6
  License: MIT
@@ -132,9 +132,14 @@ tests/test_models.py
132
132
  tests/test_page_meta.py
133
133
  tests/test_page_relation_api.py
134
134
  tests/test_pages.py
135
+ tests/test_parsers.py
135
136
  tests/test_query_parser.py
137
+ tests/test_sitemap.py
136
138
  tests/test_templates_context.py
137
139
  tests/test_utils.py
140
+ tests/test_utils_getters.py
141
+ tests/test_utils_normalization.py
142
+ tests/test_utils_setters.py
138
143
  tests/fixtures/__init__.py
139
144
  tests/utils/__init__.py
140
145
  tests/utils/api.py
@@ -1,5 +1,5 @@
1
1
  from setuptools import setup
2
2
 
3
- __version__ = "6.1.4"
3
+ __version__ = "6.2.0"
4
4
 
5
5
  setup(version=__version__)
@@ -36,7 +36,7 @@ class AdminPageFormTestCase(TestCase):
36
36
 
37
37
  page_admin = PageAdmin(Page, AdminSite())
38
38
  form = page_admin.get_form(request)()
39
- self.assertEqual(len(list(form.fields)), 33)
39
+ self.assertEqual(len(list(form.fields)), 35)
40
40
  self.assertTrue("template" in list(form.fields))
41
41
  self.assertListEqual(
42
42
  form.fields["template"].widget.choices,
@@ -54,7 +54,7 @@ class AdminPageFormTestCase(TestCase):
54
54
 
55
55
  page_admin = PageAdmin(Page, AdminSite())
56
56
  form = page_admin.get_form(request_with_cookies)()
57
- self.assertEqual(len(list(form.fields)), 33)
57
+ self.assertEqual(len(list(form.fields)), 35)
58
58
  self.assertTrue("template" in list(form.fields))
59
59
  self.assertListEqual(
60
60
  form.fields["template"].widget.choices,
@@ -42,4 +42,4 @@ class CamomillaFiltersTestCase(TestCase):
42
42
  request.META["HTTP_HOST"] = "localhost"
43
43
  page = Page.get(request)
44
44
  alt_urls = dict(alternate_urls(page, request))
45
- self.assertEqual(alt_urls, {"it": None, "en": "/path/"})
45
+ self.assertEqual(alt_urls, {"it": None})
@@ -9,14 +9,20 @@ client = APIClient()
9
9
  def test_right_permissions():
10
10
  response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
11
11
  assert response.status_code == 401
12
+ response = client.get("/api/models/test-model/")
13
+ assert response.status_code == 401
12
14
  token = login_user()
13
15
  client.credentials(HTTP_AUTHORIZATION="Token " + token)
14
16
  response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
15
17
  assert response.status_code == 403
18
+ response = client.get("/api/models/test-model/")
19
+ assert response.status_code == 200
16
20
  token = login_staff()
17
21
  client.credentials(HTTP_AUTHORIZATION="Token " + token)
18
22
  response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
19
23
  assert response.status_code == 403
24
+ response = client.get("/api/models/test-model/")
25
+ assert response.status_code == 200
20
26
  token = login_superuser()
21
27
  client.credentials(HTTP_AUTHORIZATION="Token " + token)
22
28
  response = client.post("/api/models/test-model/", {"title": "test"}, format="json")
@@ -347,3 +347,21 @@ class PagesTestCase(TestCase):
347
347
  response = self.client.get("/it/permalink_6_it/")
348
348
  assert response.status_code == 301
349
349
  assert response.url == "/it/permalink_6_it_changed/"
350
+
351
+ @pytest.mark.django_db
352
+ def test_page_keywords(self):
353
+ # Create page with keywords field and check it's given back as expected
354
+ response = self.client.post(
355
+ "/api/camomilla/pages/",
356
+ {
357
+ "og_description_it" : "Keywords Test",
358
+ "keywords_it" : ['key1', 'key2']
359
+ },
360
+ format="json",
361
+ )
362
+
363
+ assert response.status_code == 201
364
+ assert len(Page.objects.all()) == 1
365
+ page = Page.objects.first()
366
+ assert page.og_description_it == "Keywords Test"
367
+ assert page.keywords_it == ['key1', 'key2']
@@ -0,0 +1,62 @@
1
+ import io
2
+ import json
3
+ import pytest
4
+ from django.conf import settings
5
+ from django.test import RequestFactory
6
+ from django.core.files.uploadedfile import SimpleUploadedFile
7
+ from camomilla.parsers import MultipartJsonParser
8
+
9
+ @pytest.mark.django_db
10
+ def test_multipart_json_parser_parses_json_and_files(monkeypatch):
11
+ # Prepare multipart data
12
+ json_data = json.dumps({"foo": "bar", "nested": {"baz": 1}, "nested_list": [{"qux": "quux"}, {"corge": "grault"}]})
13
+ file_content = b"filecontent"
14
+ upload = SimpleUploadedFile("test.txt", file_content, content_type="text/plain")
15
+
16
+ # Simulate multipart POST
17
+ factory = RequestFactory()
18
+ data = {
19
+ "data": json_data,
20
+ "nested.file": upload,
21
+ "nested_list.1.file": upload,
22
+ }
23
+ request = factory.post("/", data)
24
+ request.upload_handlers = []
25
+ request.META["CONTENT_TYPE"] = "multipart/form-data; boundary=BoUnDaRy"
26
+
27
+ # Patch DjangoMultiPartParser to return our data
28
+ class DummyParser:
29
+ def __init__(self, *a, **kw): pass
30
+ def parse(self):
31
+ return ( {"data": json_data}, {"nested.file": upload, "nested_list.1.file": upload,},)
32
+ monkeypatch.setattr("camomilla.parsers.DjangoMultiPartParser", DummyParser)
33
+
34
+ parser = MultipartJsonParser()
35
+ parsed = parser.parse(io.BytesIO(b""), "multipart/form-data", {"request": request})
36
+ assert parsed["foo"] == "bar"
37
+ assert parsed["nested"]["baz"] == 1
38
+ assert isinstance(parsed["nested"]["file"], SimpleUploadedFile)
39
+ assert isinstance(parsed["nested_list"][1]["file"], SimpleUploadedFile)
40
+ parsed["nested"]["file"].seek(0)
41
+ assert parsed["nested"]["file"].read() == file_content
42
+ parsed["nested_list"][1]["file"].seek(0)
43
+ assert parsed["nested_list"][1]["file"].read() == file_content
44
+
45
+ @pytest.mark.django_db
46
+ def test_multipart_json_parser_handles_parse_error(monkeypatch):
47
+ factory = RequestFactory()
48
+ request = factory.post("/", {})
49
+ request.upload_handlers = []
50
+ request.META["CONTENT_TYPE"] = "multipart/form-data; boundary=BoUnDaRy"
51
+
52
+ class DummyParser:
53
+ def __init__(self, *a, **kw): pass
54
+ def parse(self):
55
+ from django.http.multipartparser import MultiPartParserError
56
+ raise MultiPartParserError("fail")
57
+ monkeypatch.setattr("camomilla.parsers.DjangoMultiPartParser", DummyParser)
58
+ parser = MultipartJsonParser()
59
+ from rest_framework.exceptions import ParseError
60
+ with pytest.raises(ParseError) as exc:
61
+ parser.parse(io.BytesIO(b""), "multipart/form-data", {"request": request})
62
+ assert "Multipart form parse error" in str(exc.value)
@@ -0,0 +1,16 @@
1
+ import pytest
2
+ from django.urls import reverse
3
+ from django.test import Client
4
+ from camomilla.models.page import Page
5
+
6
+ @pytest.mark.django_db
7
+ def test_sitemap_xml_contains_pages():
8
+ Page.objects.create(title="Test Page 1", permalink="test-page-1", status="PUB", autopermalink=False)
9
+ Page.objects.create(title="Test Page 2", permalink="test-page-2", status="PUB", autopermalink=False)
10
+
11
+ client = Client()
12
+ response = client.get(reverse('django.contrib.sitemaps.views.sitemap'))
13
+ assert response.status_code == 200
14
+ assert b"<urlset" in response.content
15
+ assert b"/test-page-1" in response.content
16
+ assert b"/test-page-2" in response.content