Skip to content

Commit 14ad7eb

Browse files
committed
feat: allow exporting contents modified after a given date
1 parent 753dfb1 commit 14ad7eb

4 files changed

Lines changed: 200 additions & 1 deletion

File tree

CHANGES.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ Changelog
55
1.16 (unreleased)
66
-----------------
77

8-
- Nothing changed yet.
8+
- Allow exporting contents modified after a given date.
9+
[erral]
910

1011

1112
1.15 (2025-07-15)

src/collective/exportimport/export_content.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
from zope.interface import alsoProvides
2727
from zope.interface import noLongerProvides
2828
from zope.schema import getFields
29+
from DateTime import DateTime
30+
from DateTime.interfaces import SyntaxError, DateError
2931

3032
import json
3133
import logging
@@ -123,11 +125,19 @@ def __call__(
123125
migration=False,
124126
include_revisions=False,
125127
write_errors=False,
128+
modified=None,
126129
):
127130
self.portal_type = portal_type or []
128131
if isinstance(self.portal_type, str):
129132
self.portal_type = [self.portal_type]
130133

134+
self.modified_datetime = None
135+
if modified:
136+
try:
137+
self.modified_datetime = DateTime(modified, fmt="international")
138+
except (DateError, SyntaxError):
139+
logger.info("Invalid datetime: %s", modified)
140+
131141
# Should we adapt the data for migration?
132142
# We had migration=True by default at first. Problem is that when you
133143
# uncheck the migration box in the form, it does not end up in the
@@ -341,6 +351,12 @@ def build_query(self):
341351
"sort_on": "path",
342352
"path": {"query": self.path, "depth": self.depth},
343353
}
354+
355+
if self.modified_datetime is not None:
356+
query.update(
357+
{"modified": {"query": self.modified_datetime, "range": "min"}}
358+
)
359+
344360
# custom setting per type
345361
for portal_type in self.portal_type:
346362
query.update(self.QUERY.get(portal_type, {}))

src/collective/exportimport/templates/export_content.pt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@
6262
</div>
6363
</div>
6464

65+
<div class="field mb-3">
66+
<label for="modified" i18n:translate="">Modification date</label>
67+
<span class="formHelp" i18n:translate="">If a value is entered, it will search for items modified after the entered date. Format: YYYY-MM-DD </span>
68+
<div class="widget">
69+
<input type="text" name="modified" id="modified" placeholder="YYYY-MM-DD" value=""/>
70+
</div>
71+
</div>
72+
6573
<div class="field mb-3">
6674
<label for="path" i18n:translate="">Path</label>
6775
<div class="widget">

src/collective/exportimport/tests/test_export.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from zope.component import getUtility
1717
from zope.intid.interfaces import IIntIds
1818
from zope.lifecycleevent import modified
19+
from DateTime import DateTime
1920

