diff --git a/.gitignore b/.gitignore
index a41d813d..bf420ce3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,3 +47,6 @@ secret-key
2024-*
2025-*
2023-*
+
+
+files/
\ No newline at end of file
diff --git a/datacenterlight/factories.py b/datacenterlight/factories.py
deleted file mode 100644
index 3bbe6e17..00000000
--- a/datacenterlight/factories.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import factory
-from django.contrib.sites.models import Site
-
-from datacenterlight.cms_models import CMSIntegration
-
-
-class SiteFactory(factory.django.DjangoModelFactory):
- class Meta:
- model = Site
- django_get_or_create = ("domain",)
-
- domain = factory.Sequence(lambda n: f"example{n}.test")
- name = factory.LazyAttribute(lambda obj: obj.domain)
-
-
-class CMSIntegrationFactory(factory.django.DjangoModelFactory):
- class Meta:
- model = CMSIntegration
-
- name = factory.Sequence(lambda n: f"integration-{n}")
- domain = factory.SubFactory(SiteFactory)
\ No newline at end of file
diff --git a/datacenterlight/test_views.py b/datacenterlight/test_views.py
index f0b1bbcf..12d3767a 100644
--- a/datacenterlight/test_views.py
+++ b/datacenterlight/test_views.py
@@ -22,7 +22,7 @@ from datacenterlight.views import (
show_error,
)
from utils.forms import BillingAddressFormSignup
-from .factories import CMSIntegrationFactory
+from factories import CMSIntegrationFactory
class SessionRequestMixin:
diff --git a/digitalglarus/templates/contact.html b/digitalglarus/templates/contact.html
index ba12c5ae..c913bd39 100755
--- a/digitalglarus/templates/contact.html
+++ b/digitalglarus/templates/contact.html
@@ -14,17 +14,17 @@
{% endfor %}
- {% static_placeholder "digital_glarus_contact_title" %}
+ {% placeholder "digital_glarus_contact_title" %}
- {% static_placeholder "digital_glarus_contact_location" %}
+ {% placeholder "digital_glarus_contact_location" %}
- {% static_placeholder "digital_glarus_contact_information" %}
+ {% placeholder "digital_glarus_contact_information" %}
@@ -36,10 +36,10 @@
- {% static_placeholder "digital_glarus_contact_form_title" %}
+ {% placeholder "digital_glarus_contact_form_title" %}
-
{% static_placeholder "digital_glarus_contact_form_text" %}
+
{% placeholder "digital_glarus_contact_form_text" %}
diff --git a/digitalglarus/tests.py b/digitalglarus/tests.py
new file mode 100644
index 00000000..b60b2282
--- /dev/null
+++ b/digitalglarus/tests.py
@@ -0,0 +1,476 @@
+from datetime import date
+from unittest.mock import patch
+
+from django.contrib.auth import get_user_model
+from django.test import RequestFactory, TestCase
+from django.urls import reverse
+
+from digitalglarus.forms import (
+ BookingDateForm
+)
+from utils.forms import ContactUsForm
+from digitalglarus.models import (
+ BookingPrice,
+ MembershipType,
+ Supporter,
+)
+from digitalglarus.views import (
+ IndexView,
+ SupportusView,
+ LoginView,
+ SignupView,
+ PasswordResetView,
+ PasswordResetConfirmView,
+ HistoryView,
+ MembershipPricingView,
+ MembershipActivatedView,
+ ContactView,
+ BookingSelectDatesView,
+)
+
+from factories import (
+ SupporterFactory,
+ BookingPriceFactory,
+ CustomUserFactory,
+ PostFactory,
+)
+
+
+User = get_user_model()
+
+
+class IndexViewTests(TestCase):
+
+ def test_get_renders_template(self):
+ response = self.client.get(
+ reverse("digitalglarus:landing")
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(
+ response,
+ "digitalglarus/index.html",
+ )
+
+
+class SupportusViewTests(TestCase):
+
+ @patch("digitalglarus.views.Post.objects.filter")
+ def test_get_renders_template(self, mock_filter):
+ mock_filter.return_value.translated.return_value = []
+
+ response = self.client.get(
+ reverse("digitalglarus:supportus")
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(
+ response,
+ "digitalglarus/supportus.html",
+ )
+ self.assertIn("post_list", response.context)
+
+ @patch("digitalglarus.views.Post.objects.filter")
+ def test_context_contains_posts(self, mock_filter):
+ posts = ["post1", "post2"]
+
+ mock_filter.return_value.translated.return_value = posts
+
+ request = RequestFactory().get(
+ reverse("digitalglarus:supportus")
+ )
+
+ view = SupportusView()
+ view.setup(request)
+
+ context = view.get_context_data()
+
+ self.assertEqual(
+ context["post_list"],
+ posts,
+ )
+
+
+class HistoryViewTests(TestCase):
+
+ def test_get_renders_template(self):
+ SupporterFactory()
+
+ response = self.client.get(
+ reverse("digitalglarus:history")
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(
+ response,
+ "digitalglarus/history.html",
+ )
+
+ self.assertIn(
+ "supporters",
+ response.context,
+ )
+
+ def test_context_contains_supporters(self):
+ supporter = SupporterFactory()
+
+ request = RequestFactory().get(
+ reverse("digitalglarus:history")
+ )
+
+ view = HistoryView()
+ view.setup(request)
+
+ context = view.get_context_data()
+
+ self.assertIn(
+ supporter,
+ context["supporters"],
+ )
+
+
+class LoginViewTests(TestCase):
+
+ def test_get_renders_template(self):
+ response = self.client.get(
+ reverse("digitalglarus:login")
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(
+ response,
+ "digitalglarus/login.html",
+ )
+
+ def test_get_success_url_without_membership_orders(self):
+ request = RequestFactory().get(
+ reverse("digitalglarus:login")
+ )
+
+ request.user = CustomUserFactory.create()
+
+ view = LoginView()
+ view.setup(request)
+
+ result = view.get_success_url()
+
+ self.assertEqual(
+ result,
+ reverse("digitalglarus:membership_pricing"),
+ )
+
+
+class SignupViewTests(TestCase):
+
+ def test_get_renders_template(self):
+ response = self.client.get(
+ reverse("digitalglarus:signup")
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(
+ response,
+ "digitalglarus/signup.html",
+ )
+
+ def test_success_url(self):
+ view = SignupView()
+
+ self.assertEqual(
+ str(view.success_url),
+ reverse("digitalglarus:login"),
+ )
+
+
+class PasswordResetViewTests(TestCase):
+
+ def test_get_renders_template(self):
+ response = self.client.get(
+ reverse("digitalglarus:reset_password")
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(
+ response,
+ "digitalglarus/reset_password.html",
+ )
+
+
+class PasswordResetConfirmViewTests(TestCase):
+
+ def test_template_name(self):
+ view = PasswordResetConfirmView()
+
+ self.assertEqual(
+ view.template_name,
+ "digitalglarus/confirm_reset_password.html",
+ )
+
+ def test_success_url(self):
+ view = PasswordResetConfirmView()
+
+ self.assertEqual(
+ str(view.success_url),
+ reverse("digitalglarus:login"),
+ )
+
+
+class MembershipPricingViewTests(TestCase):
+
+ @patch(
+ "digitalglarus.views.MembershipType.objects.last"
+ )
+ def test_get_renders_template(self, mock_last):
+ membership_type = object()
+ mock_last.return_value = membership_type
+
+ response = self.client.get(
+ reverse("digitalglarus:membership_pricing")
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(
+ response,
+ "digitalglarus/membership_pricing.html",
+ )
+
+ self.assertEqual(
+ response.context["membership_type"],
+ membership_type,
+ )
+
+
+class MembershipActivatedViewTests(TestCase):
+
+ def test_get_renders_template(self):
+ session = self.client.session
+
+ session["membership_price"] = 100
+ session["membership_dates"] = "01/01/2026 - 01/31/2026"
+ session.save()
+
+ response = self.client.get(
+ reverse("digitalglarus:membership_activated")
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(
+ response,
+ "digitalglarus/membership_activated.html",
+ )
+
+ self.assertEqual(
+ response.context["membership_price"],
+ 100,
+ )
+
+ self.assertEqual(
+ response.context["membership_dates"],
+ "01/01/2026 - 01/31/2026",
+ )
+
+
+class ContactViewTests(TestCase):
+
+ def test_get_renders_template(self):
+ response = self.client.get(
+ reverse("digitalglarus:contact")
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(
+ response,
+ "contact.html",
+ )
+
+ def test_get_uses_contact_form(self):
+ response = self.client.get(
+ reverse("digitalglarus:contact")
+ )
+
+ self.assertIsInstance(
+ response.context["form"],
+ ContactUsForm,
+ )
+
+ def test_form_valid_redirects_to_contact(self):
+ view = ContactView()
+
+ request = RequestFactory().post(
+ reverse("digitalglarus:contact")
+ )
+
+ view.setup(request)
+
+ form = ContactUsForm()
+
+ with patch.object(
+ ContactUsForm,
+ "is_valid",
+ return_value=True,
+ ):
+ response = view.form_valid(form)
+
+ self.assertEqual(
+ response.status_code,
+ 302,
+ )
+
+ self.assertEqual(
+ response.url,
+ reverse("digitalglarus:contact"),
+ )
+
+
+class BookingSelectDatesViewTests(TestCase):
+
+ def test_get_requires_login(self):
+ response = self.client.get(
+ reverse("digitalglarus:booking")
+ )
+
+ self.assertEqual(response.status_code, 302)
+
+ def test_success_url(self):
+ view = BookingSelectDatesView()
+
+ self.assertEqual(
+ str(view.success_url),
+ reverse("digitalglarus:booking_payment"),
+ )
+
+ def test_get_form_kwargs_contains_user(self):
+ user = CustomUserFactory.create(
+ email="test@example.com",
+ password="password123",
+ )
+
+ request = RequestFactory().get(
+ reverse("digitalglarus:booking")
+ )
+ request.user = user
+
+ view = BookingSelectDatesView()
+ view.setup(request)
+
+ with patch(
+ "digitalglarus.views.FormView.get_form_kwargs",
+ return_value={},
+ ):
+ kwargs = view.get_form_kwargs()
+
+ self.assertEqual(
+ kwargs["user"],
+ user,
+ )
+
+
+class OldFunctionViewTests(TestCase):
+
+ @patch("digitalglarus.views.Post.objects.translated")
+ def test_blog_renders_template(self, mock_translated):
+ mock_translated.return_value = []
+
+ response = self.client.get(
+ reverse("digitalglarus:blog")
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(
+ response,
+ "glarus_blog/post_list.html",
+ )
+
+ self.assertIn(
+ "post_list",
+ response.context,
+ )
+
+ def test_blog_detail_renders_template(self):
+ post = PostFactory.create()
+ response = self.client.get(
+ reverse(
+ "digitalglarus:blog-detail",
+ kwargs={"slug": post.slug},
+ )
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(
+ response,
+ "glarus_blog/post_detail.html",
+ )
+
+ self.assertIn(
+ "post",
+ response.context,
+ )
+
+ def test_blog_detail_returns_404_when_post_does_not_exist(self):
+ with patch(
+ "digitalglarus.views.Post.objects.translated"
+ ) as mock_translated:
+
+ mock_translated.return_value.first.return_value = None
+
+ response = self.client.get(
+ reverse(
+ "digitalglarus:blog-detail",
+ kwargs={"slug": "missing-post"},
+ )
+ )
+
+ self.assertEqual(response.status_code, 404)
+
+ def test_support_renders_template(self):
+ response = self.client.get(
+ reverse("digitalglarus:support")
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(
+ response,
+ "digitalglarus/supportus.html",
+ )
+
+ def test_supporters_renders_template(self):
+ SupporterFactory()
+
+ response = self.client.get(
+ reverse("digitalglarus:supporters")
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(
+ response,
+ "supporters.html",
+ )
+
+ self.assertIn(
+ "supporters",
+ response.context,
+ )
+
+
+class BookingPaymentViewTests(TestCase):
+
+ def test_get_without_booking_data_redirects_to_booking(self):
+ response = self.client.get(
+ reverse("digitalglarus:booking_payment")
+ )
+
+ self.assertRedirects(
+ response,
+ reverse("digitalglarus:booking"),
+ fetch_redirect_response=False,
+ )
+
+
+class MembershipDeactivateViewTests(TestCase):
+
+ def test_get_requires_login(self):
+ response = self.client.get(
+ reverse("digitalglarus:membership_deactivate")
+ )
+
+ self.assertEqual(response.status_code, 302)
\ No newline at end of file
diff --git a/digitalglarus/views.py b/digitalglarus/views.py
index 6e620843..248fd855 100644
--- a/digitalglarus/views.py
+++ b/digitalglarus/views.py
@@ -64,11 +64,9 @@ class LoginView(LoginViewMixin):
def get_success_url(self):
# redirect to membership orders list if user has at least one.
- if self.request.user \
- and MembershipOrder.objects.filter(customer__user=self.request.user):
-
+ membership_order = MembershipOrder.objects.filter(customer__user=self.request.user)
+ if self.request.user and membership_order:
return reverse_lazy('digitalglarus:membership_orders_list')
-
return reverse_lazy('digitalglarus:membership_pricing')
diff --git a/djangocms_blog/migrations/0043_alter_blogcategorytranslation_unique_together_and_more.py b/djangocms_blog/migrations/0043_alter_blogcategorytranslation_unique_together_and_more.py
index c6a7492c..36ce38b6 100644
--- a/djangocms_blog/migrations/0043_alter_blogcategorytranslation_unique_together_and_more.py
+++ b/djangocms_blog/migrations/0043_alter_blogcategorytranslation_unique_together_and_more.py
@@ -1,4 +1,4 @@
-# Generated by Django 6.1.1 on 2026-10-02 08:25
+# Generated by Django 6.1.1 on 2026-10-04 11:16
from django.db import migrations, models
diff --git a/djangocms_blog/models.py b/djangocms_blog/models.py
index 0e707b1b..ac15a7e0 100644
--- a/djangocms_blog/models.py
+++ b/djangocms_blog/models.py
@@ -4,6 +4,7 @@ from aldryn_apphooks_config.fields import AppHookConfigField
from aldryn_apphooks_config.managers.parler import AppHookConfigTranslatableManager
from cms.models import CMSPlugin
from cms.models.fields import PlaceholderField, PlaceholderRelationField
+from cms.utils.placeholder import get_placeholder_from_slot
from django.conf import settings as dj_settings
from django.contrib.auth import get_user_model
from django.contrib.sites.shortcuts import get_current_site
@@ -268,9 +269,33 @@ class Post(KnockerModel, BlogMetaMixin, TranslatableModel):
post_text=HTMLField(_("text"), default="", blank=True, configuration="BLOG_POST_TEXT_CKEDITOR"),
meta={"unique_together": (("language_code", "slug"),)},
)
- media = PlaceholderRelationField("media", related_name="media")
- content = PlaceholderRelationField("post_content", related_name="post_content")
- liveblog = PlaceholderRelationField("live_blog", related_name="live_blog")
+ placeholders = PlaceholderRelationField()
+
+ @cached_property
+ def media(self):
+ return get_placeholder_from_slot(
+ self.placeholders,
+ "media",
+ )
+
+ @cached_property
+ def content(self):
+ return get_placeholder_from_slot(
+ self.placeholders,
+ "post_content",
+ )
+
+ @cached_property
+ def liveblog(self):
+ return get_placeholder_from_slot(
+ self.placeholders,
+ "live_blog",
+ )
+
+ enable_liveblog = models.BooleanField(
+ verbose_name=_("enable liveblog on post"),
+ default=False,
+ )
enable_liveblog = models.BooleanField(verbose_name=_("enable liveblog on post"), default=False)
objects = GenericDateTaggedManager()
diff --git a/factories.py b/factories.py
new file mode 100644
index 00000000..f99728d8
--- /dev/null
+++ b/factories.py
@@ -0,0 +1,101 @@
+import factory
+from django.contrib.sites.models import Site
+from django.conf import settings
+
+from datacenterlight.cms_models import CMSIntegration
+from digitalglarus.models import BookingPrice, Supporter
+from membership.models import CustomUser
+from djangocms_blog.models import Post
+
+
+class SiteFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = Site
+ django_get_or_create = ("domain",)
+
+ domain = factory.Sequence(lambda n: f"example{n}.test")
+ name = factory.LazyAttribute(lambda obj: obj.domain)
+
+
+class CMSIntegrationFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = CMSIntegration
+
+ name = factory.Sequence(lambda n: f"integration-{n}")
+ domain = factory.SubFactory(SiteFactory)
+
+
+class SupporterFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = Supporter
+
+ name = factory.Sequence(lambda n: f"Supporter {n}")
+
+
+class BookingPriceFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = BookingPrice
+
+ price_per_day = 10
+ special_month_price = 100
+
+
+class CustomUserFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = CustomUser
+
+ site = factory.LazyFunction(lambda: Site.objects.get(pk=1))
+ name = factory.Sequence(lambda n: f"Test User {n}")
+ email = factory.Sequence(lambda n: f"user{n}@example.com")
+ username = factory.Sequence(lambda n: f"user{n}")
+
+ validated = 1
+ in_ldap = False
+ is_admin = False
+
+ @factory.post_generation
+ def password(self, create, extracted, **kwargs):
+ if not create:
+ return
+
+ password = extracted or "testpassword123"
+ self.set_password(password)
+ self.save(update_fields=["password"])
+
+
+class PostFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = Post
+
+ author = factory.SubFactory(CustomUserFactory)
+
+ publish = True
+ include_in_rss = True
+ enable_comments = False
+ enable_liveblog = False
+
+ @classmethod
+ def _create(cls, model_class, *args, **kwargs):
+ language_code = kwargs.pop(
+ "language_code",
+ settings.LANGUAGE_CODE,
+ )
+
+ title = kwargs.pop("title", "Test Post")
+ slug = kwargs.pop("slug", "test-post")
+
+ post = model_class(*args, **kwargs)
+
+ post.set_current_language(language_code)
+ post.title = title
+ post.slug = slug
+ post.subtitle = ""
+ post.abstract = ""
+ post.meta_description = ""
+ post.meta_keywords = ""
+ post.meta_title = ""
+ post.post_text = ""
+
+ post.save()
+
+ return post
\ No newline at end of file
diff --git a/utils/views.py b/utils/views.py
index dc728ce2..9a9cf26b 100644
--- a/utils/views.py
+++ b/utils/views.py
@@ -13,6 +13,7 @@ from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import cache_control
from django.views.generic import FormView, CreateView
+from django.utils.decorators import method_decorator
from datacenterlight.utils import get_cms_integration
from hosting.forms import UserHostingKeyForm
@@ -51,28 +52,40 @@ class LoginViewMixin(FormView):
success_url = None
def get_success_url(self):
- next_url = self.request.POST.get('next', self.success_url)
+ next_url = self.request.POST.get("next", self.success_url)
+
if not next_url:
return self.success_url
+
return next_url
def form_valid(self, form):
- email = form.cleaned_data.get('email')
- password = form.cleaned_data.get('password')
+ email = form.cleaned_data.get("email")
+ password = form.cleaned_data.get("password")
+
auth_user = authenticate(email=email, password=password)
if auth_user:
login(self.request, auth_user)
- return HttpResponseRedirect(self.get_success_url())
return HttpResponseRedirect(self.get_success_url())
- @cache_control(no_cache=True, must_revalidate=True, no_store=True)
+ @method_decorator(
+ cache_control(
+ no_cache=True,
+ must_revalidate=True,
+ no_store=True,
+ )
+ )
def get(self, request, *args, **kwargs):
if self.request.user.is_authenticated:
return HttpResponseRedirect(self.get_success_url())
- return super(LoginViewMixin, self).get(request, *args, **kwargs)
+ return super(LoginViewMixin, self).get(
+ request,
+ *args,
+ **kwargs
+ )
class ResendActivationLinkViewMixin(FormView):
diff --git a/vendor/stored_messages/models.py b/vendor/stored_messages/models.py
index ff40cdbd..f112fbb8 100644
--- a/vendor/stored_messages/models.py
+++ b/vendor/stored_messages/models.py
@@ -8,7 +8,7 @@ try:
except ImportError:
def python_2_unicode_compatible(cls):
return cls
-from django.utils.translation import pgettext_lazy as _
+from django.utils.translation import gettext_lazy as _
from .settings import stored_messages_settings