Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions Products/CMFPlone/browser/recyclebin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
from datetime import datetime
from Products.CMFPlone.interfaces.recyclebin import IRecycleBin
from Products.Five.browser import BrowserView
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from Products.statusmessages.interfaces import IStatusMessage
from zExceptions import NotFound
from zope.component import getUtility
from zope.interface import implementer
from zope.publisher.interfaces import IPublishTraverse
from Products.CMFCore.utils import getToolByName


class RecycleBinView(BrowserView):
"""Browser view for recycle bin management"""

template = ViewPageTemplateFile("templates/recyclebin.pt")

def __call__(self):
form = self.request.form

if form.get("form.submitted", False):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use a z3c.form form here

if form.get("form.button.Restore", None) is not None:
self.restore_items()
elif form.get("form.button.Delete", None) is not None:
self.delete_items()
elif form.get("form.button.Empty", None) is not None:
self.empty_bin()

return self.template()

def get_recycle_bin(self):
"""Get the recycle bin utility"""
return getUtility(IRecycleBin)

def get_items(self):
"""Get all items in the recycle bin"""
recycle_bin = self.get_recycle_bin()
return recycle_bin.get_items()

def format_date(self, date):
"""Format date for display"""
if date is None:
return ""
portal = getToolByName(self.context, 'portal_url').getPortalObject()
# Use long_format=True to include hours, minutes and seconds
return portal.restrictedTraverse('@@plone').toLocalizedTime(date, long_format=True)

def format_size(self, size_bytes):
"""Format size in bytes to human-readable format"""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f} KB"
else:
return f"{size_bytes / (1024 * 1024):.1f} MB"

def restore_items(self):
"""Restore selected items from the recycle bin"""
form = self.request.form
recycle_bin = self.get_recycle_bin()

# Get the selected items
selected_items = form.get("selected_items", [])
if not isinstance(selected_items, list):
selected_items = [selected_items]

if not selected_items:
message = "No items selected for restoration."
IStatusMessage(self.request).addStatusMessage(message, type="info")
return

restored_count = 0
for item_id in selected_items:
if recycle_bin.restore_item(item_id):
restored_count += 1

message = f"{restored_count} item{'s' if deleted_count != 1} restored successfully."
IStatusMessage(self.request).addStatusMessage(message, type="info")

def delete_items(self):
"""Permanently delete selected items"""
form = self.request.form
recycle_bin = self.get_recycle_bin()

# Get the selected items
selected_items = form.get("selected_items", [])
if not isinstance(selected_items, list):
selected_items = [selected_items]

if not selected_items:
message = "No items selected for deletion."
IStatusMessage(self.request).addStatusMessage(message, type="info")
return

deleted_count = 0
for item_id in selected_items:
if recycle_bin.purge_item(item_id):
deleted_count += 1

message = f"{deleted_count} item{'s' if deleted_count != 1} permanently deleted."
IStatusMessage(self.request).addStatusMessage(message, type="info")

def empty_bin(self):
"""Empty the entire recycle bin"""
recycle_bin = self.get_recycle_bin()
items = recycle_bin.get_items()
deleted_count = 0

for item in items:
item_id = item["recycle_id"]
if recycle_bin.purge_item(item_id):
deleted_count += 1

message = f"Recycle bin emptied. {deleted_count} item{'s' if deleted_count != 1} permanently deleted."
IStatusMessage(self.request).addStatusMessage(message, type="info")


@implementer(IPublishTraverse)
class RecycleBinItemView(BrowserView):
"""View for managing individual recycled items"""

template = ViewPageTemplateFile("templates/recyclebin_item.pt")
item_id = None

def publishTraverse(self, request, name):
"""Handle URLs like /recyclebin/item/[item_id]"""
if self.item_id is None: # First traversal
self.item_id = name
return self
raise NotFound(self, name, request)

def __call__(self):
"""Handle item operations"""
if self.item_id is None:
self.request.response.redirect(f"{self.context.absolute_url()}/recyclebin")
return ""

