Back to index

moin  1.9.0~rc2
Public Member Functions | Public Attributes
MoinMoin.action.SyncPages.ActionClass Class Reference

List of all members.

Public Member Functions

def __init__
def log_status
def register_rollback
def remove_rollback
def call_rollback_funcs
def generate_log_table
def parse_page
def fix_params
def show_password_form
def render
def sync

Public Attributes

 request
 pagename
 page
 status
 rollback

Detailed Description

Definition at line 38 of file SyncPages.py.


Constructor & Destructor Documentation

def MoinMoin.action.SyncPages.ActionClass.__init__ (   self,
  pagename,
  request 
)

Definition at line 41 of file SyncPages.py.

00041 
00042     def __init__(self, pagename, request):
00043         self.request = request
00044         self.pagename = pagename
00045         self.page = PageEditor(request, pagename)
00046         self.status = []
00047         self.rollback = set()


Member Function Documentation

Definition at line 62 of file SyncPages.py.

00062 
00063     def call_rollback_funcs(self):
00064         _ = lambda x: x
00065 
00066         for func in self.rollback:
00067             try:
00068                 page_name = func()
00069                 self.log_status(self.INFO, _("Rolled back changes to the page %s."), (page_name, ))
00070             except Exception:
00071                 temp_file = StringIO.StringIO()
00072                 traceback.print_exc(file=temp_file)
00073                 self.log_status(self.ERROR, _("Exception while calling rollback function:"), raw_suffix=temp_file.getvalue())

Here is the call graph for this function:

Here is the caller graph for this function:

Does some fixup on the parameters. 

Definition at line 121 of file SyncPages.py.

00121 
00122     def fix_params(self, params):
00123         """ Does some fixup on the parameters. """
00124         # Load the password
00125         if "password" in self.request.values:
00126             params["password"] = self.request.values["password"]
00127 
00128         # merge the pageList case into the pageMatch case
00129         if params["pageList"] is not None:
00130             params["pageMatch"] = u'|'.join([r'^%s$' % re.escape(name)
00131                                              for name in params["pageList"]])
00132 
00133         if params["pageMatch"] is not None:
00134             params["pageMatch"] = re.compile(params["pageMatch"], re.U)
00135 
00136         # we do not support matching or listing pages if there is a group of pages
00137         if params["groupList"]:
00138             params["pageMatch"] = None
00139             params["pageList"] = None
00140 
00141         return params

Here is the caller graph for this function:

Transforms self.status into a user readable table. 

Definition at line 74 of file SyncPages.py.

00074 
00075     def generate_log_table(self):
00076         """ Transforms self.status into a user readable table. """
00077         table_line = u"|| %(smiley)s || %(message)s%(raw_suffix)s ||"
00078         table = []
00079 
00080         for line in self.status:
00081             level, message, substitutions, raw_suffix = line
00082             if message:
00083                 if substitutions:
00084                     macro_args = [message] + list(substitutions)
00085                     message = u"<<GetText2(|%s)>>" % (packLine(macro_args), )
00086                 else:
00087                     message = u"<<GetText(%s)>>" % (message, )
00088             else:
00089                 message = u""
00090             table.append(table_line % {"smiley": level[1],
00091                                        "message": message,
00092                                        "raw_suffix": raw_suffix.replace("\n", "<<BR>>")})
00093 
00094         return "\n".join(table)

Here is the call graph for this function:

Here is the caller graph for this function:

def MoinMoin.action.SyncPages.ActionClass.log_status (   self,
  level,
  message = u"",
  substitutions = (),
  raw_suffix = u"" 
)
Appends the message with a given importance level to the internal log. 

Definition at line 48 of file SyncPages.py.

00048 
00049     def log_status(self, level, message=u"", substitutions=(), raw_suffix=u""):
00050         """ Appends the message with a given importance level to the internal log. """
00051         if isinstance(message, str):
00052             message = message.decode("utf-8")
00053         if isinstance(raw_suffix, str):
00054             raw_suffix = raw_suffix.decode("utf-8")
00055         self.status.append((level, message, substitutions, raw_suffix))

Here is the caller graph for this function:

Parses the parameter page and returns the read arguments. 

Definition at line 95 of file SyncPages.py.

