Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Simple publishing beginning and end dates #341

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
31 changes: 20 additions & 11 deletions djangocms_versioning/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from .constants import DRAFT, INDICATOR_DESCRIPTIONS, PUBLISHED, VERSION_STATES
from .emails import notify_version_author_version_unlocked
from .exceptions import ConditionFailed
from .forms import grouper_form_factory
from .forms import TimedPublishingForm, grouper_form_factory
from .helpers import (
content_is_unlocked_for_user,
create_version_lock,
Expand Down Expand Up @@ -928,11 +928,20 @@ def publish_view(self, request, object_id):
"""Publishes the specified version and redirects back to the
version changelist
"""
# This view always changes data so only POST requests should work

form = TimedPublishingForm(request.POST) if request.method == "POST" else TimedPublishingForm()
if request.method == "GET" or not form.is_valid():
return render(
request,
template_name="djangocms_versioning/admin/timed_publication.html",
context={"form": form, "errors": request.method != "GET" and not form.is_valid()},
)
if request.method != "POST":
return HttpResponseNotAllowed(
["POST"], _("This view only supports POST method.")
["GET", "POST"], _("This view only supports GET or POST method.")
)
visibility_start = form.cleaned_data["visibility_start"]
visibility_end = form.cleaned_data["visibility_end"]

# Check version exists
version = self.get_object(request, unquote(object_id))
Expand All @@ -956,7 +965,7 @@ def publish_view(self, request, object_id):
return redirect(redirect_url)

# Publish the version
version.publish(request.user)
version.publish(request.user, visibility_start, visibility_end)

# Display message
self.message_user(request, _("Version published"))
Expand Down Expand Up @@ -1165,13 +1174,13 @@ def discard_view(self, request, object_id):
)

version_url = version_list_url(version.content)
if request.POST.get("discard"):
ModelClass = version.content.__class__
deleted = version.delete()
if deleted[1]["last"]:
version_url = get_admin_url(ModelClass, "changelist")
self.message_user(request, _("The last version has been deleted"))

ModelClass = version.content.__class__
deleted = version.delete()
if deleted[1]["last"]:
version_url = get_admin_url(ModelClass, "changelist")
self.message_user(request, _("The last version has been deleted"), messages.SUCCESS)
else:
self.message_user(request, _("The version has been deleted."), messages.SUCCESS)
return redirect(version_url)

def compare_view(self, request, object_id):
Expand Down
51 changes: 44 additions & 7 deletions djangocms_versioning/cms_toolbars.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
from django.contrib.auth import get_permission_codename
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.utils import timezone
from django.utils.formats import localize
from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext, gettext_lazy as _