form = self.request.form
if form.get("form.submitted", False):
if form.get("form.button.Restore", None) is not None:
self.restore_item()
return ""
elif form.get("form.button.Delete", None) is not None:
self.delete_item()
return ""

return self.template()

def get_item(self):
"""Get the specific recycled item"""
recycle_bin = getUtility(IRecycleBin)
return recycle_bin.get_item(self.item_id)

def restore_item(self):
"""Restore this item"""
recycle_bin = getUtility(IRecycleBin)

# Get target container if specified
target_path = self.request.form.get("target_container", "")
target_container = None

if target_path:
try:
target_container = self.context.unrestrictedTraverse(target_path)
except (KeyError, AttributeError):
message = f"Target location not found: {target_path}"
IStatusMessage(self.request).addStatusMessage(message, type="error")
self.request.response.redirect(
f"{self.context.absolute_url()}/recyclebin/item/{self.item_id}"
)
return

# Restore the item
restored_obj = recycle_bin.restore_item(self.item_id, target_container)

if restored_obj:
message = f"Item '{restored_obj.Title()}' successfully restored."
IStatusMessage(self.request).addStatusMessage(message, type="info")
self.request.response.redirect(restored_obj.absolute_url())
else:
message = (
"Failed to restore item. It may have been already restored or deleted."
)
IStatusMessage(self.request).addStatusMessage(message, type="error")
self.request.response.redirect(f"{self.context.absolute_url()}/recyclebin")

def delete_item(self):
"""Permanently delete this item"""
recycle_bin = getUtility(IRecycleBin)

# Get item info before deletion
item = self.get_item()
if item:
item_title = item.get("title", "Unknown")

if recycle_bin.purge_item(self.item_id):
message = f"Item '{item_title}' permanently deleted."
IStatusMessage(self.request).addStatusMessage(message, type="info")
else:
message = f"Failed to delete item '{item_title}'."
IStatusMessage(self.request).addStatusMessage(message, type="error")
else:
message = "Item not found. It may have been already deleted."
IStatusMessage(self.request).addStatusMessage(message, type="error")

self.request.response.redirect(f"{self.context.absolute_url()}/recyclebin")
68 changes: 68 additions & 0 deletions Products/CMFPlone/browser/templates/recyclebin.pt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
metal:use-macro="context/main_template/macros/master"
i18n:domain="plone">
<body>

<metal:main fill-slot="main">
<h1 class="documentFirstHeading" i18n:translate="">Recycle Bin</h1>

<div class="documentDescription" i18n:translate="">
Items deleted from this site are stored here and can be restored or permanently deleted.
</div>

<form method="post" tal:attributes="action string:${context/absolute_url}/@@recyclebin"
tal:define="items view/get_items">
<input type="hidden" name="form.submitted" value="1" />

<div class="pat-autotoc autotabs">

<div id="all-items">
<table class="table table-striped listing">
<thead>
<tr>
<th><input type="checkbox" name="select_all" id="select_all" /></th>
<th i18n:translate="">Title</th>
<th i18n:translate="">Type</th>
<th i18n:translate="">Original Path</th>
<th i18n:translate="">Deletion Date</th>
<th i18n:translate="">Size</th>
</tr>
</thead>
<tbody>
<tr tal:condition="not:items">
<td colspan="6" i18n:translate="">No items in recycle bin</td>
</tr>
<tr tal:repeat="item items">
<td>
<input type="checkbox" name="selected_items:list"
tal:attributes="value item/recycle_id" />
</td>
<td>
<a tal:attributes="href string:${context/absolute_url}/@@recyclebin/item/${item/recycle_id}"
tal:content="item/title">Title</a>
</td>
<td tal:content="item/type">Content type</td>
<td tal:content="item/path">Original path</td>
<td tal:content="python:view.format_date(item['deletion_date'])">Deletion date</td>
<td tal:content="python:view.format_size(item.get('size', 0))">Size</td>
</tr>
</tbody>
</table>

