plone.app.discussion/plone/app/discussion/browser/migration.py

265 lines
10 KiB
Python

from Acquisition import aq_inner
from Acquisition import aq_parent
from DateTime import DateTime
from Products.CMFCore.interfaces._content import IDiscussionResponse
from Products.CMFCore.utils import getToolByName
from Products.Five.browser import BrowserView
from datetime import datetime
from plone.app.discussion.comment import CommentFactory
from plone.app.discussion.interfaces import IConversation, IReplies, IComment
from types import TupleType
import transaction
def DT2dt(DT):
"""Convert a Zope DateTime (with timezone) into a Python datetime (GMT)."""
DT = DT.toZone('GMT')
return datetime(
DT.year(),
DT.month(),
DT.day(),
DT.hour(),
DT.minute(),
int(DT.second()))
class View(BrowserView):
"""Migration View
"""
def __call__(self, filter_callback=None):
context = aq_inner(self.context)
out = []
self.total_comments_migrated = 0
self.total_comments_deleted = 0
dry_run = "dry_run" in self.request
# This is for testing only.
# Do not use transactions during a test.
test = "test" in self.request
if not test:
transaction.begin() # pragma: no cover
catalog = getToolByName(context, 'portal_catalog')
def log(msg):
# encode string before sending it to external world
if isinstance(msg, unicode):
msg = msg.encode('utf-8') # pragma: no cover
context.plone_log(msg)
out.append(msg)
def migrate_replies(context, in_reply_to, replies,
depth=0, just_delete=0):
# Recursive function to migrate all direct replies
# of a comment. Returns True if there are no replies to
# this comment left, and therefore the comment can be removed.
if len(replies) == 0:
return True
workflow = context.portal_workflow
oldchain = workflow.getChainForPortalType('Discussion Item')
new_workflow = workflow.comment_review_workflow
mt = getToolByName(self.context, 'portal_membership')
if type(oldchain) == TupleType and len(oldchain) > 0:
oldchain = oldchain[0]
for reply in replies:
# log
indent = " "
for i in range(depth):
indent += " "
log("%smigrate_reply: '%s'." % (indent, reply.title))
should_migrate = True
if filter_callback and not filter_callback(reply):
should_migrate = False
if just_delete:
should_migrate = False
new_in_reply_to = None
if should_migrate:
# create a reply object
comment = CommentFactory()
comment.title = reply.Title()
comment.text = reply.cooked_text
comment.mime_type = 'text/html'
comment.creator = reply.Creator()
try:
comment.author_username = reply.author_username
except AttributeError:
comment.author_username = reply.Creator()
member = mt.getMemberById(comment.author_username)
if member:
comment.author_name = member.fullname
if not comment.author_name:
# In migrated site member.fullname = ''
# while member.getProperty('fullname') has the
# correct value
if member:
comment.author_name = member.getProperty(
'fullname'
)
else:
comment.author_name = comment.author_username
try:
comment.author_email = reply.email
except AttributeError:
comment.author_email = None
comment.creation_date = DT2dt(reply.creation_date)
comment.modification_date = DT2dt(reply.modification_date)
comment.reply_to = in_reply_to
if in_reply_to == 0:
# Direct reply to a content object
new_in_reply_to = conversation.addComment(comment)
else:
# Reply to another comment
comment_to_reply_to = conversation.get(in_reply_to)
replies = IReplies(comment_to_reply_to)
new_in_reply_to = replies.addComment(comment)
# migrate the review state
old_status = workflow.getStatusOf(oldchain, reply)
new_status = {
'action': None,
'actor': None,
'comment': 'Migrated workflow state',
'review_state': old_status and old_status.get(
'review_state',
new_workflow.initial_state
) or 'published',
'time': DateTime()
}
workflow.setStatusOf('comment_review_workflow',
comment,
new_status)
auto_transition = new_workflow._findAutomaticTransition(
comment,
new_workflow._getWorkflowStateOf(comment))
if auto_transition is not None:
new_workflow._changeStateOf(comment, auto_transition)
else:
new_workflow.updateRoleMappingsFor(comment)
comment.reindexObject(idxs=['allowedRolesAndUsers',
'review_state'])
self.total_comments_migrated += 1
# migrate all talkbacks of the reply
talkback = getattr(reply, 'talkback', None)
no_replies_left = migrate_replies(
context,
new_in_reply_to,
talkback.getReplies(),
depth=depth + 1,
just_delete=not should_migrate)
if no_replies_left:
# remove reply and talkback
talkback.deleteReply(reply.id)
obj = aq_parent(talkback)
obj.talkback = None
log("%sremove %s" % (indent, reply.id))
self.total_comments_deleted += 1
# Return True when all comments on a certain level have been
# migrated.
return True
# Find content
brains = catalog.searchResults(
object_provides='Products.CMFCore.interfaces._content.IContentish')
log("Found %s content objects." % len(brains))
count_discussion_items = len(
catalog.searchResults(Type='Discussion Item')
)
count_comments_pad = len(
catalog.searchResults(object_provides=IComment.__identifier__)
)
count_comments_old = len(
catalog.searchResults(
object_provides=IDiscussionResponse.__identifier__
)
)
log("Found %s Discussion Item objects." % count_discussion_items)
log("Found %s old discussion items." % count_comments_old)
log("Found %s plone.app.discussion comments." % count_comments_pad)
log("\n")
log("Start comment migration.")
# This loop is necessary to get all contentish objects, but not
# the Discussion Items. This wouldn't be necessary if the
# zcatalog would support NOT expressions.
new_brains = []
for brain in brains:
if brain.portal_type != 'Discussion Item':
new_brains.append(brain)
# Recursively run through the comment tree and migrate all comments.
for brain in new_brains:
obj = brain.getObject()
talkback = getattr(obj, 'talkback', None)
if talkback:
replies = talkback.getReplies()
if replies:
conversation = IConversation(obj)
log("\n")
log("Migrate '%s' (%s)" % (obj.Title(),
obj.absolute_url(relative=1)))
migrate_replies(context, 0, replies)
obj = aq_parent(talkback)
obj.talkback = None
if self.total_comments_deleted != self.total_comments_migrated:
log(
"Something went wrong during migration. The number of " +
"migrated comments (%s) differs from the number of deleted " +
"comments (%s)." % (
self.total_comments_migrated,
self.total_comments_deleted
)
)
if not test: # pragma: no cover
transaction.abort() # pragma: no cover
log("Abort transaction") # pragma: no cover
log("\n")
log("Comment migration finished.")
log("\n")
log("%s of %s comments migrated."
% (self.total_comments_migrated, count_comments_old))
if self.total_comments_migrated != count_comments_old:
log(
"%s comments could not be migrated." % (
count_comments_old - self.total_comments_migrated
)
) # pragma: no cover
log("Please make sure your " +
"portal catalog is up-to-date.") # pragma: no cover
if dry_run and not test:
transaction.abort() # pragma: no cover
log("Dry run") # pragma: no cover
log("Abort transaction") # pragma: no cover
if not test:
transaction.commit() # pragma: no cover
return '\n'.join(out)