Zim

Merge lp:~grahamrow/zim/zim-mendeley into lp:~jaap.karssenberg/zim/pyzim

Proposed by Graham Rowlands on 2014-06-22
Status: Needs review
Proposed branch: lp:~grahamrow/zim/zim-mendeley
Merge into: lp:~jaap.karssenberg/zim/pyzim
Diff against target: 834 lines (+796/-0)
6 files modified
HACKING/notebook.zim (+1/-0)
data/manual/Plugins.txt (+1/-0)
data/manual/Plugins/Mendeley_Citations.txt (+17/-0)
zim/inc/MendeleyDesktopAPI.py (+357/-0)
zim/inc/MendeleyHttpClient.py (+228/-0)
zim/plugins/mendeley.py (+192/-0)
To merge this branch: bzr merge lp:~grahamrow/zim/zim-mendeley
Reviewer Review Type Date Requested Status
Jaap Karssenberg 2014-06-22 Pending
Review via email: mp+224053@code.launchpad.net

Description of the change

Hi Jaap, I've been meaning to tie up some loose ends and get this code into the main branch. Mendeley, if you recall, is a citation management program that is widely used in academic communities. A few colleagues and I have been using this plugin with good results over the past several months.

General Functionality and comments:
- Inserts formatted citations (as simple Zim "link" objects) with links to either a users own Mendeley library (via the local mendeley:// protocol) or to a more universal DOI redirect (via http://dx.doi.org).
- Creates a bibliography in the used-specified format by scraping the current page for mendeley:// or dx.doi.org links and polling the Mendeley application for a complete formatted bibliography.
- The backend is a slightly modified version of the code used in Mendeley's own OpenOffice plugin (https://github.com/Mendeley/openoffice-plugin). The changes don't affect the general functionality of that code, and mainly involve commenting extraneous sections and changing of the exact return values for convenience. Thus I've opted to keep this in the zim/inc directory for now.
- Since the plugin communicates with the Mendeley application through a minimal web socket, it requires Mendeley to be open. Perhaps I can later implement a feature to have the program be launched automatically.

Preferences include:
1. Whether to insert dx.doi.org or mendeley:// links
2. The citation format
3. The bibliography format

Known limitations:
1. Unit test still lacking, but since the plugin only inserts links and text and is based of the generic plugin class I think that is safe for now. I'll come up with something soon!

To post a comment you must log in.
lp:~grahamrow/zim/zim-mendeley updated on 2014-08-19
684. By Graham Rowlands on 2014-08-19

Pulled in Zim 0.61 code.

Unmerged revisions

684. By Graham Rowlands on 2014-08-19

Pulled in Zim 0.61 code.

683. By Graham Rowlands on 2014-06-22

Pulled latest lp:zim and relocated Mendeley API code back to inc/ since it is vanilla code from Mendeley. This fixed an issue where the plugin manager thought the plugins/mendeleyAPI directory was itself a plugin, causing an error message to be generated when closing the preferences dialog

682. By Graham Rowlands on 2014-03-20

Merged with main zim branch

681. By Graham Rowlands on 2014-03-20

Merged into pyzim-refactor. A bit late now...

680. By Graham Rowlands on 2014-01-21

Relocated Mendeley API code to plugins/mendeleyAPI. Added bibliography capability.

679. By Graham Rowlands on 2014-01-20

Updated reg. exps. for converting bib html to zim formatting.

678. By Graham Rowlands on 2014-01-20

Rebased to pyzim-refactor. Added rudimentary bibliography support.

677. By Graham Rowlands on 2014-01-20

Reverted inavertent changes to notebook.zim and updated documentation.

676. By Graham Rowlands on 2014-01-20

Initial commit of plugin

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'HACKING/notebook.zim'
2--- HACKING/notebook.zim 2014-07-31 19:13:17 +0000
3+++ HACKING/notebook.zim 2014-08-19 17:10:36 +0000
4@@ -10,3 +10,4 @@
5 disable_trash=False
6 profile=
7
8+
9
10=== modified file 'data/manual/Plugins.txt'
11--- data/manual/Plugins.txt 2014-07-31 19:13:17 +0000
12+++ data/manual/Plugins.txt 2014-08-19 17:10:36 +0000
13@@ -21,6 +21,7 @@
14 * [[+Link Map|Link Map]]
15 * [[+Line Sorter|Line Sorter]]
16 * [[+Log events with Zeitgeist|Log events with Zeitgeist]]
17+* [[+Mendeley Citations|Mendeley Citations]]
18 * [[+Print to Browser|Print to Browser]]
19 * [[+Quick Note|Quick Note]]
20 * [[+Score Editor|Score Editor]]
21
22=== added file 'data/manual/Plugins/Mendeley_Citations.txt'
23--- data/manual/Plugins/Mendeley_Citations.txt 1970-01-01 00:00:00 +0000
24+++ data/manual/Plugins/Mendeley_Citations.txt 2014-08-19 17:10:36 +0000
25@@ -0,0 +1,17 @@
26+Content-Type: text/x-zim-wiki
27+Wiki-Format: zim 0.4
28+Creation-Date: 2014-01-19T14:38:51-05:00
29+
30+====== Mendeley Citations ======
31+Uses the Mendeley Desktop API to insert citations as links that open in either the user's Mendeley library or the http://dx.doi.org DOI redirection service. The plugin does not currently keep track of the citations within a notebook and hence is not able to render a bibliography or keep track of numerical citation indices at this time.
32+
33+**Dependencies: **This plugin has no additional dependencies.
34+
35+===== Options =====
36+The following options may be specified for the plugin:
37+
38+**Citation Style **is the style in which the citations are rendered. Any style from http://www.zotero.org/styles/ may be used, with the style specified as the suffix of the available URLs, e.g. //apa// for http://zotero.org/styles/apa or //springer-physics-author-date// for http://www.zotero.org/styles/springer-physics-author-date.
39+
40+**Links point to **determines whether the inserted links point to the user's personal Mendeley Library or the DOI Reference service at http://dx.doi.org. The links to a personal library are not portable, but are based on a paper's UUID and are gauranteed to exist. DOI information may not be available for a paper, in which case the insertion of citations will fail.
41+
42+Numerical citation styles are, at present, not rendered properly for reasons outlined above.
43
44=== added file 'zim/inc/MendeleyDesktopAPI.py'
45--- zim/inc/MendeleyDesktopAPI.py 1970-01-01 00:00:00 +0000
46+++ zim/inc/MendeleyDesktopAPI.py 2014-08-19 17:10:36 +0000
47@@ -0,0 +1,357 @@
48+#!/usr/bin/python
49+
50+# Mendeley Desktop API
51+# This software is included on the basis of the Educational Community License,
52+# which can be found at <http://opensource.org/licenses/ECL-2.0>
53+
54+# simplejson is json (using a generic 'except' and not 'except ImportError'
55+# because MD-19770. See: https://bugs.launchpad.net/ubuntu/+source/libreoffice/+bug/1222823
56+
57+try: import simplejson as json
58+except: import json
59+
60+import os
61+import re
62+import codecs
63+import sys
64+if sys.version_info < (3, 0):
65+ python3 = False
66+else:
67+ python3 = True
68+
69+# All strings are unicode in Python 3.
70+# See http://stackoverflow.com/questions/6812031/how-to-make-unicode-string-with-python3
71+if python3:
72+ unicode = str
73+
74+# if DO_NOT_USE_UNO_HELPER environment variable exists:
75+# it doesn't try to use the unohelper package. Mendeley tests sets this
76+# variable when needed.
77+fakeUnoHelper = True
78+if not 'DO_NOT_USE_UNO_HELPER' in os.environ:
79+ try:
80+ import unohelper
81+ from com.sun.star.task import XJob
82+ fakeUnoHelper = False
83+ except ImportError:
84+ pass
85+
86+if fakeUnoHelper:
87+ from MendeleyHttpClient import MendeleyHttpClient
88+
89+ class unohelper():
90+ def __init__(self, ctx):
91+ pass
92+
93+ class Base():
94+ def __init__(self, ctx):
95+ pass
96+
97+ class XJob():
98+ def __init__(self, ctx):
99+ pass
100+
101+else:
102+ g_ImplementationHelper = unohelper.ImplementationHelper()
103+
104+class MendeleyDesktopAPI(unohelper.Base, XJob):
105+ def __init__(self, ctx):
106+ self.closed = 0
107+ self.ctx = ctx # component context
108+ self._client = MendeleyHttpClient()
109+
110+ self._formattedCitationsResponse = MendeleyHttpClient.ResponseBody()
111+
112+ self.citationClusters = []
113+ self.citationStyleUrl = ""
114+ self.formattedBibliography = []
115+
116+ self._previousResultLength = 0
117+
118+ def resetCitations(self):
119+ self.citationClusters = []
120+
121+ def _citationClusterFromFieldCode(self, fieldCode):
122+ # remove ADDIN and CSL_CITATION from start
123+ pattern = re.compile("CSL_CITATION[ ]*({.*$)")
124+ match = pattern.search(fieldCode)
125+
126+ if match == None:
127+ result = {"fieldCode" : fieldCode.decode('string_escape')}
128+ else:
129+ bareJson = match.group(1)
130+ citationCluster = json.loads(bareJson)
131+ result = {"citationCluster" : citationCluster}
132+ return result
133+
134+ def addCitationCluster(self, fieldCode):
135+ self.citationClusters.append(self._citationClusterFromFieldCode(fieldCode))
136+
137+ def addFormattedCitation(self, formattedCitation):
138+ self.citationClusters[len(self.citationClusters)-1]["formattedText"] = formattedCitation
139+
140+ def setCitationStyle(self, citationStyleUrl):
141+ self.citationStyleUrl = citationStyleUrl
142+
143+ def getCitationStyleId(self):
144+ return self.citationStyleUrl
145+
146+ def formatCitationsAndBibliography(self):
147+ self._formattedCitationsResponse = \
148+ self._client.formattedCitationsAndBibliography_Interactive(
149+ self.citationStyleUrl, self.citationClusters).body
150+
151+ return json.dumps(self._formattedCitationsResponse.__dict__)
152+
153+ def getCitationCluster(self, index):
154+ return "ADDIN CSL_CITATION " + json.dumps(self._formattedCitationsResponse.citationClusters[int(index)]["citationCluster"])
155+
156+ def getCitationClusterUUIDs(self, index):
157+ cites = self._formattedCitationsResponse.citationClusters[int(index)]["citationCluster"]["citationItems"]
158+ uris = []
159+ if len(cites) == 1:
160+ uris.append(cites[0]["uris"])
161+ else:
162+ uris.extend([cite["uris"] for cite in cites])
163+
164+ pattern = re.compile(".*\?uuid=(.*)']")
165+ matches = [pattern.search(str(uri)).group(1) for uri in uris]
166+ return matches
167+
168+ def getCitationClusterDOIs(self, index):
169+ cites = self._formattedCitationsResponse.citationClusters[int(index)]["citationCluster"]["citationItems"]
170+ dois = []
171+
172+ if len(cites) == 1:
173+ try:
174+ dois.append(str(cites[0]['itemData']["DOI"]))
175+ except KeyError:
176+ raise KeyError
177+ else:
178+ dois.extend([str(cite['itemData']["DOI"]) for cite in cites])
179+
180+ return dois
181+
182+ def getLocalURLs(self, index):
183+ return ["mendeley://library/document/"+uuid for uuid in self.getCitationClusterUUIDs(index)]
184+
185+ def getDOIURLs(self, index, addUUID=False):
186+ dois = self.getCitationClusterDOIs(index)
187+ if addUUID:
188+ uuids = self.getCitationClusterUUIDs(index)
189+ return ["http://dx.doi.org/"+doi+"?uuid="+uuid for doi, uuid in zip(dois, uuids)]
190+ else:
191+ return ["http://dx.doi.org/"+doi for doi in dois]
192+
193+ def getFormattedCitation(self, index):
194+ return self._formattedCitationsResponse.citationClusters[int(index)]["formattedText"]
195+
196+ def getFormattedCitations(self):
197+ return [c["formattedText"] for c in self._formattedCitationsResponse.citationClusters]
198+
199+ def getFormattedCitationLength(self):
200+ return len(self._formattedCitationsResponse.citationClusters)
201+
202+ def getFormattedBibliography(self):
203+ # a single string is interpreted as a file name
204+ if (type(self._formattedCitationsResponse.bibliography) == type(u"unicode string")
205+ or type(self._formattedCitationsResponse.bibliography) == type("string")):
206+ return self._formattedCitationsResponse.bibliography;
207+ else:
208+ return "<br/>".join(self._formattedCitationsResponse.bibliography)
209+
210+ def getUserAccount(self):
211+ response = self._client.userAccount()
212+
213+ if (response.status != 200):
214+ raise MendeleyHttpClient.UnexpectedResponse(response)
215+
216+ return response.body.account
217+
218+ def citationStyle_choose_interactive(self, styleId):
219+ return self._client.citationStyle_choose_interactive(
220+ {"currentStyleUrl": styleId}).body.citationStyleUrl
221+
222+ def citation_choose_interactive(self, hintText):
223+ response = self._client.citation_choose_interactive(
224+ {"citationEditorHint": hintText})
225+ try:
226+ assert(response.status == 200)
227+ fieldCode = self._fieldCodeFromCitationCluster(response.body.citationCluster)
228+ except:
229+ raise MendeleyHttpClient.UnexpectedResponse(response)
230+
231+ return fieldCode
232+
233+ def citation_edit_interactive(self, fieldCode, hintText):
234+ citationCluster = self._citationClusterFromFieldCode(fieldCode)
235+ citationCluster["citationEditorHint"] = hintText
236+ response = self._client.citation_edit_interactive(citationCluster)
237+ try:
238+ assert(response.status == 200)
239+ fieldCode = self._fieldCodeFromCitationCluster(response.body.citationCluster)
240+ except:
241+ raise MendeleyHttpClient.UnexpectedResponse(response)
242+ return fieldCode
243+
244+ def setDisplayedText(self, displayedText):
245+ self.formattedText = displayedText
246+
247+ def citation_update_interactive(self, fieldCode, formattedText):
248+ citationCluster = self._citationClusterFromFieldCode(fieldCode)
249+ citationCluster["formattedText"] = formattedText
250+
251+ response = self._client.citation_update_interactive(citationCluster)
252+ try:
253+ assert(response.status == 200)
254+ fieldCode = self._fieldCodeFromCitationCluster(response.body.citationCluster)
255+ except:
256+ raise MendeleyHttpClient.UnexpectedResponse(response)
257+ return fieldCode
258+
259+ def getFieldCodeFromUuid(self, documentUuid):
260+ response = self._client.testMethods_citationCluster_getFromUuid(
261+ {"documentUuid": documentUuid})
262+ try:
263+ assert(response.status == 200)
264+ fieldCode = self._fieldCodeFromCitationCluster(response.body.citationCluster)
265+ except:
266+ raise MendeleyHttpClient.UnexpectedResponse(response)
267+ return fieldCode
268+
269+ def _fieldCodeFromCitationCluster(self, citationCluster):
270+ if ("citationItems" in citationCluster):
271+ if (len(citationCluster["citationItems"]) == 0):
272+ return ""
273+
274+ return "ADDIN CSL_CITATION " + json.dumps(citationCluster, sort_keys=True)
275+
276+ def citation_undoManualFormat(self, fieldCode):
277+ citationCluster = self._citationClusterFromFieldCode(fieldCode)
278+ response = self._client.citation_undoManualFormat(citationCluster)
279+ try:
280+ assert(response.status == 200)
281+ fieldCode = self._fieldCodeFromCitationCluster(response.body.citationCluster)
282+ except:
283+ raise MendeleyHttpClient.UnexpectedResponse(response)
284+ return fieldCode
285+
286+ def citations_merge(self, *fieldCodes):
287+ clusters = []
288+
289+ for fieldCode in fieldCodes:
290+ clusters.append(self._citationClusterFromFieldCode(fieldCode))
291+
292+ response = self._client.citations_merge({"citationClusters": clusters})
293+ try:
294+ assert(response.status == 200)
295+ mergedFieldCode = \
296+ self._fieldCodeFromCitationCluster(response.body.citationCluster)
297+ except:
298+ raise MendeleyHttpClient.UnexpectedResponse(response)
299+
300+ return mergedFieldCode
301+
302+ def wordProcessor_set(self, wordProcessor, version):
303+ response = self._client.wordProcessor_set(
304+ {
305+ "wordProcessor": wordProcessor,
306+ "version": version
307+ })
308+
309+ try:
310+ assert(response.status == 200)
311+ except:
312+ raise MendeleyHttpClient.UnexpectedResponse(response)
313+
314+ return ""
315+
316+ def mendeleyDesktopInfo(self):
317+ response = self._client.mendeleyDesktopInfo()
318+ try:
319+ assert(response.status == 200)
320+ except:
321+ raise MendeleyHttpClient.UnexpectedResponse(response)
322+
323+ result = {"processId": response.body.processId}
324+ return result
325+
326+ def isMendeleyDesktopRunningStr(self):
327+ try:
328+ response = self._client.mendeleyDesktopInfo()
329+ return str(response.status == 200)
330+ except:
331+ return False
332+
333+ # for testing
334+ def setNumberTest(self, number):
335+ self.number = number.decode('string_escape')
336+ return ""
337+
338+ # for testing
339+ def getNumberTest(self):
340+ return str(self.number)
341+
342+ # for testing
343+ def concatenateStringsTest(self, string1, string2):
344+ return str(string1) + str(string2)
345+
346+ def previousSuccess(self):
347+ previousResponse = self._client.previousResponse
348+
349+ return str(previousResponse.status == 200)
350+
351+ def previousErrorMessage(self):
352+ previousResponse = self._client.previousResponse
353+
354+ downloadInstructions = "Please download the latest version " + \
355+ "of Mendeley Desktop here: \n" + \
356+ "http://www.mendeley.com/download-mendeley-desktop"
357+
358+ if (previousResponse.status == 406 or
359+ previousResponse.status == 415):
360+ if (previousResponse.contentType.startswith(
361+ "application/vnd.mendeley.typeDeprecatedError")):
362+ # TODO: insert link to plugin download
363+ return "Deprecated type error. Please update this plugin to work with the " + \
364+ "current version of Mendeley Desktop"
365+ else:
366+ return "Unknown type error. " + downloadInstructions
367+
368+ if (previousResponse.status == 404):
369+ return "Page not found. " + downloadInstructions
370+
371+ if (previousResponse.status != 200):
372+ return "Unknown error\n" + json.dumps(previousResponse.__dict__)
373+
374+ return ""
375+
376+ def previousResultLength(self):
377+ return self._previousResultLength
378+
379+ def previousResponse(self):
380+ return json.dumps(self.previousResponse.__dict__)
381+
382+ def execute(self, args):
383+ functionName = str(args[0].Value)
384+ statement = 'self.' + functionName + '('
385+ for arg in range(1, len(args)):
386+ statement += '"'
387+ data = codecs.getencoder('unicode_escape')(args[arg].Value)[0]
388+ if python3:
389+ data = data.decode('utf-8')
390+ statement += data.replace('"', '\\"')
391+ statement += '"'
392+ if arg < len(args) - 1:
393+ statement += ', '
394+ statement += ')'
395+
396+ if hasattr(self, functionName):
397+ try:
398+ result = eval(statement)
399+ self._previousResultLength = len(unicode(result))
400+ return result
401+ except MendeleyHttpClient.UnexpectedResponse:
402+ return ""
403+ else:
404+ raise Exception("ERROR: Function " + functionName + " doesn't exist")
405
406=== added file 'zim/inc/MendeleyHttpClient.py'
407--- zim/inc/MendeleyHttpClient.py 1970-01-01 00:00:00 +0000
408+++ zim/inc/MendeleyHttpClient.py 2014-08-19 17:10:36 +0000
409@@ -0,0 +1,228 @@
410+#!/usr/bin/python
411+
412+import time
413+import sys
414+if sys.version_info < (3, 0):
415+ import httplib
416+else:
417+ import http.client as httplib
418+ StandardError = Exception
419+
420+# Mendeley HTTP Client
421+
422+# A client for communicating with the HTTP/JSON Mendeley Desktop Word
423+# processor API
424+
425+# simplejson is json
426+# simplejson is json (using a generic 'except' and not 'except ImportError'
427+# because MD-19770. See https://bugs.launchpad.net/ubuntu/+source/libreoffice/+bug/1222823
428+try: import simplejson as json
429+except: import json
430+
431+# For communicating with the Mendeley Desktop HTTP API
432+class MendeleyHttpClient():
433+ HOST = "127.0.0.1" # much faster than "localhost" on Windows
434+ # see http://cubicspot.blogspot.com/2010/07/fixing-slow-apache-on-localhost-under.html
435+ PORT = "50002"
436+ CONTENT_TYPE = "application/vnd.mendeley.wordProcessorApi+json; version=1.0"
437+ lastRequestTime = -1
438+
439+ def __init__(self):
440+ self.previousResponse = self.Response(200, None, None, None)
441+
442+ class Response:
443+ def __init__(self, status, contentType, body, request):
444+ self.status = status
445+ self.body = body
446+ self.contentType = contentType
447+ self.request = request
448+
449+ class UnexpectedResponse(StandardError):
450+ def __init__(self, response):
451+
452+ try:
453+ message = "response: ", json.dumps(response)
454+ except:
455+ message = "status: " + str(response.status)
456+ try:
457+ # not sure why this works but the above doesn't
458+ message += ", body: " + json.dumps(response.body.__dict__)
459+ except:
460+ message += ", body: " + str(response.body)
461+ StandardError.__init__(self, message)
462+
463+ # Currently this uses the same version number for all API routes,
464+ # this could be altered to be more fine-grained
465+ class Request(object):
466+ def __init__(self, verb, path, contentType, acceptType, body):
467+ self._verb = verb
468+ self._path = path
469+ self._body = body # python dictionary
470+ self._contentType = contentType
471+ self._acceptType = acceptType
472+
473+ def verb(self):
474+ return self._verb
475+
476+ def path(self):
477+ return self._path
478+
479+ def acceptType(self):
480+ return self._acceptType
481+
482+ def contentType(self):
483+ return self._contentType
484+
485+ def body(self):
486+ return json.dumps(self._body)
487+
488+ class GetRequest(Request):
489+ def __init__(self, path):
490+ super(MendeleyHttpClient.GetRequest, self).__init__(
491+ "GET",
492+ path,
493+ "",
494+ MendeleyHttpClient.CONTENT_TYPE,
495+ "")
496+
497+ class PostRequest(Request):
498+ def __init__(self, path, body):
499+ super(MendeleyHttpClient.PostRequest, self).__init__(
500+ "POST",
501+ path,
502+ MendeleyHttpClient.CONTENT_TYPE,
503+ MendeleyHttpClient.CONTENT_TYPE,
504+ body)
505+
506+ def formattedCitationsAndBibliography_Interactive(self, citationStyleUrl, citationClusters):
507+ request = self.PostRequest(
508+ "/formattedCitationsAndBibliography/interactive",
509+ {
510+ "citationStyleUrl": citationStyleUrl,
511+ "citationClusters": citationClusters
512+ }
513+ )
514+ return self.request(request)
515+
516+ def citation_choose_interactive(self, citationEditorHint):
517+ request = self.PostRequest(
518+ "/citation/choose/interactive",
519+ citationEditorHint
520+ )
521+ return self.request(request)
522+
523+ def citation_edit_interactive(self, citationCluster):
524+ request = self.PostRequest(
525+ "/citation/edit/interactive",
526+ citationCluster
527+ )
528+ return self.request(request)
529+
530+ def citation_update_interactive(self, formattedCitationCluster):
531+ request = self.PostRequest(
532+ "/citation/update/interactive",
533+ formattedCitationCluster
534+ )
535+ return self.request(request)
536+
537+ def citationStyle_choose_interactive(self, currentStyleUrl):
538+ request = self.PostRequest(
539+ "/citationStyle/choose/interactive",
540+ currentStyleUrl
541+ )
542+ return self.request(request)
543+
544+ def styleName_getFromUrl(self, styleUrl):
545+ request = self.PostRequest(
546+ "/citationStyle/getNameFromUrl",
547+ styleUrl
548+ )
549+ return self.request(request)
550+
551+ def citationStyles_default(self):
552+ request = self.GetRequest(
553+ "/citationStyles/default",
554+ )
555+ return self.request(request)
556+
557+ def citations_merge(self, citationClusters):
558+ request = self.PostRequest(
559+ "/citations/merge",
560+ citationClusters
561+ )
562+ return self.request(request)
563+
564+ def citation_undoManualFormat(self, citationCluster):
565+ request = self.PostRequest(
566+ "/citation/undoManualFormat",
567+ citationCluster
568+ )
569+ return self.request(request)
570+
571+ def wordProcessor_set(self, wordProcessor):
572+ request = self.PostRequest(
573+ "/wordProcessor/set",
574+ wordProcessor
575+ )
576+ return self.request(request)
577+
578+ def testMethods_citationCluster_getFromUuid(self, uuid):
579+ request = self.PostRequest(
580+ "/testMethods/citationCluster/getFromUuid",
581+ uuid
582+ )
583+ return self.request(request)
584+
585+ def userAccount(self):
586+ request = self.GetRequest(
587+ "/userAccount"
588+ )
589+ return self.request(request)
590+
591+ def mendeleyDesktopInfo(self):
592+ request = self.GetRequest(
593+ "/mendeleyDesktopInfo"
594+ )
595+ return self.request(request)
596+
597+ # Need to define a class for this.
598+ # I tried using a object() instance but it doesn't contain a __dict__
599+ class ResponseBody:
600+ pass
601+
602+ # Sets up a connection to Mendeley Desktop, makes a HTTP request and
603+ # returns the data
604+ def request(self, requestData):
605+ headers = {}
606+ # putting an empty string in Content-Type causes the Mendeley Desktop HTTP
607+ # server to put the Accept header value in a field called "content-typeaccept"
608+ # TODO: check where this error comes from
609+ if requestData.contentType() != "":
610+ headers["Content-Type"] = requestData.contentType()
611+ if requestData.acceptType() != "":
612+ headers["Accept"] = requestData.acceptType()
613+ startTime = time.time()
614+ connection = httplib.HTTPConnection(self.HOST + ":" + self.PORT)
615+ connection.request(requestData.verb(), requestData.path(), requestData.body(), headers)
616+ response = connection.getresponse()
617+ data = response.read()
618+ data = data.decode('utf-8')
619+
620+ if (response.status == 200 and (not response.getheader("Content-Type") is None) and
621+ response.getheader("Content-Type") != requestData.acceptType()):
622+ # TODO: abort if the wrong content type is returned
623+ pass
624+
625+ responseBody = MendeleyHttpClient.ResponseBody()
626+ try:
627+ responseBody.__dict__.update(json.loads(data))
628+ except:
629+ responseBody = data
630+ connection.close()
631+ self.lastRequestTime = 1000 * (time.time() - startTime)
632+
633+ self.previousResponse = \
634+ self.Response(response.status, response.getheader("Content-Type"), responseBody,
635+ requestData)
636+
637+ return self.previousResponse
638
639=== added file 'zim/plugins/mendeley.py'
640--- zim/plugins/mendeley.py 1970-01-01 00:00:00 +0000
641+++ zim/plugins/mendeley.py 2014-08-19 17:10:36 +0000
642@@ -0,0 +1,192 @@
643+# -*- coding: utf-8 -*-
644+#
645+# Copyright: 2014 Graham Rowlands <grahamrow@gmail.com>
646+# License: GNU GPL v2 or higher
647+#
648+# Uses API code from Mendeley's OpenOffice plugin to insert
649+# reference links that open in Mendeley.
650+
651+from __future__ import with_statement
652+from __future__ import division # We are doing math in this module ...
653+
654+import logging
655+import re
656+
657+from zim.plugins import PluginClass, extends, WindowExtension
658+from zim.errors import Error
659+from zim.actions import action
660+from zim.applications import Application, ApplicationError
661+
662+from zim.inc import MendeleyDesktopAPI
663+
664+logger = logging.getLogger('zim.plugins.insertcitation')
665+
666+mendeleycmd = ('mendeleydesktop', '--help')
667+
668+class MendeleyError(Error):
669+ description = _(
670+ 'The Mendeley plugin was not able to fulfill\n'
671+ 'this request. Please ensure that the Mendeley\n'
672+ 'Desktop application is open.' )
673+ # T: error description
674+
675+class DOIError(Error):
676+ description = _(
677+ 'The Mendeley plugin was not able to fulfill\n'
678+ 'this request. One or more DOI numbers may\n'
679+ 'by undefined in your list of citations.' )
680+ # T: error description
681+
682+class MendeleyPlugin(PluginClass):
683+
684+ plugin_info = {
685+ 'name': _('Mendeley Citations'), # T: plugin name
686+ 'description': _('Mendeley is a free cross-platform desktop reference and paper management program (http://www.mendeley.com/).'
687+ 'This plugin allows you to insert mendeley citations that link directly to the Mendeley desktop application '
688+ 'or to a DOI URL. This is accomplished by interfacing with the Mendeley application, which must be open for the '
689+ 'plugin to function. Any reference style from http://www.zotero.org/styles may be chosen.'), # T: plugin description
690+ 'author': 'Graham Rowlands',
691+ 'help': 'Plugins:Mendeley Citations',
692+ }
693+
694+ global DOI, MENDELEY_LIB # Hack - to make sure translation is loaded
695+ MENDELEY_LIB = _('Mendeley Library') # T: option value
696+ DOI = _('DOI Link') # T: option value
697+
698+ @classmethod
699+ def check_dependencies(klass):
700+ has_mendeley = Application(mendeleycmd).tryexec()
701+ return has_mendeley, [('Mendeley Desktop', has_mendeley, True)]
702+
703+ # Links either point to http://dx.doi.org/DOI-number-goes-here or mendeley://library/document/UUID-Number-goes-here
704+ # We let the use decide which to insert.
705+ plugin_preferences = (
706+ ('citation_style', 'string', _('Citation Style'), 'apa'), # T: input label# key, type, label, default
707+ ('bibliography_style', 'string', _('Bibliography Style'), 'physical-review-letters'), # T: input label# key, type, label, default
708+ ('citation_link', 'choice', _('Links Points To'), MENDELEY_LIB, (MENDELEY_LIB, DOI)),
709+ )
710+
711+ def get_uuid_from_link(self, url):
712+ if "dx.doi.org" in url:
713+ return url.split("=")[-1]
714+ else:
715+ return url.split("/")[-1]
716+
717+ def insert_citation(self, buffer):
718+ style = self.preferences['citation_style'] or "apa"
719+
720+ try:
721+ api = MendeleyDesktopAPI.MendeleyDesktopAPI("component context (unused)")
722+ api.resetCitations()
723+ api.setCitationStyle("http://www.zotero.org/styles/"+style)
724+ api.addCitationCluster(api.citation_choose_interactive(""))
725+ api.formatCitationsAndBibliography()
726+
727+ uuids = api.getCitationClusterUUIDs(0)
728+ hrefs = []
729+ cites = []
730+
731+ for uuid in uuids:
732+ citation = api.getFieldCodeFromUuid("{"+uuid+"}")
733+ api.resetCitations()
734+ api.addCitationCluster(citation)
735+ api.formatCitationsAndBibliography()
736+ if self.preferences['citation_link'] == MENDELEY_LIB:
737+ cites.append(api.getFormattedCitation(0))
738+ hrefs.append(api.getLocalURLs(0)[0])
739+ # buffer.insert_link_at_cursor(api.getFormattedCitation(0)+" ", href=api.getLocalURLs(0)[0])
740+ else:
741+ cites.append(api.getFormattedCitation(0))
742+ hrefs.append(api.getDOIURLs(0, addUUID=True)[0])
743+ # buffer.insert_link_at_cursor(api.getFormattedCitation(0)+" ", href=api.getDOIURLs(0)[0])
744+
745+ # Defer insertion so that we either insert all (success) or none (failure)
746+ for cite, href, uuid in zip(cites, hrefs, uuids):
747+ buffer.insert_link_at_cursor(cite, href=href)
748+ buffer.insert_at_cursor(" ")
749+
750+ except KeyError as error:
751+ msg = '%s: %s' % (error.__class__.__name__, error)
752+ raise DOIError, msg
753+
754+ except Exception as error:
755+ msg = '%s: %s' % (error.__class__.__name__, error)
756+ raise MendeleyError, msg
757+
758+ def html_to_zim(self, string):
759+ lines = string.splitlines()
760+ output = []
761+ ignores = [r'<!DOCTYPE',r'</*html', r'</*meta', r'</*title', r'</*head', r'</*body']
762+ replacements = [
763+ [r"</*?p.?>", "\n"],
764+ [r"<p *\w+='.*?'>", ""],
765+ [r"</*b.*?>", "**"],
766+ [r"</*i.*?>", "//"],
767+ [r"&nbsp;", ""],
768+ [r"</*span.*?>", ""],
769+ [r"</*.*?>", ""], # Catch All!
770+ [r" +", " "],
771+ ]
772+ for line in lines:
773+ # Ignore lines that contains items in the above list
774+ if len([ignore for ignore in ignores if re.compile(ignore).search(line)!=None]) == 0:
775+ for old, new in replacements:
776+ line = re.sub(r'%s' % old, r'%s' % new, line)
777+ output.append(line)
778+
779+ return "\n".join(output)
780+
781+ def render_bibliography(self, uuids, buffer):
782+ api = MendeleyDesktopAPI.MendeleyDesktopAPI("component context (unused)")
783+ style = self.preferences['bibliography_style'] or "apa"
784+ api.setCitationStyle("http://www.zotero.org/styles/"+style)
785+ api.resetCitations()
786+ for uuid in uuids:
787+ citation = api.getFieldCodeFromUuid("{"+uuid+"}")
788+ api.addCitationCluster(citation)
789+ api.formatCitationsAndBibliography()
790+ link = api.getFormattedBibliography()
791+ with open(link, "r") as bibfile:
792+ buffer.insert_at_cursor(self.html_to_zim(bibfile.read()))
793+
794+@extends('MainWindow')
795+class MainWindowExtension(WindowExtension):
796+
797+ uimanager_xml = '''
798+ <ui>
799+ <menubar name='menubar'>
800+ <menu action='insert_menu'>
801+ <placeholder name='plugin_items'>
802+ <menuitem action='insert_citation'/>
803+ </placeholder>
804+ </menu>
805+ <menu action='tools_menu'>
806+ <placeholder name='plugin_items'>
807+ <menuitem action='generate_mendeley_bibliography'/>
808+ </placeholder>
809+ </menu>
810+ </menubar>
811+ </ui>
812+ '''
813+
814+ @action(_('_Citation...'), '', '<Shift><Primary>C') # T: menu item
815+ def insert_citation(self):
816+ '''Action called by the menu item or key binding,
817+ will call the Mendeley API to insert a citation.
818+ '''
819+ buffer = self.window.ui.mainwindow.pageview.view.get_buffer()
820+ self.plugin.insert_citation(buffer)
821+
822+ def get_mendeley_uuids(self):
823+ uuids = []
824+ for link in self.window.ui.page.get_links():
825+ link_type, href, attrib = link
826+ uuids.append(self.plugin.get_uuid_from_link(href))
827+ # buffer = self.window.ui.mainwindow.pageview.view.get_buffer()
828+ # buffer.insert_at_cursor("Link of type %s and uuid %s.\n" % (link_type, self.plugin.get_uuid_from_link(href)))
829+ return uuids
830+
831+ @action(_('_Generate Mendeley Bibliography'), '', '') # T: menu item
832+ def generate_mendeley_bibliography(self):
833+ buffer = self.window.ui.mainwindow.pageview.view.get_buffer()
834+ self.plugin.render_bibliography(self.get_mendeley_uuids(), buffer)