Skip to content

Commit 2eac050

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

4 files changed

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

0 commit comments

Comments
 (0)