from djangocms_versioning.conf import LOCK_VERSIONS
from djangocms_versioning.constants import DRAFT, PUBLISHED
from djangocms_versioning.constants import DRAFT
from djangocms_versioning.helpers import (
get_latest_admin_viewable_content,
version_list_url,
Expand Down Expand Up @@ -193,9 +195,34 @@ def _add_versioning_menu(self):
return

version_menu_label = version.short_name()
if version.visibility_start or version.visibility_end:
# Mark time-restricted visibility in the toolbar
version_menu_label += "*"

versioning_menu = self.toolbar.get_or_create_menu(
VERSIONING_MENU_IDENTIFIER, version_menu_label, disabled=False
)
# Inform about time restrictions
if version.visibility_start:
if version.visibility_start < timezone.now():
msg = gettext("Visible since %(datetime)s") % {"datetime": localize(version.visibility_start)}
else:
msg = gettext("Visible after %(datetime)s") % {"datetime": localize(version.visibility_start)}
versioning_menu.add_link_item(
msg,
url="",
disabled=True,
)
if version.visibility_end:
versioning_menu.add_link_item(
gettext("Visible until %(datetime)s") % {"datetime": localize(version.visibility_end)},
url="",
disabled=True,
)
if version.visibility_start or version.visibility_end:
# Add a break if info fields on time restrictions have been added
versioning_menu.add_item(Break())

version = version.convert_to_proxy()
if self.request.user.has_perm(
"{app_label}.{codename}".format(
Expand All @@ -219,10 +246,22 @@ def _add_versioning_menu(self):
"back": self.request.get_full_path(),
})
versioning_menu.add_link_item(name, url=url)
# Need separator?
if version.check_discard.as_bool(self.request.user) or version.check_publish.as_bool(self.request.user):
versioning_menu.add_item(Break())
# Timed publishibng
if version.check_publish.as_bool(self.request.user):
versioning_menu.add_modal_item(
_("Publish with time limits"),
url=reverse(
f"admin:{proxy_model._meta.app_label}_{proxy_model.__name__.lower()}_publish",
args=(version.pk,)
),
on_close=version_list_url(version.content)
)
# Discard changes menu entry (wrt to source)
if version.check_discard.as_bool(self.request.user): # pragma: no cover
versioning_menu.add_item(Break())
versioning_menu.add_link_item(
versioning_menu.add_modal_item(
_("Discard Changes"),
url=reverse(
f"admin:{proxy_model._meta.app_label}_{proxy_model.__name__.lower()}_discard",
Expand All @@ -239,9 +278,7 @@ def _get_published_page_version(self):
if not isinstance(self.toolbar.obj, PageContent) or not self.page:
return

return PageContent._original_manager.filter(
page=self.page, language=language, versions__state=PUBLISHED
).first()
return PageContent.objects.filter(page=self.page, language=language).first()

def _add_view_published_button(self):
"""Helper method to add a publish button to the toolbar
Expand Down
43 changes: 43 additions & 0 deletions djangocms_versioning/forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from functools import lru_cache

from django import forms
from django.contrib.admin.widgets import AdminSplitDateTime
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from . import versionables

Expand Down Expand Up @@ -50,3 +54,42 @@ def grouper_form_factory(content_model, language=None):
),
},
)


class TimedPublishingForm(forms.Form):
visibility_start = forms.SplitDateTimeField(
required=False,
label=_("Visible after"),
help_text=_("Leave empty for immediate public visibility"),
widget=AdminSplitDateTime,
)

visibility_end = forms.SplitDateTimeField(
required=False,
label=_("Visible until"),
help_text=_("Leave empty for unrestricted public visibility"),
widget=AdminSplitDateTime,
)

def clean_visibility_start(self):
visibility_start = self.cleaned_data["visibility_start"]
if visibility_start and visibility_start < timezone.now():
raise ValidationError(
_("The date and time must be in the future."), code="future"
)
return visibility_start

def clean_visibility_end(self):
visibility_end = self.cleaned_data["visibility_end"]
if visibility_end and visibility_end < timezone.now():
raise ValidationError(
_("The date and time must be in the future."), code="future"
)
return visibility_end

def clean(self):
if self.cleaned_data.get("visibility_start") and self.cleaned_data.get("visibility_end"):
if self.cleaned_data["visibility_start"] >= self.cleaned_data["visibility_end"]:
raise ValidationError(
_("The time until the content is visible must be after the time "
"the content becomes visible."), code="time_interval")
2 changes: 1 addition & 1 deletion djangocms_versioning/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ def remove_published_where(queryset):
that are published are returned. If you need to return the full queryset
use the "admin_manager" instead of "objects"
"""
raise NotImplementedError("remove_published_where has been replaced by ContentObj.admin_manager")
raise NotImplementedError("remove_published_where has beenreplaced by ContentObj.admin_manager")


def get_latest_admin_viewable_content(
Expand Down
9 changes: 8 additions & 1 deletion djangocms_versioning/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from django.contrib.auth import get_user_model
from django.db import models
from django.db.models import Q
from django.utils import timezone

from . import constants
from .constants import PUBLISHED
Expand All @@ -19,7 +21,12 @@ def get_queryset(self):
queryset = super().get_queryset()
if not self.versioning_enabled:
return queryset
return queryset.filter(versions__state=PUBLISHED)
now = timezone.now()
return queryset.filter(
Q(versions__visibility_start=None) | Q(versions__visibility_start__lt=now),
Q(versions__visibility_end=None) | Q(versions__visibility_end__gt=now),
versions__state=PUBLISHED,
)

def create(self, *args, **kwargs):
obj = super().create(*args, **kwargs)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.1.7 on 2023-07-03 11:39

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('djangocms_versioning', '0017_merge_20230514_1027'),
]

operations = [
migrations.AddField(
model_name='version',
name='visibility_end',
field=models.DateTimeField(blank=True, default=None, help_text='Leave empty for unrestricted public visibility', null=True, verbose_name='visible until'),
),
migrations.AddField(
model_name='version',
name='visibility_start',
field=models.DateTimeField(blank=True, default=None, help_text='Leave empty for immediate public visibility', null=True, verbose_name='visible after'),
),
]
35 changes: 33 additions & 2 deletions djangocms_versioning/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,21 @@ class Version(models.Model):
verbose_name=_("locked by"),
related_name="locking_users",
)
visibility_start = models.DateTimeField(
default=None,
blank=True,
null=True,
verbose_name=_("visible after"),
help_text=_("Leave empty for immediate public visibility"),
)

visibility_end = models.DateTimeField(
default=None,
blank=True,
null=True,
verbose_name=_("visible until"),
help_text=_("Leave empty for unrestricted public visibility"),
)

source = models.ForeignKey(
"self",
Expand Down Expand Up @@ -140,8 +155,14 @@ def verbose_name(self):
)

def short_name(self):
state = dict(constants.VERSION_STATES)[self.state]
if self.state == constants.PUBLISHED:
if self.visibility_start and self.visibility_start > timezone.now():
state = _("Pending")
elif self.visibility_end and self.visibility_end < timezone.now():
state = _("Expired")
return _("Version #{number} ({state})").format(
number=self.number, state=dict(constants.VERSION_STATES)[self.state]
number=self.number, state=state
)

def locked_message(self):
Expand Down Expand Up @@ -337,14 +358,16 @@ def _set_archive(self, user):
def can_be_published(self):
return can_proceed(self._set_publish)

def publish(self, user):
def publish(self, user, visibility_start=None, visibility_end=None):
"""Change state to PUBLISHED and unpublish currently
published versions"""
# trigger pre operation signal
action_token = send_pre_version_operation(
constants.OPERATION_PUBLISH, version=self
)
self._set_publish(user)
self.visibility_start = visibility_start
self.visibility_end = visibility_end
self.modified = timezone.now()
self.save()
StateTracking.objects.create(
Expand Down Expand Up @@ -393,6 +416,14 @@ def _set_publish(self, user):
possible to be left with inconsistent data)"""
pass

def is_visible(self):
now = timezone.now()
return self.state == constants.PUBLISHED and (
self.visibility_start is None or self.visibility_start < now
) and (
self.visibility_end is None or self.visibility_end > now
)

check_unpublish = Conditions([
user_can_publish(permission_error_message),
in_state([constants.PUBLISHED], _("Version is not in published state")),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static %}
{% block extrahead %}{{ block.super }}
<script src="{% url 'admin:jsi18n' %}"></script>
<script src="{% static "admin/js/core.js" %}"></script>
{{ form.media }}
{% endblock %}

{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/forms.css" %}">{% endblock %}

{% block breadcrumbs %}{% endblock %}

{% block content %}
<h1>{% block title %}{% translate "Publish with time limits" %}{% endblock %}</h1>

<form action="" method="POST">
{% blocktrans %}
<p>You can publish with optional dates and times in the future when the content will become visible
and/or ceases to be visible to visitors of the site.</p>
<p>Once published the contents or times cannot be changed anymore.</p>
{% endblocktrans %}
{% if errors %}
<p class="errornote">
{% blocktranslate count counter=errors|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktranslate %}
</p>
{% endif %}
{% csrf_token %}
<div>

{{ form.non_field_errors }}
<div class="form-row">
<div class="fieldBox">
{{ form.visibility_start.errors }}
<label for="{{ form.visibility_start.id_for_label }}"><b>{{ form.visibility_start.label }}</b></label>
{{ form.visibility_start }}
</div>
<div class="fieldBox">
{{ form.visibility_end.errors }}
<label for="{{ form.visibility_end.id_for_label }}"><b>{{ form.visibility_end.label }}</b></label>
{{ form.visibility_end }}
</div>
</div>
</div>
<div class="submit-row">
<input class="button default"
type="submit"
value="{% translate "Publish" %}">
</div>
</form>
{% endblock %}
6 changes: 3 additions & 3 deletions tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1505,15 +1505,15 @@ def test_publish_view_cant_be_accessed_by_get_request(self):
)

with self.login_user_context(self.get_staff_user_with_no_permissions()):
response = self.client.get(url)
response = self.client.put(url)

self.assertEqual(response.status_code, 405)

# Django 2.2 backwards compatibility
if hasattr(response, "_headers"):
self.assertEqual(response._headers.get("allow"), ("Allow", "POST"))
self.assertEqual(response._headers.get("allow"), ("Allow", "GET, POST"))
else:
self.assertEqual(response.headers.get("Allow"), "POST")
self.assertEqual(response.headers.get("Allow"), "GET, POST")

# status hasn't changed
poll_version_ = Version.objects.get(pk=poll_version.pk)
Expand Down
Loading