Personal wiki notebook (not under development)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Notebooks.py 70KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901
  1. import re
  2. import cgi
  3. import cherrypy
  4. from datetime import datetime
  5. from Expose import expose
  6. from Validate import validate, Valid_string, Validation_error, Valid_bool, Valid_int
  7. from Database import Valid_id, Valid_revision, end_transaction
  8. from Users import grab_user_id, Access_error
  9. from Expire import strongly_expire, weakly_expire
  10. from Html_nuker import Html_nuker
  11. from Html_differ import Html_differ
  12. from Files import Upload_file
  13. from model.Notebook import Notebook
  14. from model.Note import Note
  15. from model.Invite import Invite
  16. from model.User import User
  17. from model.User_revision import User_revision
  18. from model.File import File
  19. from model.Tag import Tag
  20. from view.Main_page import Main_page
  21. from view.Json import Json
  22. from view.Note_tree_area import Note_tree_area
  23. from view.Notebook_rss import Notebook_rss
  24. from view.Updates_rss import Updates_rss
  25. from view.Update_link_page import Update_link_page
  26. class Import_error( Exception ):
  27. def __init__( self, message = None ):
  28. if message is None:
  29. message = u"An error occurred when trying to import your file. Please try a different file, or contact support for help."
  30. Exception.__init__( self, message )
  31. self.__message = message
  32. def to_dict( self ):
  33. return dict(
  34. error = self.__message
  35. )
  36. class Notebooks( object ):
  37. WHITESPACE_PATTERN = re.compile( u"\s+" )
  38. LINK_PATTERN = re.compile( u'<a\s+((?:[^>]+\s)?href="([^"]+)"(?:\s+target="([^"]*)")?[^>]*)>(<img [^>]+>)?([^<]*)</a>', re.IGNORECASE )
  39. FILE_PATTERN = re.compile( u'/files/' )
  40. NEW_FILE_PATTERN = re.compile( u'/files/new' )
  41. EXPORT_FORMAT_PATTERN = re.compile( u"^[a-zA-Z0-9_]+$" )
  42. """
  43. Controller for dealing with notebooks and their notes, corresponding to the "/notebooks" URL.
  44. """
  45. def __init__( self, database, users, files, https_url ):
  46. """
  47. Create a new Notebooks object.
  48. @type database: controller.Database
  49. @param database: database that notebooks are stored in
  50. @type users: controller.Users
  51. @param users: controller for all users, used here for updating storage utilization
  52. @type files: controller.Files
  53. @param files: controller for all uploaded files, used here for deleting files that are no longer
  54. referenced within saved notes
  55. @type https_url: unicode
  56. @param https_url: base URL to use for SSL http requests, or an empty string
  57. @return: newly constructed Notebooks
  58. """
  59. self.__database = database
  60. self.__users = users
  61. self.__files = files
  62. self.__https_url = https_url
  63. @expose( view = Main_page, rss = Notebook_rss )
  64. @strongly_expire
  65. @end_transaction
  66. @grab_user_id
  67. @validate(
  68. notebook_id = Valid_id(),
  69. note_id = Valid_id(),
  70. parent_id = Valid_id(),
  71. revision = Valid_revision(),
  72. previous_revision = Valid_revision( none_okay = True ),
  73. rename = Valid_bool(),
  74. deleted_id = Valid_id(),
  75. preview = Valid_string(),
  76. user_id = Valid_id( none_okay = True ),
  77. )
  78. def default( self, notebook_id, note_id = None, parent_id = None, revision = None,
  79. previous_revision = None, rename = False, deleted_id = None, preview = None,
  80. user_id = None ):
  81. """
  82. Provide the information necessary to display the page for a particular notebook. If a
  83. particular note id is given without a revision, then the most recent version of that note is
  84. displayed.
  85. @type notebook_id: unicode
  86. @param notebook_id: id of the notebook to display
  87. @type note_id: unicode or NoneType
  88. @param note_id: id of single note in this notebook to display (optional)
  89. @type parent_id: unicode or NoneType
  90. @param parent_id: id of parent notebook to this notebook (optional)
  91. @type revision: unicode or NoneType
  92. @param revision: revision timestamp of the provided note (optional)
  93. @type previous_revision: unicode or NoneType
  94. @param previous_revision: older revision timestamp to diff with the given revision (optional)
  95. @type rename: bool or NoneType
  96. @param rename: whether this is a new notebook and should be renamed (optional, defaults to False)
  97. @type deleted_id: unicode or NoneType
  98. @param deleted_id: id of the notebook that was just deleted, if any (optional)
  99. @type preview: unicode
  100. @param preview: type of access with which to preview this notebook, either "collaborator",
  101. "viewer", "owner", or "default" (optional, defaults to "default"). access must
  102. be equal to or lower than user's own access level to this notebook
  103. @type user_id: unicode or NoneType
  104. @param user_id: id of current logged-in user (if any)
  105. @rtype: unicode
  106. @return: rendered HTML page
  107. """
  108. result = self.__users.current( user_id )
  109. if preview == u"collaborator":
  110. read_write = True
  111. owner = False
  112. result[ u"notebooks" ] = [
  113. notebook for notebook in result[ "notebooks" ] if notebook.object_id == notebook_id
  114. ]
  115. if len( result[ u"notebooks" ] ) == 1:
  116. result[ u"notebooks" ][ 0 ].owner = False
  117. elif preview == u"viewer":
  118. read_write = False
  119. owner = False
  120. result[ u"notebooks" ] = [
  121. notebook for notebook in result[ "notebooks" ] if notebook.object_id == notebook_id
  122. ]
  123. if len( result[ u"notebooks" ] ) == 1:
  124. result[ u"notebooks" ][ 0 ].read_write = Notebook.READ_ONLY
  125. result[ u"notebooks" ][ 0 ].owner = False
  126. elif preview in ( u"owner", u"default", None ):
  127. read_write = True
  128. owner = True
  129. else:
  130. raise Access_error()
  131. result.update( self.contents( notebook_id, note_id, revision, previous_revision, read_write, owner, user_id ) )
  132. result[ "parent_id" ] = parent_id
  133. if revision:
  134. result[ "note_read_write" ] = False
  135. notebook = result[ u"notebook" ]
  136. # if this is a forum thread notebook, redirect to the forum thread page
  137. forum_tags = [ tag for tag in notebook.tags if tag.name == u"forum" ]
  138. if forum_tags:
  139. forum_name = forum_tags[ 0 ].value
  140. if forum_name == "blog":
  141. redirect = u"/blog/%s" % notebook.friendly_id
  142. else:
  143. redirect = u"/forums/%s/%s" % ( forum_name, notebook_id )
  144. if note_id:
  145. redirect += u"?note_id=%s" % note_id
  146. return dict(
  147. redirect = redirect,
  148. )
  149. if notebook.name != u"Luminotes":
  150. result[ "recent_notes" ] = self.__database.select_many( Note, notebook.sql_load_notes_in_update_order( start = 0, count = 10 ) )
  151. # if the user doesn't have any storage bytes yet, they're a new user, so see what type of
  152. # conversion this is (demo or signup)
  153. if result[ "user" ].username != u"anonymous" and result[ "user" ].storage_bytes == 0:
  154. if u"this is a demo" in [ note.title for note in result[ "startup_notes" ] ]:
  155. result[ "conversion" ] = u"demo"
  156. else:
  157. result[ "conversion" ] = u"signup"
  158. result[ "rename" ] = rename
  159. result[ "deleted_id" ] = deleted_id
  160. return result
  161. def contents( self, notebook_id, note_id = None, revision = None, previous_revision = None,
  162. read_write = True, owner = True, user_id = None ):
  163. """
  164. Return information about the requested notebook, including its startup notes. Optionally include
  165. a single requested note as well.
  166. @type notebook_id: unicode
  167. @param notebook_id: id of notebook to return
  168. @type note_id: unicode or NoneType
  169. @param note_id: id of single note in this notebook to return (optional)
  170. @type revision: unicode or NoneType
  171. @param revision: revision timestamp of the provided note (optional)
  172. @type previous_revision: unicode or NoneType
  173. @param previous_revision: older revision timestamp to diff with the given revision (optional)
  174. @type read_write: bool or NoneType
  175. @param read_write: whether the notebook should be returned as read-write (optional, defaults to True).
  176. this can only lower access, not elevate it
  177. @type owner: bool or NoneType
  178. @param owner: whether the notebook should be returned as owner-level access (optional, defaults to True).
  179. this can only lower access, not elevate it
  180. @type user_id: unicode or NoneType
  181. @param user_id: id of current logged-in user (if any)
  182. @rtype: dict
  183. @return: {
  184. 'notebook': notebook,
  185. 'startup_notes': notelist,
  186. 'total_notes_count': notecount,
  187. 'notes': notelist,
  188. 'invites': invitelist
  189. }
  190. @raise Access_error: the current user doesn't have access to the given notebook or note
  191. @raise Validation_error: one of the arguments is invalid
  192. """
  193. notebook = self.__users.load_notebook( user_id, notebook_id )
  194. anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ), use_cache = True )
  195. if notebook is None or anonymous is None:
  196. raise Access_error()
  197. if read_write is False:
  198. notebook.read_write = Notebook.READ_ONLY
  199. if owner is False:
  200. notebook.owner = False
  201. if note_id:
  202. note = self.__database.load( Note, note_id, revision )
  203. if note and note.notebook_id != notebook_id:
  204. if note.notebook_id == notebook.trash_id:
  205. note = None
  206. else:
  207. raise Access_error()
  208. # if two revisions were provided, then make the returned note's contents into a diff
  209. if note and revision and previous_revision:
  210. previous_note = self.__database.load( Note, note_id, previous_revision )
  211. if previous_note and previous_note.contents:
  212. note.replace_contents( Html_differ().diff( previous_note.contents, note.contents ) )
  213. else:
  214. note = None
  215. notebook.tags = \
  216. self.__database.select_many( Tag, notebook.sql_load_tags( user_id ) ) + \
  217. self.__database.select_many( Tag, notebook.sql_load_tags( anonymous.object_id ) )
  218. startup_notes = self.__database.select_many( Note, notebook.sql_load_startup_notes() )
  219. total_notes_count = self.__database.select_one( int, notebook.sql_count_notes(), use_cache = True )
  220. if self.__users.load_notebook( user_id, notebook_id, owner = True ):
  221. invites = self.__database.select_many( Invite, Invite.sql_load_notebook_invites( notebook_id ) )
  222. else:
  223. invites = []
  224. return dict(
  225. notebook = notebook,
  226. startup_notes = startup_notes,
  227. total_notes_count = total_notes_count,
  228. notes = note and [ note ] or [],
  229. invites = invites or [],
  230. )
  231. @expose( view = None, rss = Updates_rss )
  232. @strongly_expire
  233. @end_transaction
  234. @validate(
  235. notebook_id = Valid_id(),
  236. notebook_name = Valid_string(),
  237. )
  238. def updates( self, notebook_id, notebook_name ):
  239. """
  240. Provide the information necessary to display an updated notes RSS feed for the given notebook.
  241. This method does not require any sort of login.
  242. @type notebook_id: unicode
  243. @param notebook_id: id of the notebook to provide updates for
  244. @type notebook_name: unicode
  245. @param notebook_name: name of the notebook to include in the RSS feed
  246. @rtype: unicode
  247. @return: rendered RSS feed
  248. """
  249. notebook = self.__database.load( Notebook, notebook_id )
  250. if not notebook:
  251. return dict(
  252. recent_notes = [],
  253. notebook_id = notebook_id,
  254. notebook_name = notebook_name,
  255. https_url = self.__https_url,
  256. )
  257. recent_notes = self.__database.select_many( Note, notebook.sql_load_notes_in_update_order( start = 0, count = 10 ) )
  258. return dict(
  259. recent_notes = [ ( note.object_id, note.revision ) for note in recent_notes ],
  260. notebook_id = notebook_id,
  261. notebook_name = notebook_name,
  262. https_url = self.__https_url,
  263. )
  264. @expose( view = Update_link_page )
  265. @strongly_expire
  266. @end_transaction
  267. @validate(
  268. notebook_id = Valid_id(),
  269. notebook_name = Valid_string(),
  270. note_id = Valid_id(),
  271. revision = Valid_revision(),
  272. )
  273. def get_update_link( self, notebook_id, notebook_name, note_id, revision ):
  274. """
  275. Provide the information necessary to display a link to an updated note. This method does not
  276. require any sort of login.
  277. @type notebook_id: unicode
  278. @param notebook_id: id of the notebook the note is in
  279. @type notebook_name: unicode
  280. @param notebook_name: name of the notebook
  281. @type note_id: unicode
  282. @param note_id: id of the note to link to
  283. @type revision: unicode
  284. @param revision: ignored; present so RSS feed readers distinguish between different revisions
  285. @rtype: unicode
  286. @return: rendered HTML page
  287. """
  288. return dict(
  289. notebook_id = notebook_id,
  290. notebook_name = notebook_name,
  291. note_id = note_id,
  292. https_url = self.__https_url,
  293. )
  294. @expose( view = Json )
  295. @strongly_expire
  296. @end_transaction
  297. @grab_user_id
  298. @validate(
  299. notebook_id = Valid_id(),
  300. note_id = Valid_id(),
  301. revision = Valid_revision(),
  302. previous_revision = Valid_revision( none_okay = True ),
  303. summarize = Valid_bool(),
  304. user_id = Valid_id( none_okay = True ),
  305. )
  306. def load_note( self, notebook_id, note_id, revision = None, previous_revision = None, summarize = False, user_id = None ):
  307. """
  308. Return the information on a particular note by its id.
  309. @type notebook_id: unicode
  310. @param notebook_id: id of notebook the note is in
  311. @type note_id: unicode
  312. @param note_id: id of note to return
  313. @type revision: unicode or NoneType
  314. @param revision: revision timestamp of the note (optional)
  315. @type previous_revision: unicode or NoneType
  316. @param previous_revision: older revision timestamp to diff with the given revision (optional)
  317. @type summarize: bool or NoneType
  318. @param summarize: True to return a summary of the note's contents, False to return full text
  319. (optional, defaults to False)
  320. @type user_id: unicode or NoneType
  321. @param user_id: id of current logged-in user (if any), determined by @grab_user_id
  322. @rtype: json dict
  323. @return: { 'note': notedict or None }
  324. @raise Access_error: the current user doesn't have access to the given notebook or note
  325. @raise Validation_error: one of the arguments is invalid
  326. """
  327. notebook = self.__users.load_notebook( user_id, notebook_id )
  328. if not notebook:
  329. raise Access_error()
  330. note = self.__database.load( Note, note_id, revision )
  331. # if the note has no notebook, it has been deleted "forever"
  332. if note and note.notebook_id is None:
  333. return dict(
  334. note = None,
  335. )
  336. if note and note.notebook_id != notebook_id:
  337. if note.notebook_id == notebook.trash_id:
  338. if revision:
  339. return dict(
  340. note = summarize and self.summarize_note( note ) or note,
  341. )
  342. return dict(
  343. note = None,
  344. note_id_in_trash = note.object_id,
  345. )
  346. raise Access_error()
  347. if note and revision and previous_revision:
  348. previous_note = self.__database.load( Note, note_id, previous_revision )
  349. if previous_note and previous_note.contents:
  350. note.replace_contents( Html_differ().diff( previous_note.contents, note.contents ) )
  351. return dict(
  352. note = summarize and self.summarize_note( note ) or note,
  353. )
  354. @expose( view = Json )
  355. @strongly_expire
  356. @end_transaction
  357. @grab_user_id
  358. @validate(
  359. notebook_id = Valid_id(),
  360. note_title = Valid_string( min = 1, max = 500 ),
  361. summarize = Valid_bool(),
  362. user_id = Valid_id( none_okay = True ),
  363. )
  364. def load_note_by_title( self, notebook_id, note_title, summarize = False, user_id = None ):
  365. """
  366. Return the information on a particular note by its title. The lookup by title is performed
  367. case-insensitively.
  368. @type notebook_id: unicode
  369. @param notebook_id: id of notebook the note is in
  370. @type note_title: unicode
  371. @param note_title: title of the note to return
  372. @type summarize: bool or NoneType
  373. @param summarize: True to return a summary of the note's contents, False to return full text
  374. (optional, defaults to False)
  375. @type user_id: unicode or NoneType
  376. @param user_id: id of current logged-in user (if any), determined by @grab_user_id
  377. @rtype: json dict
  378. @return: { 'note': notedict or None }
  379. @raise Access_error: the current user doesn't have access to the given notebook
  380. @raise Validation_error: one of the arguments is invalid
  381. """
  382. notebook = self.__users.load_notebook( user_id, notebook_id )
  383. if not notebook:
  384. raise Access_error()
  385. note = self.__database.select_one( Note, notebook.sql_load_note_by_title( note_title ) )
  386. return dict(
  387. note = summarize and self.summarize_note( note ) or note,
  388. )
  389. def summarize_note( self, note, max_summary_length = None, word_count = None, highlight_text = None ):
  390. """
  391. Create a truncated, HTML-free note summary for the given note, and then return the note with
  392. its summary set.
  393. @type note: model.Note or NoneType
  394. @param note: note to summarize, or None
  395. @type max_summary_length: int or NoneType
  396. @param max_summary_length: the length to which the summary is truncated (optional, defaults
  397. to a reasonable length)
  398. @type word_count: int or NoneType
  399. @param word_count: the number of words to which the summary is truncated (optional, defaults
  400. to a reasonable number of words)
  401. @type highlight_text: unicode or NoneType
  402. @param highlight_text: text to emphasize within the summary (optional, defaults to no emphasis)
  403. @rtype: model.Note or NoneType
  404. @return: note with its summary member set, or None if no note was provided
  405. """
  406. DEFAULT_MAX_SUMMARY_LENGTH = 40
  407. DEFAULT_WORD_COUNT = 10
  408. if not max_summary_length:
  409. max_summary_length = DEFAULT_MAX_SUMMARY_LENGTH
  410. if not word_count:
  411. word_count = DEFAULT_WORD_COUNT
  412. if note is None:
  413. return None
  414. if note.contents is None:
  415. return note
  416. # remove all HTML from the contents and also remove the title
  417. summary = Html_nuker().nuke( note.contents )
  418. if note.title and summary.startswith( note.title ):
  419. summary = summary[ len( note.title ) : ]
  420. # split the summary on whitespace
  421. words = self.WHITESPACE_PATTERN.split( summary )
  422. def first_words( words, word_count ):
  423. return u" ".join( words[ : word_count ] )
  424. # find a summary less than MAX_SUMMARY_LENGTH and, if possible, truncated on a word boundary
  425. truncated = False
  426. summary = first_words( words, word_count )
  427. while len( summary ) > max_summary_length:
  428. word_count -= 1
  429. summary = first_words( words, word_count )
  430. # if the first word is just ridiculously long, truncate it without finding a word boundary
  431. if word_count == 1:
  432. summary = summary[ : max_summary_length ]
  433. truncated = True
  434. break
  435. if truncated or word_count < len( words ):
  436. summary += " ..."
  437. if highlight_text:
  438. summary = summary.replace( highlight_text, "<b>%s</b>" % highlight_text )
  439. note.summary = summary
  440. return note
  441. @expose( view = Json )
  442. @strongly_expire
  443. @end_transaction
  444. @grab_user_id
  445. @validate(
  446. notebook_id = Valid_id(),
  447. note_title = Valid_string( min = 1, max = 500 ),
  448. user_id = Valid_id( none_okay = True ),
  449. )
  450. def lookup_note_id( self, notebook_id, note_title, user_id ):
  451. """
  452. Return a note's id by looking up its title. The lookup by title is performed
  453. case-insensitively.
  454. @type notebook_id: unicode
  455. @param notebook_id: id of notebook the note is in
  456. @type note_title: unicode
  457. @param note_title: title of the note id to return
  458. @type user_id: unicode or NoneType
  459. @param user_id: id of current logged-in user (if any), determined by @grab_user_id
  460. @rtype: json dict
  461. @return: { 'note_id': noteid or None }
  462. @raise Access_error: the current user doesn't have access to the given notebook
  463. @raise Validation_error: one of the arguments is invalid
  464. """
  465. notebook = self.__users.load_notebook( user_id, notebook_id )
  466. if not notebook:
  467. raise Access_error()
  468. note = self.__database.select_one( Note, notebook.sql_load_note_by_title( note_title ) )
  469. return dict(
  470. note_id = note and note.object_id or None,
  471. )
  472. @expose( view = Json )
  473. @strongly_expire
  474. @end_transaction
  475. @grab_user_id
  476. @validate(
  477. notebook_id = Valid_id(),
  478. note_id = Valid_id(),
  479. user_id = Valid_id( none_okay = True ),
  480. )
  481. def load_note_revisions( self, notebook_id, note_id, user_id = None ):
  482. """
  483. Return the full list of revision timestamps for this note in chronological order.
  484. @type notebook_id: unicode
  485. @param notebook_id: id of notebook the note is in
  486. @type note_id: unicode
  487. @param note_id: id of note in question
  488. @type user_id: unicode or NoneType
  489. @param user_id: id of current logged-in user (if any), determined by @grab_user_id
  490. @rtype: json dict
  491. @return: { 'revisions': userrevisionlist or None }
  492. @raise Access_error: the current user doesn't have access to the given notebook or note
  493. @raise Validation_error: one of the arguments is invalid
  494. """
  495. notebook = self.__users.load_notebook( user_id, notebook_id )
  496. if not notebook:
  497. raise Access_error()
  498. note = self.__database.load( Note, note_id )
  499. if note:
  500. if note and note.notebook_id is None:
  501. return dict(
  502. revisions = None,
  503. )
  504. if note.notebook_id != notebook_id:
  505. if note.notebook_id == notebook.trash_id:
  506. return dict(
  507. revisions = None,
  508. )
  509. raise Access_error()
  510. revisions = self.__database.select_many( User_revision, note.sql_load_revisions() )
  511. else:
  512. revisions = None
  513. return dict(
  514. revisions = revisions,
  515. )
  516. @expose( view = Json )
  517. @strongly_expire
  518. @end_transaction
  519. @grab_user_id
  520. @validate(
  521. notebook_id = Valid_id(),
  522. note_id = Valid_id(),
  523. user_id = Valid_id( none_okay = True ),
  524. )
  525. def load_note_links( self, notebook_id, note_id, user_id = None ):
  526. """
  527. Return a list of HTTP links found within the contents of the given note.
  528. @type notebook_id: unicode
  529. @param notebook_id: id of notebook the note is in
  530. @type note_id: unicode
  531. @param note_id: id of note in question
  532. @type user_id: unicode or NoneType
  533. @param user_id: id of current logged-in user (if any), determined by @grab_user_id
  534. @rtype: json dict
  535. @return: { 'tree_html': html_fragment }
  536. @raise Access_error: the current user doesn't have access to the given notebook or note
  537. @raise Validation_error: one of the arguments is invalid
  538. """
  539. notebook = self.__users.load_notebook( user_id, notebook_id )
  540. if not notebook:
  541. raise Access_error()
  542. note = self.__database.load( Note, note_id )
  543. if note is None or note.notebook_id not in ( notebook_id, notebook.trash_id ):
  544. raise Access_error()
  545. items = []
  546. for match in self.LINK_PATTERN.finditer( note.contents ):
  547. ( attributes, href, target, embedded_image, title ) = match.groups()
  548. # if it has a link target, it's a link to an external web site
  549. if target:
  550. items.append( Note_tree_area.make_item( title, attributes, u"note_tree_external_link" ) )
  551. continue
  552. # if it has '/files/' in its path, it's an uploaded file link
  553. if self.FILE_PATTERN.search( href ):
  554. if not self.NEW_FILE_PATTERN.search( href ): # ignore files that haven't been uploaded yet
  555. if embedded_image:
  556. title = u"embedded image"
  557. items.append( Note_tree_area.make_item( title, attributes, u"note_tree_file_link", target = u"_new" ) )
  558. continue
  559. # if it has a note_id, load that child note and see whether it has any children of its own
  560. child_note_ids = cgi.parse_qs( href.split( '?' )[ -1 ] ).get( u"note_id" )
  561. if child_note_ids:
  562. child_note_id = child_note_ids[ 0 ]
  563. child_note = self.__database.load( Note, child_note_id )
  564. if child_note and child_note.contents and self.LINK_PATTERN.search( child_note.contents ):
  565. items.append( Note_tree_area.make_item( title, attributes, u"note_tree_link", has_children = True ) )
  566. continue
  567. # otherwise, it's childless
  568. items.append( Note_tree_area.make_item( title, attributes, u"note_tree_link", has_children = False ) )
  569. return dict(
  570. tree_html = unicode( Note_tree_area.make_tree( items ) ),
  571. )
  572. @expose( view = Json )
  573. @end_transaction
  574. @grab_user_id
  575. @validate(
  576. notebook_id = Valid_id(),
  577. note_id = Valid_id(),
  578. contents = Valid_string( min = 1, max = 50000, escape_html = False ),
  579. startup = Valid_bool(),
  580. previous_revision = Valid_revision( none_okay = True ),
  581. position_after = Valid_id( none_okay = True ),
  582. position_before = Valid_id( none_okay = True ),
  583. user_id = Valid_id( none_okay = True ),
  584. )
  585. def save_note( self, notebook_id, note_id, contents, startup, previous_revision = None,
  586. position_after = None, position_before = None, user_id = None ):
  587. """
  588. Save a new revision of the given note. This function will work both for creating a new note and
  589. for updating an existing note. If the note exists and the given contents are identical to the
  590. existing contents for the given previous_revision, then no saving takes place and a new_revision
  591. of None is returned. Otherwise this method returns the timestamp of the new revision.
  592. @type notebook_id: unicode
  593. @param notebook_id: id of notebook the note is in
  594. @type note_id: unicode
  595. @param note_id: id of note to save
  596. @type contents: unicode
  597. @param contents: new textual contents of the note, including its title
  598. @type startup: bool
  599. @param startup: whether the note should be displayed on startup
  600. @type previous_revision: unicode or NoneType
  601. @param previous_revision: previous known revision timestamp of the provided note, or None if
  602. the note is new
  603. @type position_after: unicode or NoneType
  604. @param position_after: id of note to position the saved note after (optional)
  605. @type position_before: unicode or NoneType
  606. @param position_before: id of note to position the saved note before (optional)
  607. @type user_id: unicode or NoneType
  608. @param user_id: id of current logged-in user (if any), determined by @grab_user_id
  609. @rtype: json dict
  610. @return: {
  611. 'new_revision': User_revision of saved note, or None if nothing was saved
  612. 'previous_revision': User_revision immediately before new_revision, or None if the note is new
  613. 'storage_bytes': current storage usage by user
  614. 'rank': float rank of the saved note, or None
  615. }
  616. @raise Access_error: the current user doesn't have access to the given notebook
  617. @raise Validation_error: one of the arguments is invalid
  618. """
  619. notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, note_id = note_id )
  620. user = self.__database.load( User, user_id )
  621. if not user or not notebook:
  622. raise Access_error();
  623. note = self.__database.load( Note, note_id )
  624. # if the user has read-write access only to their own notes in this notebook, force the startup
  625. # flag to be True for this note. also ignore note positioning parameters
  626. if notebook.read_write == Notebook.READ_WRITE_FOR_OWN_NOTES:
  627. startup = True
  628. position_before = None
  629. position_after = None
  630. def update_rank( position_after, position_before ):
  631. after_note = position_after and self.__database.load( Note, position_after ) or None
  632. before_note = position_before and self.__database.load( Note, position_before ) or None
  633. if after_note and before_note:
  634. new_rank = float( after_note.rank ) + 1.0
  635. # if necessary, increment the rank of all subsequent notes to make "room" for this note
  636. if new_rank >= before_note.rank:
  637. # clear the cache of before_note and all notes with subsequent rank
  638. self.__database.uncache_many(
  639. Note,
  640. self.__database.select_many(
  641. unicode,
  642. notebook.sql_load_note_ids_starting_from_rank( before_note.rank )
  643. )
  644. )
  645. self.__database.execute( notebook.sql_increment_rank( before_note.rank ), commit = False )
  646. return new_rank
  647. elif after_note:
  648. return float( after_note.rank ) + 1.0
  649. elif before_note:
  650. return float( before_note.rank ) - 1.0
  651. return 0.0
  652. # check whether the provided note contents have been changed since the previous revision
  653. def update_note( current_notebook, old_note, startup, user ):
  654. # the note hasn't been changed, so bail without updating it
  655. if not position_after and not position_before and startup == old_note.startup and \
  656. contents.replace( u"\n", u"" ) == old_note.contents.replace( u"\n", "" ):
  657. new_revision = None
  658. # the note has changed, so update it
  659. else:
  660. note.contents = contents
  661. note.startup = startup
  662. if position_after or position_before:
  663. note.rank = update_rank( position_after, position_before )
  664. elif note.rank is None:
  665. note.rank = self.__database.select_one( float, notebook.sql_highest_note_rank() ) + 1
  666. note.user_id = user.object_id
  667. new_revision = User_revision( note.revision, note.user_id, user.username )
  668. self.__files.purge_unused( note )
  669. return new_revision
  670. # if the note is already in the given notebook, load it and update it
  671. if note and note.notebook_id == notebook.object_id:
  672. old_note = self.__database.load( Note, note_id, previous_revision )
  673. previous_user = self.__database.load( User, note.user_id )
  674. previous_revision = User_revision( note.revision, note.user_id, previous_user and previous_user.username or None )
  675. new_revision = update_note( notebook, old_note, startup, user )
  676. # the note is not already in the given notebook, so look for it in the trash
  677. elif note and notebook.trash_id and note.notebook_id == notebook.trash_id:
  678. old_note = self.__database.load( Note, note_id, previous_revision )
  679. # undelete the note, putting it back in the given notebook
  680. previous_user = self.__database.load( User, note.user_id )
  681. previous_revision = User_revision( note.revision, note.user_id, previous_user and previous_user.username or None )
  682. note.notebook_id = notebook.object_id
  683. note.deleted_from_id = None
  684. new_revision = update_note( notebook, old_note, startup, user )
  685. # otherwise, create a new note
  686. else:
  687. if position_after or position_before:
  688. rank = update_rank( position_after, position_before )
  689. else:
  690. rank = self.__database.select_one( float, notebook.sql_highest_note_rank() ) + 1
  691. previous_revision = None
  692. note = Note.create( note_id, contents, notebook_id = notebook.object_id, startup = startup, rank = rank, user_id = user_id )
  693. new_revision = User_revision( note.revision, note.user_id, user.username )
  694. if new_revision:
  695. self.__database.save( note, commit = False )
  696. user = self.__users.update_storage( user_id, commit = False )
  697. self.__database.uncache_command( notebook.sql_count_notes() ) # cached note count is now invalid
  698. self.__database.commit()
  699. user.group_storage_bytes = self.__users.calculate_group_storage( user )
  700. else:
  701. user = None
  702. if note.rank is None:
  703. rank = None
  704. else:
  705. rank = float( note.rank )
  706. return dict(
  707. new_revision = new_revision,
  708. previous_revision = previous_revision,
  709. storage_bytes = user and user.storage_bytes or 0,
  710. rank = rank,
  711. )
  712. @expose( view = Json )
  713. @end_transaction
  714. @grab_user_id
  715. @validate(
  716. notebook_id = Valid_id(),
  717. note_id = Valid_id(),
  718. revision = Valid_revision(),
  719. user_id = Valid_id( none_okay = True ),
  720. )
  721. def revert_note( self, notebook_id, note_id, revision, user_id ):
  722. """
  723. Revert the contents of a note to that of an earlier revision, thereby creating a new revision.
  724. The timestamp of the new revision is returned.
  725. @type notebook_id: unicode
  726. @param notebook_id: id of notebook the note is in
  727. @type note_id: unicode
  728. @param note_id: id of note to revert
  729. @type revision: unicode or NoneType
  730. @param revision: revision timestamp to revert to for the provided note
  731. @type user_id: unicode or NoneType
  732. @param user_id: id of current logged-in user (if any), determined by @grab_user_id
  733. @rtype: json dict
  734. @return: {
  735. 'new_revision': User_revision of the reverted note
  736. 'previous_revision': User_revision immediately before new_revision
  737. 'storage_bytes': current storage usage by user,
  738. }
  739. @raise Access_error: the current user doesn't have access to the given notebook
  740. @raise Validation_error: one of the arguments is invalid
  741. """
  742. notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True )
  743. user = self.__database.load( User, user_id )
  744. if not user or not notebook:
  745. raise Access_error()
  746. note = self.__database.load( Note, note_id )
  747. if not note:
  748. raise Access_error()
  749. if not self.__users.load_notebook( user_id, note.notebook_id, read_write = True, note_id = note.object_id ):
  750. raise Access_error()
  751. # check whether the provided note contents have been changed since the previous revision
  752. def update_note( current_notebook, old_note, user ):
  753. # if the revision to revert to is already the newest revision, bail without updating the note
  754. if old_note.revision == note.revision:
  755. new_revision = None
  756. # otherwise, revert the note's contents to that of the older revision
  757. else:
  758. note.contents = old_note.contents
  759. note.user_id = user.object_id
  760. new_revision = User_revision( note.revision, note.user_id, user.username )
  761. self.__files.purge_unused( note )
  762. return new_revision
  763. previous_user = self.__database.load( User, note.user_id )
  764. previous_revision = User_revision( note.revision, note.user_id, previous_user and previous_user.username or None )
  765. # if the note is already in the given notebook, load it and revert it
  766. if note and note.notebook_id == notebook.object_id:
  767. old_note = self.__database.load( Note, note_id, revision )
  768. new_revision = update_note( notebook, old_note, user )
  769. # the note is not already in the given notebook, so look for it in the trash
  770. elif note and notebook.trash_id and note.notebook_id == notebook.trash_id:
  771. old_note = self.__database.load( Note, note_id, revision )
  772. # undelete the note, putting it back in the given notebook
  773. note.notebook_id = notebook.object_id
  774. note.deleted_from_id = None
  775. new_revision = update_note( notebook, old_note, user )
  776. # otherwise, the note doesn't exist
  777. else:
  778. raise Access_error()
  779. if new_revision:
  780. self.__database.save( note, commit = False )
  781. user = self.__users.update_storage( user_id, commit = False )
  782. self.__database.commit()
  783. user.group_storage_bytes = self.__users.calculate_group_storage( user )
  784. else:
  785. user = None
  786. return dict(
  787. new_revision = new_revision,
  788. previous_revision = previous_revision,
  789. storage_bytes = user and user.storage_bytes or 0,
  790. contents = note.contents,
  791. )
  792. @expose( view = Json )
  793. @end_transaction
  794. @grab_user_id
  795. @validate(
  796. notebook_id = Valid_id(),
  797. note_id = Valid_id(),
  798. user_id = Valid_id( none_okay = True ),
  799. )
  800. def delete_note( self, notebook_id, note_id, user_id ):
  801. """
  802. Delete the given note from its notebook and move it to the notebook's trash. The note is added
  803. as a startup note within the trash. If the given notebook is the trash and the given note is
  804. already there, then it is deleted from the trash forever.
  805. @type notebook_id: unicode
  806. @param notebook_id: id of notebook the note is in
  807. @type note_id: unicode
  808. @param note_id: id of note to delete
  809. @type user_id: unicode or NoneType
  810. @param user_id: id of current logged-in user (if any), determined by @grab_user_id
  811. @rtype: json dict
  812. @return: { 'storage_bytes': current storage usage by user }
  813. @raise Access_error: the current user doesn't have access to the given notebook
  814. @raise Validation_error: one of the arguments is invalid
  815. """
  816. notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, note_id = note_id )
  817. if not notebook:
  818. raise Access_error()
  819. note = self.__database.load( Note, note_id )
  820. if note and note.notebook_id == notebook_id:
  821. if notebook.trash_id:
  822. note.deleted_from_id = notebook_id
  823. note.notebook_id = notebook.trash_id
  824. note.startup = True
  825. else:
  826. self.__files.purge_unused( note, purge_all_links = True )
  827. note.notebook_id = None
  828. note.user_id = user_id
  829. self.__database.save( note, commit = False )
  830. user = self.__users.update_storage( user_id, commit = False )
  831. self.__database.uncache_command( notebook.sql_count_notes() ) # cached note count is now invalid
  832. self.__database.commit()
  833. user.group_storage_bytes = self.__users.calculate_group_storage( user )
  834. return dict( storage_bytes = user.storage_bytes )
  835. else:
  836. return dict( storage_bytes = 0 )
  837. @expose( view = Json )
  838. @end_transaction
  839. @grab_user_id
  840. @validate(
  841. notebook_id = Valid_id(),
  842. note_id = Valid_id(),
  843. user_id = Valid_id( none_okay = True ),
  844. )
  845. def undelete_note( self, notebook_id, note_id, user_id ):
  846. """
  847. Undelete the given note from the trash, moving it back into its notebook. The note is added
  848. as a startup note within its notebook.
  849. @type notebook_id: unicode
  850. @param notebook_id: id of notebook the note was in
  851. @type note_id: unicode
  852. @param note_id: id of note to undelete
  853. @type user_id: unicode or NoneType
  854. @param user_id: id of current logged-in user (if any), determined by @grab_user_id
  855. @rtype: json dict
  856. @return: { 'storage_bytes': current storage usage by user }
  857. @raise Access_error: the current user doesn't have access to the given notebook
  858. @raise Validation_error: one of the arguments is invalid
  859. """
  860. notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, note_id = note_id )
  861. if not notebook:
  862. raise Access_error()
  863. note = self.__database.load( Note, note_id )
  864. if note and notebook.trash_id:
  865. # if the note isn't deleted, and it's already in this notebook, just return
  866. if note.deleted_from_id is None and note.notebook_id == notebook_id:
  867. return dict( storage_bytes = 0 )
  868. # if the note was deleted from a different notebook than the notebook given, raise
  869. if note.deleted_from_id != notebook_id:
  870. raise Access_error()
  871. note.notebook_id = note.deleted_from_id
  872. note.deleted_from_id = None
  873. note.startup = True
  874. note.user_id = user_id
  875. self.__database.save( note, commit = False )
  876. user = self.__users.update_storage( user_id, commit = False )
  877. self.__database.uncache_command( notebook.sql_count_notes() ) # cached note count is now invalid
  878. self.__database.commit()
  879. user.group_storage_bytes = self.__users.calculate_group_storage( user )
  880. return dict( storage_bytes = user.storage_bytes )
  881. else:
  882. return dict( storage_bytes = 0 )
  883. @expose( view = Json )
  884. @end_transaction
  885. @grab_user_id
  886. @validate(
  887. notebook_id = Valid_id(),
  888. user_id = Valid_id( none_okay = True ),
  889. )
  890. def delete_all_notes( self, notebook_id, user_id ):
  891. """
  892. Delete all notes from the given notebook and move them to the notebook's trash (if any). The
  893. notes are added as startup notes within the trash. If the given notebook is the trash, then
  894. all notes in the trash are deleted forever.
  895. @type notebook_id: unicode
  896. @param notebook_id: id of notebook the note is in
  897. @type user_id: unicode or NoneType
  898. @param user_id: id of current logged-in user (if any), determined by @grab_user_id
  899. @rtype: json dict
  900. @return: { 'storage_bytes': current storage usage by user }
  901. @raise Access_error: the current user doesn't have access to the given notebook
  902. @raise Validation_error: one of the arguments is invalid
  903. """
  904. notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True )
  905. if not notebook or notebook.read_write == Notebook.READ_WRITE_FOR_OWN_NOTES:
  906. raise Access_error()
  907. notes = self.__database.select_many( Note, notebook.sql_load_notes_in_update_order() )
  908. for note in notes:
  909. if notebook.trash_id:
  910. note.deleted_from_id = notebook_id
  911. note.notebook_id = notebook.trash_id
  912. note.startup = True
  913. else:
  914. self.__files.purge_unused( note, purge_all_links = True )
  915. note.notebook_id = None
  916. note.user_id = user_id
  917. self.__database.save( note, commit = False )
  918. user = self.__users.update_storage( user_id, commit = False )
  919. self.__database.uncache_command( notebook.sql_count_notes() ) # cached note count is now invalid
  920. self.__database.commit()
  921. user.group_storage_bytes = self.__users.calculate_group_storage( user )
  922. return dict(
  923. storage_bytes = user.storage_bytes,
  924. )
  925. @expose( view = Json )
  926. @strongly_expire
  927. @end_transaction
  928. @grab_user_id
  929. @validate(
  930. notebook_id = Valid_id(),
  931. search_text = unicode,
  932. user_id = Valid_id( none_okay = True ),
  933. )
  934. def search_titles( self, notebook_id, search_text, user_id ):
  935. """
  936. Search the note titles within the given notebook for the given search text, and return matching
  937. notes. The search is case-insensitive. The returned notes include title summaries with the
  938. search term highlighted and are ordered by descending revision timestamp.
  939. @type notebook_id: unicode
  940. @param notebook_id: id of notebook to search
  941. @type search_text: unicode
  942. @param search_text: search term
  943. @type user_id: unicode or NoneType
  944. @param user_id: id of current logged-in user (if any), determined by @grab_user_id
  945. @rtype: json dict
  946. @return: { 'notes': [ matching notes ] }
  947. @raise Access_error: the current user doesn't have access to the given notebook
  948. @raise Validation_error: one of the arguments is invalid
  949. @raise Search_error: the provided search_text is invalid
  950. """
  951. notebook = self.__users.load_notebook( user_id, notebook_id )
  952. if not notebook:
  953. raise Access_error()
  954. MAX_SEARCH_TEXT_LENGTH = 256
  955. if len( search_text ) > MAX_SEARCH_TEXT_LENGTH:
  956. raise Validation_error( u"search_text", None, unicode, message = u"is too long" )
  957. if len( search_text ) == 0:
  958. raise Validation_error( u"search_text", None, unicode, message = u"is missing" )
  959. notes = self.__database.select_many( Note, Notebook.sql_search_titles( notebook_id, search_text ) )
  960. for note in notes:
  961. # do a case-insensitive replace to wrap the search term with bold
  962. search_text_pattern = re.compile( u"(%s)" % re.escape( search_text ), re.I )
  963. note.summary = search_text_pattern.sub( r"<b>\1</b>", note.summary )
  964. return dict(
  965. notes = notes,
  966. )
  967. @expose( view = Json )
  968. @strongly_expire
  969. @end_transaction
  970. @grab_user_id
  971. @validate(
  972. notebook_id = Valid_id(),
  973. search_text = unicode,
  974. user_id = Valid_id( none_okay = True ),
  975. )
  976. def search( self, notebook_id, search_text, user_id ):
  977. """
  978. Search the notes within all notebooks that the user has access to for the given search text.
  979. Note that the search is case-insensitive, and all HTML tags are ignored. Notes with title
  980. matches are generally ranked higher than matches that are only in the note contents. The
  981. returned notes include content summaries with the search terms highlighted.
  982. @type notebook_id: unicode
  983. @param notebook_id: id of notebook to show first in search results
  984. @type search_text: unicode
  985. @param search_text: search term
  986. @type user_id: unicode or NoneType
  987. @param user_id: id of current logged-in user (if any), determined by @grab_user_id
  988. @rtype: json dict
  989. @return: { 'notes': [ matching notes ] }
  990. @raise Access_error: the current user doesn't have access to the given notebook
  991. @raise Validation_error: one of the arguments is invalid
  992. @raise Search_error: the provided search_text is invalid
  993. """
  994. # if the anonymous user has access to the given notebook, then run the search as the anonymous
  995. # user instead of the given user id
  996. anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ), use_cache = True )
  997. if not anonymous:
  998. raise Access_error()
  999. notebook = self.__users.load_notebook( anonymous.object_id, notebook_id )
  1000. if notebook:
  1001. user_id = anonymous.object_id
  1002. else:
  1003. notebook = self.__users.load_notebook( user_id, notebook_id )
  1004. if not notebook:
  1005. raise Access_error()
  1006. MAX_SEARCH_TEXT_LENGTH = 256
  1007. if len( search_text ) > MAX_SEARCH_TEXT_LENGTH:
  1008. raise Validation_error( u"search_text", None, unicode, message = u"is too long" )
  1009. if len( search_text ) == 0:
  1010. raise Validation_error( u"search_text", None, unicode, message = u"is missing" )
  1011. notes = self.__database.select_many( Note, Notebook.sql_search_notes( user_id, notebook_id, search_text, self.__database.backend ) )
  1012. # make a summary for each note that doesn't have one
  1013. notes = [
  1014. note.summary and note or
  1015. self.summarize_note( note, max_summary_length = 80, word_count = 30, highlight_text = search_text )
  1016. for note in notes
  1017. ]
  1018. return dict(
  1019. notes = notes,
  1020. )
  1021. @expose()
  1022. @weakly_expire
  1023. @end_transaction
  1024. @grab_user_id
  1025. @validate(
  1026. notebook_id = Valid_id(),
  1027. format = Valid_string( min = 1, max = 100 ),
  1028. note_id = Valid_id( none_okay = True ),
  1029. user_id = Valid_id( none_okay = True ),
  1030. )
  1031. def export( self, notebook_id, format, note_id = None, user_id = None ):
  1032. """
  1033. Download the entire contents of the given notebook as a stand-alone file.
  1034. @type notebook_id: unicode
  1035. @param notebook_id: id of notebook to export
  1036. @type format: unicode
  1037. @param format: string indicating the export plugin to use, currently one of: "html", "csv"
  1038. @type notebook_id: unicode
  1039. @param note_id: id of single note within the notebook to export (optional)
  1040. @type user_id: unicode
  1041. @param user_id: id of current logged-in user (if any), determined by @grab_user_id
  1042. @rtype: unicode or generator (for streaming files)
  1043. @return: exported file with appropriate headers to trigger a download
  1044. @raise Access_error: the current user doesn't have access to the given notebook
  1045. @raise Validation_error: one of the arguments is invalid or the format is unknown
  1046. """
  1047. if not self.EXPORT_FORMAT_PATTERN.search( format ):
  1048. raise Validation_error( u"format", format, Valid_string, message = u"is invalid" )
  1049. notebook = self.__users.load_notebook( user_id, notebook_id )
  1050. if not notebook:
  1051. raise Access_error()
  1052. if note_id:
  1053. note = self.__database.load( Note, note_id )
  1054. if not note:
  1055. raise Access_error()
  1056. notes = [ note ]
  1057. notebook = None
  1058. else:
  1059. startup_notes = self.__database.select_many( Note, notebook.sql_load_startup_notes() )
  1060. other_notes = self.__database.select_many( Note, notebook.sql_load_non_startup_notes() )
  1061. notes = startup_notes + other_notes
  1062. from plugins.Invoke import invoke
  1063. try:
  1064. return invoke(
  1065. plugin_type = u"export",
  1066. plugin_name = format,
  1067. database = self.__database,
  1068. notebook = notebook,
  1069. notes = notes,
  1070. response_headers = cherrypy.response.headerMap,
  1071. )
  1072. except ( ImportError, AttributeError ):
  1073. raise Validation_error( u"format", format, Valid_string, message = u"is unknown" )
  1074. @expose( view = Json )
  1075. @end_transaction
  1076. @grab_user_id
  1077. @validate(
  1078. user_id = Valid_id( none_okay = True ),
  1079. )
  1080. def create( self, user_id ):
  1081. """
  1082. Create a new notebook and give it a default name.
  1083. @type user_id: unicode or NoneType
  1084. @param user_id: id of current logged-in user (if any)
  1085. @rtype dict
  1086. @return { 'redirect': new_notebook_url }
  1087. @raise Access_error: the current user doesn't have access to create a notebook
  1088. @raise Validation_error: one of the arguments is invalid
  1089. """
  1090. if user_id is None:
  1091. raise Access_error()
  1092. user = self.__database.load( User, user_id )
  1093. notebook = self.__create_notebook( u"new notebook", user )
  1094. return dict(
  1095. redirect = u"/notebooks/%s?rename=true" % notebook.object_id,
  1096. )
  1097. def __create_notebook( self, name, user, commit = True ):
  1098. # create the notebook along with a trash
  1099. trash_id = self.__database.next_id( Notebook, commit = False )
  1100. trash = Notebook.create( trash_id, u"trash", user_id = user.object_id )
  1101. self.__database.save( trash, commit = False )
  1102. notebook_id = self.__database.next_id( Notebook, commit = False )
  1103. notebook = Notebook.create( notebook_id, name, trash_id, user_id = user.object_id )
  1104. self.__database.save( notebook, commit = False )
  1105. # record the fact that the user has access to their new notebook
  1106. rank = self.__database.select_one( float, user.sql_highest_notebook_rank() ) + 1
  1107. self.__database.execute( user.sql_save_notebook( notebook_id, read_write = True, owner = True, rank = rank ), commit = False )
  1108. self.__database.execute( user.sql_save_notebook( trash_id, read_write = True, owner = True ), commit = False )
  1109. if commit:
  1110. self.__database.commit()
  1111. return notebook
  1112. @expose( view = Json )
  1113. @end_transaction
  1114. @grab_user_id
  1115. @validate(
  1116. notebook_id = Valid_id(),
  1117. name = Valid_string( min = 1, max = 100 ),
  1118. user_id = Valid_id( none_okay = True ),
  1119. )
  1120. def rename( self, notebook_id, name, user_id ):
  1121. """
  1122. Change the name of the given notebook.
  1123. @type notebook_id: unicode
  1124. @param notebook_id: id of notebook to rename
  1125. @type name: unicode
  1126. @param name: new name of the notebook
  1127. @type user_id: unicode or NoneType
  1128. @param user_id: id of current logged-in user (if any)
  1129. @rtype dict
  1130. @return {}
  1131. @raise Access_error: the current user doesn't have access to the given notebook
  1132. @raise Validation_error: one of the arguments is invalid
  1133. """
  1134. notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, owner = True )
  1135. # special case to allow the creator of a READ_WRITE_FOR_OWN_NOTES notebook to rename it
  1136. if notebook is None:
  1137. notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True )
  1138. if not notebook or not ( notebook.read_write == Notebook.READ_WRITE_FOR_OWN_NOTES and
  1139. notebook.user_id == user_id ):
  1140. raise Access_error()
  1141. user = self.__database.load( User, user_id )
  1142. if not user or not notebook:
  1143. raise Access_error()
  1144. # prevent renaming of the trash notebook to anything
  1145. if notebook.name == u"trash":
  1146. raise Access_error()
  1147. # prevent just anyone from making official Luminotes notebooks
  1148. if name.startswith( u"Luminotes" ) and not notebook.name.startswith( u"Luminotes" ):
  1149. raise Access_error()
  1150. # prevent renaming of another notebook to "trash"
  1151. if name == u"trash":
  1152. raise Access_error()
  1153. notebook.name = name
  1154. notebook.user_id = user_id
  1155. self.__database.save( notebook, commit = False )
  1156. self.__database.commit()
  1157. return dict()
  1158. @expose( view = Json )
  1159. @end_transaction
  1160. @grab_user_id
  1161. @validate(
  1162. notebook_id = Valid_id(),
  1163. user_id = Valid_id( none_okay = True ),
  1164. )
  1165. def delete( self, notebook_id, user_id ):
  1166. """
  1167. Delete the given notebook and redirect to a remaining read-write notebook. If there is none,
  1168. create one.
  1169. @type notebook_id: unicode
  1170. @param notebook_id: id of notebook to delete
  1171. @type user_id: unicode or NoneType
  1172. @param user_id: id of current logged-in user (if any)
  1173. @rtype dict
  1174. @return { 'redirect': remaining_notebook_url }
  1175. @raise Access_error: the current user doesn't have access to the given notebook
  1176. @raise Validation_error: one of the arguments is invalid
  1177. """
  1178. if user_id is None:
  1179. raise Access_error()
  1180. notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, owner = True )
  1181. user = self.__database.load( User, user_id )
  1182. if not user or not notebook:
  1183. raise Access_error()
  1184. # prevent deletion of a trash notebook directly
  1185. if notebook.name == u"trash":
  1186. raise Access_error()
  1187. notebook.deleted = True
  1188. notebook.user_id = user_id
  1189. self.__database.save( notebook, commit = False )
  1190. # redirect to a remaining undeleted read-write notebook, or if there isn't one, create an empty notebook
  1191. remaining_notebook = self.__database.select_one( Notebook, user.sql_load_notebooks(
  1192. parents_only = True, undeleted_only = True, read_write = True,
  1193. ) )
  1194. if remaining_notebook is None:
  1195. remaining_notebook = self.__create_notebook( u"my notebook", user, commit = False )
  1196. self.__database.commit()
  1197. return dict(
  1198. redirect = u"/notebooks/%s?deleted_id=%s" % ( remaining_notebook.object_id, notebook.object_id ),
  1199. )
  1200. @expose( view = Json )
  1201. @end_transaction
  1202. @grab_user_id
  1203. @validate(
  1204. notebook_id = Valid_id(),
  1205. user_id = Valid_id( none_okay = True ),
  1206. )
  1207. def delete_forever( self, notebook_id, user_id ):
  1208. """
  1209. Delete the given notebook permanently (by simply revoking the user's access to it).
  1210. @type notebook_id: unicode
  1211. @param notebook_id: id of notebook to delete
  1212. @type user_id: unicode or NoneType
  1213. @param user_id: id of current logged-in user (if any)
  1214. @rtype dict
  1215. @return: { 'storage_bytes': current storage usage by user }
  1216. @raise Access_error: the current user doesn't have access to the given notebook
  1217. @raise Validation_error: one of the arguments is invalid
  1218. """
  1219. if user_id is None:
  1220. raise Access_error()
  1221. notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, owner = True )
  1222. user = self.__database.load( User, user_id )
  1223. if not user or not notebook:
  1224. raise Access_error()
  1225. # prevent deletion of a trash notebook directly
  1226. if notebook.name == u"trash":
  1227. raise Access_error()
  1228. self.__database.execute( user.sql_remove_notebook( notebook_id ), commit = False )
  1229. user = self.__users.update_storage( user_id, commit = False )
  1230. self.__database.commit()
  1231. user.group_storage_bytes = self.__users.calculate_group_storage( user )
  1232. return dict( storage_bytes = user.storage_bytes )
  1233. @expose( view = Json )
  1234. @end_transaction
  1235. @grab_user_id
  1236. @validate(
  1237. notebook_id = Valid_id(),
  1238. user_id = Valid_id( none_okay = True ),
  1239. )
  1240. def undelete( self, notebook_id, user_id ):
  1241. """
  1242. Undelete the given notebook and redirect to it.
  1243. @type notebook_id: unicode
  1244. @param notebook_id: id of notebook to undelete
  1245. @type user_id: unicode or NoneType
  1246. @param user_id: id of current logged-in user (if any)
  1247. @rtype dict
  1248. @return { 'redirect': notebook_url }
  1249. @raise Access_error: the current user doesn't have access to the given notebook
  1250. @raise Validation_error: one of the arguments is invalid
  1251. """
  1252. if user_id is None:
  1253. raise Access_error()
  1254. notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, owner = True )
  1255. if not notebook:
  1256. raise Access_error()
  1257. notebook.deleted = False
  1258. notebook.user_id = user_id
  1259. self.__database.save( notebook, commit = False )
  1260. self.__database.commit()
  1261. return dict(
  1262. redirect = u"/notebooks/%s" % notebook.object_id,
  1263. )
  1264. @expose( view = Json )
  1265. @end_transaction
  1266. @grab_user_id
  1267. @validate(
  1268. notebook_id = Valid_id(),
  1269. user_id = Valid_id( none_okay = True ),
  1270. )
  1271. def move_up( self, notebook_id, user_id ):
  1272. """
  1273. Reorder the user's notebooks by moving the given notebook up by one. If the notebook is already
  1274. first, then wrap it around to be the last notebook.
  1275. @type notebook_id: unicode
  1276. @param notebook_id: id of notebook to move up
  1277. @type user_id: unicode or NoneType
  1278. @param user_id: id of current logged-in user (if any)
  1279. @rtype json dict
  1280. @return {}
  1281. @raise Access_error: the current user doesn't have access to the given notebook
  1282. @raise Validation_error: one of the arguments is invalid
  1283. """
  1284. notebook = self.__users.load_notebook( user_id, notebook_id )
  1285. user = self.__database.load( User, user_id )
  1286. if not user or not notebook:
  1287. raise Access_error()
  1288. # load the notebooks to which this user has access
  1289. notebooks = self.__database.select_many(
  1290. Notebook,
  1291. user.sql_load_notebooks( parents_only = True, undeleted_only = True ),
  1292. )
  1293. if not notebooks:
  1294. raise Access_error()
  1295. # find the given notebook and the one previous to it
  1296. previous_notebook = None
  1297. current_notebook = None
  1298. for notebook in notebooks:
  1299. if notebook.object_id == notebook_id:
  1300. current_notebook = notebook
  1301. break
  1302. previous_notebook = notebook
  1303. if current_notebook is None:
  1304. raise Access_error()
  1305. # if there is no previous notebook, then the current notebook is first. so, move it after the
  1306. # last notebook
  1307. if previous_notebook is None:
  1308. last_notebook = notebooks[ -1 ]
  1309. self.__database.execute(
  1310. user.sql_update_notebook_rank( current_notebook.object_id, last_notebook.rank + 1 ),
  1311. commit = False,
  1312. )
  1313. # otherwise, save the current and previous notebooks back to the database with swapped ranks
  1314. else:
  1315. self.__database.execute(
  1316. user.sql_update_notebook_rank( current_notebook.object_id, previous_notebook.rank ),
  1317. commit = False,
  1318. )
  1319. self.__database.execute(
  1320. user.sql_update_notebook_rank( previous_notebook.object_id, current_notebook.rank ),
  1321. commit = False,
  1322. )
  1323. self.__database.commit()
  1324. return dict()
  1325. @expose( view = Json )
  1326. @end_transaction
  1327. @grab_user_id
  1328. @validate(
  1329. notebook_id = Valid_id(),
  1330. user_id = Valid_id( none_okay = True ),
  1331. )
  1332. def move_down( self, notebook_id, user_id ):
  1333. """
  1334. Reorder the user's notebooks by moving the given notebook down by one. If the notebook is
  1335. already last, then wrap it around to be the first notebook.
  1336. @type notebook_id: unicode
  1337. @param notebook_id: id of notebook to move down
  1338. @type user_id: unicode or NoneType
  1339. @param user_id: id of current logged-in user (if any)
  1340. @rtype json dict
  1341. @return {}
  1342. @raise Access_error: the current user doesn't have access to the given notebook
  1343. @raise Validation_error: one of the arguments is invalid
  1344. """
  1345. notebook = self.__users.load_notebook( user_id, notebook_id )
  1346. user = self.__database.load( User, user_id )
  1347. if not user or not notebook:
  1348. raise Access_error()
  1349. # load the notebooks to which this user has access
  1350. notebooks = self.__database.select_many(
  1351. Notebook,
  1352. user.sql_load_notebooks( parents_only = True, undeleted_only = True ),
  1353. )
  1354. if not notebooks:
  1355. raise Access_error()
  1356. # find the given notebook and the one after it
  1357. current_notebook = None
  1358. next_notebook = None
  1359. for notebook in notebooks:
  1360. if notebook.object_id == notebook_id:
  1361. current_notebook = notebook
  1362. elif current_notebook:
  1363. next_notebook = notebook
  1364. break
  1365. if current_notebook is None:
  1366. raise Access_error()
  1367. # if there is no next notebook, then the current notebook is last. so, move it before the
  1368. # first notebook
  1369. if next_notebook is None:
  1370. first_notebook = notebooks[ 0 ]
  1371. self.__database.execute(
  1372. user.sql_update_notebook_rank( current_notebook.object_id, first_notebook.rank - 1 ),
  1373. commit = False,
  1374. )
  1375. # otherwise, save the current and next notebooks back to the database with swapped ranks
  1376. else:
  1377. self.__database.execute(
  1378. user.sql_update_notebook_rank( current_notebook.object_id, next_notebook.rank ),
  1379. commit = False,
  1380. )
  1381. self.__database.execute(
  1382. user.sql_update_notebook_rank( next_notebook.object_id, current_notebook.rank ),
  1383. commit = False,
  1384. )
  1385. self.__database.commit()
  1386. return dict()
  1387. @expose( view = Json )
  1388. @strongly_expire
  1389. @end_transaction
  1390. @grab_user_id
  1391. @validate(
  1392. notebook_id = Valid_id(),
  1393. start = Valid_int( min = 0 ),
  1394. count = Valid_int( min = 1 ),
  1395. user_id = Valid_id( none_okay = True ),
  1396. )
  1397. def load_recent_updates( self, notebook_id, start, count, user_id = None ):
  1398. """
  1399. Provide the information necessary to display a notebook's recent updated/created notes, in
  1400. reverse chronological order by update time.
  1401. @type notebook_id: unicode
  1402. @param notebook_id: id of the notebook containing the notes
  1403. @type start: unicode or NoneType
  1404. @param start: index of recent note to start with (defaults to 0, the most recent note)
  1405. @type count: int or NoneType
  1406. @param count: number of recent notes to display (defaults to 10 notes)
  1407. @type user_id: unicode or NoneType
  1408. @param user_id: id of current logged-in user (if any)
  1409. @rtype: json dict
  1410. @return: { 'notes': recent_notes_list }
  1411. @raise Access_error: the current user doesn't have access to the given notebook or note
  1412. """
  1413. notebook = self.__users.load_notebook( user_id, notebook_id )
  1414. if notebook is None:
  1415. raise Access_error()
  1416. recent_notes = self.__database.select_many( Note, notebook.sql_load_notes_in_update_order( start = start, count = count ) )
  1417. return dict(
  1418. notes = recent_notes,
  1419. )
  1420. def recent_notes( self, notebook_id, start = 0, count = 10, user_id = None ):
  1421. """
  1422. Return the given notebook's recently created notes in reverse chronological order by creation
  1423. time.
  1424. @type notebook_id: unicode
  1425. @param notebook_id: id of the notebook containing the notes
  1426. @type start: unicode or NoneType
  1427. @param start: index of recent note to start with (defaults to 0, the most recent note)
  1428. @type count: int or NoneType
  1429. @param count: number of recent notes to return (defaults to 10 notes)
  1430. @type user_id: unicode or NoneType
  1431. @param user_id: id of current logged-in user (if any)
  1432. @rtype: dict
  1433. @return: data for Main_page() constructor
  1434. @raise Access_error: the current user doesn't have access to the given notebook or note
  1435. """
  1436. notebook = self.__users.load_notebook( user_id, notebook_id )
  1437. if notebook is None:
  1438. raise Access_error()
  1439. notes = self.__database.select_many( Note, notebook.sql_load_notes_in_creation_order( start, count ) )
  1440. result = self.__users.current( user_id )
  1441. result.update( self.contents( notebook_id, user_id = user_id ) )
  1442. result[ "notes" ] = notes
  1443. result[ "start" ] = start
  1444. result[ "count" ] = count
  1445. return result
  1446. def old_notes( self, notebook_id, start = 0, count = 10, user_id = None ):
  1447. """
  1448. Return the given notebook's oldest notes in chronological order by creation time.
  1449. @type notebook_id: unicode
  1450. @param notebook_id: id of the notebook containing the notes
  1451. @type start: unicode or NoneType
  1452. @param start: index of recent note to start with (defaults to 0, the oldest note)
  1453. @type count: int or NoneType
  1454. @param count: number of notes to return (defaults to 10 notes)
  1455. @type user_id: unicode or NoneType
  1456. @param user_id: id of current logged-in user (if any)
  1457. @rtype: dict
  1458. @return: data for Main_page() constructor
  1459. @raise Access_error: the current user doesn't have access to the given notebook or note
  1460. """
  1461. notebook = self.__users.load_notebook( user_id, notebook_id )
  1462. if notebook is None:
  1463. raise Access_error()
  1464. notes = self.__database.select_many( Note, notebook.sql_load_notes_in_creation_order( start, count, reverse = True ) )
  1465. result = self.__users.current( user_id )
  1466. result.update( self.contents( notebook_id, user_id = user_id ) )
  1467. result[ "notes" ] = notes
  1468. result[ "start" ] = start
  1469. result[ "count" ] = count
  1470. return result
  1471. WHITESPACE_PATTERN = re.compile( "\s+" )
  1472. NEWLINE_PATTERN = re.compile( "\r?\n" )
  1473. NOTE_LINK_PATTERN = re.compile( '(<a\s+(?:[^>]+\s+)?href=")[^"]*/notebooks/(\w+)\?note_id=(\w+)("[^>]*>)', re.IGNORECASE )
  1474. @expose( view = Json )
  1475. @strongly_expire
  1476. @end_transaction
  1477. @grab_user_id
  1478. @validate(
  1479. file_id = Valid_id(),
  1480. content_column = Valid_int( min = 0 ),
  1481. title_column = Valid_int( min = 0, none_okay = True ),
  1482. plaintext = Valid_bool(),
  1483. import_button = unicode,
  1484. user_id = Valid_id( none_okay = True ),
  1485. )
  1486. def import_csv( self, file_id, content_column, title_column, plaintext, import_button, user_id = None ):
  1487. """
  1488. Import a previously uploaded CSV file of notes as a new notebook. Delete the file once the
  1489. import is complete.
  1490. Plaintext contents are left mostly untouched, just stripping HTML and converting newlines to
  1491. <br> tags. HTML contents are cleaned of any disallowed/harmful HTML tags, and target="_new"
  1492. attributes are added to all links without targets, except internal note links.
  1493. Internal note links are rewritten such that they point to the newly imported notes. This is
  1494. accomplished by looking for a "note_id" column and determining what note each link points to.
  1495. Then each internal note link is rewritten to point at the new notebook id and note id.
  1496. @type file_id: unicode
  1497. @param file_id: id of the previously uploaded CSV file to import
  1498. @type content_column: int
  1499. @param content_column: zero-based index of the column containing note contents
  1500. @type title_column: int or NoneType
  1501. @param title_column: zero-based index of the column containing note titles (None indicates
  1502. the lack of any such column, in which case titles are derived from the
  1503. first few words of each note's contents if no title is already present
  1504. in the note's contents)
  1505. @type plaintext: bool
  1506. @param plaintext: True if the note contents are plaintext, or False if they're HTML
  1507. @type import_button: unicode
  1508. @param import_button: ignored
  1509. @type user_id: unicode or NoneType
  1510. @param user_id: id of current logged-in user (if any)
  1511. @rtype: dict
  1512. @return: { 'redirect': new_notebook_url }
  1513. @raise Access_error: the current user doesn't have access to the given file
  1514. @raise Files.Parse_error: there was an error in parsing the given file
  1515. @raise Import_error: there was an error in importing the notes from the file
  1516. """
  1517. TRUNCATED_TITLE_CHAR_LENGTH = 80
  1518. if user_id is None:
  1519. raise Access_error()
  1520. user = self.__database.load( User, user_id )
  1521. if user is None:
  1522. raise Access_error()
  1523. db_file = self.__database.load( File, file_id )
  1524. if db_file is None:
  1525. raise Access_error()
  1526. db_notebook = self.__users.load_notebook( user_id, db_file.notebook_id )
  1527. if db_notebook is None or db_notebook.read_write == Notebook.READ_WRITE_FOR_OWN_NOTES:
  1528. raise Access_error()
  1529. # if the file has a "note_id" header column, record its index
  1530. note_id_column = None
  1531. note_ids = {} # map of original CSV note id to imported note id
  1532. parser = self.__files.parse_csv( file_id, skip_header = False )
  1533. row = parser.next()
  1534. if row and u"note_id" in row:
  1535. note_id_column = row.index( u"note_id" )
  1536. parser = self.__files.parse_csv( file_id, skip_header = True )
  1537. # create a new notebook for the imported notes
  1538. notebook = self.__create_notebook( u"imported notebook", user, commit = False )
  1539. # import the notes into the new notebook
  1540. for row in parser:
  1541. row_length = len( row )
  1542. if content_column >= row_length:
  1543. raise Import_error()
  1544. if title_column is not None and title_column >= row_length:
  1545. raise Import_error()
  1546. title = None
  1547. # if there is a title column, use it. otherwise, if the note doesn't already contain a title,
  1548. # use the first line of the content column as the title
  1549. if title_column and title_column != content_column and len( row[ title_column ].strip() ) > 0:
  1550. title = Html_nuker( allow_refs = True ).nuke( Valid_string( escape_html = plaintext )( row[ title_column ].strip() ) )
  1551. elif plaintext or not Note.TITLE_PATTERN.search( row[ content_column ] ):
  1552. content_text = Html_nuker( allow_refs = True ).nuke( Valid_string( escape_html = plaintext )( row[ content_column ].strip() ) )
  1553. content_lines = [ line for line in self.NEWLINE_PATTERN.split( content_text ) if line.strip() ]
  1554. # skip notes with empty contents
  1555. if len( content_lines ) == 0:
  1556. continue
  1557. title = content_lines[ 0 ]
  1558. # truncate the makeshift title to a reasonable length, but truncate on a word boundary
  1559. if len( title ) > TRUNCATED_TITLE_CHAR_LENGTH:
  1560. title_words = self.WHITESPACE_PATTERN.split( title )
  1561. for i in range( 1, len( title_words ) ):
  1562. title_candidate = u" ".join( title_words[ : i ] )
  1563. if len( title_candidate ) <= TRUNCATED_TITLE_CHAR_LENGTH:
  1564. title = title_candidate
  1565. else:
  1566. break
  1567. contents = Valid_string( max = 50000, escape_html = plaintext, require_link_target = True )( row[ content_column ] )
  1568. if plaintext:
  1569. contents = contents.replace( u"\n", u"<br />" )
  1570. note_id = self.__database.next_id( Note, commit = False )
  1571. note = Note.create( note_id, contents, notebook_id = notebook.object_id, startup = False, rank = None, user_id = user_id )
  1572. # if the note doesn't have a title yet, then tack the given title onto the start of the contents
  1573. if title and note.title is None:
  1574. note.contents = u"<h3>%s</h3>%s" % ( title, note.contents )
  1575. # if there is a note id column, then map the original CSV note id to its new imported note id
  1576. if note_id_column:
  1577. try:
  1578. original_note_id = Valid_id( none_okay = True )( row[ note_id_column ].strip() )
  1579. except ValueError:
  1580. original_note_id = None
  1581. if original_note_id:
  1582. note_ids[ original_note_id ] = note_id
  1583. self.__database.save( note, commit = False )
  1584. def rewrite_link( match ):
  1585. ( link_start, original_notebook_id, original_note_id, link_end ) = match.groups()
  1586. note_id = note_ids.get( original_note_id )
  1587. if note_id:
  1588. return "%s/notebooks/%s?note_id=%s%s" % ( link_start, notebook.object_id, note_id, link_end )
  1589. # if we don't know how to rewrite the link (for lack of the new note id), then don't rewrite
  1590. # it and leave the link as it is
  1591. return "%s/notebooks/%s?note_id=%s%s" % ( link_start, original_notebook_id, original_note_id, link_end )
  1592. # do a pass over all the imported notes to rewrite internal note links so that they point to
  1593. # the newly imported note ids in the new notebook
  1594. for ( original_note_id, note_id ) in note_ids.items():
  1595. note = self.__database.load( Note, note_id )
  1596. if note:
  1597. ( rewritten_contents, rewritten_count ) = self.NOTE_LINK_PATTERN.subn( rewrite_link, note.contents )
  1598. if rewritten_count > 0:
  1599. note.contents = rewritten_contents
  1600. self.__database.save( note, commit = False )
  1601. # delete the CSV file now that it's been imported
  1602. self.__database.execute( db_file.sql_delete(), commit = False )
  1603. self.__database.uncache( db_file )
  1604. self.__database.commit()
  1605. Upload_file.delete_file( file_id )
  1606. return dict(
  1607. redirect = u"/notebooks/%s?rename=true" % notebook.object_id,
  1608. )