00095 
00096     def parse_page(self):
00097         """ Parses the parameter page and returns the read arguments. """
00098         options = {
00099             "remotePrefix": "",
00100             "localPrefix": "",
00101             "remoteWiki": "",
00102             "pageMatch": None,
00103             "pageList": None,
00104             "groupList": None,
00105             "direction": "foo", # is defaulted below
00106             "user": None, # XXX should be refactored into a password agent or OpenID like solution
00107             "password": None,
00108         }
00109 
00110         options.update(self.request.dicts[self.pagename])
00111 
00112         # Convert page and group list strings to lists
00113         if options["pageList"] is not None:
00114             options["pageList"] = unpackLine(options["pageList"], ",")
00115         if options["groupList"] is not None:
00116             options["groupList"] = unpackLine(options["groupList"], ",")
00117 
00118         options["direction"] = directions_map.get(options["direction"].lower(), BOTH)
00119 
00120         return options

Here is the call graph for this function:

Here is the caller graph for this function:

Definition at line 56 of file SyncPages.py.

00056 
00057     def register_rollback(self, func):
00058         self.rollback.add(func)

Here is the caller graph for this function:

Definition at line 59 of file SyncPages.py.

00059 
00060     def remove_rollback(self, func):
00061         self.rollback.remove(func)

Here is the caller graph for this function:

Render action

This action returns a status message.

Definition at line 170 of file SyncPages.py.

00170 
00171     def render(self):
00172         """ Render action
00173 
00174         This action returns a status message.
00175         """
00176         _ = self.request.getText
00177 
00178         params = self.fix_params(self.parse_page())
00179 
00180         try:
00181             if "cancel" in self.request.values:
00182                 raise ActionStatus(_("Operation was canceled."), "error")
00183 
00184             if params["direction"] == UP:
00185                 raise ActionStatus(_("The only supported directions are BOTH and DOWN."), "error")
00186 
00187             if not self.request.cfg.interwikiname:
00188                 raise ActionStatus(_("Please set an interwikiname in your wikiconfig (see HelpOnConfiguration) to be able to use this action.", wiki=True), "error")
00189 
00190             if not params["remoteWiki"]:
00191                 raise ActionStatus(_("Incorrect parameters. Please supply at least the ''remoteWiki'' parameter. Refer to HelpOnSynchronisation for help.", wiki=True), "error")
00192 
00193             local = MoinLocalWiki(self.request, params["localPrefix"], params["pageList"])
00194             try:
00195                 remote = MoinRemoteWiki(self.request, params["remoteWiki"], params["remotePrefix"], params["pageList"], params["user"], params["password"], verbose=debug)
00196             except (UnsupportedWikiException, NotAllowedException), (msg, ):
00197                 raise ActionStatus(msg, "error")
00198 
00199             if not remote.valid:
00200                 raise ActionStatus(_("The ''remoteWiki'' is unknown.", wiki=True), "error")
00201             # if only the username is supplied, we ask for the password
00202             if params["user"] and not params["password"]:
00203                 return self.show_password_form()
00204         except ActionStatus, e:
00205             self.request.theme.add_msg(*e.args)
00206         else:
00207             try:
00208                 try:
00209                     self.sync(params, local, remote)
00210                 except Exception, e:
00211                     temp_file = StringIO.StringIO()
00212                     traceback.print_exc(file=temp_file)
00213                     self.log_status(self.ERROR, _("A severe error occurred:"), raw_suffix=temp_file.getvalue())
00214                     raise
00215                 else:
00216                     self.request.theme.add_msg(u"%s" % (_("Synchronisation finished. Look below for the status messages."), ), "info")
00217             finally:
00218                 self.call_rollback_funcs()
00219                 # XXX aquire readlock on self.page
00220                 self.page.saveText(self.page.get_raw_body() + "\n\n" + self.generate_log_table(), 0)
00221                 # XXX release readlock on self.page
00222 
00223                 remote.delete_auth_token()
00224 
00225         return self.page.send_page()

Here is the call graph for this function:

Definition at line 142 of file SyncPages.py.