2021
import json
2122
import os
@@ -865,3 +866,176 @@ def test_export_blob_as_download_urls(self):
865866
info["file"]["download"], "http://nohost/plone/file1/@@download/file"
866867
)
867868
self.assertEqual(info["file"]["size"], 8561)
869+
870+
def test_export_modified_items(self):
871+
"""test that we can export items modified after a given datetime"""
872+
app = self.layer["app"]
873+
portal = self.layer["portal"]
874+
login(app, SITE_OWNER_NAME)
875+
folder1 = api.content.create(
876+
container=portal, type="Folder", id="folder1", title="Folder 1"
877+
)
878+
folder1.modification_date = DateTime("2000-01-01", fmt="international")
879+
doc1 = api.content.create(
880+
container=portal, type="Document", id="doc1", title="Document 1"
881+
)
882+
doc1.modification_date = DateTime("2000-01-02", fmt="international")
883+
doc2 = api.content.create(
884+
container=portal, type="Document", id="doc2", title="Document 2"
885+
)
886+
doc2.modification_date = DateTime("2000-01-03", fmt="international")
887+
888+
doc3 = api.content.create(
889+
container=portal, type="Document", id="doc3", title="Document 3"
890+
)
891+
doc3.modification_date = DateTime("2000-01-04", fmt="international")
892+
893+
transaction.commit()
894+
895+
# Now export complete portal.
896+
browser = self.open_page("@@export_content")
897+
portal_type = browser.getControl(name="portal_type")
898+
self.assertEqual(portal_type.value, [])
899+
portal_type.value = ["Folder", "Document"]
900+
901+
path = browser.getControl(label="Path")
902+
self.assertEqual(path.value, "/plone")
903+
904+
modification_date = browser.getControl(label="Modification date")
905+
modification_date.value = "2000-01-03"
906+
907+
depth = browser.getControl(name="depth")
908+
self.assertEqual(depth.value, ["-1"])
909+
910+
try:
911+
# Plone 5.2
912+
browser.getControl("Export").click()
913+
contents = browser.contents
914+
except LookupError:
915+
# Plone 5.1 and lower
916+
browser.getForm(index=1).submit()
917+
if not browser.contents:
918+
contents = DATA[-1]
919+
920+
# We should have gotten json.
921+
data = json.loads(contents)
922+
self.assertEqual(len(data), 2)
923+
924+
# Check a few important keys.
925+
info = data[0]
926+
self.assertEqual(info["@id"], portal.absolute_url() + "/doc2")
927+
self.assertEqual(info["@type"], "Document")
928+
self.assertEqual(info["title"], doc2.Title())
929+
930+
info = data[1]
931+
self.assertEqual(info["@id"], portal.absolute_url() + "/doc3")
932+
self.assertEqual(info["@type"], "Document")
933+
self.assertEqual(info["title"], doc3.Title())
934+
935+
def test_wrong_modified_format(self):
936+
"""test that if we enter an invalid datetime we get all the items"""
937+
app = self.layer["app"]
938+
portal = self.layer["portal"]
939+
login(app, SITE_OWNER_NAME)
940+
folder1 = api.content.create(
941+
container=portal, type="Folder", id="folder1", title="Folder 1"
942+
)
943+
folder1.modification_date = DateTime("2000-01-01", fmt="international")
944+
doc1 = api.content.create(
945+
container=portal, type="Document", id="doc1", title="Document 1"
946+
)
947+
doc1.modification_date = DateTime("2000-01-02", fmt="international")
948+
doc2 = api.content.create(
949+
container=portal, type="Document", id="doc2", title="Document 2"
950+
)
951+
doc2.modification_date = DateTime("2000-01-03", fmt="international")
952+
953+
doc3 = api.content.create(
954+
container=portal, type="Document", id="doc3", title="Document 3"
955+
)
956+
doc3.modification_date = DateTime("2000-01-04", fmt="international")
957+
958+
transaction.commit()
959+
960+
# Now export complete portal.
961+
browser = self.open_page("@@export_content")
962+
portal_type = browser.getControl(name="portal_type")
963+
self.assertEqual(portal_type.value, [])
964+
portal_type.value = ["Folder", "Document"]
965+
966+
path = browser.getControl(label="Path")
967+
self.assertEqual(path.value, "/plone")
968+
969+
modification_date = browser.getControl(label="Modification date")
970+
modification_date.value = "2000-25-01"
971+
972+
depth = browser.getControl(name="depth")
973+
self.assertEqual(depth.value, ["-1"])
974+
975+
try:
976+
# Plone 5.2
977+
browser.getControl("Export").click()
978+
contents = browser.contents
979+
except LookupError:
980+
# Plone 5.1 and lower
981+
browser.getForm(index=1).submit()
982+
if not browser.contents:
983+
contents = DATA[-1]
984+
985+
# We should have gotten json.
986+
data = json.loads(contents)
987+
self.assertEqual(len(data), 4)
988+
989+
def test_non_datetime_modified_format(self):
990+
"""test that if we enter a non-datetime value in modified field we get all the items"""
991+
app = self.layer["app"]
992+
portal = self.layer["portal"]
993+
login(app, SITE_OWNER_NAME)
994+
folder1 = api.content.create(
995+
container=portal, type="Folder", id="folder1", title="Folder 1"
996+
)
997+
folder1.modification_date = DateTime("2000-01-01", fmt="international")
998+
doc1 = api.content.create(
999+
container=portal, type="Document", id="doc1", title="Document 1"
1000+
)
1001+
doc1.modification_date = DateTime("2000-01-02", fmt="international")
1002+
doc2 = api.content.create(
1003+
container=portal, type="Document", id="doc2", title="Document 2"
1004+
)
1005+
doc2.modification_date = DateTime("2000-01-03", fmt="international")
1006+
1007+
doc3 = api.content.create(
1008+
container=portal, type="Document", id="doc3", title="Document 3"
1009+
)
1010+
doc3.modification_date = DateTime("2000-01-04", fmt="international")
1011+
1012+
transaction.commit()
1013+
1014+
# Now export complete portal.
1015+
browser = self.open_page("@@export_content")
1016+
portal_type = browser.getControl(name="portal_type")
1017+
self.assertEqual(portal_type.value, [])
1018+
portal_type.value = ["Folder", "Document"]
1019+
1020+
path = browser.getControl(label="Path")
1021+
self.assertEqual(path.value, "/plone")
1022+
1023+
modification_date = browser.getControl(label="Modification date")
1024+
modification_date.value = "Plone"
1025+
1026+
depth = browser.getControl(name="depth")
1027+
self.assertEqual(depth.value, ["-1"])
1028+
1029+
try:
1030+
# Plone 5.2
1031+
browser.getControl("Export").click()
1032+
contents = browser.contents
1033+
except LookupError:
1034+
# Plone 5.1 and lower
1035+
browser.getForm(index=1).submit()
1036+
if not browser.contents:
1037+
contents = DATA[-1]
1038+
1039+
# We should have gotten json.
1040+
data = json.loads(contents)
1041+
self.assertEqual(len(data), 4)

0 commit comments

Comments
 (0)