# -*- coding: utf-8 -*-from__future__importwith_statementimportosimportrefromsqlite3importdbapi2assqlite3fromcontextlibimportclosingfromflaskimport(Flask,request,g,redirect,url_for,render_template,flash,send_from_directory)importjinja2fromneorg.configimportDefaultConfigfromneorg.wikiimportgene_html,safecallfromneorgimportsearchdefregex_from_temp_path(path):""" Generate regex expression from URL for matching template page >>> regex_from_temp_path('/non/temp/url/') '^/non/temp/url/$' >>> regex_from_temp_path('/just/one/_temp_/') '^/just/one/([^/]*)/$' >>> regex_from_temp_path('/try/two/_temp_/_temp_/') '^/try/two/([^/]*)/([^/]*)/$' """return'^%s$'%path.replace('_temp_','([^/]*)')defmatch_temp_path(path,temp_path_list):""" Returs matched template path and the matched object >>> (temp_path, match) = match_temp_path( ... '/my/url', ['/some/url', '/my/_temp_', '/_temp_']) ... >>> temp_path '/my/_temp_' >>> match.groups() ('url',) >>> (temp_path, match) = match_temp_path( ... '/my/url', ['/some/url', '/my/_temp_', '/_temp_/_temp_']) ... >>> temp_path '/my/_temp_' >>> match.groups() ('url',) """fortemp_pathinsorted(temp_path_list,reverse=True):match=re.match(regex_from_temp_path(temp_path),path)ifmatch:return(temp_path,match)return(None,None)deffind_temp_path(path):""" Find the template path matches the given path """temp_path_list=g.db.execute("select page_path from pages").fetchall()returnmatch_temp_path(path,[row[0]forrowintemp_path_list])deftemp_parent_path(temp_path):""" The path of the parent page of the leftmost ``_temp_`` page >>> temp_parent_path('/parent/_temp_/') '/parent/' >>> temp_parent_path('/parent/_temp_/complicated/_temp_') '/parent/' """i=temp_path.find('/_temp_')ifi>=0:returntemp_path[:i]+'/'else:raiseValueError("cannot find '_temp_' in '%s'"%temp_path)defrelpath_from_temp(page_path,temp_path):""" Relative path from the parent of the leftmost ``_temp_`` page >>> relpath_from_temp('/parent/generated', '/parent/_temp_') '/generated' """parent_path=temp_parent_path(temp_path)parent_len=len(parent_path)ifpage_path[:parent_len]!=parent_path:raiseValueError("temp_path '%s' is not the correct template path of ""page_path '%s'."%(temp_path,page_path))returnpage_path[parent_len-1:]defremove_leading_slash(path):returnpathiflen(path)==0orpath[0]!='/'elsepath[1:]deffilter_descendants(path,path_list):""" Find descendants of `path` in `path_list` and returns relative paths >>> path_list = ['a', 'a/b', 'a/b/c'] >>> filter_descendants('', path_list) ['a', 'a/b', 'a/b/c'] >>> filter_descendants('a', path_list) ['', 'b', 'b/c'] >>> filter_descendants('a/b', path_list) ['', 'c'] """lenpath=len(path)return[remove_leading_slash(p[lenpath:])forpinpath_listifp.startswith(path)]deffind_descendants(path):""" Find the pages which has `path` as its heading path. """path_list=[row[0]forrowing.db.execute("select page_path from pages").fetchall()]returnfilter_descendants(path,path_list)defrecent_pages(path,num):""" Find the `num` most recently updated sub-pages of `path` """path_list=find_descendants(path)date_list=[g.db.execute("select max(updated) from page_history where page_path = ?",[os.path.join(path,subpath).rstrip('/')]).fetchone()[0]forsubpathinpath_list]date_path_list=sorted(zip(date_list,path_list),reverse=True)[:num]returndate_path_listdefhas_descendants(path):returnbool(find_descendants(path))defpath_as_title(path):""" Convert page path to a title """returnu' « '.join(reversed(path.split('/')))app=Flask('neorg')app.config.from_object(DefaultConfig)defconnect_db():"""Returns a new connection to the database."""returnsqlite3.connect(app.config['DATABASE'])definit_db():"""Creates the database tables."""fromneorg.verutilsimportcurrent_versioncurver=current_version()withclosing(connect_db())asdb:withapp.open_resource('schema.sql')asf:db.cursor().executescript(f.read())db.execute('insert into system_info (version) values (?)',[str(curver)])db.commit()defsystem_info():""" Get current system info stored in `system_info` table .. warning:: Do NOT use this in app. New function using `g.db` should be implemented to use system_info table in app. """withclosing(connect_db())asdb:sysinfo_current=db.execute(# fetch the newest one'select *, max(updated) from system_info').fetchone()ifsysinfo_current:returndict(zip(['version','updated'],sysinfo_current[:-1]))# ignore the tailing max(update)defupdate_system_info():""" Check and update current system info if the stored one is old This function **fail** with RuntimeError if the version in the stored system info is newer than the current running one. .. warning:: Do NOT use this in app. """fromneorg.verutilsimportNEOrgVersion,current_versionsysinfo=system_info()oldver=NEOrgVersion(sysinfo['version'])curver=current_version()ifoldver==curver:passelifoldver<curver:print"You updated NEOrg. Updating database..."withclosing(connect_db())asdb:db.execute('insert into system_info (version) values (?)',[str(curver)])db.commit()print"Finished."else:raiseRuntimeError('The old version ({0}) is newer than the version of the ''running version ({1}). Please install newer version'.format(oldver,curver))defget_search_index():returnsearch.get_index(app.config['SEARCHINDEX'])defupdate_search_index():""" Update all search index or create new index. .. warning:: Do NOT use this in app. Call this function once just before `app.run`. """withclosing(connect_db())asdb:pages=db.execute('select * from pages').fetchall()doc_list=[dict(zip(('page_path','page_text'),doc))fordocinpages]ix=get_search_index()# make new index if it does not existsearch.update_all(ix,doc_list)@app.before_requestdefbefore_request():"""Make sure we are connected to the database each request."""g.db=connect_db()@app.after_requestdefafter_request(response):"""Closes the database again at the end of the request."""g.db.close()returnresponsedefget_page_text(page_path):row=g.db.execute('select page_text from pages where page_path = ?',[page_path]).fetchone()ifrow:returnrow[0]else:returnNonedefget_page_text_and_html(page_path):page_text=get_page_text(page_path)ifpage_text:page_html=gene_html(page_text,page_path,_debug=app.config['DEBUG'])else:page_html=''return(page_text,page_html)@app.route('/_delete',defaults={'page_path':''},methods=['POST'])@app.route('/<path:page_path>/_delete',methods=['POST'])defdelete(page_path):ifrequest.form.get('yes')=='Yes':g.db.execute('delete from pages where page_path = ?',[page_path])g.db.execute('insert into page_history (page_path, page_text, page_exists) ''values (?, ?, 0)',[page_path,''])g.db.commit()search.delete(get_search_index(),page_path)flash('Page "%s" was deleted.'%page_path)returnredirect(url_for('page',page_path=''))elifrequest.form.get('no')=='No':flash('Cancel delete.')returnredirect(url_for('page',page_path=page_path))@app.route('/_confirm_delete',defaults={'page_path':''})@app.route('/<path:page_path>/_confirm_delete')defconfirm_delete(page_path):(page_text,page_html)=get_page_text_and_html(page_path)returnrender_template("confirm_delete.html",title=path_as_title(page_path),page_path=page_path,page_html=page_html)@app.route('/_save',defaults={'page_path':''},methods=['POST'])@app.route('/<path:page_path>/_save',methods=['POST'])defsave(page_path):ifrequest.form.get('save')=='Save':page_text=request.form['page_text']ifget_page_text(page_path)==page_text:flash('No change was found.')returnredirect(url_for("page",page_path=page_path))g.db.execute('insert or replace into pages (page_path, page_text)'' values (?, ?)',[page_path,page_text])g.db.execute('insert into page_history (page_path, page_text) values (?, ?)',[page_path,page_text])g.db.commit()search.update(get_search_index(),page_path,page_text)flash('Saved!')returnredirect(url_for("page",page_path=page_path))elifrequest.form.get('preview')=='Preview':page_text=request.form['page_text']page_html=gene_html(page_text,page_path,_debug=app.config['DEBUG'])ifget_page_text(page_path)==page_text:flash('Previewing... No change was found.')else:flash('Previewing... Not yet saved!')returnrender_template("preview.html",title=path_as_title(page_path),page_path=page_path,page_text=page_text,page_html=page_html)elifrequest.form.get('cancel')=='Cancel':flash('Discarded changes!')returnredirect(url_for("page",page_path=page_path))elifrequest.form.get('delete')=='Delete':returnredirect(url_for("confirm_delete",page_path=page_path))@app.route('/_edit',defaults={'page_path':''})@app.route('/<path:page_path>/_edit')defedit(page_path):(page_text,page_html)=get_page_text_and_html(page_path)returnrender_template("edit.html",title=path_as_title(page_path),page_path=page_path,page_html=page_html,page_text=page_textifpage_textelse'')@app.route('/_edit_form',defaults={'page_path':''})@app.route('/<path:page_path>/_edit_form')defedit_form(page_path):page_text=get_page_text(page_path)returnrender_template("edit_form.html",page_path=page_path,page_text=page_textifpage_textelse'')@app.route('/',defaults={'page_path':''})@app.route('/<path:page_path>/')defpage(page_path):(page_text,page_html)=get_page_text_and_html(page_path)ifpage_text:returnrender_template("page.html",title=path_as_title(page_path),page_path=page_path,page_html=page_html)else:generated=gene_from_template(page_path)ifgenerated:returngeneratedelse:ifhas_descendants(page_path):flash('Page {0} does not exist. Showing sub-pages.'.format(page_path))returnredirect(url_for('descendants',page_path=page_path))else:flash('Page {0} does not exist and does not have sub-pages. ''You can add new contents.'.format(page_path))returnredirect(url_for('edit',page_path=page_path))_HTML_TEMP_GENE_TEXT_FAIL="""<h1>Failed to generate from the template</h1><p>Maybe the template variables (such as <code>{{ args[0] }}</code>) usedwere not correct.See the help page for the valid variables.</p><pre>%s</pre>"""@safecall(_HTML_TEMP_GENE_TEXT_FAIL)defgene_text_from_temp(page_path,temp_path,match):temp_text=get_page_text(temp_path)template=jinja2.Environment().from_string(temp_text)page_text=template.render({'path':page_path,'relpath':relpath_from_temp(page_path,temp_path),'args':match.groups(),})returnpage_textdefgene_from_template(page_path):(temp_path,match)=find_temp_path(page_path)ifmatch:(page_text,tb_text,)=gene_text_from_temp(page_path,temp_path,match,_mix=False,_debug=app.config['DEBUG'])ifpage_textisNone:page_html=tb_textelse:page_html=gene_html(page_text,page_path,_debug=app.config['DEBUG'])returnrender_template("page.html",title=path_as_title(page_path),temp_path=temp_path,page_path=page_path,page_html=page_html)deflist_descendants(page_path):returnmap(u'./{0}'.format,sorted(find_descendants(page_path)))@app.route('/_descendants',defaults={'page_path':''})@app.route('/<path:page_path>/_descendants')defdescendants(page_path):link_list=list_descendants(page_path)returnrender_template("descendants.html",title=path_as_title(page_path),link_list=link_list,page_path=page_path)@app.route('/_history',defaults={'page_path':''})@app.route('/<path:page_path>/_history')defhistory(page_path):page_history_list=g.db.execute('select history_id, page_text, updated ''from page_history where page_path = ?',[page_path]).fetchall()page_history_keys=('history_id','page_text','updated')page_history=[dict(zip(page_history_keys,row))forrowinreversed(page_history_list)]returnrender_template("history.html",title=path_as_title(page_path),page_path=page_path,page_history=page_history)@app.route('/_old/<int:history_id>',defaults={'page_path':''})@app.route('/<path:page_path>/_old/<int:history_id>')defold(page_path,history_id):page_text=g.db.execute('select page_text from page_history where history_id = ?',[history_id]).fetchone()page_html=gene_html(page_text[0],page_path,_debug=app.config['DEBUG'])returnrender_template("page.html",title=path_as_title(page_path),page_path=page_path,page_html=page_html)@app.route('/_data/<path:filepath>')defdata_file(filepath):returnsend_from_directory(app.config['DATADIRPATH'],filepath)@app.route('/favicon.ico')@app.route('/favicon.ico/')deffavicon():favicon_path=os.path.join(app.config['NEORG_DIR'],'favicon.ico')ifos.path.exists(favicon_path):returnsend_from_directory(app.config['NEORG_DIR'],'favicon.ico')else:returnredirect(url_for('static',filename='favicon.ico'))@app.route('/_help/<path:filename>')defhelp(filename):helpdirpath=app.config.get('HELPDIRPATH')if(isinstance(helpdirpath,basestring)andos.path.exists(helpdirpath)):returnsend_from_directory(helpdirpath,filename)else:returnredirect(url_for('static',filename=os.path.join('help',filename)))@app.route('/_search')defsearch_results():query=request.args.get('q')ifquery:results=search.search(get_search_index(),query,get_page_text)returnrender_template("search.html",title=query,search_query=query,results=list(results))else:returnredirect(url_for('page',page_path=''))