00142 
00143     def show_password_form(self):
00144         _ = self.request.getText
00145         d = {"message": _(r"Please enter your password of your account at the remote wiki below. <<BR>> /!\ You should trust both wikis because the password could be read by the particular administrators.", wiki=True),
00146              "passwordlabel": _("Password"),
00147              "submit": _("Login"),
00148              "cancel": _("Cancel"),
00149         }
00150         html_form = """
00151 %(message)s
00152 <form method="post">
00153 <div>
00154 <input type="hidden" name="action" value="SyncPages">
00155 <label for="iPassword" style="font-weight: bold;">%(passwordlabel)s:</label>
00156 <input type="password" name="password" id="iPassword" size="20">
00157 </div>
00158 <div style="margin-top:1em; margin-bottom:1em;">
00159 <div style="float:left">
00160 <input type="submit" value="%(submit)s">
00161 </div>
00162 <div style="margin-left: 10em; margin-right: 10em;">
00163 <input type="submit" value="%(cancel)s" name="cancel">
00164 </div>
00165 </div>
00166 </form>
00167 """ % d
00168         self.request.theme.add_msg(html_form, "dialog")
00169         self.page.send_page()

Here is the caller graph for this function:

def MoinMoin.action.SyncPages.ActionClass.sync (   self,
  params,
  local,
  remote 
)
This method does the synchronisation work.
    Currently, it handles nearly all cases.
    The major missing part is rename handling.
    There are a few other cases left that have to be implemented:
Wiki A    | Wiki B   | Remark
----------+----------+------------------------------
exists    | non-     | Now the wiki knows that the page was renamed.
with tags | existing | There should be an RPC method that asks
          |          | for the new name (which could be recorded
          |          | on page rename). Then the page is
          |          | renamed in Wiki A as well and the sync
          |          | is done normally.
          |          | Every wiki retains a dict that maps
          |          | (IWID, oldname) => newname and that is
          |          | updated on every rename. oldname refers
          |          | to the pagename known by the old wiki (can be
          |          | gathered from tags).
----------+----------+-------------------------------
exists    | any case | Try a rename search first, then
          |          | do a sync without considering tags
with tags | with non | to ensure data integrity.
          | matching | Hmm, how do we detect this
          | tags     | case if the unmatching tags are only
          |          | on the remote side?
----------+----------+-------------------------------

Definition at line 226 of file SyncPages.py.

