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)