<div class="formControls" tal:condition="items">
<input class="btn btn-primary" type="submit" name="form.button.Restore" value="Restore Selected" i18n:attributes="value" />
<input class="btn btn-danger" type="submit" name="form.button.Delete" value="Delete Selected" i18n:attributes="value" />
<input class="btn btn-danger" type="submit" name="form.button.Empty" value="Empty Recycle Bin"
onclick="return confirm('Are you sure you want to permanently delete all items in the recycle bin?');"
i18n:attributes="value" />
</div>
</div>
</div>
</form>
</metal:main>

</body>
</html>
85 changes: 85 additions & 0 deletions Products/CMFPlone/browser/templates/recyclebin_item.pt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
metal:use-macro="context/main_template/macros/master"
i18n:domain="plone">
<body>

<metal:main fill-slot="main">
<tal:item define="item view/get_item">
<div tal:condition="not:item">
<h1 class="documentFirstHeading" i18n:translate="">Item Not Found</h1>
<p i18n:translate="">
The requested item was not found in the recycle bin. It may have been already restored or deleted.
</p>
<p>
<a tal:attributes="href string:${context/absolute_url}/@@recyclebin"
i18n:translate="">Return to recycle bin</a>
</p>
</div>

<div tal:condition="item">
<h1 class="documentFirstHeading">
<span tal:content="item/title">Title</span>
<span class="discreet"> (<span tal:content="item/type">Content Type</span>)</span>
</h1>

<div class="documentDescription" i18n:translate="">
Deleted item information
</div>

<table class="listing">
<tbody>
<tr>
<th i18n:translate="">Original ID</th>
<td tal:content="item/id">ID</td>
</tr>
<tr>
<th i18n:translate="">Original Path</th>
<td tal:content="item/path">Path</td>
</tr>
<tr>
<th i18n:translate="">Parent Path</th>
<td tal:content="item/parent_path">Parent</td>
</tr>
<tr>
<th i18n:translate="">Deletion Date</th>
<td tal:content="python:view.format_date(item['deletion_date'])">Date</td>
</tr>
</tbody>
</table>

<form method="post"
tal:attributes="action string:${context/absolute_url}/@@recyclebin/item/${view/item_id}">
<input type="hidden" name="form.submitted" value="1" />

<div class="field">
<label i18n:translate="">Custom Restore Location (optional)</label>
<div class="formHelp" i18n:translate="">
Leave blank to restore to the original location. If the original location no longer exists,
the item will be restored to the site root.
</div>
<input type="text" name="target_container" size="50" />
</div>

<div class="formControls">
<input class="context" type="submit" name="form.button.Restore" value="Restore Item" i18n:attributes="value" />
<input class="destructive" type="submit" name="form.button.Delete" value="Permanently Delete"
onclick="return confirm('Are you sure you want to permanently delete this item?');"
i18n:attributes="value" />
</div>
</form>

<div class="visualClear"><!-- --></div>

<p>
<a tal:attributes="href string:${context/absolute_url}/@@recyclebin"
i18n:translate="">Return to recycle bin</a>
</p>
</div>
</tal:item>
</metal:main>

</body>
</html>
26 changes: 26 additions & 0 deletions Products/CMFPlone/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,30 @@
for="zope.pagetemplate.engine.ZopeBaseEngine"
/>

<!-- Recycle Bin utility -->
<utility
factory=".recyclebin.RecycleBin"
provides=".interfaces.recyclebin.IRecycleBin" />

<!-- Event handlers -->
<subscriber
for="Products.CMFCore.interfaces.IContentish
zope.lifecycleevent.interfaces.IObjectRemovedEvent"
handler=".events.handle_content_removal" />

<!-- Browser views -->
<browser:page
name="recyclebin"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
class=".browser.recyclebin.RecycleBinView"
permission="cmf.ManagePortal"
/>

<browser:page
name="recyclebin/item"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
class=".browser.recyclebin.RecycleBinItemView"
permission="cmf.ManagePortal"
/>

</configure>
Loading