00226 
00227     def sync(self, params, local, remote):
00228         """ This method does the synchronisation work.
00229             Currently, it handles nearly all cases.
00230             The major missing part is rename handling.
00231             There are a few other cases left that have to be implemented:
00232                 Wiki A    | Wiki B   | Remark
00233                 ----------+----------+------------------------------
00234                 exists    | non-     | Now the wiki knows that the page was renamed.
00235                 with tags | existing | There should be an RPC method that asks
00236                           |          | for the new name (which could be recorded
00237                           |          | on page rename). Then the page is
00238                           |          | renamed in Wiki A as well and the sync
00239                           |          | is done normally.
00240                           |          | Every wiki retains a dict that maps
00241                           |          | (IWID, oldname) => newname and that is
00242                           |          | updated on every rename. oldname refers
00243                           |          | to the pagename known by the old wiki (can be
00244                           |          | gathered from tags).
00245                 ----------+----------+-------------------------------
00246                 exists    | any case | Try a rename search first, then
00247                           |          | do a sync without considering tags
00248                 with tags | with non | to ensure data integrity.
00249                           | matching | Hmm, how do we detect this
00250                           | tags     | case if the unmatching tags are only
00251                           |          | on the remote side?
00252                 ----------+----------+-------------------------------
00253         """
00254         _ = lambda x: x # we will translate it later
00255 
00256         direction = params["direction"]
00257         if direction == BOTH:
00258             match_direction = direction
00259         else:
00260             match_direction = None
00261 
00262         local_full_iwid = packLine([local.get_iwid(), local.get_interwiki_name()])
00263         remote_full_iwid = remote.iwid_full
00264 
00265         self.log_status(self.INFO, _("Synchronisation started -"), raw_suffix=" <<DateTime(%s)>>" % self.page._get_local_timestamp())
00266 
00267         l_pages = local.get_pages()
00268         r_pages = remote.get_pages(exclude_non_writable=direction != DOWN)
00269 
00270         if params["groupList"]:
00271             pages_from_groupList = set(local.getGroupItems(params["groupList"]))
00272             r_pages = SyncPage.filter(r_pages, pages_from_groupList.__contains__)
00273             l_pages = SyncPage.filter(l_pages, pages_from_groupList.__contains__)
00274 
00275         m_pages = [elem.add_missing_pagename(local, remote) for elem in SyncPage.merge(l_pages, r_pages)]
00276 
00277         self.log_status(self.INFO, _("Got a list of %s local and %s remote pages. This results in %s pages to process."),
00278                         (str(len(l_pages)), str(len(r_pages)), str(len(m_pages))))
00279 
00280         if params["pageMatch"]:
00281             m_pages = SyncPage.filter(m_pages, params["pageMatch"].match)
00282             self.log_status(self.INFO, _("After filtering: %s pages"), (str(len(m_pages)), ))
00283 
00284         class handle_page(rpc_aggregator.RPCYielder):
00285             def run(yielder, sp):
00286                 # XXX add locking, acquire read-lock on sp
00287                 if debug:
00288                     self.log_status(ActionClass.INFO, raw_suffix="Processing %r" % sp)
00289 
00290                 local_pagename = sp.local_name
00291                 if not self.request.user.may.write(local_pagename):
00292                     self.log_status(ActionClass.WARN, _("Skipped page %s because of no write access to local page."), (local_pagename, ))
00293                     return
00294 
00295                 current_page = PageEditor(self.request, local_pagename) # YYY direct access
00296                 comment = u"Local Merge - %r" % (remote.get_interwiki_name() or remote.get_iwid())
00297 
00298                 tags = TagStore(current_page)
00299 
00300                 matching_tags = tags.fetch(iwid_full=remote.iwid_full, direction=match_direction)
00301                 matching_tags.sort()
00302                 if debug:
00303                     self.log_status(ActionClass.INFO, raw_suffix="Tags: %r <<BR>> All: %r" % (matching_tags, tags.tags))
00304 
00305                 # some default values for non matching tags
00306                 normalised_name = None
00307                 remote_rev = None
00308                 local_rev = sp.local_rev # merge against the newest version
00309                 old_contents = ""
00310 
00311                 if matching_tags:
00312                     newest_tag = matching_tags[-1]
00313 
00314                     local_change = newest_tag.current_rev != sp.local_rev
00315                     remote_change = newest_tag.remote_rev != sp.remote_rev
00316 
00317                     # handle some cases where we cannot continue for this page
00318                     if not remote_change and (direction == DOWN or not local_change):
00319                         return # no changes done, next page
00320                     if sp.local_deleted and sp.remote_deleted:
00321                         return
00322                     if sp.remote_deleted and not local_change:
00323                         msg = local.delete_page(sp.local_name, comment)
00324                         if not msg:
00325                             self.log_status(ActionClass.INFO, _("Deleted page %s locally."), (sp.name, ))
00326                         else:
00327                             self.log_status(ActionClass.ERROR, _("Error while deleting page %s locally:"), (sp.name, ), msg)
00328                         return
00329                     if sp.local_deleted and not remote_change:
00330                         if direction == DOWN:
00331                             return
00332                         yield remote.delete_page_pre(sp.remote_name, sp.remote_rev, local_full_iwid)
00333                         msg = remote.delete_page_post(yielder.fetch_result())
00334                         if not msg:
00335                             self.log_status(ActionClass.INFO, _("Deleted page %s remotely."), (sp.name, ))
00336                         else:
00337                             self.log_status(ActionClass.ERROR, _("Error while deleting page %s remotely:"), (sp.name, ), msg)
00338                         return
00339                     if sp.local_mime_type != MIMETYPE_MOIN and not (local_change ^ remote_change):
00340                         self.log_status(ActionClass.WARN, _("The item %s cannot be merged automatically but was changed in both wikis. Please delete it in one of both wikis and try again."), (sp.name, ))
00341                         return
00342                     if sp.local_mime_type != sp.remote_mime_type:
00343                         self.log_status(ActionClass.WARN, _("The item %s has different mime types in both wikis and cannot be merged. Please delete it in one of both wikis or unify the mime type, and try again."), (sp.name, ))
00344                         return
00345                     if newest_tag.normalised_name != sp.name:
00346                         self.log_status(ActionClass.WARN, _("The item %s was renamed locally. This is not implemented yet. Therefore the full synchronisation history is lost for this page."), (sp.name, )) # XXX implement renames
00347                     else:
00348                         normalised_name = newest_tag.normalised_name
00349                         local_rev = newest_tag.current_rev
00350                         remote_rev = newest_tag.remote_rev
00351                         old_contents = Page(self.request, local_pagename, rev=newest_tag.current_rev).get_raw_body_str() # YYY direct access
00352                 else:
00353                     if (sp.local_deleted and not sp.remote_rev) or (
00354                         sp.remote_deleted and not sp.local_rev):
00355                         return
00356 
00357                 self.log_status(ActionClass.INFO, _("Synchronising page %s with remote page %s ..."), (local_pagename, sp.remote_name))
00358 
00359                 if direction == DOWN:
00360                     remote_rev = None # always fetch the full page, ignore remote conflict check
00361                     patch_base_contents = ""
00362                 else:
00363                     patch_base_contents = old_contents
00364 
00365                 # retrieve remote contents diff
00366                 if remote_rev != sp.remote_rev:
00367                     if sp.remote_deleted: # ignore remote changes
00368                         current_remote_rev = sp.remote_rev
00369                         is_remote_conflict = False
00370                         diff = None
00371                         self.log_status(ActionClass.WARN, _("The page %s was deleted remotely but changed locally."), (sp.name, ))
00372                     else:
00373                         yield remote.get_diff_pre(sp.remote_name, remote_rev, None, normalised_name)
00374                         diff_result = remote.get_diff_post(yielder.fetch_result())
00375                         if diff_result is None:
00376                             self.log_status(ActionClass.ERROR, _("The page %s could not be synced. The remote page was renamed. This is not supported yet. You may want to delete one of the pages to get it synced."), (sp.remote_name, ))
00377                             return
00378                         is_remote_conflict = diff_result["conflict"]
00379                         assert diff_result["diffversion"] == 1
00380                         diff = diff_result["diff"]
00381                         current_remote_rev = diff_result["current"]
00382                 else:
00383                     current_remote_rev = remote_rev
00384                     if sp.local_mime_type == MIMETYPE_MOIN:
00385                         is_remote_conflict = wikiutil.containsConflictMarker(old_contents.decode("utf-8"))
00386                     else:
00387                         is_remote_conflict = NotImplemented
00388                     diff = None
00389 
00390                 # do not sync if the conflict is remote and local, or if it is local
00391                 # and the page has never been synchronised
00392                 if (sp.local_mime_type == MIMETYPE_MOIN and wikiutil.containsConflictMarker(current_page.get_raw_body()) # YYY direct access
00393                     and (remote_rev is None or is_remote_conflict)):
00394                     self.log_status(ActionClass.WARN, _("Skipped page %s because of a locally or remotely unresolved conflict."), (local_pagename, ))
00395                     return
00396 
00397                 if remote_rev is None and direction == BOTH:
00398                     self.log_status(ActionClass.INFO, _("This is the first synchronisation between the local and the remote wiki for the page %s."), (sp.name, ))
00399 
00400                 # calculate remote page contents from diff
00401                 if sp.remote_deleted:
00402                     remote_contents = ""
00403                 elif diff is None:
00404                     remote_contents = old_contents
00405                 else:
00406                     remote_contents = patch(patch_base_contents, decompress(diff))
00407 
00408                 if diff is None: # only a local change
00409                     if debug:
00410                         self.log_status(ActionClass.INFO, raw_suffix="Only local changes for %r" % sp.name)
00411                     merged_text_raw = current_page.get_raw_body_str()
00412                     if sp.local_mime_type == MIMETYPE_MOIN:
00413                         merged_text = merged_text_raw.decode("utf-8")
00414                 elif local_rev == sp.local_rev:
00415                     if debug:
00416                         self.log_status(ActionClass.INFO, raw_suffix="Only remote changes for %r" % sp.name)
00417                     merged_text_raw = remote_contents
00418                     if sp.local_mime_type == MIMETYPE_MOIN:
00419                         merged_text = merged_text_raw.decode("utf-8")
00420                 else:
00421                     # this is guaranteed by a check above
00422                     assert sp.local_mime_type == MIMETYPE_MOIN
00423                     remote_contents_unicode = remote_contents.decode("utf-8")
00424                     # here, the actual 3-way merge happens
00425                     merged_text = diff3.text_merge(old_contents.decode("utf-8"), remote_contents_unicode, current_page.get_raw_body(), 1, *conflict_markers) # YYY direct access
00426                     if debug:
00427                         self.log_status(ActionClass.INFO, raw_suffix="Merging %r, %r and %r into %r" % (old_contents.decode("utf-8"), remote_contents_unicode, current_page.get_raw_body(), merged_text))
00428                     merged_text_raw = merged_text.encode("utf-8")
00429 
00430                 # generate binary diff
00431                 diff = textdiff(remote_contents, merged_text_raw)
00432                 if debug:
00433                     self.log_status(ActionClass.INFO, raw_suffix="Diff against %r" % remote_contents)
00434 
00435                 # XXX upgrade to write lock
00436                 try:
00437                     local_change_done = True
00438                     current_page.saveText(merged_text, sp.local_rev or 0, comment=comment) # YYY direct access
00439                 except PageEditor.Unchanged:
00440                     local_change_done = False
00441                 except PageEditor.EditConflict:
00442                     local_change_done = False
00443                     assert False, "You stumbled on a problem with the current storage system - I cannot lock pages"
00444 
00445                 new_local_rev = current_page.get_real_rev() # YYY direct access
00446 
00447                 def rollback_local_change(): # YYY direct local access
00448                     comment = u"Wikisync rollback"
00449                     rev = new_local_rev - 1
00450                     revstr = '%08d' % rev
00451                     oldpg = Page(self.request, sp.local_name, rev=rev)
00452                     pg = PageEditor(self.request, sp.local_name)
00453                     if not oldpg.exists():
00454                         pg.deletePage(comment)
00455                     else:
00456                         try:
00457                             savemsg = pg.saveText(oldpg.get_raw_body(), 0, comment=comment, extra=revstr, action="SAVE/REVERT")
00458                         except PageEditor.Unchanged:
00459                             pass
00460                     return sp.local_name
00461 
00462                 if local_change_done:
00463                     self.register_rollback(rollback_local_change)
00464 
00465                 if direction == BOTH:
00466                     yield remote.merge_diff_pre(sp.remote_name, compress(diff), new_local_rev, current_remote_rev, current_remote_rev, local_full_iwid, sp.name)
00467                     try:
00468                         very_current_remote_rev = remote.merge_diff_post(yielder.fetch_result())
00469                     except NotAllowedException:
00470                         self.log_status(ActionClass.ERROR, _("The page %s could not be merged because you are not allowed to modify the page in the remote wiki."), (sp.name, ))
00471                         return
00472                 else:
00473                     very_current_remote_rev = current_remote_rev
00474 
00475 
00476                 if local_change_done:
00477                     self.remove_rollback(rollback_local_change)
00478 
00479                 # this is needed at least for direction both and cgi sync to standalone for immutable pages on both
00480                 # servers. It is not needed for the opposite direction
00481                 try:
00482                     tags.add(remote_wiki=remote_full_iwid, remote_rev=very_current_remote_rev, current_rev=new_local_rev, direction=direction, normalised_name=sp.name)
00483                 except:
00484                     self.log_status(ActionClass.ERROR, _("The page %s could not be merged because you are not allowed to modify the page in the remote wiki."), (sp.name, ))
00485                     return
00486 
00487                 if sp.local_mime_type != MIMETYPE_MOIN or not wikiutil.containsConflictMarker(merged_text):
00488                     self.log_status(ActionClass.INFO, _("Page %s successfully merged."), (sp.name, ))
00489                 elif is_remote_conflict:
00490                     self.log_status(ActionClass.WARN, _("Page %s contains conflicts that were introduced on the remote side."), (sp.name, ))
00491                 else:
00492                     self.log_status(ActionClass.WARN, _("Page %s merged with conflicts."), (sp.name, ))
00493 
00494                 # XXX release lock
00495 
00496         rpc_aggregator.scheduler(remote.create_multicall_object, handle_page, m_pages, 8, remote.prepare_multicall)
00497 

Here is the call graph for this function:

Here is the caller graph for this function:


Member Data Documentation

Definition at line 44 of file SyncPages.py.

Definition at line 43 of file SyncPages.py.

Definition at line 42 of file SyncPages.py.

Definition at line 46 of file SyncPages.py.

Definition at line 45 of file SyncPages.py.


The documentation for this class was generated from the following file: