views.py 94 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389
  1. # -*- coding: utf-8 -*-
  2. # noinspection PyUnresolvedReferences
  3. from datetime import datetime, timedelta, date
  4. from sets import Set
  5. from random import choice
  6. from pprint import pprint
  7. import json
  8. import dateutil.parser
  9. from bson.json_util import dumps
  10. import p929
  11. from django.views.decorators.cache import cache_page
  12. from django.template import RequestContext
  13. from django.shortcuts import render_to_response, get_object_or_404, redirect
  14. from django.http import Http404, HttpResponse
  15. from django.contrib.auth.decorators import login_required
  16. from django.utils.http import urlquote
  17. from django.utils.encoding import iri_to_uri
  18. from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt, csrf_protect
  19. from django.contrib.auth.models import User
  20. from sefaria.client.wrapper import format_object_for_client, format_note_object_for_client, get_notes, get_links
  21. from sefaria.system.exceptions import InputError, PartialRefInputError, BookNameError, NoVersionFoundError
  22. # noinspection PyUnresolvedReferences
  23. from sefaria.client.util import jsonResponse
  24. from sefaria.history import text_history, get_maximal_collapsed_activity, top_contributors, make_leaderboard, make_leaderboard_condition, text_at_revision, record_version_deletion, record_index_deletion
  25. from sefaria.system.decorators import catch_error_as_json
  26. from sefaria.workflows import *
  27. from sefaria.reviews import *
  28. from sefaria.summaries import get_toc, flatten_toc, get_or_make_summary_node, REORDER_RULES
  29. from sefaria.model import *
  30. from sefaria.sheets import get_sheets_for_ref
  31. from sefaria.utils.users import user_link, user_started_text
  32. from sefaria.utils.util import list_depth, text_preview
  33. from sefaria.utils.hebrew import hebrew_plural, hebrew_term, encode_hebrew_numeral, encode_hebrew_daf, is_hebrew, strip_cantillation, has_cantillation
  34. from sefaria.utils.talmud import section_to_daf, daf_to_section
  35. from sefaria.datatype.jagged_array import JaggedArray
  36. import sefaria.utils.calendars
  37. import sefaria.tracker as tracker
  38. from sefaria.system.cache import django_cache_decorator
  39. try:
  40. from sefaria.settings import USE_VARNISH
  41. except ImportError:
  42. USE_VARNISH = False
  43. if USE_VARNISH:
  44. from sefaria.system.sf_varnish import invalidate_ref, invalidate_linked
  45. import logging
  46. logger = logging.getLogger(__name__)
  47. @ensure_csrf_cookie
  48. def reader(request, tref, lang=None, version=None):
  49. # Redirect to standard URLs
  50. def reader_redirect(uref, lang, version):
  51. url = "/" + uref
  52. if lang and version:
  53. url += "/%s/%s" % (lang, version)
  54. response = redirect(iri_to_uri(url), permanent=True)
  55. params = request.GET.urlencode()
  56. response['Location'] += "?%s" % params if params else ""
  57. return response
  58. try:
  59. oref = model.Ref(tref)
  60. except PartialRefInputError as e:
  61. logger.warning(u'{}'.format(e))
  62. matched_ref = Ref(e.matched_part)
  63. return reader_redirect(matched_ref.url(), lang, version)
  64. except InputError:
  65. raise Http404
  66. uref = oref.url()
  67. if uref and tref != uref:
  68. return reader_redirect(uref, lang, version)
  69. # Return Text TOC if this is a bare text title
  70. if oref.sections == [] and (oref.index.title == oref.normal() or getattr(oref.index_node, "depth", 0) > 1):
  71. return text_toc(request, oref)
  72. # or if this is a schema node with multiple sections underneath it
  73. if (not getattr(oref.index_node, "depth", None)):
  74. return text_toc(request, oref)
  75. if request.flavour == "mobile":
  76. return s2(request, ref=tref)
  77. # BANDAID - for spanning refs, return the first section
  78. oref = oref.padded_ref()
  79. if oref.is_spanning():
  80. first_oref = oref.first_spanned_ref()
  81. return reader_redirect(first_oref.url(), lang, version)
  82. version = version.replace("_", " ") if version else None
  83. try:
  84. text = TextFamily(oref, lang=lang, version=version, commentary=False, alts=True).contents()
  85. except NoVersionFoundError:
  86. raise Http404
  87. text.update({"commentary": [], "notes": [], "sheets": [], "layer": [], "connectionsLoadNeeded": True})
  88. hasSidebar = True
  89. layer_name = request.GET.get("layer", None)
  90. if layer_name and not "error" in text:
  91. layer = Layer().load({"urlkey": layer_name})
  92. if not layer:
  93. raise InputError("Layer not found.")
  94. text["layer"] = [format_note_object_for_client(n) for n in layer.all(tref=tref)]
  95. text["_loadSourcesFromDiscussion"] = True
  96. text["next"] = oref.next_section_ref().normal() if oref.next_section_ref() else None
  97. text["prev"] = oref.prev_section_ref().normal() if oref.prev_section_ref() else None
  98. text["ref"] = Ref(text["ref"]).normal()
  99. if lang and version:
  100. text['new_preferred_version'] = {'lang': lang, 'version': version}
  101. zipped_text = map(None, text["text"], text["he"]) if not "error" in text else []
  102. if "error" not in text:
  103. if len(text["sections"]) == text["textDepth"]:
  104. section = text["sections"][-1] - 1
  105. en = text["text"][section] if len(text.get("text", [])) > section else ""
  106. en = "" if not isinstance(en, basestring) else en
  107. he = text["he"][section] if len(text.get("he", [])) > section else ""
  108. he = "" if not isinstance(he, basestring) else he
  109. description_text = " ".join((en, he))
  110. else:
  111. en = text.get("text", []) if isinstance(text.get("text", []), list) else []
  112. he = text.get("he", []) if isinstance(text.get("he", []), list) else []
  113. lines = [line for line in (en+he) if isinstance(line, basestring)]
  114. description_text = " ".join(lines)
  115. description_text = strip_tags(description_text)[:600] + "..."
  116. else:
  117. description_text = "Unknown Text."
  118. initJSON = json.dumps(text)
  119. lines = request.GET.get("layout", None) or "lines" if "error" in text or text["type"] not in ('Tanach', 'Talmud') or text["book"] == "Psalms" else "block"
  120. layout = request.GET.get("layout") if request.GET.get("layout") in ("heLeft", "heRight") else "heLeft"
  121. sidebarLang = request.GET.get('sidebarLang', None) or request.COOKIES.get('sidebarLang', "all")
  122. sidebarLang = {"all": "sidebarAll", "he": "sidebarHebrew", "en": "sidebarEnglish"}.get(sidebarLang, "sidebarAll")
  123. lexicon = request.GET.get('lexicon', 0)
  124. template_vars = {'text': text,
  125. 'hasSidebar': hasSidebar,
  126. 'initJSON': initJSON,
  127. 'zipped_text': zipped_text,
  128. 'description_text': description_text,
  129. 'page_title': oref.normal() if "error" not in text else "Unknown Text",
  130. 'title_variants': "(%s)" % ", ".join(text.get("titleVariants", []) + text.get("heTitleVariants", [])),
  131. 'sidebarLang': sidebarLang,
  132. 'lines': lines,
  133. 'layout': layout,
  134. 'lexicon': lexicon,
  135. }
  136. if "error" not in text:
  137. # Override Content Language Settings if text not available in given langauge
  138. if is_text_empty(text["text"]):
  139. template_vars["contentLang"] = "hebrew"
  140. if is_text_empty(text["he"]):
  141. template_vars["contentLang"] = "english"
  142. # Override if a specfic version was requested
  143. if lang:
  144. template_vars["contentLang"] = {"he": "hebrew", "en": "english"}[lang]
  145. return render_to_response('reader.html', template_vars, RequestContext(request))
  146. def esi_account_box(request):
  147. return render_to_response('elements/accountBox.html', {}, RequestContext(request))
  148. def s2(request, ref, version=None, lang=None):
  149. """
  150. New interfaces in development
  151. """
  152. try:
  153. oref = Ref(ref)
  154. except InputError:
  155. raise Http404
  156. if oref.sections == [] and (oref.index.title == oref.normal() or getattr(oref.index_node, "depth", 0) > 1):
  157. initialMenu = "text toc"
  158. oref = oref.first_available_section_ref()
  159. else:
  160. initialMenu = ""
  161. try:
  162. text = TextFamily(oref, version=version, lang=lang, commentary=False, context=True, pad=True, alts=True).contents()
  163. except NoVersionFoundError:
  164. raise Http404
  165. text["next"] = oref.next_section_ref().normal() if oref.next_section_ref() else None
  166. text["prev"] = oref.prev_section_ref().normal() if oref.prev_section_ref() else None
  167. return render_to_response('s2.html', {
  168. "ref": oref.normal(),
  169. "data": text,
  170. "initialMenu": initialMenu,
  171. }, RequestContext(request))
  172. def s2_texts_category(request, cats):
  173. """
  174. Listing of texts in a category.
  175. """
  176. cats = cats.split("/")
  177. toc = get_toc()
  178. cat_toc = get_or_make_summary_node(toc, cats)
  179. if len(cat_toc) == 0:
  180. return s2_texts(request)
  181. return render_to_response('s2.html', {
  182. "initialMenu": "navigation",
  183. "initialNavigationCategories": json.dumps(cats),
  184. }, RequestContext(request))
  185. def s2_page(request, page):
  186. """
  187. View into an S2 page
  188. """
  189. return render_to_response('s2.html', {
  190. "initialMenu": page
  191. }, RequestContext(request))
  192. def s2_home(request):
  193. return s2_page(request, "home")
  194. def s2_search(request):
  195. return s2_page(request, "search")
  196. def s2_texts(request):
  197. return s2_page(request, "navigation")
  198. def s2_sheets(request):
  199. return s2_page(request, "sheets")
  200. def s2_sheets_by_tag(request, tag):
  201. """
  202. Standalone page for new sheets list
  203. """
  204. return render_to_response('s2.html', {
  205. "initialMenu": "sheets",
  206. "initialSheetsTag": tag,
  207. }, RequestContext(request))
  208. @ensure_csrf_cookie
  209. def edit_text(request, ref=None, lang=None, version=None):
  210. """
  211. Opens a view directly to adding, editing or translating a given text.
  212. """
  213. if ref is not None:
  214. try:
  215. oref = Ref(ref)
  216. if oref.sections == []:
  217. # Only text name specified, let them chose section first
  218. initJSON = json.dumps({"mode": "add new", "newTitle": oref.normal()})
  219. mode = "Add"
  220. else:
  221. # Pull a particular section to edit
  222. version = version.replace("_", " ") if version else None
  223. #text = get_text(ref, lang=lang, version=version)
  224. text = TextFamily(Ref(ref), lang=lang, version=version).contents()
  225. text["mode"] = request.path.split("/")[1]
  226. mode = text["mode"].capitalize()
  227. initJSON = json.dumps(text)
  228. except:
  229. index = library.get_index(ref)
  230. if index: # a commentator titlein
  231. ref = None
  232. initJSON = json.dumps({"mode": "add new", "newTitle": index.contents()['title']})
  233. else:
  234. initJSON = json.dumps({"mode": "add new"})
  235. titles = json.dumps(model.library.full_title_list())
  236. page_title = "%s %s" % (mode, ref) if ref else "Add a New Text"
  237. return render_to_response('reader.html',
  238. {'titles': titles,
  239. 'initJSON': initJSON,
  240. 'page_title': page_title,
  241. },
  242. RequestContext(request))
  243. @ensure_csrf_cookie
  244. def edit_text_info(request, title=None, new_title=None):
  245. """
  246. Opens the Edit Text Info page.
  247. """
  248. if title:
  249. # Edit Existing
  250. title = title.replace("_", " ")
  251. i = library.get_index(title)
  252. if not (request.user.is_staff or user_started_text(request.user.id, title)):
  253. return render_to_response('static/generic.html', {"title": "Permission Denied", "content": "The Text Info for %s is locked.<br><br>Please email hello@sefaria.org if you believe edits are needed." % title}, RequestContext(request))
  254. indexJSON = json.dumps(i.contents(v2=True) if "toc" in request.GET else i.contents())
  255. versions = VersionSet({"title": title})
  256. text_exists = versions.count() > 0
  257. new = False
  258. elif new_title:
  259. # Add New
  260. new_title = new_title.replace("_", " ")
  261. try: # Redirect to edit path if this title already exists
  262. i = library.get_index(new_title)
  263. return redirect("/edit/textinfo/%s" % new_title)
  264. except:
  265. pass
  266. indexJSON = json.dumps({"title": new_title})
  267. text_exists = False
  268. new = True
  269. return render_to_response('edit_text_info.html',
  270. {'title': title,
  271. 'indexJSON': indexJSON,
  272. 'text_exists': text_exists,
  273. 'new': new,
  274. },
  275. RequestContext(request))
  276. @django_cache_decorator(6000)
  277. def make_toc_html(oref, zoom=1):
  278. """
  279. Returns the HTML of a text's Table of Contents, including any alternate structures.
  280. :param oref - Ref of the text to create. Ref is used instead of Index to allow
  281. for a different table of contents focusing on a single node of a complex text.
  282. :param zoom - integar specifying the level of granularity to show. 0 = Segment level,
  283. 1 = Section level etc.
  284. """
  285. index = oref.index
  286. if index.is_complex():
  287. html = make_complex_toc_html(oref)
  288. else:
  289. state = StateNode(index.title)
  290. he_counts, en_counts = state.var("he", "availableTexts"), state.var("en", "availableTexts")
  291. html = make_simple_toc_html(he_counts, en_counts, index.nodes.sectionNames, index.nodes.addressTypes, index.title, zoom=zoom)
  292. if index.has_alt_structures():
  293. default_name = index.nodes.sectionNames[0] if not index.is_complex() else "Contents"
  294. default_struct = getattr(index, "default_struct", default_name)
  295. structs = {default_name: html } # store HTML for each structure
  296. alts = index.get_alt_structures().items()
  297. for alt in alts:
  298. structs[alt[0]] = make_alt_toc_html(alt[1])
  299. items = sorted(structs.items(), key=lambda x: 0 if x[0] == default_struct else 1)
  300. toggle, tocs = "", ""
  301. for item in items:
  302. toggle += "<span class='toggleDivider'>|</span>" if item[0] != default_struct else ""
  303. toggle += "<div class='altStructToggle" + (" active" if item[0] == default_struct else "") + "'>"
  304. toggle += "<span class='en'>" + item[0] + "</span>"
  305. toggle += "<span class='he'>" + hebrew_term(item[0]) + "</span>"
  306. toggle += "</div>"
  307. tocs += "<div class='altStruct' " + ("style='display:none'" if item[0] != default_struct else "") + ">" + item[1] + "</div>"
  308. html = "<div id='structToggles'>" + toggle + "</div>" + tocs
  309. return html
  310. def make_complex_toc_html(oref):
  311. """
  312. Returns the HTML of a complex text's Table of Contents.
  313. :param oref - Ref of the text to create. Ref is used instead of Index to allow
  314. for a different table of contents focusing on a single node.
  315. """
  316. index = oref.index
  317. req_node = oref.index_node
  318. def node_line(node, depth, **kwargs):
  319. if depth == 0:
  320. return ""
  321. linked = "linked" if node.is_leaf() and node.depth == 1 else ""
  322. default = "default" if node.is_default() else ""
  323. url = "/" + node.ref().url()
  324. en_icon = '<i class="schema-node-control fa ' + ('fa-angle-right' if linked else 'fa-angle-down') + '"></i>'
  325. he_icon = '<i class="schema-node-control fa ' + ('fa-angle-left' if linked else 'fa-angle-down') + '"></i>'
  326. html = '<a href="' + urlquote(url) + '"' if linked else "<div "
  327. html += ' class="schema-node-toc depth' + str(depth) + ' ' + linked + ' ' + default + '">'
  328. if not default:
  329. html += '<span class="schema-node-title">'
  330. html += '<span class="en">' + node.primary_title() + en_icon + '</span>'
  331. html += '<span class="he">' + node.primary_title(lang='he') + he_icon + '</span>'
  332. html += '</span>'
  333. if node.is_leaf():
  334. focused = node is req_node
  335. html += '<div class="schema-node-contents ' + ('open' if focused or default else 'closed') + '">'
  336. node_state = StateNode(snode=node)
  337. #Todo, handle Talmud and other address types, as well as commentary
  338. zoom = 0 if node.depth == 1 else 1
  339. he_counts, en_counts = node_state.var("he", "availableTexts"), node_state.var("en", "availableTexts")
  340. content = make_simple_toc_html(he_counts, en_counts, node.sectionNames, node.addressTypes, node.full_title(), zoom=zoom)
  341. content = content or "<div class='emptyMessage'>No text here.</div>"
  342. html += content + '</div>'
  343. html += "</a>" if linked else "</div>"
  344. return html
  345. html = index.nodes.traverse_to_string(node_line)
  346. return html
  347. def make_alt_toc_html(alt):
  348. """
  349. Returns HTML Table of Contents for an alternate structure.
  350. :param alt - a TitledTreeNode representing an alternate structure.
  351. """
  352. def node_line(node, depth, **kwargs):
  353. if depth == 0 and node.has_children():
  354. return ""
  355. refs = getattr(node, "refs", False)
  356. includeSections = getattr(node, "includeSections", False)
  357. linked = "linked" if not refs and not includeSections else ""
  358. default = "default" if node.is_default() else ""
  359. url = "/" + Ref(node.wholeRef).url()
  360. en_icon = '<i class="schema-node-control fa ' + ('fa-angle-right' if linked else 'fa-angle-down') + '"></i>'
  361. he_icon = '<i class="schema-node-control fa ' + ('fa-angle-left' if linked else 'fa-angle-down') + '"></i>'
  362. html = '<a href="' + urlquote(url) + '"' if linked else "<div "
  363. html += ' class="schema-node-toc depth' + str(depth) + ' ' + linked + ' ' + default + '" >'
  364. wrap_counts = lambda counts: counts if list_depth(counts) == 2 else wrap_counts([counts])
  365. # wrap counts to ensure they are as though at section level, handles segment level refs
  366. if not default and depth > 0:
  367. html += '<span class="schema-node-title">'
  368. html += '<span class="en">' + node.primary_title() + en_icon + '</span>'
  369. html += '<span class="he">' + node.primary_title(lang='he') + he_icon + '</span>'
  370. html += '</span>'
  371. if refs:
  372. # todo handle refs with depth > 1
  373. html += "<div class='schema-node-contents"
  374. html += " closed" if depth > 0 else ""
  375. html += "'>"
  376. html += "<div class='sectionName'>"
  377. html += "<span class='en'>" + hebrew_plural(node.sectionNames[0]) + "</span>"
  378. html += "<span class='he'>" + hebrew_term(node.sectionNames[0]) + "</span>"
  379. html += "</div>"
  380. for i in range(len(node.refs)):
  381. if not node.refs[i]:
  382. continue
  383. target_ref = Ref(node.refs[i])
  384. state = StateNode(snode=target_ref.index_node)
  385. he_counts, en_counts = state.var("he", "availableTexts"), state.var("en", "availableTexts")
  386. he = wrap_counts(JaggedArray(he_counts).subarray_with_ref(target_ref).array())
  387. en = wrap_counts(JaggedArray(en_counts).subarray_with_ref(target_ref).array())
  388. klass = "en%s he%s" % (toc_availability_class(en), toc_availability_class(he))
  389. html += '<a class="sectionLink %s" href="/%s">%s</a>' % (klass, urlquote(node.refs[i]), (i+1))
  390. html += "</div>"
  391. elif includeSections:
  392. # Display each section included in node.wholeRef
  393. # todo handle case where wholeRef points to complex node
  394. # todo handle case where wholeRef points to book name (root of simple index or commentary index)
  395. target_ref = Ref(node.wholeRef)
  396. state = StateNode(snode=target_ref.index_node)
  397. he_counts, en_counts = state.var("he", "availableTexts"), state.var("en", "availableTexts")
  398. refs = target_ref.split_spanning_ref()
  399. first, last = refs[0], refs[-1]
  400. offset = first.sections[-2]-1 if first.is_segment_level() else first.sections[-1]-1
  401. offset_lines = (first.normal().rsplit(":", 1)[1] if first.is_segment_level() else "",
  402. last.normal().rsplit(":", 1)[1] if last.is_segment_level() else "")
  403. he = wrap_counts(JaggedArray(he_counts).subarray_with_ref(target_ref).array())
  404. en = wrap_counts(JaggedArray(en_counts).subarray_with_ref(target_ref).array())
  405. depth = len(first.index_node.sectionNames) - len(first.section_ref().sections)
  406. sectionNames = first.index_node.sectionNames[depth:]
  407. addressTypes = first.index_node.addressTypes[depth:]
  408. ref = first.context_ref(level=2) if first.is_segment_level() else first.context_ref()
  409. content = make_simple_toc_html(he, en, sectionNames, addressTypes, ref.url(), offset=offset, offset_lines=offset_lines)
  410. html += "<div class='schema-node-contents open'>" + content + "</div>"
  411. html += "</a>" if linked else "</div>"
  412. return html
  413. html = "<div class='tocLevel'>" + alt.traverse_to_string(node_line) + "</div>"
  414. return html
  415. def make_simple_toc_html(he_toc, en_toc, labels, addresses, ref, zoom=1, offset=0, offset_lines=None):
  416. """
  417. Returns HTML Table of Contents corresponding to jagged count arrays he_toc and en_toc.
  418. Runs recursively.
  419. :param he_toc - jagged int array of available counts in hebrew
  420. :param en_toc - jagged int array of available counts in english
  421. :param labels - list of section names for levels corresponding to toc
  422. :param addresses - list of address types, from Index record
  423. :param ref - text to prepend to final links. Starts with text title, recursively adding sections.
  424. :param zoom - sets how many levels of final depth to summarize
  425. (e.g., 1 will hide verses and only show chapter level)
  426. :param offset - int to add to each listed section
  427. :param offset_lines - tuple of strings to be appended to the URL of the first and last
  428. section (allows pointing to spans inside a section).
  429. """
  430. he_toc = [] if isinstance(he_toc, int) else he_toc
  431. en_toc = [] if isinstance(en_toc, int) else en_toc
  432. assert(len(he_toc) == len(en_toc))
  433. length = len(he_toc)
  434. assert(list_depth(he_toc, deep=True) == list_depth(en_toc, deep=True))
  435. depth = list_depth(he_toc, deep=True)
  436. # todo: have this use the address classes in schema.py
  437. talmudBase = (len(addresses) > 0 and addresses[0] == "Talmud")
  438. html = ""
  439. if depth == zoom + 1:
  440. # We're at the terminal level, list sections links
  441. for i in range(length):
  442. klass = "he%s en%s" % (toc_availability_class(he_toc[i]), toc_availability_class(en_toc[i]))
  443. if klass == "heNone enNone":
  444. continue # Don't display sections with no content
  445. en_section = section_to_daf(i+offset+1) if talmudBase else str(i+offset+1)
  446. he_section = encode_hebrew_daf(en_section) if talmudBase else encode_hebrew_numeral(int(en_section), punctuation=False)
  447. section_html = "<span class='en'>%s</span><span class='he'>%s</span>" % (en_section, he_section)
  448. path = "%s.%s" % (ref, en_section)
  449. if offset_lines and i == 0 and offset_lines[0]:
  450. path += "." + offset_lines[0]
  451. elif offset_lines and (i+1) == length and offset_lines[1]:
  452. path += "." + offset_lines[1]
  453. if zoom > 1: # Make links point to first available content
  454. available = Ref(ref + "." + en_section).first_available_section_ref()
  455. path = available.url() if available else path
  456. html += '<a class="sectionLink %s" href="/%s">%s</a>' % (klass, urlquote(path), section_html)
  457. if html:
  458. sectionName = "<div class='sectionName'>"
  459. sectionName += "<span class='en'>" + hebrew_plural(labels[0]) + "</span>"
  460. sectionName += "<span class='he'>" + hebrew_term(labels[0]) + "</span>"
  461. sectionName += "</div>"
  462. html = sectionName + html
  463. else:
  464. # We're above terminal level, list sections and recur
  465. for i in range(length):
  466. section = section_to_daf(i + 1) if talmudBase else str(i + 1)
  467. section_html = make_simple_toc_html(he_toc[i], en_toc[i], labels[1:], addresses[1:], ref + "." + section, zoom=zoom)
  468. if section_html:
  469. he_section = encode_hebrew_daf(section) if talmudBase else encode_hebrew_numeral(int(section), punctuation=False)
  470. html += "<div class='tocSection'>"
  471. html += "<div class='sectionName'>"
  472. html += "<span class='en'>" + labels[0] + " " + section + "</span>"
  473. html += "<span class='he'>" + hebrew_term(labels[0]) + " " + he_section + "</span>"
  474. html += "</div>" + section_html + "</div>"
  475. html = "<div class='tocLevel'>" + html + "</div>" if html else ""
  476. return html
  477. def toc_availability_class(toc):
  478. """
  479. Returns the string of a class name in ("All", "Some", "None")
  480. according to how much content is available in toc,
  481. which may be either a list of ints or an int representing available counts.
  482. """
  483. if isinstance(toc, int):
  484. return "All" if toc else "None"
  485. else:
  486. counts = set([toc_availability_class(x) for x in toc])
  487. if counts == set(["All"]):
  488. return "All"
  489. elif "Some" in counts or counts == set(["All", "None"]):
  490. return "Some"
  491. else:
  492. return "None"
  493. @ensure_csrf_cookie
  494. def text_toc(request, oref):
  495. """
  496. Page representing a single text, showing its Table of Contents and related info.
  497. """
  498. index = oref.index
  499. title = index.title
  500. heTitle = index.get_title(lang='he')
  501. state = StateNode(title)
  502. versions = VersionSet({"title": title}, sort=[["language", -1]])
  503. categories = index.categories[:]
  504. if categories[0] in REORDER_RULES:
  505. categories = REORDER_RULES[categories[0]] + categories[1:]
  506. if categories[0] == "Commentary":
  507. categories = [categories[1], "Commentary", index.toc_contents()["commentator"]]
  508. cat_slices = [categories[:n+1] for n in range(len(categories))] # successive sublists of cats, for category links
  509. c_titles = model.library.get_commentary_version_titles_on_book(title, with_commentary2=True)
  510. c_indexes = [library.get_index(commentary) for commentary in c_titles]
  511. commentaries = [i.toc_contents() for i in c_indexes]
  512. if index.is_complex():
  513. zoom = 1
  514. else:
  515. zoom = 0 if index.nodes.depth == 1 else 2 if "Commentary" in index.categories else 1
  516. zoom = int(request.GET.get("zoom", zoom))
  517. toc_html = make_toc_html(oref, zoom=zoom)
  518. if index.is_complex():
  519. count_strings = False
  520. complex = True
  521. zoom = 1
  522. else: # simple text
  523. complex = False
  524. talmud = Ref(index.title).is_talmud()
  525. count_strings = {
  526. "en": ", ".join([str(state.get_available_counts("en")[i]) + " " + hebrew_plural(index.nodes.sectionNames[i]) for i in range(index.nodes.depth)]),
  527. "he": ", ".join([str(state.get_available_counts("he")[i]) + " " + hebrew_plural(index.nodes.sectionNames[i]) for i in range(index.nodes.depth)]),
  528. } if state else None #why the condition?
  529. if talmud and count_strings:
  530. count_strings["he"] = count_strings["he"].replace("Dappim", "Amudim")
  531. count_strings["en"] = count_strings["en"].replace("Dappim", "Amudim")
  532. if "Commentary" in index.categories and state.get_flag("heComplete"):
  533. # Because commentary text is sparse, the code in make_toc_hmtl doens't work for completeness
  534. # Trust a flag if its set instead
  535. toc_html = toc_html.replace("heSome", "heAll")
  536. auths = index.author_objects()
  537. index_contents = index.contents(v2=True)
  538. if index_contents["categories"][0] in REORDER_RULES:
  539. index_contents["categories"] = REORDER_RULES[index_contents["categories"][0]] + index_contents["categories"][1:]
  540. template_vars = {
  541. "index": index_contents,
  542. "authors": auths,
  543. "versions": versions,
  544. "commentaries": commentaries,
  545. "heComplete": state.get_flag("heComplete"),
  546. "enComplete": state.get_flag("enComplete"),
  547. "count_strings": count_strings,
  548. "zoom": zoom,
  549. "toc_html": toc_html,
  550. "cat_slices": cat_slices,
  551. "complex": complex,
  552. }
  553. composition_time_period = index.composition_time_period()
  554. publication_time_period = index.publication_time_period()
  555. composition_place = index.composition_place()
  556. publication_place = index.publication_place()
  557. if composition_time_period:
  558. template_vars["comp_time_string"] = {
  559. "en": composition_time_period.period_string("en"),
  560. "he": composition_time_period.period_string("he"),
  561. }
  562. if publication_time_period:
  563. template_vars["pub_time_string"] = {
  564. "en": publication_time_period.period_string("en"),
  565. "he": publication_time_period.period_string("he"),
  566. }
  567. if composition_place:
  568. template_vars["comp_place"] = {
  569. "en": composition_place.primary_name("en"),
  570. "he": composition_place.primary_name("he"),
  571. }
  572. if publication_place:
  573. template_vars["pub_place"] = {
  574. "en": publication_place.primary_name("en"),
  575. "he": publication_place.primary_name("he"),
  576. }
  577. return render_to_response('text_toc.html',
  578. template_vars,
  579. RequestContext(request))
  580. def text_toc_html_fragment(request, title):
  581. """
  582. Returns an HTML fragment of the Text TOC for title
  583. """
  584. oref = Ref(title)
  585. zoom = 0 if not oref.index.is_complex() and oref.index_node.depth == 1 else 1
  586. return HttpResponse(make_toc_html(oref, zoom=zoom))
  587. @ensure_csrf_cookie
  588. def texts_list(request):
  589. """
  590. Page listing every text in the library.
  591. """
  592. if request.flavour == "mobile":
  593. return s2_page(request, "texts")
  594. return render_to_response('texts.html',
  595. {},
  596. RequestContext(request))
  597. def texts_category_list(request, cats):
  598. """
  599. Page listing every text in category
  600. """
  601. if request.flavour == "mobile":
  602. return s2_texts_category(request, cats)
  603. cats = cats.split("/")
  604. toc = get_toc()
  605. cat_toc = get_or_make_summary_node(toc, cats)
  606. if (len(cat_toc) == 0):
  607. raise Http404
  608. category = cats[-1]
  609. heCategory = hebrew_term(category)
  610. if category in ("Bavli", "Yerushalmi"):
  611. category = "Talmud " + category
  612. heCategory = hebrew_term("Talmud") + " " + heCategory
  613. if "Commentary" in cats:
  614. category = category + " on " + cats[0]
  615. heCategory = heCategory + u" על " + hebrew_term(cats[0])
  616. return render_to_response('text_category.html',
  617. {
  618. "categories": cats,
  619. "category": category,
  620. "heCategory": heCategory,
  621. "cat_toc": cat_toc,
  622. "cat_path": "/" + "/".join(cats),
  623. },
  624. RequestContext(request))
  625. @ensure_csrf_cookie
  626. def search(request):
  627. if request.flavour == "mobile":
  628. return s2_page(request, "search")
  629. return render_to_response('search.html',
  630. {},
  631. RequestContext(request))
  632. #todo: is this used elsewhere? move it?
  633. def count_and_index(c_oref, c_lang, vtitle, to_count=1):
  634. # count available segments of text
  635. if to_count:
  636. summaries.update_summaries_on_change(c_oref.book)
  637. from sefaria.settings import SEARCH_INDEX_ON_SAVE
  638. if SEARCH_INDEX_ON_SAVE:
  639. model.IndexQueue({
  640. "ref": c_oref.normal(),
  641. "lang": c_lang,
  642. "version": vtitle,
  643. "type": "ref",
  644. }).save()
  645. @catch_error_as_json
  646. @csrf_exempt
  647. def texts_api(request, tref, lang=None, version=None):
  648. oref = Ref(tref)
  649. if request.method == "GET":
  650. uref = oref.url()
  651. if uref and tref != uref: # This is very similar to reader.reader_redirect subfunction, above.
  652. url = "/api/texts/" + uref
  653. if lang and version:
  654. url += "/%s/%s" % (lang, version)
  655. response = redirect(iri_to_uri(url), permanent=True)
  656. params = request.GET.urlencode()
  657. response['Location'] += "?%s" % params if params else ""
  658. return response
  659. cb = request.GET.get("callback", None)
  660. context = int(request.GET.get("context", 1))
  661. commentary = bool(int(request.GET.get("commentary", True)))
  662. pad = bool(int(request.GET.get("pad", 1)))
  663. version = version.replace("_", " ") if version else None
  664. layer_name = request.GET.get("layer", None)
  665. alts = bool(int(request.GET.get("alts", True)))
  666. try:
  667. text = TextFamily(oref, version=version, lang=lang, commentary=commentary, context=context, pad=pad, alts=alts).contents()
  668. except AttributeError as e:
  669. oref = oref.default_child_ref()
  670. text = TextFamily(oref, version=version, lang=lang, commentary=commentary, context=context, pad=pad, alts=alts).contents()
  671. # Use a padded ref for calculating next and prev
  672. # TODO: what if pad is false and the ref is of an entire book?
  673. # Should next_section_ref return None in that case?
  674. oref = oref.padded_ref() if pad else oref
  675. text["next"] = oref.next_section_ref().normal() if oref.next_section_ref() else None
  676. text["prev"] = oref.prev_section_ref().normal() if oref.prev_section_ref() else None
  677. text["commentary"] = text.get("commentary", [])
  678. text["sheets"] = get_sheets_for_ref(tref) if int(request.GET.get("sheets", 0)) else []
  679. if layer_name:
  680. layer = Layer().load({"urlkey": layer_name})
  681. if not layer:
  682. raise InputError("Layer not found.")
  683. layer_content = [format_note_object_for_client(n) for n in layer.all(tref=tref)]
  684. text["layer"] = layer_content
  685. text["layer_name"] = layer_name
  686. text["_loadSourcesFromDiscussion"] = True
  687. else:
  688. text["layer"] = []
  689. return jsonResponse(text, cb)
  690. if request.method == "POST":
  691. j = request.POST.get("json")
  692. if not j:
  693. return jsonResponse({"error": "Missing 'json' parameter in post data."})
  694. oref = oref.default_child_ref() # Make sure we're on the textual child
  695. if not request.user.is_authenticated():
  696. key = request.POST.get("apikey")
  697. if not key:
  698. return jsonResponse({"error": "You must be logged in or use an API key to save texts."})
  699. apikey = db.apikeys.find_one({"key": key})
  700. if not apikey:
  701. return jsonResponse({"error": "Unrecognized API key."})
  702. t = json.loads(j)
  703. chunk = tracker.modify_text(apikey["uid"], oref, t["versionTitle"], t["language"], t["text"], t["versionSource"], method="API")
  704. count_after = int(request.GET.get("count_after", 0))
  705. count_and_index(oref, chunk.lang, chunk.vtitle, count_after)
  706. return jsonResponse({"status": "ok"})
  707. else:
  708. @csrf_protect
  709. def protected_post(request):
  710. t = json.loads(j)
  711. chunk = tracker.modify_text(request.user.id, oref, t["versionTitle"], t["language"], t["text"], t["versionSource"])
  712. count_after = int(request.GET.get("count_after", 1))
  713. count_and_index(oref, chunk.lang, chunk.vtitle, count_after)
  714. return jsonResponse({"status": "ok"})
  715. return protected_post(request)
  716. if request.method == "DELETE":
  717. if not request.user.is_staff:
  718. return jsonResponse({"error": "Only moderators can delete texts."})
  719. if not (tref and lang and version):
  720. return jsonResponse({"error": "To delete a text version please specifiy a text title, version title and language."})
  721. tref = tref.replace("_", " ")
  722. version = version.replace("_", " ")
  723. v = Version().load({"title": tref, "versionTitle": version, "language": lang})
  724. if not v:
  725. return jsonResponse({"error": "Text version not found."})
  726. v.delete()
  727. record_version_deletion(tref, version, lang, request.user.id)
  728. if USE_VARNISH:
  729. invalidate_linked(oref)
  730. invalidate_ref(oref, lang, version)
  731. return jsonResponse({"status": "ok"})
  732. return jsonResponse({"error": "Unsuported HTTP method."})
  733. @catch_error_as_json
  734. def parashat_hashavua_api(request):
  735. callback = request.GET.get("callback", None)
  736. p = sefaria.utils.calendars.this_weeks_parasha(datetime.now())
  737. p["date"] = p["date"].isoformat()
  738. #p.update(get_text(p["ref"]))
  739. p.update(TextFamily(Ref(p["ref"])).contents())
  740. return jsonResponse(p, callback)
  741. @catch_error_as_json
  742. def table_of_contents_api(request):
  743. return jsonResponse(get_toc(), callback=request.GET.get("callback", None))
  744. @catch_error_as_json
  745. def text_titles_api(request):
  746. return jsonResponse({"books": model.library.full_title_list(with_commentary=True)}, callback=request.GET.get("callback", None))
  747. @catch_error_as_json
  748. @csrf_exempt
  749. def index_node_api(request, title):
  750. pass
  751. @catch_error_as_json
  752. @csrf_exempt
  753. def index_api(request, title, v2=False, raw=False):
  754. """
  755. API for manipulating text index records (aka "Text Info")
  756. """
  757. if request.method == "GET":
  758. try:
  759. i = library.get_index(title).contents(v2=v2, raw=raw)
  760. except InputError as e:
  761. node = library.get_schema_node(title) # If the request were for v1 and fails, this falls back to v2.
  762. if not node:
  763. raise e
  764. if node.is_default():
  765. node = node.parent
  766. i = node.as_index_contents()
  767. return jsonResponse(i, callback=request.GET.get("callback", None))
  768. if request.method == "POST":
  769. # use the update function if update is in the params
  770. func = tracker.update if request.GET.get("update", False) else tracker.add
  771. j = json.loads(request.POST.get("json"))
  772. if not j:
  773. return jsonResponse({"error": "Missing 'json' parameter in post data."})
  774. j["title"] = title.replace("_", " ")
  775. if not request.user.is_authenticated():
  776. key = request.POST.get("apikey")
  777. if not key:
  778. return jsonResponse({"error": "You must be logged in or use an API key to save texts."})
  779. apikey = db.apikeys.find_one({"key": key})
  780. if not apikey:
  781. return jsonResponse({"error": "Unrecognized API key."})
  782. return jsonResponse(func(apikey["uid"], model.Index, j, method="API", v2=v2, raw=raw).contents(v2=v2, raw=raw))
  783. else:
  784. title = j.get("oldTitle", j.get("title"))
  785. try:
  786. i = library.get_index(title) # Only allow staff and the person who submitted a text to edit
  787. if not request.user.is_staff and not user_started_text(request.user.id, title):
  788. return jsonResponse({"error": "{} is protected from change.<br/><br/>See a mistake?<br/>Email hello@sefaria.org.".format(title)})
  789. except BookNameError:
  790. pass # if this is a new text, allow any logged in user to submit
  791. @csrf_protect
  792. def protected_index_post(request):
  793. return jsonResponse(
  794. func(request.user.id, model.Index, j, v2=v2, raw=raw).contents(v2=v2, raw=raw)
  795. )
  796. return protected_index_post(request)
  797. if request.method == "DELETE":
  798. if not request.user.is_staff:
  799. return jsonResponse({"error": "Only moderators can delete texts indices."})
  800. title = title.replace("_", " ")
  801. i = library.get_index(title)
  802. i.delete()
  803. record_index_deletion(title, request.user.id)
  804. return jsonResponse({"status": "ok"})
  805. return jsonResponse({"error": "Unsuported HTTP method."})
  806. @catch_error_as_json
  807. def bare_link_api(request, book, cat):
  808. if request.method == "GET":
  809. resp = jsonResponse(get_book_link_collection(book, cat), callback=request.GET.get("callback", None))
  810. resp['Content-Type'] = "application/json; charset=utf-8"
  811. return resp
  812. elif request.method == "POST":
  813. return jsonResponse({"error": "Not implemented."})
  814. @catch_error_as_json
  815. def link_count_api(request, cat1, cat2):
  816. """
  817. Return a count document with the number of links between every text in cat1 and every text in cat2
  818. """
  819. if request.method == "GET":
  820. resp = jsonResponse(get_link_counts(cat1, cat2))
  821. resp['Access-Control-Allow-Origin'] = '*'
  822. return resp
  823. elif request.method == "POST":
  824. return jsonResponse({"error": "Not implemented."})
  825. @catch_error_as_json
  826. def word_count_api(request, title, version, language):
  827. """
  828. Return a count document with the number of links between every text in cat1 and every text in cat2
  829. """
  830. if request.method == "GET":
  831. counts = VersionSet({"title": title, "versionTitle": version, "language": language}).word_count()
  832. resp = jsonResponse({"wordCount": counts})
  833. return resp
  834. elif request.method == "POST":
  835. return jsonResponse({"error": "Not implemented."})
  836. @catch_error_as_json
  837. def counts_api(request, title):
  838. """
  839. API for retrieving the counts document for a given text node.
  840. :param title: A valid node title
  841. """
  842. title = title.replace("_", " ")
  843. if request.method == "GET":
  844. return jsonResponse(StateNode(title).contents(), callback=request.GET.get("callback", None))
  845. elif request.method == "POST":
  846. if not request.user.is_staff:
  847. return jsonResponse({"error": "Not permitted."})
  848. if "update" in request.GET:
  849. flag = request.GET.get("flag", None)
  850. if not flag:
  851. return jsonResponse({"error": "'flag' parameter missing."})
  852. val = request.GET.get("val", None)
  853. val = True if val == "true" else False
  854. vs = VersionState(title)
  855. if not vs:
  856. raise InputError("State not found for : {}".format(title))
  857. vs.set_flag(flag, val).save()
  858. return jsonResponse({"status": "ok"})
  859. return jsonResponse({"error": "Not implemented."})
  860. @catch_error_as_json
  861. def text_preview_api(request, title):
  862. """
  863. API for retrieving a document that gives preview text (first characters of each section)
  864. for text 'title'
  865. """
  866. oref = Ref(title)
  867. response = oref.index.contents(v2=True)
  868. response['node_title'] = oref.index_node.full_title()
  869. def get_preview(prev_oref):
  870. text = TextFamily(prev_oref, pad=False, commentary=False)
  871. if prev_oref.index_node.depth == 1:
  872. # Give deeper previews for texts with depth 1 (boring to look at otherwise)
  873. text.text, text.he = [[i] for i in text.text], [[i] for i in text.he]
  874. preview = text_preview(text.text, text.he) if (text.text or text.he) else []
  875. return preview if isinstance(preview, list) else [preview]
  876. if not oref.index_node.has_children():
  877. response['preview'] = get_preview(oref)
  878. elif oref.index_node.has_default_child():
  879. r = oref.index_node.get_default_child().ref() # Get ref through ref() to get default leaf node and avoid getting parent node
  880. response['preview'] = get_preview(r)
  881. return jsonResponse(response, callback=request.GET.get("callback", None))
  882. def revarnish_link(link):
  883. if USE_VARNISH:
  884. for ref in link.refs:
  885. invalidate_ref(Ref(ref), purge=True)
  886. @catch_error_as_json
  887. @csrf_exempt
  888. def links_api(request, link_id_or_ref=None):
  889. """
  890. API for textual links.
  891. Currently also handles post notes.
  892. """
  893. #TODO: can we distinguish between a link_id (mongo id) for POSTs and a ref for GETs?
  894. if request.method == "GET":
  895. callback=request.GET.get("callback", None)
  896. if link_id_or_ref is None:
  897. return jsonResponse({"error": "Missing text identifier"}, callback)
  898. #The Ref instanciation is just to validate the Ref and let an error bubble up.
  899. #TODO is there are better way to validate the ref from GET params?
  900. model.Ref(link_id_or_ref)
  901. with_text = int(request.GET.get("with_text", 1))
  902. return jsonResponse(get_links(link_id_or_ref, with_text), callback)
  903. if request.method == "POST":
  904. # delegate according to single/multiple objects posted
  905. j = request.POST.get("json")
  906. if not j:
  907. return jsonResponse({"error": "Missing 'json' parameter in post data."})
  908. j = json.loads(j)
  909. if isinstance(j, list):
  910. #todo: this seems goofy. It's at least a bit more expensive than need be.
  911. res = []
  912. for i in j:
  913. res.append(post_single_link(request, i))
  914. return jsonResponse(res)
  915. else:
  916. return jsonResponse(post_single_link(request, j))
  917. if request.method == "DELETE":
  918. if not link_id_or_ref:
  919. return jsonResponse({"error": "No link id given for deletion."})
  920. return jsonResponse(
  921. tracker.delete(request.user.id, model.Link, link_id_or_ref, callback=revarnish_link)
  922. )
  923. return jsonResponse({"error": "Unsuported HTTP method."})
  924. def post_single_link(request, link):
  925. func = tracker.update if "_id" in link else tracker.add
  926. # use the correct function if params indicate this is a note save
  927. # func = save_note if "type" in j and j["type"] == "note" else save_link
  928. if not request.user.is_authenticated():
  929. key = request.POST.get("apikey")
  930. if not key:
  931. return {"error": "You must be logged in or use an API key to add, edit or delete links."}
  932. apikey = db.apikeys.find_one({"key": key})
  933. if not apikey:
  934. return {"error": "Unrecognized API key."}
  935. obj = func(apikey["uid"], model.Link, link, method="API")
  936. if USE_VARNISH:
  937. revarnish_link(obj)
  938. response = format_object_for_client(obj)
  939. else:
  940. @csrf_protect
  941. def protected_link_post(req):
  942. obj=func(req.user.id, model.Link, link)
  943. if USE_VARNISH:
  944. revarnish_link(obj)
  945. resp = format_object_for_client(obj)
  946. return resp
  947. response = protected_link_post(request)
  948. return response
  949. @catch_error_as_json
  950. @csrf_exempt
  951. def link_summary_api(request, ref):
  952. """
  953. Returns a summary of links available for ref.
  954. """
  955. oref = Ref(ref)
  956. summary = oref.linkset().summary(oref)
  957. return jsonResponse(summary)
  958. @catch_error_as_json
  959. @csrf_exempt
  960. def notes_api(request, note_id_or_ref):
  961. """
  962. API for user notes.
  963. Is this still true? "Currently only handles deleting. Adding and editing are handled throughout the links API."
  964. A called to this API with GET returns the list of public notes and private notes belong to the current user on this Ref.
  965. """
  966. if request.method == "GET":
  967. oref = Ref(note_id_or_ref)
  968. cb = request.GET.get("callback", None)
  969. res = get_notes(oref, uid=request.user.id)
  970. return jsonResponse(res, cb)
  971. if request.method == "POST":
  972. j = request.POST.get("json")
  973. if not j:
  974. return jsonResponse({"error": "Missing 'json' parameter in post data."})
  975. note = json.loads(j)
  976. func = tracker.update if "_id" in note else tracker.add
  977. if not request.user.is_authenticated():
  978. key = request.POST.get("apikey")
  979. if not key:
  980. return jsonResponse({"error": "You must be logged in or use an API key to add, edit or delete links."})
  981. apikey = db.apikeys.find_one({"key": key})
  982. if not apikey:
  983. return jsonResponse({"error": "Unrecognized API key."})
  984. note["owner"] = apikey["uid"]
  985. response = format_object_for_client(
  986. func(apikey["uid"], model.Note, note, method="API")
  987. )
  988. else:
  989. note["owner"] = request.user.id
  990. @csrf_protect
  991. def protected_note_post(req):
  992. resp = format_object_for_client(
  993. func(req.user.id, model.Note, note)
  994. )
  995. return resp
  996. response = protected_note_post(request)
  997. if request.POST.get("layer", None):
  998. layer = Layer().load({"urlkey": request.POST.get("layer")})
  999. if not layer:
  1000. raise InputError("Layer not found.")
  1001. else:
  1002. # Create notifications for this activity
  1003. path = "/" + note["ref"] + "?layer=" + layer.urlkey
  1004. if ObjectId(response["_id"]) not in layer.note_ids:
  1005. # only notify for new notes, not edits
  1006. for uid in layer.listeners():
  1007. if request.user.id == uid:
  1008. continue
  1009. n = Notification({"uid": uid})
  1010. n.make_discuss(adder_id=request.user.id, discussion_path=path)
  1011. n.save()
  1012. layer.add_note(response["_id"])
  1013. layer.save()
  1014. return jsonResponse(response)
  1015. if request.method == "DELETE":
  1016. if not request.user.is_authenticated():
  1017. return jsonResponse({"error": "You must be logged in to delete notes."})
  1018. return jsonResponse(
  1019. tracker.delete(request.user.id, model.Note, note_id_or_ref)
  1020. )
  1021. return jsonResponse({"error": "Unsuported HTTP method."})
  1022. @catch_error_as_json
  1023. def versions_api(request, tref):
  1024. """
  1025. API for retrieving available text versions list of a ref.
  1026. """
  1027. oref = model.Ref(tref)
  1028. versions = model.VersionSet({"title": oref.book})
  1029. results = []
  1030. for v in versions:
  1031. results.append({
  1032. "title": v.versionTitle,
  1033. "source": v.versionSource,
  1034. "langauge": v.language
  1035. })
  1036. return jsonResponse(results, callback=request.GET.get("callback", None))
  1037. @catch_error_as_json
  1038. def set_lock_api(request, tref, lang, version):
  1039. """
  1040. API to set an edit lock on a text segment.
  1041. """
  1042. user = request.user.id if request.user.is_authenticated() else 0
  1043. model.set_lock(model.Ref(tref).normal(), lang, version.replace("_", " "), user)
  1044. return jsonResponse({"status": "ok"})
  1045. @catch_error_as_json
  1046. def release_lock_api(request, tref, lang, version):
  1047. """
  1048. API to release the edit lock on a text segment.
  1049. """
  1050. model.release_lock(model.Ref(tref).normal(), lang, version.replace("_", " "))
  1051. return jsonResponse({"status": "ok"})
  1052. @catch_error_as_json
  1053. def check_lock_api(request, tref, lang, version):
  1054. """
  1055. API to check whether a text segment currently has an edit lock.
  1056. """
  1057. locked = model.check_lock(model.Ref(tref).normal(), lang, version.replace("_", " "))
  1058. return jsonResponse({"locked": locked})
  1059. @catch_error_as_json
  1060. def lock_text_api(request, title, lang, version):
  1061. """
  1062. API for locking or unlocking a text as a whole.
  1063. To unlock, include the URL parameter "action=unlock"
  1064. """
  1065. if not request.user.is_staff:
  1066. return {"error": "Only Sefaria Moderators can lock texts."}
  1067. title = title.replace("_", " ")
  1068. version = version.replace("_", " ")
  1069. vobj = Version().load({"title": title, "language": lang, "versionTitle": version})
  1070. if request.GET.get("action", None) == "unlock":
  1071. vobj.status = None
  1072. else:
  1073. vobj.status = "locked"
  1074. vobj.save()
  1075. return jsonResponse({"status": "ok"})
  1076. @catch_error_as_json
  1077. @csrf_exempt
  1078. def flag_text_api(request, title, lang, version):
  1079. """
  1080. API for locking or unlocking a text as a whole.
  1081. To unlock, include the URL parameter "action=unlock"
  1082. """
  1083. if not request.user.is_authenticated():
  1084. key = request.POST.get("apikey")
  1085. if not key:
  1086. return jsonResponse({"error": "You must be logged in or use an API key to perform this action."})
  1087. apikey = db.apikeys.find_one({"key": key})
  1088. if not apikey:
  1089. return jsonResponse({"error": "Unrecognized API key."})
  1090. user = User.objects.get(id=apikey["uid"])
  1091. if not user.is_staff:
  1092. return jsonResponse({"error": "Only Sefaria Moderators can flag texts."})
  1093. flags = json.loads(request.POST.get("json"))
  1094. title = title.replace("_", " ")
  1095. version = version.replace("_", " ")
  1096. vobj = Version().load({"title": title, "language": lang, "versionTitle": version})
  1097. for flag in vobj.optional_attrs:
  1098. if flag in flags:
  1099. setattr(vobj, flag, flags[flag])
  1100. vobj.save()
  1101. return jsonResponse({"status": "ok"})
  1102. elif request.user.is_staff:
  1103. @csrf_protect
  1104. def protected_post(request):
  1105. flags = json.loads(request.POST.get("json"))
  1106. title = title.replace("_", " ")
  1107. version = version.replace("_", " ")
  1108. vobj = Version().load({"title": title, "language": lang, "versionTitle": version})
  1109. for flag in vobj.optional_attrs:
  1110. if flag in flags:
  1111. setattr(vobj, flag, flags[flag])
  1112. vobj.save()
  1113. return jsonResponse({"status": "ok"})
  1114. return protected_post(request)
  1115. else:
  1116. return jsonResponse({"error": "Unauthorized"})
  1117. @catch_error_as_json
  1118. def dictionary_api(request, word):
  1119. lookup_ref=request.GET.get("lookup_ref", None)
  1120. wform_pkey = 'form'
  1121. if is_hebrew(word):
  1122. word = strip_cantillation(word)
  1123. if not has_cantillation(word, detect_vowels=True):
  1124. wform_pkey = 'c_form'
  1125. query_obj = {wform_pkey: word}
  1126. if lookup_ref:
  1127. nref = Ref(lookup_ref).normal()
  1128. query_obj["refs"] = {'$regex': '^{}'.format(nref)}
  1129. form = WordForm().load(query_obj)
  1130. if not form:
  1131. del query_obj["refs"]
  1132. form = WordForm().load(query_obj)
  1133. if form:
  1134. result = []
  1135. for lookup in form.lookups:
  1136. #TODO: if we want the 'lookups' in wf to be a dict we can pass as is to the lexiconentry, we need to change the key 'lexicon' to 'parent_lxicon' in word forms
  1137. ls = LexiconEntrySet({'headword': lookup['headword']})
  1138. for l in ls:
  1139. result.append(l.contents())
  1140. return jsonResponse(result)
  1141. else:
  1142. return jsonResponse({"error": "No information found for given word."})
  1143. @catch_error_as_json
  1144. def notifications_api(request):
  1145. """
  1146. API for retrieving user notifications.
  1147. """
  1148. if not request.user.is_authenticated():
  1149. return jsonResponse({"error": "You must be logged in to access your notifications."})
  1150. page = int(request.GET.get("page", 1))
  1151. page_size = int(request.GET.get("page_size", 10))
  1152. notifications = NotificationSet().recent_for_user(request.user.id, limit=page_size, page=page)
  1153. return jsonResponse({
  1154. "html": notifications.to_HTML(),
  1155. "page": page,
  1156. "page_size": page_size,
  1157. "count": notifications.count()
  1158. })
  1159. @catch_error_as_json
  1160. def notifications_read_api(request):
  1161. """
  1162. API for marking notifications as read
  1163. Takes JSON in the "notifications" parameter of an array of
  1164. notifcation ids as strings.
  1165. """
  1166. if request.method == "POST":
  1167. notifications = request.POST.get("notifications")
  1168. if not notifications:
  1169. return jsonResponse({"error": "'notifications' post parameter missing."})
  1170. notifications = json.loads(notifications)
  1171. for id in notifications:
  1172. notification = Notification().load_by_id(id)
  1173. if notification.uid != request.user.id:
  1174. # Only allow expiring your own notifications
  1175. continue
  1176. notification.mark_read().save()
  1177. return jsonResponse({"status": "ok"})
  1178. else:
  1179. return jsonResponse({"error": "Unsupported HTTP method."})
  1180. @catch_error_as_json
  1181. def messages_api(request):
  1182. """
  1183. API for posting user to user messages
  1184. """
  1185. if not request.user.is_authenticated():
  1186. return jsonResponse({"error": "You must be logged in to access your messages."})
  1187. if request.method == "POST":
  1188. j = request.POST.get("json")
  1189. if not j:
  1190. return jsonResponse({"error": "No post JSON."})
  1191. j = json.loads(j)
  1192. Notification({"uid": j["recipient"]}).make_message(sender_id=request.user.id, message=j["message"]).save()
  1193. return jsonResponse({"status": "ok"})
  1194. elif request.method == "GET":
  1195. return jsonResponse({"error": "Unsupported HTTP method."})
  1196. @catch_error_as_json
  1197. def follow_api(request, action, uid):
  1198. """
  1199. API for following and unfollowing another user.
  1200. """
  1201. if request.method != "POST":
  1202. return jsonResponse({"error": "Unsupported HTTP method."})
  1203. if not request.user.is_authenticated():
  1204. return jsonResponse({"error": "You must be logged in to follow."})
  1205. follow = FollowRelationship(follower=request.user.id, followee=int(uid))
  1206. if action == "follow":
  1207. follow.follow()
  1208. elif action == "unfollow":
  1209. follow.unfollow()
  1210. return jsonResponse({"status": "ok"})
  1211. @catch_error_as_json
  1212. def follow_list_api(request, kind, uid):
  1213. """
  1214. API for retrieving a list of followers/followees for a given user.
  1215. """
  1216. if kind == "followers":
  1217. f = FollowersSet(int(uid))
  1218. elif kind == "followees":
  1219. f = FolloweesSet(int(uid))
  1220. return jsonResponse(annotate_user_list(f.uids))
  1221. @catch_error_as_json
  1222. def texts_history_api(request, tref, lang=None, version=None):
  1223. """
  1224. API for retrieving history information about a given text.
  1225. """
  1226. if request.method != "GET":
  1227. return jsonResponse({"error": "Unsuported HTTP method."})
  1228. tref = model.Ref(tref).normal()
  1229. refRe = '^%s$|^%s:' % (tref, tref)
  1230. if lang and version:
  1231. query = {"ref": {"$regex": refRe }, "language": lang, "version": version.replace("_", " ")}
  1232. else:
  1233. query = {"ref": {"$regex": refRe }}
  1234. history = db.history.find(query)
  1235. summary = {"copiers": Set(), "translators": Set(), "editors": Set(), "reviewers": Set() }
  1236. updated = history[0]["date"].isoformat() if history.count() else "Unknown"
  1237. for act in history:
  1238. if act["rev_type"].startswith("edit"):
  1239. summary["editors"].update([act["user"]])
  1240. elif act["rev_type"] == "review":
  1241. summary["reviewers"].update([act["user"]])
  1242. elif act["version"] == "Sefaria Community Translation":
  1243. summary["translators"].update([act["user"]])
  1244. else:
  1245. summary["copiers"].update([act["user"]])
  1246. # Don't list copiers and translators as editors as well
  1247. summary["editors"].difference_update(summary["copiers"])
  1248. summary["editors"].difference_update(summary["translators"])
  1249. for group in summary:
  1250. uids = list(summary[group])
  1251. names = []
  1252. for uid in uids:
  1253. try:
  1254. user = User.objects.get(id=uid)
  1255. name = "%s %s" % (user.first_name, user.last_name)
  1256. link = user_link(uid)
  1257. except User.DoesNotExist:
  1258. name = "Someone"
  1259. link = user_link(-1)
  1260. u = {
  1261. 'name': name,
  1262. 'link': link
  1263. }
  1264. names.append(u)
  1265. summary[group] = names
  1266. summary["lastUpdated"] = updated
  1267. return jsonResponse(summary)
  1268. @catch_error_as_json
  1269. def reviews_api(request, tref=None, lang=None, version=None, review_id=None):
  1270. if request.method == "GET":
  1271. callback=request.GET.get("callback", None)
  1272. if tref and lang and version:
  1273. nref = model.Ref(tref).normal()
  1274. version = version.replace("_", " ")
  1275. reviews = get_reviews(nref, lang, version)
  1276. last_edit = get_last_edit_date(nref, lang, version)
  1277. score_since_last_edit = get_review_score_since_last_edit(nref, lang, version, reviews=reviews, last_edit=last_edit)
  1278. for r in reviews:
  1279. r["date"] = r["date"].isoformat()
  1280. response = {
  1281. "ref": nref,
  1282. "lang": lang,
  1283. "version": version,
  1284. "reviews": reviews,
  1285. "reviewCount": len(reviews),
  1286. "scoreSinceLastEdit": score_since_last_edit,
  1287. "lastEdit": last_edit.isoformat() if last_edit else None,
  1288. }
  1289. elif review_id:
  1290. response = {}
  1291. return jsonResponse(response, callback)
  1292. elif request.method == "POST":
  1293. if not request.user.is_authenticated():
  1294. return jsonResponse({"error": "You must be logged in to write reviews."})
  1295. j = request.POST.get("json")
  1296. if not j:
  1297. return jsonResponse({"error": "No post JSON."})
  1298. j = json.loads(j)
  1299. response = save_review(j, request.user.id)
  1300. return jsonResponse(response)
  1301. elif request.method == "DELETE":
  1302. if not review_id:
  1303. return jsonResponse({"error": "No review ID given for deletion."})
  1304. return jsonResponse(delete_review(review_id, request.user.id))
  1305. else:
  1306. return jsonResponse({"error": "Unsuported HTTP method."})
  1307. @ensure_csrf_cookie
  1308. def global_activity(request, page=1):
  1309. """
  1310. Recent Activity page listing all recent actions and contributor leaderboards.
  1311. """
  1312. page = int(page)
  1313. page_size = 100
  1314. if page > 40:
  1315. generic_response = { "title": "Activity Unavailable", "content": "You have requested a page deep in Sefaria's history.<br><br>For performance reasons, this page is unavailable. If you need access to this information, please <a href='mailto:dev@sefaria.org'>email us</a>." }
  1316. return render_to_response('static/generic.html', generic_response, RequestContext(request))
  1317. if "api" in request.GET:
  1318. q = {}
  1319. else:
  1320. q = {"method": {"$ne": "API"}}
  1321. filter_type = request.GET.get("type", None)
  1322. activity, page = get_maximal_collapsed_activity(query=q, page_size=page_size, page=page, filter_type=filter_type)
  1323. next_page = page + 1 if page else None
  1324. next_page = "/activity/%d" % next_page if next_page else None
  1325. next_page = "%s?type=%s" % (next_page, filter_type) if next_page and filter_type else next_page
  1326. email = request.user.email if request.user.is_authenticated() else False
  1327. return render_to_response('activity.html',
  1328. {'activity': activity,
  1329. 'filter_type': filter_type,
  1330. 'leaders': top_contributors(),
  1331. 'leaders30': top_contributors(30),
  1332. 'leaders7': top_contributors(7),
  1333. 'leaders1': top_contributors(1),
  1334. 'email': email,
  1335. 'next_page': next_page,
  1336. },
  1337. RequestContext(request))
  1338. @ensure_csrf_cookie
  1339. def segment_history(request, tref, lang, version):
  1340. """
  1341. View revision history for the text segment named by ref / lang / version.
  1342. """
  1343. try:
  1344. oref = model.Ref(tref)
  1345. except InputError:
  1346. raise Http404
  1347. nref = oref.normal()
  1348. version = version.replace("_", " ")
  1349. if not Version().load({"title":oref.index.title, "versionTitle":version, "language":lang}):
  1350. raise Http404(u"We do not have a version of {} called '{}'. Please use the menu to find the text you are looking for.".format(oref.index.title, version))
  1351. filter_type = request.GET.get("type", None)
  1352. history = text_history(oref, version, lang, filter_type=filter_type)
  1353. email = request.user.email if request.user.is_authenticated() else False
  1354. return render_to_response('activity.html',
  1355. {'activity': history,
  1356. "single": True,
  1357. "ref": nref,
  1358. "lang": lang,
  1359. "version": version,
  1360. 'email': email,
  1361. 'filter_type': filter_type,
  1362. },
  1363. RequestContext(request))
  1364. @catch_error_as_json
  1365. def revert_api(request, tref, lang, version, revision):
  1366. """
  1367. API for reverting a text segment to a previous revision.
  1368. """
  1369. if not request.user.is_authenticated():
  1370. return jsonResponse({"error": "You must be logged in to revert changes."})
  1371. if request.method != "POST":
  1372. return jsonResponse({"error": "Unsupported HTTP method."})
  1373. revision = int(revision)
  1374. version = version.replace("_", " ")
  1375. oref = model.Ref(tref)
  1376. new_text = text_at_revision(oref.normal(), version, lang, revision)
  1377. tracker.modify_text(request.user.id, oref, version, lang, new_text, type="revert")
  1378. return jsonResponse({"status": "ok"})
  1379. @ensure_csrf_cookie
  1380. def user_profile(request, username, page=1):
  1381. """
  1382. User's profile page.
  1383. """
  1384. try:
  1385. profile = UserProfile(slug=username)
  1386. except Exception, e:
  1387. # Couldn't find by slug, try looking up by username (old style urls)
  1388. # If found, redirect to new URL
  1389. # If we no longer want to support the old URLs, we can remove this
  1390. user = get_object_or_404(User, username=username)
  1391. profile = UserProfile(id=user.id)
  1392. return redirect("/profile/%s" % profile.slug, permanent=True)
  1393. following = profile.followed_by(request.user.id) if request.user.is_authenticated() else False
  1394. page_size = 20
  1395. page = int(page) if page else 1
  1396. if page > 40:
  1397. generic_response = { "title": "Activity Unavailable", "content": "You have requested a page deep in Sefaria's history.<br><br>For performance reasons, this page is unavailable. If you need access to this information, please <a href='mailto:dev@sefaria.org'>email us</a>." }
  1398. return render_to_response('static/generic.html', generic_response, RequestContext(request))
  1399. query = {"user": profile.id}
  1400. filter_type = request.GET["type"] if "type" in request.GET else None
  1401. activity, apage= get_maximal_collapsed_activity(query=query, page_size=page_size, page=page, filter_type=filter_type)
  1402. notes, npage = get_maximal_collapsed_activity(query=query, page_size=page_size, page=page, filter_type="add_note")
  1403. contributed = activity[0]["date"] if activity else None
  1404. scores = db.leaders_alltime.find_one({"_id": profile.id})
  1405. score = int(scores["count"]) if scores else 0
  1406. user_texts = scores.get("texts", None) if scores else None
  1407. sheets = db.sheets.find({"owner": profile.id, "status": "public"}, {"id": 1, "datePublished": 1}).sort([["datePublished", -1]])
  1408. next_page = apage + 1 if apage else None
  1409. next_page = "/profile/%s/%d" % (username, next_page) if next_page else None
  1410. return render_to_response("profile.html",
  1411. {
  1412. 'profile': profile,
  1413. 'following': following,
  1414. 'activity': activity,
  1415. 'sheets': sheets,
  1416. 'notes': notes,
  1417. 'joined': profile.date_joined,
  1418. 'contributed': contributed,
  1419. 'score': score,
  1420. 'scores': scores,
  1421. 'user_texts': user_texts,
  1422. 'filter_type': filter_type,
  1423. 'next_page': next_page,
  1424. "single": False,
  1425. },
  1426. RequestContext(request))
  1427. @catch_error_as_json
  1428. def profile_api(request):
  1429. """
  1430. API for user profiles.
  1431. """
  1432. if not request.user.is_authenticated():
  1433. return jsonResponse({"error": "You must be logged in to update your profile."})
  1434. if request.method == "POST":
  1435. profileJSON = request.POST.get("json")
  1436. if not profileJSON:
  1437. return jsonResponse({"error": "No post JSON."})
  1438. profileUpdate = json.loads(profileJSON)
  1439. profile = UserProfile(id=request.user.id)
  1440. profile.update(profileUpdate)
  1441. error = profile.errors()
  1442. #TODO: should validation not need to be called manually? maybe inside the save
  1443. if error:
  1444. return jsonResponse({"error": error})
  1445. else:
  1446. profile.save()
  1447. return jsonResponse(profile.to_DICT())
  1448. return jsonResponse({"error": "Unsupported HTTP method."})
  1449. def profile_redirect(request, username, page=1):
  1450. """
  1451. Redirect to a user profile
  1452. """
  1453. return redirect("/profile/%s" % username, permanent=True)
  1454. @login_required
  1455. def my_profile(request):
  1456. """"
  1457. Redirect to the profile of the logged in user.
  1458. """
  1459. return redirect("/profile/%s" % UserProfile(id=request.user.id).slug)
  1460. @login_required
  1461. @ensure_csrf_cookie
  1462. def edit_profile(request):
  1463. """
  1464. Page for managing a user's account settings.
  1465. """
  1466. profile = UserProfile(id=request.user.id)
  1467. sheets = db.sheets.find({"owner": profile.id, "status": "public"}, {"id": 1, "datePublished": 1}).sort([["datePublished", -1]])
  1468. return render_to_response('edit_profile.html',
  1469. {
  1470. 'user': request.user,
  1471. 'profile': profile,
  1472. 'sheets': sheets,
  1473. },
  1474. RequestContext(request))
  1475. @login_required
  1476. @ensure_csrf_cookie
  1477. def account_settings(request):
  1478. """
  1479. Page for managing a user's account settings.
  1480. """
  1481. profile = UserProfile(id=request.user.id)
  1482. return render_to_response('account_settings.html',
  1483. {
  1484. 'user': request.user,
  1485. 'profile': profile,
  1486. },
  1487. RequestContext(request))
  1488. @ensure_csrf_cookie
  1489. def home(request):
  1490. """
  1491. Homepage
  1492. """
  1493. if request.flavour == "mobile":
  1494. return s2_page(request, "home")
  1495. today = date.today()
  1496. daf_today = sefaria.utils.calendars.daf_yomi(today)
  1497. daf_tomorrow = sefaria.utils.calendars.daf_yomi(today + timedelta(1))
  1498. parasha = sefaria.utils.calendars.this_weeks_parasha(datetime.now())
  1499. p929_chapter = p929.Perek(date = today)
  1500. p929_ref = "%s %s" % (p929_chapter.book_name, p929_chapter.book_chapter)
  1501. metrics = db.metrics.find().sort("timestamp", -1).limit(1)[0]
  1502. return render_to_response('static/home.html',
  1503. {
  1504. "metrics": metrics,
  1505. "daf_today": daf_today,
  1506. "daf_tomorrow": daf_tomorrow,
  1507. "parasha": parasha,
  1508. "p929": p929_ref,
  1509. },
  1510. RequestContext(request))
  1511. @ensure_csrf_cookie
  1512. def discussions(request):
  1513. """
  1514. Discussions page.
  1515. """
  1516. discussions = LayerSet({"owner": request.user.id})
  1517. return render_to_response('discussions.html',
  1518. {
  1519. "discussions": discussions,
  1520. },
  1521. RequestContext(request))
  1522. @catch_error_as_json
  1523. def new_discussion_api(request):
  1524. """
  1525. API for user profiles.
  1526. """
  1527. if not request.user.is_authenticated():
  1528. return jsonResponse({"error": "You must be logged in to start a discussion."})
  1529. if request.method == "POST":
  1530. import uuid
  1531. attempts = 10
  1532. while attempts > 0:
  1533. key = str(uuid.uuid4())[:8]
  1534. if LayerSet({"urlkey": key}).count() > 0:
  1535. attempts -= 1
  1536. continue
  1537. discussion = Layer({
  1538. "urlkey": key,
  1539. "owner": request.user.id,
  1540. })
  1541. discussion.save()
  1542. return jsonResponse(discussion.contents())
  1543. return jsonResponse({"error": "An extremely unlikley event has occurred."})
  1544. return jsonResponse({"error": "Unsupported HTTP method."})
  1545. @ensure_csrf_cookie
  1546. def dashboard(request):
  1547. """
  1548. Dashboard page -- table view of all content
  1549. """
  1550. states = VersionStateSet(
  1551. {},
  1552. proj={"title": 1, "flags": 1, "linksCount": 1, "content._en.percentAvailable": 1, "content._he.percentAvailable": 1}
  1553. ).array()
  1554. toc = get_toc()
  1555. flat_toc = flatten_toc(toc)
  1556. def toc_sort(a):
  1557. try:
  1558. return flat_toc.index(a["title"])
  1559. except:
  1560. return 9999
  1561. states = sorted(states, key=toc_sort)
  1562. return render_to_response('dashboard.html',
  1563. {
  1564. "states": states,
  1565. },
  1566. RequestContext(request))
  1567. @ensure_csrf_cookie
  1568. def translation_requests(request, completed_only=False, featured_only=False):
  1569. """
  1570. Page listing all outstnading translation requests.
  1571. """
  1572. page = int(request.GET.get("page", 1)) - 1
  1573. page_size = 100
  1574. query = {"completed": False, "section_level": False} if not completed_only else {"completed": True}
  1575. query = {"completed": True, "featured": True} if completed_only and featured_only else query
  1576. requests = TranslationRequestSet(query, limit=page_size, page=page, sort=[["request_count", -1]])
  1577. request_count = TranslationRequestSet({"completed": False, "section_level": False}).count()
  1578. complete_count = TranslationRequestSet({"completed": True}).count()
  1579. featured_complete = TranslationRequestSet({"completed": True, "featured": True}).count()
  1580. next_page = page + 2 if True or requests.count() == page_size else 0
  1581. featured_query = {"featured": True, "featured_until": { "$gt": datetime.now() } }
  1582. featured = TranslationRequestSet(featured_query, sort=[["completed", 1], ["featured_until", 1]])
  1583. today = datetime.today()
  1584. featured_end = today + timedelta(7 - ((today.weekday()+1) % 7)) # This coming Sunday
  1585. featured_end = featured_end.replace(hour=0, minute=0) # At midnight
  1586. current = [d.featured_until <= featured_end for d in featured]
  1587. featured_current = sum(current)
  1588. show_featured = not completed_only and not page and ((request.user.is_staff and featured.count()) or (featured_current))
  1589. return render_to_response('translation_requests.html',
  1590. {
  1591. "featured": featured,
  1592. "featured_current": featured_current,
  1593. "show_featured": show_featured,
  1594. "requests": requests,
  1595. "request_count": request_count,
  1596. "completed_only": completed_only,
  1597. "complete_count": complete_count,
  1598. "featured_complete": featured_complete,
  1599. "featured_only": featured_only,
  1600. "next_page": next_page,
  1601. "page_offset": page * page_size
  1602. },
  1603. RequestContext(request))
  1604. def completed_translation_requests(request):
  1605. """
  1606. Wrapper for listing completed translations requests.
  1607. """
  1608. return translation_requests(request, completed_only=True)
  1609. def completed_featured_translation_requests(request):
  1610. """
  1611. Wrapper for listing completed translations requests.
  1612. """
  1613. return translation_requests(request, completed_only=True, featured_only=True)
  1614. @catch_error_as_json
  1615. def translation_request_api(request, tref):
  1616. """
  1617. API for requesting a text segment for translation.
  1618. """
  1619. if not request.user.is_authenticated():
  1620. return jsonResponse({"error": "You must be logged in to request a translation."})
  1621. oref = Ref(tref)
  1622. ref = oref.normal()
  1623. if "unrequest" in request.POST:
  1624. TranslationRequest.remove_request(ref, request.user.id)
  1625. response = {"status": "ok"}
  1626. elif "feature" in request.POST:
  1627. if not request.user.is_staff:
  1628. response = {"error": "Only admins can feature requests."}
  1629. else:
  1630. tr = TranslationRequest().load({"ref": ref})
  1631. tr.featured = True
  1632. tr.featured_until = dateutil.parser.parse(request.POST.get("feature"))
  1633. tr.save()
  1634. response = {"status": "ok"}
  1635. elif "unfeature" in request.POST:
  1636. if not request.user.is_staff:
  1637. response = {"error": "Only admins can unfeature requests."}
  1638. else:
  1639. tr = TranslationRequest().load({"ref": ref})
  1640. tr.featured = False
  1641. tr.featured_until = None
  1642. tr.save()
  1643. response = {"status": "ok"}
  1644. else:
  1645. if oref.is_text_translated():
  1646. response = {"error": "Sefaria already has a translation for %s." % ref}
  1647. else:
  1648. tr = TranslationRequest.make_request(ref, request.user.id)
  1649. response = tr.contents()
  1650. return jsonResponse(response)
  1651. @ensure_csrf_cookie
  1652. def translation_flow(request, tref):
  1653. """
  1654. Assign a user a paritcular bit of text to translate within 'ref',
  1655. either a text title or category.
  1656. """
  1657. tref = tref.replace("_", " ")
  1658. generic_response = { "title": "Help Translate %s" % tref, "content": "" }
  1659. categories = model.library.get_text_categories()
  1660. next_text = None
  1661. next_section = None
  1662. # expire old locks before checking for a currently unlocked text
  1663. model.expire_locks()
  1664. try:
  1665. oref = model.Ref(tref)
  1666. except InputError:
  1667. oref = False
  1668. if oref and len(oref.sections) == 0:
  1669. # tref is an exact text Title
  1670. # normalize URL
  1671. if request.path != "/translate/%s" % oref.url():
  1672. return redirect("/translate/%s" % oref.url(), permanent=True)
  1673. # Check for completion
  1674. if oref.get_state_node().get_percent_available("en") == 100:
  1675. generic_response["content"] = "<h3>Sefaria now has a complete translation of %s</h3>But you can still contribute in other ways.</h3> <a href='/contribute'>Learn More.</a>" % tref
  1676. return render_to_response('static/generic.html', generic_response, RequestContext(request))
  1677. if "random" in request.GET:
  1678. # choose a ref from a random section within this text
  1679. if "skip" in request.GET:
  1680. if oref.is_talmud():
  1681. skip = int(daf_to_section(request.GET.get("skip")))
  1682. else:
  1683. skip = int(request.GET.get("skip"))
  1684. else:
  1685. skip = None
  1686. assigned_ref = random_untranslated_ref_in_text(oref.normal(), skip=skip)
  1687. if assigned_ref:
  1688. next_section = model.Ref(assigned_ref).padded_ref().sections[0]
  1689. elif "section" in request.GET:
  1690. # choose the next ref within the specified section
  1691. next_section = int(request.GET["section"])
  1692. assigned_ref = next_untranslated_ref_in_text(oref.normal(), section=next_section)
  1693. else:
  1694. # choose the next ref in this text in order
  1695. assigned_ref = next_untranslated_ref_in_text(oref.normal())
  1696. if not assigned_ref:
  1697. generic_response["content"] = "All remaining sections in %s are being worked on by other contributors. Work on <a href='/translate/%s'>another text</a> for now." % (oref.normal(), tref)
  1698. return render_to_response('static/generic.html', generic_response, RequestContext(request))
  1699. elif oref and len(oref.sections) > 0:
  1700. # ref is a citation to a particular location in a text
  1701. # for now, send this to the edit_text view
  1702. return edit_text(request, tref)
  1703. elif tref in categories: #todo: Fix me to work with Version State!
  1704. # ref is a text Category
  1705. raise InputError("This function is under repair. Our Apologies.")
  1706. '''
  1707. cat = tref
  1708. # Check for completion
  1709. if get_percent_available(cat) == 100:
  1710. generic_response["content"] = "<h3>Sefaria now has a complete translation of %s</h3>But you can still contribute in other ways.</h3> <a href='/contribute'>Learn More.</a>" % tref
  1711. return render_to_response('static/generic.html', generic_response, RequestContext(request))
  1712. if "random" in request.GET:
  1713. # choose a random text from this cateogory
  1714. skip = int(request.GET.get("skip")) if "skip" in request.GET else None
  1715. text = random_untranslated_text_in_category(cat, skip=skip)
  1716. assigned_ref = next_untranslated_ref_in_text(text)
  1717. next_text = text
  1718. elif "text" in request.GET:
  1719. # choose the next text requested in URL
  1720. oref = model.Ref(request.GET["text"])
  1721. text = oref.normal()
  1722. next_text = text
  1723. if oref.get_state_node().get_percent_available("en") == 100:
  1724. generic_response["content"] = "%s is complete! Work on <a href='/translate/%s'>another text</a>." % (text, tref)
  1725. return render_to_response('static/generic.html', generic_response, RequestContext(request))
  1726. try:
  1727. assigned_ref = next_untranslated_ref_in_text(text)
  1728. except InputError:
  1729. generic_response["content"] = "All remaining sections in %s are being worked on by other contributors. Work on <a href='/translate/%s'>another text</a> for now." % (text, tref)
  1730. return render_to_response('static/generic.html', generic_response, RequestContext(request))
  1731. else:
  1732. # choose the next text in order
  1733. skip = 0
  1734. success = 0
  1735. # TODO -- need an escape valve here
  1736. while not success:
  1737. try:
  1738. text = next_untranslated_text_in_category(cat, skip=skip)
  1739. assigned_ref = next_untranslated_ref_in_text(text)
  1740. skip += 1
  1741. except InputError:
  1742. pass
  1743. else:
  1744. success = 1
  1745. '''
  1746. else:
  1747. # we don't know what this is
  1748. generic_response["content"] = "<b>%s</b> isn't a known text or category.<br>But you can still contribute in other ways.</h3> <a href='/contribute'>Learn More.</a>" % (tref)
  1749. return render_to_response('static/generic.html', generic_response, RequestContext(request))
  1750. # get the assigned text
  1751. assigned = TextFamily(Ref(assigned_ref), context=0, commentary=False).contents()
  1752. # Put a lock on this assignment
  1753. user = request.user.id if request.user.is_authenticated() else 0
  1754. model.set_lock(assigned_ref, "en", "Sefaria Community Translation", user)
  1755. # if the assigned text is actually empty, run this request again
  1756. # but leave the new lock in place to skip over it
  1757. if "he" not in assigned or not len(assigned["he"]):
  1758. return translation_flow(request, tref)
  1759. # get percentage and remaining counts
  1760. # percent = get_percent_available(assigned["book"])
  1761. translated = StateNode(assigned["book"]).get_translated_count_by_unit(assigned["sectionNames"][-1])
  1762. remaining = StateNode(assigned["book"]).get_untranslated_count_by_unit(assigned["sectionNames"][-1])
  1763. percent = 100 * translated / float(translated + remaining)
  1764. return render_to_response('translate_campaign.html',
  1765. {"title": "Help Translate %s" % tref,
  1766. "base_ref": tref,
  1767. "assigned_ref": assigned_ref,
  1768. "assigned_ref_url": model.Ref(assigned_ref).url(),
  1769. "assigned_text": assigned["he"],
  1770. "assigned_segment_name": assigned["sectionNames"][-1],
  1771. "assigned": assigned,
  1772. "translated": translated,
  1773. "remaining": remaining,
  1774. "percent": percent,
  1775. "thanks": "thank" in request.GET,
  1776. "random_param": "&skip={}".format(assigned["sections"][0]) if request.GET.get("random") else "",
  1777. "next_text": next_text,
  1778. "next_section": next_section,
  1779. },
  1780. RequestContext(request))
  1781. @ensure_csrf_cookie
  1782. def contest_splash(request, slug):
  1783. """
  1784. Splash page for contest.
  1785. Example of adding a contest record to the DB:
  1786. db.contests.save({
  1787. "contest_start" : datetime.strptime("3/5/14", "%m/%d/%y"),
  1788. "contest_end" : datetime.strptime("3/26/14", "%m/%d/%y"),
  1789. "version" : "Sefaria Community Translation",
  1790. "ref_regex" : "^Shulchan Arukh, Even HaEzer ",
  1791. "assignment_url" : "/translate/Shulchan_Arukh,_Even_HaEzer",
  1792. "title" : "Translate Shulchan Arukh, Even HaEzer",
  1793. "slug" : "shulchan-arukh-even-haezer"
  1794. })
  1795. """
  1796. settings = db.contests.find_one({"slug": slug})
  1797. if not settings:
  1798. raise Http404
  1799. settings["copy_template"] = "static/contest/%s.html" % settings["slug"]
  1800. leaderboard_condition = make_leaderboard_condition( start = settings["contest_start"],
  1801. end = settings["contest_end"],
  1802. version = settings["version"],
  1803. ref_regex = settings["ref_regex"])
  1804. now = datetime.now()
  1805. if now < settings["contest_start"]:
  1806. settings["phase"] = "pre"
  1807. settings["leaderboard"] = None
  1808. settings["time_to_start"] = td_format(settings["contest_start"] - now)
  1809. elif settings["contest_start"] < now < settings["contest_end"]:
  1810. settings["phase"] = "active"
  1811. settings["leaderboard_title"] = "Current Leaders"
  1812. settings["leaderboard"] = make_leaderboard(leaderboard_condition)
  1813. settings["time_to_end"] = td_format(settings["contest_end"] - now)
  1814. elif settings["contest_end"] < now:
  1815. settings["phase"] = "post"
  1816. settings["leaderboard_title"] = "Contest Leaders (Unreviewed)"
  1817. settings["leaderboard"] = make_leaderboard(leaderboard_condition)
  1818. return render_to_response("contest_splash.html",
  1819. settings,
  1820. RequestContext(request))
  1821. @ensure_csrf_cookie
  1822. def metrics(request):
  1823. """
  1824. Metrics page. Shows graphs of core metrics.
  1825. """
  1826. metrics = db.metrics.find().sort("timestamp", 1)
  1827. metrics_json = dumps(metrics)
  1828. return render_to_response('metrics.html',
  1829. {
  1830. "metrics_json": metrics_json,
  1831. },
  1832. RequestContext(request))
  1833. @ensure_csrf_cookie
  1834. def digitized_by_sefaria(request):
  1835. """
  1836. Metrics page. Shows graphs of core metrics.
  1837. """
  1838. texts = VersionSet({"digitizedBySefaria": True}, sort=[["title", 1]])
  1839. return render_to_response('static/digitized-by-sefaria.html',
  1840. {
  1841. "texts": texts,
  1842. },
  1843. RequestContext(request))
  1844. def random_ref():
  1845. """
  1846. Returns a valid random ref within the Sefaria library.
  1847. """
  1848. # refs = library.ref_list()
  1849. # ref = choice(refs)
  1850. # picking by text first biases towards short texts
  1851. text = choice(VersionSet().distinct("title"))
  1852. try:
  1853. # ref = choice(VersionStateSet({"title": text}).all_refs()) # check for orphaned texts
  1854. ref = Ref(text).normal()
  1855. except Exception:
  1856. return random_ref()
  1857. return ref
  1858. def random_redirect(request):
  1859. """
  1860. Redirect to a random text page.
  1861. """
  1862. response = redirect(iri_to_uri("/" + random_ref()), permanent=False)
  1863. return response
  1864. def random_text_page(request):
  1865. """
  1866. Page for generating random texts.
  1867. """
  1868. return render_to_response('random.html', {}, RequestContext(request))
  1869. def random_text_api(request):
  1870. """
  1871. Return Texts API data for a random ref.
  1872. """
  1873. response = redirect(iri_to_uri("/api/texts/" + random_ref()) + "?commentary=0", permanent=False)
  1874. return response
  1875. @ensure_csrf_cookie
  1876. def serve_static(request, page):
  1877. """
  1878. Serve a static page whose template matches the URL
  1879. """
  1880. return render_to_response('static/%s.html' % page, {}, RequestContext(request))
  1881. @ensure_csrf_cookie
  1882. def explore(request, book1, book2, lang=None):
  1883. """
  1884. Serve the explorer, with the provided deep linked books
  1885. """
  1886. books = []
  1887. for book in [book1, book2]:
  1888. if book:
  1889. books.append(book)
  1890. template_vars = {"books": json.dumps(books)}
  1891. if lang == "he": # Override language settings if 'he' is in URL
  1892. template_vars["contentLang"] = "hebrew"
  1893. return render_to_response('explore.html', template_vars, RequestContext(request))
  1894. def person_page(request, name):
  1895. person = Person().load({"key": name})
  1896. if not person:
  1897. raise Http404
  1898. assert isinstance(person, Person)
  1899. template_vars = person.contents()
  1900. template_vars["primary_name"] = {
  1901. "en": person.primary_name("en"),
  1902. "he": person.primary_name("he")
  1903. }
  1904. template_vars["secondary_names"] = {
  1905. "en": person.secondary_names("en"),
  1906. "he": person.secondary_names("he")
  1907. }
  1908. template_vars["time_period_name"] = {
  1909. "en": person.mostAccurateTimePeriod().primary_name("en"),
  1910. "he": person.mostAccurateTimePeriod().primary_name("he")
  1911. }
  1912. template_vars["time_period"] = {
  1913. "en": person.mostAccurateTimePeriod().period_string("en"),
  1914. "he": person.mostAccurateTimePeriod().period_string("he")
  1915. }
  1916. template_vars["relationships"] = person.get_grouped_relationships()
  1917. template_vars["indexes"] = person.get_indexes()
  1918. template_vars["post_talmudic"] = person.is_post_talmudic()
  1919. template_vars["places"] = person.get_places()
  1920. return render_to_response('person.html', template_vars, RequestContext(request))
  1921. def person_index(request):
  1922. eras = ["GN", "RI", "AH", "CO"]
  1923. template_vars = {
  1924. "eras": []
  1925. }
  1926. for era in eras:
  1927. tp = TimePeriod().load({"symbol": era})
  1928. template_vars["eras"].append(
  1929. {
  1930. "name_en": tp.primary_name("en"),
  1931. "name_he": tp.primary_name("he"),
  1932. "years_en": tp.period_string("en"),
  1933. "years_he": tp.period_string("he"),
  1934. "people": [p for p in PersonSet({"era": era}, sort=[('deathYear', 1)]) if p.has_indexes()]
  1935. }
  1936. )
  1937. return render_to_response('people.html', template_vars, RequestContext(request))
  1938. def talmud_person_index(request):
  1939. gens = TimePeriodSet.get_generations()
  1940. template_vars = {
  1941. "gens": []
  1942. }
  1943. for gen in gens:
  1944. people = gen.get_people_in_generation()
  1945. template_vars["gens"].append({
  1946. "name_en": gen.primary_name("en"),
  1947. "name_he": gen.primary_name("he"),
  1948. "years_en": gen.period_string("en"),
  1949. "years_he": gen.period_string("he"),
  1950. "people": [p for p in people]
  1951. })
  1952. return render_to_response('talmud_people.html', template_vars, RequestContext(request))
  1953. def _get_sheet_tag_garden(tag):
  1954. garden_key = u"sheets.tagged.{}".format(tag)
  1955. g = Garden().load({"key": garden_key})
  1956. if not g:
  1957. g = Garden({"key": garden_key, "title": u"Sources from Sheets Tagged {}".format(tag), "heTitle": u"מקורות מדפים מתויגים:" + u" " + unicode(tag)})
  1958. g.import_sheets_by_tag(tag)
  1959. g.save()
  1960. return g
  1961. def sheet_tag_garden_page(request, key):
  1962. g = _get_sheet_tag_garden(key)
  1963. return garden_page(request, g)
  1964. def sheet_tag_visual_garden_page(request, key):
  1965. g = _get_sheet_tag_garden(key)
  1966. return visual_garden_page(request, g)
  1967. def custom_visual_garden_page(request, key):
  1968. g = Garden().load({"key": "sefaria.custom.{}".format(key)})
  1969. if not g:
  1970. raise Http404
  1971. return visual_garden_page(request, g)
  1972. def _get_search_garden(q):
  1973. garden_key = u"search.query.{}".format(q)
  1974. g = Garden().load({"key": garden_key})
  1975. if not g:
  1976. g = Garden({"key": garden_key, "title": u"Search: {}".format(q), "heTitle": u"חיפוש:" + u" " + unicode(q)})
  1977. g.import_search(q)
  1978. g.save()
  1979. return g
  1980. def search_query_visual_garden_page(request, q):
  1981. g = _get_search_garden(q)
  1982. return visual_garden_page(request, g)
  1983. def garden_page(request, g):
  1984. template_vars = {
  1985. 'title': g.title,
  1986. 'heTitle': g.heTitle,
  1987. 'key': g.key,
  1988. 'stopCount': g.stopSet().count(),
  1989. 'stopsByTime': g.stopsByTime(),
  1990. 'stopsByPlace': g.stopsByPlace(),
  1991. 'stopsByAuthor': g.stopsByAuthor(),
  1992. 'stopsByTag': g.stopsByTag()
  1993. }
  1994. return render_to_response('garden.html', template_vars, RequestContext(request))
  1995. def visual_garden_page(request, g):
  1996. template_vars = {
  1997. 'title': g.title,
  1998. 'heTitle': g.heTitle,
  1999. 'key': g.key,
  2000. 'stopCount': g.stopSet().count(),
  2001. 'stops': json.dumps(g.stopData()),
  2002. 'places': g.placeSet().asGeoJson(as_string=True),
  2003. 'config': json.dumps(getattr(g, "config", {}))
  2004. }
  2005. return render_to_response('visual_garden.html', template_vars, RequestContext(request))