views.py 147 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821
  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 urlparse
  9. import urllib2
  10. import urllib
  11. import dateutil.parser
  12. import base64
  13. import zlib
  14. from bson.json_util import dumps
  15. import p929
  16. import socket
  17. import bleach
  18. from collections import OrderedDict
  19. from django.views.decorators.cache import cache_page
  20. from django.template import RequestContext
  21. from django.template.loader import render_to_string, get_template
  22. from django.shortcuts import render, get_object_or_404, redirect
  23. from django.http import Http404
  24. from django.contrib.auth.decorators import login_required
  25. from django.contrib.admin.views.decorators import staff_member_required
  26. from django.utils.http import urlquote
  27. from django.utils.encoding import iri_to_uri
  28. from django.utils.translation import ugettext as _
  29. from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt, csrf_protect, requires_csrf_token
  30. from django.contrib.auth.models import User
  31. from django import http
  32. from django.utils import timezone
  33. from sefaria.model import *
  34. from sefaria.workflows import *
  35. from sefaria.reviews import *
  36. from sefaria.model.user_profile import user_link, user_started_text, unread_notifications_count_for_user, public_user_data
  37. from sefaria.model.group import GroupSet
  38. from sefaria.model.topic import get_topics
  39. from sefaria.model.schema import DictionaryEntryNotFound
  40. from sefaria.client.wrapper import format_object_for_client, format_note_object_for_client, get_notes, get_links
  41. from sefaria.system.exceptions import InputError, PartialRefInputError, BookNameError, NoVersionFoundError, DuplicateRecordError
  42. # noinspection PyUnresolvedReferences
  43. from sefaria.client.util import jsonResponse
  44. 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
  45. from sefaria.system.decorators import catch_error_as_json
  46. from sefaria.summaries import get_or_make_summary_node
  47. from sefaria.sheets import get_sheets_for_ref, public_sheets, get_sheets_by_tag, user_sheets, user_tags, recent_public_tags, sheet_to_dict, get_top_sheets, public_tag_list, group_sheets, get_sheet_for_panel, annotate_user_links
  48. from sefaria.utils.util import list_depth, text_preview
  49. from sefaria.utils.hebrew import hebrew_plural, hebrew_term, encode_hebrew_numeral, encode_hebrew_daf, is_hebrew, strip_cantillation, has_cantillation
  50. from sefaria.utils.talmud import section_to_daf, daf_to_section
  51. from sefaria.datatype.jagged_array import JaggedArray
  52. from sefaria.utils.calendars import get_all_calendar_items, get_keyed_calendar_items, this_weeks_parasha
  53. from sefaria.utils.util import short_to_long_lang_code, titlecase
  54. import sefaria.tracker as tracker
  55. from sefaria.system.cache import django_cache_decorator
  56. from sefaria.settings import USE_VARNISH, USE_NODE, NODE_HOST, DOMAIN_LANGUAGES, MULTISERVER_ENABLED
  57. from sefaria.system.multiserver.coordinator import server_coordinator
  58. from django.utils.html import strip_tags
  59. if USE_VARNISH:
  60. from sefaria.system.varnish.wrapper import invalidate_ref, invalidate_linked
  61. import logging
  62. logger = logging.getLogger(__name__)
  63. # # #
  64. # Initialized cache library objects that depend on sefaria.model being completely loaded.
  65. logger.warn("Initializing library objects.")
  66. library.get_toc_tree()
  67. library.build_full_auto_completer()
  68. library.build_ref_auto_completer()
  69. library.build_lexicon_auto_completers()
  70. if server_coordinator:
  71. server_coordinator.connect()
  72. # # #
  73. @ensure_csrf_cookie
  74. def catchall(request, tref, sheet=None):
  75. """
  76. Handle any URL not explicitly covers in urls.py.
  77. Catches text refs for text content and text titles for text table of contents.
  78. """
  79. def reader_redirect(uref):
  80. # Redirect to standard URLs
  81. url = "/" + uref
  82. response = redirect(iri_to_uri(url), permanent=True)
  83. params = request.GET.urlencode()
  84. response['Location'] += "?%s" % params if params else ""
  85. return response
  86. if sheet is None:
  87. try:
  88. oref = model.Ref(tref)
  89. except PartialRefInputError as e:
  90. logger.warning(u'{}'.format(e))
  91. matched_ref = Ref(e.matched_part)
  92. return reader_redirect(matched_ref.url())
  93. except InputError:
  94. raise Http404
  95. uref = oref.url()
  96. if uref and tref != uref:
  97. return reader_redirect(uref)
  98. return text_panels(request, ref=tref)
  99. return text_panels(request, ref=tref, sheet=sheet)
  100. @ensure_csrf_cookie
  101. def old_versions_redirect(request, tref, lang, version):
  102. url = "/{}?v{}={}".format(tref, lang, version)
  103. response = redirect(iri_to_uri(url), permanent=True)
  104. params = request.GET.urlencode()
  105. response['Location'] += "&{}".format(params) if params else ""
  106. return response
  107. def render_react_component(component, props):
  108. """
  109. Asks the Node Server to render `component` with `props`.
  110. `props` may either be JSON (to save reencoding) or a dictionary.
  111. Returns HTML.
  112. """
  113. if not USE_NODE:
  114. return render_to_string("elements/loading.html")
  115. from sefaria.settings import NODE_TIMEOUT, NODE_TIMEOUT_MONITOR
  116. propsJSON = json.dumps(props) if isinstance(props, dict) else props
  117. cache_key = "todo" # zlib.compress(propsJSON)
  118. url = NODE_HOST + "/" + component + "/" + cache_key
  119. encoded_args = urllib.urlencode({
  120. "propsJSON": propsJSON,
  121. })
  122. try:
  123. response = urllib2.urlopen(url, encoded_args, NODE_TIMEOUT)
  124. html = response.read()
  125. return html
  126. except Exception as e:
  127. # Catch timeouts, however they may come. Write to file NODE_TIMEOUT_MONITOR, which forever monitors to restart process
  128. if isinstance(e, socket.timeout) or (hasattr(e, "reason") and isinstance(e.reason, socket.timeout)):
  129. logger.exception("Node timeout: Fell back to client-side rendering.")
  130. with open(NODE_TIMEOUT_MONITOR, "a") as myfile:
  131. props = json.loads(props) if isinstance(props, str) else props
  132. myfile.write("Timeout at {}: {} / {} / {} / {}\n".format(
  133. datetime.now().isoformat(),
  134. props.get("initialPath"),
  135. "MultiPanel" if props.get("multiPanel", True) else "Mobile",
  136. "Logged In" if props.get("loggedIn", False) else "Logged Out",
  137. props.get("interfaceLang")
  138. ))
  139. return render_to_string("elements/loading.html")
  140. else:
  141. # If anything else goes wrong with Node, just fall back to client-side rendering
  142. logger.exception("Node error: Fell back to client-side rendering.")
  143. return render_to_string("elements/loading.html")
  144. def make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, mode, **kwargs):
  145. """
  146. Returns a dictionary corresponding to the React panel state,
  147. additionally setting `text` field with textual content.
  148. """
  149. if oref.is_book_level():
  150. if kwargs.get('extended notes', 0) and (versionEn is not None or versionHe is not None):
  151. currVersions = {"en": versionEn, "he": versionHe}
  152. if versionEn is not None and versionHe is not None:
  153. curr_lang = kwargs.get("panelDisplayLanguage", "en")
  154. for key in currVersions.keys():
  155. if key == curr_lang:
  156. continue
  157. else:
  158. currVersions[key] = None
  159. panel = {
  160. "menuOpen": "extended notes",
  161. "bookRef": oref.normal(),
  162. "indexDetails": library.get_index(oref.normal()).contents_with_content_counts(),
  163. "currVersions": currVersions
  164. }
  165. else:
  166. panel = {
  167. "menuOpen": "book toc",
  168. "bookRef": oref.normal(),
  169. "indexDetails": library.get_index(oref.normal()).contents_with_content_counts(),
  170. "versions": oref.version_list()
  171. }
  172. else:
  173. section_ref = oref.first_available_section_ref()
  174. oref = section_ref if section_ref else oref
  175. panel = {
  176. "mode": mode,
  177. "ref": oref.normal(),
  178. "refs": [oref.normal()] if not oref.is_spanning() else [r.normal() for r in oref.split_spanning_ref()],
  179. "currVersions": {
  180. "en": versionEn,
  181. "he": versionHe,
  182. },
  183. "filter": filter,
  184. "versionFilter": versionFilter,
  185. }
  186. if filter and len(filter):
  187. if filter[0] in ("Sheets", "Notes", "About", "Versions", "Version Open", "extended notes"):
  188. panel["connectionsMode"] = filter[0]
  189. else:
  190. panel["connectionsMode"] = "TextList"
  191. settings_override = {}
  192. panelDisplayLanguage = kwargs.get("panelDisplayLanguage")
  193. aliyotOverride = kwargs.get("aliyotOverride")
  194. if panelDisplayLanguage:
  195. settings_override.update({"language" : short_to_long_lang_code(panelDisplayLanguage)})
  196. if aliyotOverride:
  197. settings_override.update({"aliyotTorah": aliyotOverride})
  198. if settings_override:
  199. panel["settings"] = settings_override
  200. if mode != "Connections":
  201. try:
  202. text_family = TextFamily(oref, version=panel["currVersions"]["en"], lang="en", version2=panel["currVersions"]["he"], lang2="he", commentary=False,
  203. context=True, pad=True, alts=True, wrapLinks=False).contents()
  204. except NoVersionFoundError:
  205. text_family = {}
  206. text_family["updateFromAPI"] = True
  207. text_family["next"] = oref.next_section_ref().normal() if oref.next_section_ref() else None
  208. text_family["prev"] = oref.prev_section_ref().normal() if oref.prev_section_ref() else None
  209. panel["text"] = text_family
  210. if oref.index.categories == [u"Tanakh", u"Torah"]:
  211. panel["indexDetails"] = oref.index.contents(v2=True) # Included for Torah Parashah titles rendered in text
  212. if oref.is_segment_level(): # Note: a ranging or spanning ref like "Genesis 1:2-3:4" is considered segment level
  213. panel["highlightedRefs"] = [subref.normal() for subref in oref.range_list()]
  214. return panel
  215. def make_search_panel_dict(get_dict, i, **kwargs):
  216. search_params = get_search_params(get_dict, i)
  217. # TODO hard to pass search params related to textSearchState and sheetSearchState as those are JS objects
  218. # TODO this is not such a pressing issue though
  219. panel = {
  220. "menuOpen": "search",
  221. "searchQuery": search_params["query"],
  222. "searchTab": search_params["tab"],
  223. }
  224. panelDisplayLanguage = kwargs.get("panelDisplayLanguage")
  225. if panelDisplayLanguage:
  226. panel["settings"] = {"language": short_to_long_lang_code(panelDisplayLanguage)}
  227. return panel
  228. def make_sheet_panel_dict(sheet_id, filter, **kwargs):
  229. highlighted_node = None
  230. if "." in sheet_id:
  231. highlighted_node = sheet_id.split(".")[1]
  232. sheet_id = sheet_id.split(".")[0]
  233. db.sheets.update({"id": int(sheet_id)}, {"$inc": {"views": 1}})
  234. sheet = get_sheet_for_panel(int(sheet_id))
  235. sheet["ownerProfileUrl"] = public_user_data(sheet["owner"])["profileUrl"]
  236. if "assigner_id" in sheet:
  237. asignerData = public_user_data(sheet["assigner_id"])
  238. sheet["assignerName"] = asignerData["name"]
  239. sheet["assignerProfileUrl"] = asignerData["profileUrl"]
  240. if "viaOwner" in sheet:
  241. viaOwnerData = public_user_data(sheet["viaOwner"])
  242. sheet["viaOwnerName"] = viaOwnerData["name"]
  243. sheet["viaOwnerProfileUrl"] = viaOwnerData["profileUrl"]
  244. sheet["sources"] = annotate_user_links(sheet["sources"])
  245. panel = {
  246. "sheetID": sheet_id,
  247. "mode": "Sheet",
  248. "sheet": sheet,
  249. "highlightedNodes": highlighted_node
  250. }
  251. if highlighted_node:
  252. ref = next((element["ref"] for element in sheet["sources"] if element.get("ref") and element["node"] == int(highlighted_node)), None)
  253. panelDisplayLanguage = kwargs.get("panelDisplayLanguage")
  254. if panelDisplayLanguage:
  255. panel["settings"] = {"language": short_to_long_lang_code(panelDisplayLanguage)}
  256. panels = []
  257. panels.append(panel)
  258. if filter is not None and ref is not None:
  259. panels += [make_panel_dict(Ref(ref), None, None, filter, None, "Connections", **kwargs)]
  260. return panels
  261. else:
  262. return panels
  263. def make_panel_dicts(oref, versionEn, versionHe, filter, versionFilter, multi_panel, **kwargs):
  264. """
  265. Returns an array of panel dictionaries.
  266. Depending on whether `multi_panel` is True, connections set in `filter` are displayed in either 1 or 2 panels.
  267. """
  268. panels = []
  269. # filter may have value [], meaning "all". Therefore we test filter with "is not None".
  270. if filter is not None and multi_panel:
  271. panels += [make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, "Text", **kwargs)]
  272. panels += [make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, "Connections", **kwargs)]
  273. elif filter is not None and not multi_panel:
  274. panels += [make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, "TextAndConnections", **kwargs)]
  275. else:
  276. panels += [make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, "Text", **kwargs)]
  277. return panels
  278. def base_props(request):
  279. """
  280. Returns a dictionary of props that all App pages get based on the request.
  281. """
  282. from sefaria.system.context_processors import user_and_notifications
  283. return {
  284. "multiPanel": not request.user_agent.is_mobile and not "mobile" in request.GET,
  285. "initialPath": request.get_full_path(),
  286. "loggedIn": True if request.user.is_authenticated else False, # Django 1.10 changed this to a CallableBool, so it doesnt have a direct value of True/False,
  287. "_uid": request.user.id,
  288. "interfaceLang": request.interfaceLang,
  289. "initialSettings": {
  290. "language": request.contentLang,
  291. "layoutDefault": request.COOKIES.get("layoutDefault", "segmented"),
  292. "layoutTalmud": request.COOKIES.get("layoutTalmud", "continuous"),
  293. "layoutTanakh": request.COOKIES.get("layoutTanakh", "segmented"),
  294. "aliyotTorah": request.COOKIES.get("aliyotTorah", "aliyotOff"),
  295. "vowels": request.COOKIES.get("vowels", "all"),
  296. "biLayout": request.COOKIES.get("biLayout", "stacked"),
  297. "color": request.COOKIES.get("color", "light"),
  298. "fontSize": request.COOKIES.get("fontSize", 62.5),
  299. },
  300. }
  301. def text_panels(request, ref, version=None, lang=None, sheet=None):
  302. """
  303. Handles views of ReaderApp that involve texts, connections, and text table of contents in panels.
  304. """
  305. if sheet == None:
  306. try:
  307. primary_ref = oref = Ref(ref)
  308. if primary_ref.book == "Sheet":
  309. sheet = True
  310. ref = '.'.join(map(str, primary_ref.sections))
  311. except InputError:
  312. raise Http404
  313. props = base_props(request)
  314. panels = []
  315. multi_panel = props["multiPanel"]
  316. # Handle first panel which has a different signature in params
  317. versionEn = request.GET.get("ven", None)
  318. if versionEn:
  319. versionEn = versionEn.replace("_", " ")
  320. versionHe = request.GET.get("vhe", None)
  321. if versionHe:
  322. versionHe = versionHe.replace("_", " ")
  323. filter = request.GET.get("with").replace("_", " ").split("+") if request.GET.get("with") else None
  324. filter = [] if filter == ["all"] else filter
  325. if sheet == None:
  326. versionFilter = [request.GET.get("vside").replace("_", " ")] if request.GET.get("vside") else []
  327. if versionEn and not Version().load({"versionTitle": versionEn, "language": "en"}):
  328. raise Http404
  329. if versionHe and not Version().load({"versionTitle": versionHe, "language": "he"}):
  330. raise Http404
  331. kwargs = {
  332. "panelDisplayLanguage": request.GET.get("lang", props["initialSettings"]["language"]),
  333. 'extended notes': int(request.GET.get("notes", 0)),
  334. }
  335. if request.GET.get("aliyot", None):
  336. kwargs["aliyotOverride"] = "aliyotOn" if int(request.GET.get("aliyot")) == 1 else "aliyotOff"
  337. panels += make_panel_dicts(oref, versionEn, versionHe, filter, versionFilter, multi_panel, **kwargs)
  338. elif sheet == True:
  339. panels += make_sheet_panel_dict(ref, filter, **{"panelDisplayLanguage": request.GET.get("lang", "bi")})
  340. # Handle any panels after 1 which are identified with params like `p2`, `v2`, `l2`.
  341. i = 2
  342. while True:
  343. ref = request.GET.get("p{}".format(i))
  344. if not ref:
  345. break
  346. if ref == "search":
  347. panelDisplayLanguage = request.GET.get("lang{}".format(i), props["initialSettings"]["language"])
  348. panels += [make_search_panel_dict(request.GET, i, **{"panelDisplayLanguage": panelDisplayLanguage})]
  349. elif ref == "sheet":
  350. sheet_id = request.GET.get("s{}".format(i))
  351. panelDisplayLanguage = request.GET.get("lang", "bi")
  352. panels += make_sheet_panel_dict(sheet_id, None, **{"panelDisplayLanguage": panelDisplayLanguage})
  353. else:
  354. try:
  355. oref = Ref(ref)
  356. except InputError:
  357. i += 1
  358. continue # Stop processing all panels?
  359. # raise Http404
  360. versionEn = request.GET.get("ven{}".format(i)).replace(u"_", u" ") if request.GET.get("ven{}".format(i)) else None
  361. versionHe = request.GET.get("vhe{}".format(i)).replace(u"_", u" ") if request.GET.get("vhe{}".format(i)) else None
  362. if not versionEn and not versionHe:
  363. # potential link using old version format
  364. language = request.GET.get("l{}".format(i))
  365. if language == "en":
  366. versionEn = request.GET.get("v{}".format(i)).replace(u"_", u" ") if request.GET.get("v{}".format(i)) else None
  367. else: # he
  368. versionHe = request.GET.get("v{}".format(i)).replace(u"_", u" ") if request.GET.get("v{}".format(i)) else None
  369. filter = request.GET.get("w{}".format(i)).replace("_", " ").split("+") if request.GET.get("w{}".format(i)) else None
  370. filter = [] if filter == ["all"] else filter
  371. versionFilter = [request.GET.get("vside").replace("_", " ")] if request.GET.get("vside") else []
  372. kwargs = {
  373. "panelDisplayLanguage": request.GET.get("lang{}".format(i), props["initialSettings"]["language"]),
  374. 'extended notes': int(request.GET.get("notes{}".format(i), 0)),
  375. }
  376. if request.GET.get("aliyot{}".format(i), None):
  377. kwargs["aliyotOverride"] = "aliyotOn" if int(request.GET.get("aliyot{}".format(i))) == 1 else "aliyotOff"
  378. if (versionEn and not Version().load({"versionTitle": versionEn, "language": "en"})) or \
  379. (versionHe and not Version().load({"versionTitle": versionHe, "language": "he"})):
  380. i += 1
  381. continue # Stop processing all panels?
  382. # raise Http404
  383. panels += make_panel_dicts(oref, versionEn, versionHe, filter, versionFilter, multi_panel, **kwargs)
  384. i += 1
  385. props.update({
  386. "headerMode": False,
  387. "initialRefs": panels[0].get("refs", []),
  388. "initialFilter": panels[0].get("filter", None), # used only for mobile, TextAndConnections case.
  389. "initialBookRef": panels[0].get("bookRef", None),
  390. "initialPanels": panels,
  391. "initialPanelCap": len(panels),
  392. "initialQuery": None,
  393. "initialSheetsTag": None,
  394. "initialNavigationCategories": None,
  395. })
  396. if sheet == None:
  397. title = primary_ref.he_normal() if request.interfaceLang == "hebrew" else primary_ref.normal()
  398. breadcrumb = ld_cat_crumbs(request, oref=primary_ref)
  399. if primary_ref.is_book_level():
  400. if request.interfaceLang == "hebrew":
  401. desc = getattr(primary_ref.index, 'heDesc', "")
  402. book = primary_ref.he_normal()
  403. else:
  404. desc = getattr(primary_ref.index, 'enDesc', "")
  405. book = primary_ref.normal()
  406. read = _("Read the text of %(book)s online with commentaries and connections.") % {'book': book}
  407. desc = desc + " " + read if desc else read
  408. else:
  409. segmentIndex = primary_ref.sections[-1] - 1 if primary_ref.is_segment_level() else 0
  410. try:
  411. enText = _reduce_ranged_ref_text_to_first_section(props["initialPanels"][0]["text"].get("text", []))
  412. heText = _reduce_ranged_ref_text_to_first_section(props["initialPanels"][0]["text"].get("he", []))
  413. enDesc = enText[segmentIndex] if segmentIndex < len(enText) else "" # get english text for section if it exists
  414. heDesc = heText[segmentIndex] if segmentIndex < len(heText) else "" # get hebrew text for section if it exists
  415. if request.interfaceLang == "hebrew":
  416. desc = heDesc or enDesc # if no hebrew, fall back on hebrew
  417. else:
  418. desc = enDesc or heDesc # if no english, fall back on hebrew
  419. desc = bleach.clean(desc, strip=True, tags=())
  420. desc = desc[:160].rsplit(' ', 1)[0] + "..." # truncate as close to 160 characters as possible while maintaining whole word. Append ellipses.
  421. except (IndexError, KeyError):
  422. desc = _("Explore 3,000 years of Jewish texts in Hebrew and English translation.")
  423. else:
  424. sheet = panels[0].get("sheet",{})
  425. title = "Sefaria Source Sheet: " + strip_tags(sheet["title"])
  426. breadcrumb = "/sheets/"+str(sheet["id"])
  427. desc = sheet.get("summary","A source sheet created with Sefaria's Source Sheet Builder")
  428. propsJSON = json.dumps(props)
  429. html = render_react_component("ReaderApp", propsJSON)
  430. return render(request, 'base.html', {
  431. "propsJSON": propsJSON,
  432. "html": html,
  433. "title": title,
  434. "desc": desc,
  435. "ldBreadcrumbs": breadcrumb
  436. })
  437. def _reduce_ranged_ref_text_to_first_section(text_list):
  438. """
  439. given jagged-array-like list, return only first section
  440. :param text_list: list
  441. :return: returns list of text representing first section
  442. """
  443. if len(text_list) == 0:
  444. return text_list
  445. while not isinstance(text_list[0], basestring):
  446. text_list = text_list[0]
  447. return text_list
  448. def texts_category_list(request, cats):
  449. """
  450. List of texts in a category.
  451. """
  452. if "Tanach" in cats:
  453. cats = cats.replace("Tanach", "Tanakh")
  454. return redirect("/texts/%s" % cats)
  455. props = base_props(request)
  456. cats = cats.split("/")
  457. if cats != ["recent"]:
  458. toc = library.get_toc()
  459. cat_toc = get_or_make_summary_node(toc, cats, make_if_not_found=False)
  460. if cat_toc is None or len(cats) == 0:
  461. return texts_list(request)
  462. cat_string = u", ".join(cats) if request.interfaceLang == "english" else u", ".join([hebrew_term(cat) for cat in cats])
  463. title = cat_string + _(" | Sefaria")
  464. desc = _("Read %(categories)s texts online with commentaries and connections.") % {'categories': cat_string}
  465. else:
  466. title = _("Recently Viewed")
  467. desc = _("Texts that you've recently viewed on Sefaria.")
  468. props.update({
  469. "initialMenu": "navigation",
  470. "initialNavigationCategories": cats,
  471. })
  472. propsJSON = json.dumps(props)
  473. html = render_react_component("ReaderApp", propsJSON)
  474. return render(request, 'base.html', {
  475. "propsJSON": propsJSON,
  476. "html": html,
  477. "title": title,
  478. "desc": desc,
  479. "ldBreadcrumbs": ld_cat_crumbs(request, cats)
  480. })
  481. def get_param(param, i=None):
  482. return "{}{}".format(param, "" if i is None else i)
  483. def get_search_params(get_dict, i=None):
  484. gp = get_param
  485. sheet_group_search_filters = map(lambda f: urllib.unquote(f),
  486. get_dict.get(gp("sgroupFilters", i)).split("|")) if get_dict.get(gp("sgroupFilters", i),
  487. "") else []
  488. sheet_tags_search_filters = map(lambda f: urllib.unquote(f),
  489. get_dict.get(gp("stagsFilters", i), "").split("|")) if get_dict.get(gp("stagsFilters", i),
  490. "") else []
  491. sheet_agg_types = ['group'] * len(sheet_group_search_filters) + ['tags'] * len(
  492. sheet_tags_search_filters) # i got a tingly feeling writing this
  493. text_filters = map(lambda f: urllib.unquote(f), get_dict.get(gp("tpathFilters", i)).split("|")) if get_dict.get(gp("tpathFilters", i)) else []
  494. return {
  495. "query": urllib.unquote(get_dict.get(gp("q", i), "")),
  496. "tab": urllib.unquote(get_dict.get(gp("tab", i), "text")),
  497. "textField": ("naive_lemmatizer" if get_dict.get(gp("tvar", i)) == "1" else "exact") if get_dict.get(gp("tvar", i)) else "",
  498. "textSort": get_dict.get(gp("tsort", i), None),
  499. "textFilters": text_filters,
  500. "textFilterAggTypes": [None for _ in text_filters], # currently unused. just needs to be equal len as text_filters
  501. "sheetSort": get_dict.get(gp("ssort", i), None),
  502. "sheetFilters": (sheet_group_search_filters + sheet_tags_search_filters),
  503. "sheetFilterAggTypes": sheet_agg_types,
  504. }
  505. @ensure_csrf_cookie
  506. def search(request):
  507. """
  508. Search or Search Results page.
  509. """
  510. search_params = get_search_params(request.GET)
  511. props = base_props(request)
  512. props.update({
  513. "initialMenu": "search",
  514. "initialQuery": search_params["query"],
  515. "initialSearchTab": search_params["tab"],
  516. "initialTextSearchFilters": search_params["textFilters"],
  517. "initialTextSearchFilterAggTypes": search_params["textFilterAggTypes"],
  518. "initialTextSearchField": search_params["textField"],
  519. "initialTextSearchSortType": search_params["textSort"],
  520. "initialSheetSearchFilters": search_params["sheetFilters"],
  521. "initialSheetSearchFilterAggTypes": search_params["sheetFilterAggTypes"],
  522. "initialSheetSearchSortType": search_params["sheetSort"]
  523. })
  524. propsJSON = json.dumps(props)
  525. html = render_react_component("ReaderApp", propsJSON)
  526. return render(request,'base.html', {
  527. "propsJSON": propsJSON,
  528. "html": html,
  529. "title": (search_params["query"] + " | " if search_params["query"] else "") + _("Sefaria Search"),
  530. "desc": _("Search 3,000 years of Jewish texts in Hebrew and English translation.")
  531. })
  532. def sheets(request):
  533. """
  534. Source Sheets Home Page.
  535. """
  536. props = base_props(request)
  537. props.update({
  538. "initialMenu": "sheets",
  539. "topSheets": get_top_sheets(),
  540. "tagList": public_tag_list(sort_by="count"),
  541. "trendingTags": recent_public_tags(days=14, ntags=18)
  542. })
  543. title = _("Sefaria Source Sheets")
  544. desc = _("Explore thousands of public Source Sheets and use our Source Sheet Builder to create your own online.")
  545. propsJSON = json.dumps(props)
  546. html = render_react_component("ReaderApp", propsJSON)
  547. return render(request, 'base.html', {
  548. "propsJSON": propsJSON,
  549. "title": title,
  550. "desc": desc,
  551. "html": html,
  552. })
  553. def get_group_page(request, group, authenticated):
  554. props = base_props(request)
  555. props.update({
  556. "initialMenu": "sheets",
  557. "initialSheetsTag": "sefaria-groups",
  558. "initialGroup": group,
  559. })
  560. group = GroupSet({"name": group})
  561. if not len(group):
  562. raise Http404
  563. props["groupData"] = group[0].contents(with_content=True, authenticated=authenticated)
  564. propsJSON = json.dumps(props)
  565. html = render_react_component("ReaderApp", propsJSON)
  566. return render(request, 'base.html', {
  567. "propsJSON": propsJSON,
  568. "html": html,
  569. "title": group[0].name + " | " + _("Sefaria Groups"),
  570. "desc": props["groupData"].get("description", ""),
  571. })
  572. def public_groups(request):
  573. props = base_props(request)
  574. title = _("Sefaria Groups")
  575. return menu_page(request, props, "publicGroups")
  576. @login_required
  577. def my_groups(request):
  578. props = base_props(request)
  579. title = _("Sefaria Groups")
  580. return menu_page(request, props, "myGroups")
  581. @login_required
  582. def my_notes(request):
  583. title = _("My Notes on Sefaria")
  584. props = base_props(request)
  585. return menu_page(request, props, "myNotes", title)
  586. def sheets_by_tag(request, tag):
  587. """
  588. Page of sheets by tag.
  589. Currently used to for "My Sheets" and "All Sheets" as well.
  590. """
  591. if tag != Term.normalize(tag):
  592. return redirect("/sheets/tags/%s" % Term.normalize(tag))
  593. props = base_props(request)
  594. props.update({
  595. "initialMenu": "sheets",
  596. "initialSheetsTag": tag,
  597. })
  598. if tag == "My Sheets" and request.user.is_authenticated:
  599. props["userSheets"] = user_sheets(request.user.id)["sheets"]
  600. props["userTags"] = user_tags(request.user.id)
  601. title = _("My Source Sheets | Sefaria Source Sheets")
  602. desc = _("My Sources Sheets on Sefaria, both private and public.")
  603. elif tag == "My Sheets" and not request.user.is_authenticated:
  604. return redirect("/login?next=/sheets/private")
  605. elif tag == "All Sheets":
  606. props["publicSheets"] = {"offset0num50": public_sheets(limit=50)["sheets"]}
  607. title = _("Public Source Sheets | Sefaria Source Sheets")
  608. desc = _("Explore thousands of public Source Sheets drawing on Sefaria's library of Jewish texts.")
  609. else:
  610. props["tagSheets"] = [sheet_to_dict(s) for s in get_sheets_by_tag(tag)]
  611. tag = Term.normalize(tag, lang=request.LANGUAGE_CODE)
  612. title = tag + _(" | Sefaria")
  613. desc = _('Public Source Sheets on tagged with "%(tag)s", drawing from Sefaria\'s library of Jewish texts.') % {'tag': tag}
  614. propsJSON = json.dumps(props)
  615. html = render_react_component("ReaderApp", propsJSON)
  616. return render(request,'base.html', {
  617. "propsJSON": propsJSON,
  618. "title": title,
  619. "desc": desc,
  620. "html": html,
  621. })
  622. ## Sheet Views
  623. def sheets_list(request, type=None):
  624. """
  625. List of all public/your/all sheets
  626. either as a full page or as an HTML fragment
  627. """
  628. if not type:
  629. # Sheet Splash page
  630. return sheets(request)
  631. response = { "status": 0 }
  632. if type == "public":
  633. return sheets_by_tag(request,"All Sheets")
  634. elif type == "private" and request.user.is_authenticated:
  635. return sheets_by_tag(request,"My Sheets")
  636. elif type == "private" and not request.user.is_authenticated:
  637. return redirect("/login?next=/sheets/private")
  638. def sheets_tags_list(request):
  639. """
  640. Redirect to Sheets homepage which has tags list.
  641. Previously: View public sheets organized by tags.
  642. """
  643. return redirect("/sheets")
  644. def group_page(request, group):
  645. """
  646. Main page for group `group`
  647. """
  648. group = group.replace("-", " ").replace("_", " ")
  649. group = Group().load({"name": group})
  650. if not group:
  651. raise Http404
  652. if request.user.is_authenticated and group.is_member(request.user.id):
  653. return get_group_page(request, group.name, True)
  654. else:
  655. return get_group_page(request, group.name, False)
  656. @login_required()
  657. def edit_group_page(request, group=None):
  658. if group:
  659. group = group.replace("-", " ").replace("_", " ")
  660. group = Group().load({"name": group})
  661. if not group:
  662. raise Http404
  663. groupData = group.contents()
  664. else:
  665. groupData = None
  666. return render(request, 'edit_group.html', {"groupData": groupData})
  667. @staff_member_required
  668. def groups_admin_page(request):
  669. """
  670. Page listing all groups for admins
  671. """
  672. groups = GroupSet(sort=[["name", 1]])
  673. return render(request, "groups.html", {"groups": groups})
  674. def topics_page(request):
  675. """
  676. Page of sheets by tag.
  677. Currently used to for "My Sheets" and "All Sheets" as well.
  678. """
  679. topics = get_topics()
  680. props = base_props(request)
  681. props.update({
  682. "initialMenu": "topics",
  683. "initialTopic": None,
  684. "topicList": topics.list(sort_by="count"),
  685. "trendingTags": recent_public_tags(days=14, ntags=12),
  686. })
  687. propsJSON = json.dumps(props)
  688. html = render_react_component("ReaderApp", propsJSON)
  689. return render(request, 'base.html', {
  690. "propsJSON": propsJSON,
  691. "title": _("Topics") + " | " + _("Sefaria"),
  692. "desc": _("Explore Jewish Texts by Topic on Sefaria"),
  693. "html": html,
  694. })
  695. def topic_page(request, topic):
  696. """
  697. Page of sheets by tag.
  698. Currently used to for "My Sheets" and "All Sheets" as well.
  699. """
  700. if topic != Term.normalize(topic):
  701. return redirect("/topics/%s" % Term.normalize(topic))
  702. topics = get_topics()
  703. props = base_props(request)
  704. props.update({
  705. "initialMenu": "topics",
  706. "initialTopic": topic,
  707. "topicData": topics.get(topic).contents(),
  708. })
  709. title = u"%(topic)s | Sefaria" % {"topic": topic}
  710. desc = u'Explore "%(topic)s" on Sefaria, drawing from our library of Jewish texts.' % {"topic": topic}
  711. propsJSON = json.dumps(props)
  712. html = render_react_component("ReaderApp", propsJSON)
  713. return render(request,'base.html', {
  714. "propsJSON": propsJSON,
  715. "title": title,
  716. "desc": desc,
  717. "html": html,
  718. })
  719. def menu_page(request, props, page, title="", desc=""):
  720. """
  721. View for any App page that can described with the `menuOpen` param in React
  722. """
  723. props.update({
  724. "initialMenu": page,
  725. })
  726. propsJSON = json.dumps(props)
  727. html = render_react_component("ReaderApp", propsJSON)
  728. return render(request, 'base.html', {
  729. "propsJSON": propsJSON,
  730. "title": title,
  731. "desc": desc,
  732. "html": html,
  733. })
  734. def mobile_home(request):
  735. props = base_props(request)
  736. return menu_page(request, props, "home")
  737. def texts_list(request):
  738. props = base_props(request)
  739. title = _("The Sefaria Library")
  740. desc = _("Browse 1,000s of Jewish texts in the Sefaria Library by category and title.")
  741. return menu_page(request, props, "navigation", title, desc)
  742. def saved(request):
  743. props = base_props(request)
  744. title = _("My Saved Content")
  745. desc = _("See your saved content on Sefaria")
  746. return menu_page(request, props, "saved", title, desc)
  747. def user_history(request):
  748. props = base_props(request)
  749. title = _("My User History")
  750. desc = _("See your user history on Sefaria")
  751. return menu_page(request, props, "history", title, desc)
  752. def updates(request):
  753. props = base_props(request)
  754. title = _("New Additions to the Sefaria Library")
  755. desc = _("See texts, translations and connections that have been recently added to Sefaria.")
  756. return menu_page(request, props, "updates", title, desc)
  757. @login_required
  758. def account(request):
  759. title = _("Sefaria Account")
  760. props = base_props(request)
  761. return menu_page(request, props, "account", title)
  762. @login_required
  763. def notifications(request):
  764. # Notifications content is not rendered server side
  765. title = _("Sefaria Notifications")
  766. props = base_props(request)
  767. return menu_page(request, props, "notifications", title)
  768. @login_required
  769. def modtools(request):
  770. title = _("Moderator Tools")
  771. props = base_props(request)
  772. return menu_page(request, props, "modtools", title)
  773. def s2_extended_notes(request, tref, lang, version_title):
  774. if not Ref.is_ref(tref):
  775. raise Http404
  776. version_title = version_title.replace("_", " ")
  777. version = Version().load({'title': tref, 'language': lang, 'versionTitle': version_title})
  778. if version is None:
  779. return reader(request, tref)
  780. if not hasattr(version, 'extendedNotes') and not hasattr(version, 'extendedNotesHebrew'):
  781. return reader(request, tref, lang, version_title)
  782. title = _("Extended Notes")
  783. props = s2_props(request)
  784. panel = {
  785. "mode": "extended notes",
  786. "ref": tref,
  787. "refs": [tref],
  788. "version": version_title,
  789. "versionLanguage": lang,
  790. "extendedNotes": getattr(version, "extendedNotes", ""),
  791. "extendedNotesHebrew": getattr(version, "extendedNotesHebrew", "")
  792. }
  793. props['panels'] = [panel]
  794. return s2_page(request, props, "extended notes", title)
  795. """
  796. JSON - LD snippets for use in "rich snippets" - semantic markup.
  797. """
  798. def _crumb(pos, id, name):
  799. return {
  800. "@type": "ListItem",
  801. "position": pos,
  802. "item": {
  803. "@id": id,
  804. "name": name
  805. }}
  806. def ld_cat_crumbs(request, cats=None, title=None, oref=None):
  807. """
  808. JSON - LD breadcrumbs(https://developers.google.com/search/docs/data-types/breadcrumbs)
  809. :param cats: List of category names
  810. :param title: String
  811. :return: serialized json-ld object, for inclusion in <script> tag.
  812. """
  813. if cats is None and title is None and oref is None:
  814. return u""
  815. # Fill in missing information
  816. if oref is not None:
  817. assert isinstance(oref, Ref)
  818. if cats is None:
  819. cats = oref.index.categories[:]
  820. if title is None:
  821. title = oref.index.title
  822. elif title is not None and cats is None:
  823. cats = library.get_index(title).categories[:]
  824. breadcrumbJsonList = [_crumb(1, "/texts", _("Texts"))]
  825. nextPosition = 2
  826. for i,c in enumerate(cats):
  827. name = hebrew_term(c) if request.interfaceLang == "hebrew" else c
  828. breadcrumbJsonList += [_crumb(nextPosition, "/texts/" + "/".join(cats[0:i+1]), name)]
  829. nextPosition += 1
  830. if title:
  831. name = hebrew_term(title) if request.interfaceLang == "hebrew" else title
  832. breadcrumbJsonList += [_crumb(nextPosition, "/" + title.replace(" ", "_"), name)]
  833. nextPosition += 1
  834. if oref and oref.index_node != oref.index.nodes:
  835. for snode in oref.index_node.ancestors()[1:] + [oref.index_node]:
  836. if snode.is_default():
  837. continue
  838. name = snode.primary_title("he") if request.interfaceLang == "hebrew" else snode.primary_title("en")
  839. breadcrumbJsonList += [_crumb(nextPosition, "/" + snode.ref().url(), name)]
  840. nextPosition += 1
  841. #todo: range?
  842. if oref and getattr(oref.index_node, "depth", None) and not oref.is_range():
  843. depth = oref.index_node.depth
  844. for i in range(len(oref.sections)):
  845. if request.interfaceLang == "english":
  846. name = oref.index_node.sectionNames[i] + u" " + oref.normal_section(i, "en")
  847. else:
  848. name = hebrew_term(oref.index_node.sectionNames[i]) + u" " + oref.normal_section(i, "he")
  849. breadcrumbJsonList += [_crumb(nextPosition, "/" + oref.context_ref(depth - i - 1).url(), name)]
  850. nextPosition += 1
  851. return json.dumps({
  852. "@context": "http://schema.org",
  853. "@type": "BreadcrumbList",
  854. "itemListElement": breadcrumbJsonList
  855. })
  856. @ensure_csrf_cookie
  857. def edit_text(request, ref=None, lang=None, version=None):
  858. """
  859. Opens a view directly to adding, editing or translating a given text.
  860. """
  861. if ref is not None:
  862. try:
  863. oref = Ref(ref)
  864. if oref.sections == []:
  865. # Only text name specified, let them chose section first
  866. initJSON = json.dumps({"mode": "add new", "newTitle": oref.normal()})
  867. mode = "Add"
  868. else:
  869. # Pull a particular section to edit
  870. version = version.replace("_", " ") if version else None
  871. #text = get_text(ref, lang=lang, version=version)
  872. text = TextFamily(Ref(ref), lang=lang, version=version).contents()
  873. text["mode"] = request.path.split("/")[1]
  874. mode = text["mode"].capitalize()
  875. text["edit_lang"] = lang if lang is not None else request.contentLang
  876. text["edit_version"] = version
  877. initJSON = json.dumps(text)
  878. except:
  879. index = library.get_index(ref)
  880. if index:
  881. ref = None
  882. initJSON = json.dumps({"mode": "add new", "newTitle": index.contents()['title']})
  883. else:
  884. initJSON = json.dumps({"mode": "add new"})
  885. titles = json.dumps(model.library.full_title_list())
  886. page_title = "%s %s" % (mode, ref) if ref else "Add a New Text"
  887. return render(request,'edit_text.html',
  888. {'titles': titles,
  889. 'initJSON': initJSON,
  890. 'page_title': page_title,
  891. })
  892. @ensure_csrf_cookie
  893. def edit_text_info(request, title=None, new_title=None):
  894. """
  895. Opens the Edit Text Info page.
  896. """
  897. if title:
  898. # Edit Existing
  899. title = title.replace("_", " ")
  900. i = library.get_index(title)
  901. if not (request.user.is_staff or user_started_text(request.user.id, title)):
  902. return render(request,'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})
  903. indexJSON = json.dumps(i.contents(v2=True) if "toc" in request.GET else i.contents(force_complex=True))
  904. versions = VersionSet({"title": title})
  905. text_exists = versions.count() > 0
  906. new = False
  907. elif new_title:
  908. # Add New
  909. new_title = new_title.replace("_", " ")
  910. try: # Redirect to edit path if this title already exists
  911. i = library.get_index(new_title)
  912. return redirect("/edit/textinfo/%s" % new_title)
  913. except:
  914. pass
  915. indexJSON = json.dumps({"title": new_title})
  916. text_exists = False
  917. new = True
  918. return render(request,'edit_text_info.html',
  919. {'title': title,
  920. 'indexJSON': indexJSON,
  921. 'text_exists': text_exists,
  922. 'new': new,
  923. 'toc': library.get_toc()
  924. })
  925. @ensure_csrf_cookie
  926. @staff_member_required
  927. def terms_editor(request, term=None):
  928. """
  929. Add/Editor a term using the JSON Editor.
  930. """
  931. if term is not None:
  932. existing_term = Term().load_by_title(term)
  933. data = existing_term.contents() if existing_term else {"name": term, "titles": []}
  934. else:
  935. generic_response = { "title": "Terms Editor", "content": "Please include the primary Term name in the URL to uses the Terms Editor." }
  936. return render(request,'static/generic.html', generic_response)
  937. dataJSON = json.dumps(data)
  938. return render(request,'edit_term.html',
  939. {
  940. 'term': term,
  941. 'dataJSON': dataJSON,
  942. 'is_update': "true" if existing_term else "false"
  943. })
  944. def interface_language_redirect(request, language):
  945. """
  946. Set the interfaceLang cookie, saves to UserProfile (if logged in)
  947. and redirects to `next` url param.
  948. """
  949. next = request.GET.get("next", "/?home")
  950. next = "/?home" if next == "undefined" else next
  951. for domain in DOMAIN_LANGUAGES:
  952. if DOMAIN_LANGUAGES[domain] == language and not request.get_host() in domain:
  953. next = domain + next
  954. next = next + ("&" if "?" in next else "?") + "set-language-cookie"
  955. break
  956. response = redirect(next)
  957. response.set_cookie("interfaceLang", language)
  958. if request.user.is_authenticated:
  959. p = UserProfile(id=request.user.id)
  960. p.settings["interface_language"] = language
  961. p.save()
  962. return response
  963. #todo: is this used elsewhere? move it?
  964. def count_and_index(c_oref, c_lang, vtitle, to_count=1):
  965. # count available segments of text
  966. if to_count:
  967. library.recount_index_in_toc(c_oref.index)
  968. if MULTISERVER_ENABLED:
  969. server_coordinator.publish_event("library", "recount_index_in_toc", [c_oref.index.title])
  970. from sefaria.settings import SEARCH_INDEX_ON_SAVE
  971. if SEARCH_INDEX_ON_SAVE:
  972. model.IndexQueue({
  973. "ref": c_oref.normal(),
  974. "lang": c_lang,
  975. "version": vtitle,
  976. "type": "ref",
  977. }).save()
  978. @catch_error_as_json
  979. @csrf_exempt
  980. def texts_api(request, tref):
  981. oref = Ref(tref)
  982. if request.method == "GET":
  983. uref = oref.url()
  984. if uref and tref != uref: # This is very similar to reader.reader_redirect subfunction, above.
  985. url = "/api/texts/" + uref
  986. response = redirect(iri_to_uri(url), permanent=True)
  987. params = request.GET.urlencode()
  988. response['Location'] += "?%s" % params if params else ""
  989. return response
  990. cb = request.GET.get("callback", None)
  991. context = int(request.GET.get("context", 1))
  992. commentary = bool(int(request.GET.get("commentary", False)))
  993. pad = bool(int(request.GET.get("pad", 1)))
  994. versionEn = request.GET.get("ven", None)
  995. if versionEn:
  996. versionEn = versionEn.replace("_", " ")
  997. versionHe = request.GET.get("vhe", None)
  998. if versionHe:
  999. versionHe = versionHe.replace("_", " ")
  1000. layer_name = request.GET.get("layer", None)
  1001. alts = bool(int(request.GET.get("alts", True)))
  1002. wrapLinks = bool(int(request.GET.get("wrapLinks", False)))
  1003. multiple = int(request.GET.get("multiple", 0)) # Either undefined, or a positive integer (indicating how many sections forward) or negtive integer (indicating backward)
  1004. def _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=commentary, context=context, pad=pad,
  1005. alts=alts, wrapLinks=wrapLinks, layer_name=layer_name):
  1006. try:
  1007. text = TextFamily(oref, version=versionEn, lang="en", version2=versionHe, lang2="he", commentary=commentary, context=context, pad=pad, alts=alts, wrapLinks=wrapLinks).contents()
  1008. except AttributeError as e:
  1009. oref = oref.default_child_ref()
  1010. text = TextFamily(oref, version=versionEn, lang="en", version2=versionHe, lang2="he", commentary=commentary, context=context, pad=pad, alts=alts, wrapLinks=wrapLinks).contents()
  1011. except NoVersionFoundError as e:
  1012. return {"error": unicode(e), "ref": oref.normal(), "enVersion": versionEn, "heVersion": versionHe}
  1013. # TODO: what if pad is false and the ref is of an entire book? Should next_section_ref return None in that case?
  1014. oref = oref.padded_ref() if pad else oref
  1015. try:
  1016. text["next"] = oref.next_section_ref().normal() if oref.next_section_ref() else None
  1017. text["prev"] = oref.prev_section_ref().normal() if oref.prev_section_ref() else None
  1018. except AttributeError as e:
  1019. # There are edge cases where the TextFamily call above works on a default node, but the next section call here does not.
  1020. oref = oref.default_child_ref()
  1021. text["next"] = oref.next_section_ref().normal() if oref.next_section_ref() else None
  1022. text["prev"] = oref.prev_section_ref().normal() if oref.prev_section_ref() else None
  1023. text["commentary"] = text.get("commentary", [])
  1024. text["sheets"] = get_sheets_for_ref(tref) if int(request.GET.get("sheets", 0)) else []
  1025. if layer_name:
  1026. layer = Layer().load({"urlkey": layer_name})
  1027. if not layer:
  1028. raise InputError("Layer not found.")
  1029. layer_content = [format_note_object_for_client(n) for n in layer.all(tref=tref)]
  1030. text["layer"] = layer_content
  1031. text["layer_name"] = layer_name
  1032. text["_loadSourcesFromDiscussion"] = True
  1033. else:
  1034. text["layer"] = []
  1035. return text
  1036. if not multiple or abs(multiple) == 1:
  1037. text = _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=commentary, context=context, pad=pad,
  1038. alts=alts, wrapLinks=wrapLinks, layer_name=layer_name)
  1039. return jsonResponse(text, cb)
  1040. else:
  1041. # Return list of many sections
  1042. target_count = int(multiple)
  1043. assert target_count != 0
  1044. direction = "next" if target_count > 0 else "prev"
  1045. target_count = abs(target_count)
  1046. current = 0
  1047. texts = []
  1048. while current < target_count:
  1049. text = _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=commentary, context=context, pad=pad,
  1050. alts=alts, wrapLinks=wrapLinks, layer_name=layer_name)
  1051. texts += [text]
  1052. if not text[direction]:
  1053. break
  1054. oref = Ref(text[direction])
  1055. current += 1
  1056. return jsonResponse(texts, cb)
  1057. if request.method == "POST":
  1058. j = request.POST.get("json")
  1059. if not j:
  1060. return jsonResponse({"error": "Missing 'json' parameter in post data."})
  1061. oref = oref.default_child_ref() # Make sure we're on the textual child
  1062. skip_links = request.GET.get("skip_links", False)
  1063. if not request.user.is_authenticated:
  1064. key = request.POST.get("apikey")
  1065. if not key:
  1066. return jsonResponse({"error": "You must be logged in or use an API key to save texts."})
  1067. apikey = db.apikeys.find_one({"key": key})
  1068. if not apikey:
  1069. return jsonResponse({"error": "Unrecognized API key."})
  1070. t = json.loads(j)
  1071. chunk = tracker.modify_text(apikey["uid"], oref, t["versionTitle"], t["language"], t["text"], t["versionSource"], method="API", skip_links=skip_links)
  1072. count_after = int(request.GET.get("count_after", 0))
  1073. count_and_index(oref, chunk.lang, chunk.vtitle, count_after)
  1074. return jsonResponse({"status": "ok"})
  1075. else:
  1076. @csrf_protect
  1077. def protected_post(request):
  1078. t = json.loads(j)
  1079. chunk = tracker.modify_text(request.user.id, oref, t["versionTitle"], t["language"], t["text"], t.get("versionSource", None), skip_links=skip_links)
  1080. count_after = int(request.GET.get("count_after", 1))
  1081. count_and_index(oref, chunk.lang, chunk.vtitle, count_after)
  1082. return jsonResponse({"status": "ok"})
  1083. return protected_post(request)
  1084. if request.method == "DELETE":
  1085. versionEn = request.GET.get("ven", None)
  1086. versionHe = request.GET.get("vhe", None)
  1087. if not request.user.is_staff:
  1088. return jsonResponse({"error": "Only moderators can delete texts."})
  1089. if not (tref and (versionEn or versionHe)):
  1090. return jsonResponse({"error": "To delete a text version please specifiy a text title, version title and language."})
  1091. tref = tref.replace("_", " ")
  1092. if versionEn:
  1093. versionEn = versionEn.replace("_", " ")
  1094. v = Version().load({"title": tref, "versionTitle": versionEn, "language": "en"})
  1095. if not v:
  1096. return jsonResponse({"error": "Text version not found."})
  1097. v.delete()
  1098. record_version_deletion(tref, versionEn, "en", request.user.id)
  1099. if USE_VARNISH:
  1100. invalidate_linked(oref)
  1101. invalidate_ref(oref, "en", versionEn)
  1102. if versionHe:
  1103. versionHe = versionHe.replace("_", " ")
  1104. v = Version().load({"title": tref, "versionTitle": versionHe, "language": "he"})
  1105. if not v:
  1106. return jsonResponse({"error": "Text version not found."})
  1107. v.delete()
  1108. record_version_deletion(tref, versionHe, "he", request.user.id)
  1109. if USE_VARNISH:
  1110. invalidate_linked(oref)
  1111. invalidate_ref(oref, "he", versionHe)
  1112. return jsonResponse({"status": "ok"})
  1113. return jsonResponse({"error": "Unsupported HTTP method."}, callback=request.GET.get("callback", None))
  1114. @catch_error_as_json
  1115. @csrf_exempt
  1116. def old_text_versions_api_redirect(request, tref, lang, version):
  1117. url = u"/api/texts/{}?v{}={}".format(tref, lang, version)
  1118. response = redirect(iri_to_uri(url), permanent=True)
  1119. params = request.GET.urlencode()
  1120. response['Location'] += "&{}".format(params) if params else ""
  1121. return response
  1122. def old_recent_redirect(request):
  1123. return redirect("/texts/history", permanent=True)
  1124. @catch_error_as_json
  1125. def parashat_hashavua_api(request):
  1126. callback = request.GET.get("callback", None)
  1127. p = this_weeks_parasha(datetime.now(), request.diaspora)
  1128. p["date"] = p["date"].isoformat()
  1129. #p.update(get_text(p["ref"]))
  1130. p.update(TextFamily(Ref(p["ref"])).contents())
  1131. return jsonResponse(p, callback)
  1132. @catch_error_as_json
  1133. def table_of_contents_api(request):
  1134. return jsonResponse(library.get_toc(), callback=request.GET.get("callback", None))
  1135. @catch_error_as_json
  1136. def search_filter_table_of_contents_api(request):
  1137. return jsonResponse(library.get_search_filter_toc(), callback=request.GET.get("callback", None))
  1138. @catch_error_as_json
  1139. def search_autocomplete_redirecter(request):
  1140. query = request.GET.get("q", "")
  1141. completions_dict = get_name_completions(query, 1, False)
  1142. ref = completions_dict['ref']
  1143. object_data = completions_dict['object_data']
  1144. if ref:
  1145. response = redirect(u'/{}'.format(ref.url()), permanent=False)
  1146. elif object_data is not None and object_data.get('type', '') == 'Person':
  1147. response = redirect(u'/person/{}'.format(object_data['key']), permanent=False)
  1148. elif object_data is not None and object_data.get('type', '') == 'TocCategory':
  1149. response = redirect(u'/{}'.format(object_data['key']), permanent=False)
  1150. else:
  1151. response = redirect(u'/search?q={}'.format(query), permanent=False)
  1152. return response
  1153. @catch_error_as_json
  1154. def opensearch_suggestions_api(request):
  1155. # see here for docs: http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.1
  1156. query = request.GET.get("q", "")
  1157. completions_dict = get_name_completions(query, 5, False)
  1158. ret_data = [
  1159. query,
  1160. completions_dict["completions"]
  1161. ]
  1162. return jsonResponse(ret_data, callback=request.GET.get("callback", None))
  1163. @catch_error_as_json
  1164. def text_titles_api(request):
  1165. return jsonResponse({"books": model.library.full_title_list()}, callback=request.GET.get("callback", None))
  1166. @catch_error_as_json
  1167. @csrf_exempt
  1168. def index_node_api(request, title):
  1169. pass
  1170. @catch_error_as_json
  1171. @csrf_exempt
  1172. def index_api(request, title, v2=False, raw=False):
  1173. """
  1174. API for manipulating text index records (aka "Text Info")
  1175. """
  1176. if request.method == "GET":
  1177. try:
  1178. if request.GET.get("with_content_counts", False):
  1179. i = library.get_index(title).contents_with_content_counts()
  1180. else:
  1181. i = library.get_index(title).contents(v2=v2, raw=raw)
  1182. except InputError as e:
  1183. node = library.get_schema_node(title) # If the request were for v1 and fails, this falls back to v2.
  1184. if not node:
  1185. raise e
  1186. if node.is_default():
  1187. node = node.parent
  1188. i = node.as_index_contents()
  1189. return jsonResponse(i, callback=request.GET.get("callback", None))
  1190. if request.method == "POST":
  1191. # use the update function if update is in the params
  1192. func = tracker.update if request.GET.get("update", False) else tracker.add
  1193. j = json.loads(request.POST.get("json"))
  1194. if not j:
  1195. return jsonResponse({"error": "Missing 'json' parameter in post data."})
  1196. j["title"] = title.replace("_", " ")
  1197. #todo: move this to texts_api, pass the changes down through the tracker and text chunk
  1198. #if "versionTitle" in j:
  1199. # if j["versionTitle"] == "Sefaria Community Translation":
  1200. # j["license"] = "CC0"
  1201. # j["licenseVetter"] = True
  1202. if not request.user.is_authenticated:
  1203. key = request.POST.get("apikey")
  1204. if not key:
  1205. return jsonResponse({"error": "You must be logged in or use an API key to save texts."})
  1206. apikey = db.apikeys.find_one({"key": key})
  1207. if not apikey:
  1208. return jsonResponse({"error": "Unrecognized API key."})
  1209. return jsonResponse(func(apikey["uid"], model.Index, j, method="API", v2=v2, raw=raw, force_complex=True).contents(v2=v2, raw=raw, force_complex=True))
  1210. else:
  1211. title = j.get("oldTitle", j.get("title"))
  1212. try:
  1213. library.get_index(title) # getting the index just to tell if it exists
  1214. # Only allow staff and the person who submitted a text to edit
  1215. if not request.user.is_staff and not user_started_text(request.user.id, title):
  1216. return jsonResponse({"error": "{} is protected from change.<br/><br/>See a mistake?<br/>Email hello@sefaria.org.".format(title)})
  1217. except BookNameError:
  1218. pass # if this is a new text, allow any logged in user to submit
  1219. @csrf_protect
  1220. def protected_index_post(request):
  1221. return jsonResponse(
  1222. func(request.user.id, model.Index, j, v2=v2, raw=raw, force_complex=True).contents(v2=v2, raw=raw, force_complex=True)
  1223. )
  1224. return protected_index_post(request)
  1225. if request.method == "DELETE":
  1226. if not request.user.is_staff:
  1227. return jsonResponse({"error": "Only moderators can delete texts indices."})
  1228. title = title.replace("_", " ")
  1229. i = library.get_index(title)
  1230. i.delete()
  1231. record_index_deletion(title, request.user.id)
  1232. return jsonResponse({"status": "ok"})
  1233. return jsonResponse({"error": "Unsupported HTTP method."}, callback=request.GET.get("callback", None))
  1234. @catch_error_as_json
  1235. def bare_link_api(request, book, cat):
  1236. if request.method == "GET":
  1237. resp = jsonResponse(get_book_link_collection(book, cat), callback=request.GET.get("callback", None))
  1238. resp['Content-Type'] = "application/json; charset=utf-8"
  1239. return resp
  1240. elif request.method == "POST":
  1241. return jsonResponse({"error": "Not implemented."})
  1242. @catch_error_as_json
  1243. def link_count_api(request, cat1, cat2):
  1244. """
  1245. Return a count document with the number of links between every text in cat1 and every text in cat2
  1246. """
  1247. if request.method == "GET":
  1248. resp = jsonResponse(get_link_counts(cat1, cat2))
  1249. return resp
  1250. elif request.method == "POST":
  1251. return jsonResponse({"error": "Not implemented."})
  1252. @catch_error_as_json
  1253. def word_count_api(request, title, version, language):
  1254. if request.method == "GET":
  1255. counts = VersionSet({"title": title, "versionTitle": version, "language": language}).word_count()
  1256. resp = jsonResponse({"wordCount": counts}, callback=request.GET.get("callback", None))
  1257. return resp
  1258. elif request.method == "POST":
  1259. return jsonResponse({"error": "Not implemented."})
  1260. @catch_error_as_json
  1261. def counts_api(request, title):
  1262. """
  1263. API for retrieving the counts document for a given text node.
  1264. :param title: A valid node title
  1265. """
  1266. title = title.replace("_", " ")
  1267. if request.method == "GET":
  1268. return jsonResponse(StateNode(title).contents(), callback=request.GET.get("callback", None))
  1269. elif request.method == "POST":
  1270. if not request.user.is_staff:
  1271. return jsonResponse({"error": "Not permitted."})
  1272. if "update" in request.GET:
  1273. flag = request.GET.get("flag", None)
  1274. if not flag:
  1275. return jsonResponse({"error": "'flag' parameter missing."})
  1276. val = request.GET.get("val", None)
  1277. val = True if val == "true" else False
  1278. vs = VersionState(title)
  1279. if not vs:
  1280. raise InputError("State not found for : {}".format(title))
  1281. vs.set_flag(flag, val).save()
  1282. return jsonResponse({"status": "ok"})
  1283. return jsonResponse({"error": "Not implemented."})
  1284. @catch_error_as_json
  1285. def shape_api(request, title):
  1286. """
  1287. API for retrieving a shape document for a given text or category.
  1288. For simple texts, returns a dict with keys:
  1289. {
  1290. "section": Category immediately above book?,
  1291. [Perhaps, instead, "categories"]
  1292. "heTitle": Hebrew title of node
  1293. "length": Number of chapters,
  1294. "chapters": List of Chapter Lengths (think about depth 1 & 3)
  1295. "title": English title of node
  1296. "book": English title of Book
  1297. }
  1298. For complex texts or categories, returns a list of dicts.
  1299. :param title: A valid node title or a path to a category, separated by /.
  1300. The "depth" parameter in the query string indicates how many levels in the category tree to descend. Default is 2.
  1301. If depth == 0, descends to end of tree
  1302. The "dependents" parameter, if true, includes dependent texts. By default, they are filtered out.
  1303. """
  1304. def _simple_shape(snode):
  1305. sn = StateNode(snode=snode)
  1306. shape = sn.var("all", "shape")
  1307. return {
  1308. "section": snode.index.categories[-1],
  1309. "heTitle": snode.primary_title("he"),
  1310. "title": snode.primary_title("en"),
  1311. "length": len(shape) if isinstance(shape, list) else 1, # hmmmm
  1312. "chapters": shape,
  1313. "book": snode.index.title,
  1314. }
  1315. title = title.replace("_", " ")
  1316. if request.method == "GET":
  1317. sn = library.get_schema_node(title, "en")
  1318. # Leaf Node
  1319. if sn and not sn.children:
  1320. res = _simple_shape(sn)
  1321. # Branch Node
  1322. elif sn and sn.children:
  1323. res = [_simple_shape(n) for n in sn.get_leaf_nodes()]
  1324. # Category
  1325. else:
  1326. cat = library.get_toc_tree().lookup(title.split("/"))
  1327. if not cat:
  1328. res = {"error": "No index or category found to match {}".format(title)}
  1329. else:
  1330. depth = request.GET.get("depth", 2)
  1331. include_dependents = request.GET.get("dependents", False)
  1332. leaves = cat.get_leaf_nodes() if depth == 0 else [n for n in cat.get_leaf_nodes_to_depth(depth)]
  1333. if not include_dependents:
  1334. leaves = [n for n in leaves if not n.dependence]
  1335. res = [_simple_shape(jan) for toc_index in leaves for jan in toc_index.get_index_object().nodes.get_leaf_nodes()]
  1336. return jsonResponse(res, callback=request.GET.get("callback", None))
  1337. @catch_error_as_json
  1338. def text_preview_api(request, title):
  1339. """
  1340. API for retrieving a document that gives preview text (first characters of each section)
  1341. for text 'title'
  1342. """
  1343. oref = Ref(title)
  1344. response = oref.index.contents(v2=True)
  1345. response['node_title'] = oref.index_node.full_title()
  1346. def get_preview(prev_oref):
  1347. text = TextFamily(prev_oref, pad=False, commentary=False)
  1348. if prev_oref.index_node.depth == 1:
  1349. # Give deeper previews for texts with depth 1 (boring to look at otherwise)
  1350. text.text, text.he = [[i] for i in text.text], [[i] for i in text.he]
  1351. preview = text_preview(text.text, text.he) if (text.text or text.he) else []
  1352. return preview if isinstance(preview, list) else [preview]
  1353. if not oref.index_node.has_children():
  1354. response['preview'] = get_preview(oref)
  1355. elif oref.index_node.has_default_child():
  1356. r = oref.index_node.get_default_child().ref() # Get ref through ref() to get default leaf node and avoid getting parent node
  1357. response['preview'] = get_preview(r)
  1358. return jsonResponse(response, callback=request.GET.get("callback", None))
  1359. def revarnish_link(link):
  1360. if USE_VARNISH:
  1361. for ref in link.refs:
  1362. invalidate_ref(Ref(ref), purge=True)
  1363. @catch_error_as_json
  1364. @csrf_exempt
  1365. def links_api(request, link_id_or_ref=None):
  1366. """
  1367. API for textual links.
  1368. Currently also handles post notes.
  1369. #TODO: can we distinguish between a link_id (mongo id) for POSTs and a ref for GETs?
  1370. """
  1371. if request.method == "GET":
  1372. callback=request.GET.get("callback", None)
  1373. if link_id_or_ref is None:
  1374. return jsonResponse({"error": "Missing text identifier"}, callback)
  1375. #The Ref instanciation is just to validate the Ref and let an error bubble up.
  1376. #TODO is there are better way to validate the ref from GET params?
  1377. model.Ref(link_id_or_ref)
  1378. with_text = int(request.GET.get("with_text", 1))
  1379. return jsonResponse(get_links(link_id_or_ref, with_text), callback)
  1380. if request.method == "POST":
  1381. def _internal_do_post(request, link, uid, **kwargs):
  1382. func = tracker.update if "_id" in link else tracker.add
  1383. # use the correct function if params indicate this is a note save
  1384. # func = save_note if "type" in j and j["type"] == "note" else save_link
  1385. #obj = func(apikey["uid"], model.Link, link, **kwargs)
  1386. obj = func(uid, model.Link, link, **kwargs)
  1387. try:
  1388. if USE_VARNISH:
  1389. revarnish_link(obj)
  1390. except Exception as e:
  1391. logger.error(e)
  1392. return format_object_for_client(obj)
  1393. # delegate according to single/multiple objects posted
  1394. if not request.user.is_authenticated:
  1395. key = request.POST.get("apikey")
  1396. if not key:
  1397. return jsonResponse({"error": "You must be logged in or use an API key to add, edit or delete links."})
  1398. apikey = db.apikeys.find_one({"key": key})
  1399. if not apikey:
  1400. return jsonResponse({"error": "Unrecognized API key."})
  1401. uid = apikey["uid"]
  1402. kwargs = {"method": "API"}
  1403. else:
  1404. uid = request.user.id
  1405. kwargs = {}
  1406. _internal_do_post = csrf_protect(_internal_do_post)
  1407. j = request.POST.get("json")
  1408. if not j:
  1409. return jsonResponse({"error": "Missing 'json' parameter in post data."})
  1410. j = json.loads(j)
  1411. if isinstance(j, list):
  1412. res = []
  1413. for i in j:
  1414. try:
  1415. retval = _internal_do_post(request, i, uid, **kwargs)
  1416. res.append({"status": "ok. Link: {} | {} Saved".format(retval["ref"], retval["anchorRef"])})
  1417. except Exception as e:
  1418. res.append({"error": "Link: {} | {} Error: {}".format(i["refs"][0], i["refs"][1], unicode(e))})
  1419. try:
  1420. res_slice = request.GET.get("truncate_response", None)
  1421. if res_slice:
  1422. res_slice = int(res_slice)
  1423. except Exception as e:
  1424. res_slice = None
  1425. return jsonResponse(res[:res_slice])
  1426. else:
  1427. return jsonResponse(_internal_do_post(request, j, uid, **kwargs))
  1428. if request.method == "DELETE":
  1429. if not link_id_or_ref:
  1430. return jsonResponse({"error": "No link id given for deletion."})
  1431. return jsonResponse(
  1432. tracker.delete(request.user.id, model.Link, link_id_or_ref, callback=revarnish_link)
  1433. )
  1434. return jsonResponse({"error": "Unsupported HTTP method."})
  1435. @catch_error_as_json
  1436. @csrf_exempt
  1437. def link_summary_api(request, ref):
  1438. """
  1439. Returns a summary of links available for ref.
  1440. """
  1441. oref = Ref(ref)
  1442. summary = oref.linkset().summary(oref)
  1443. return jsonResponse(summary, callback=request.GET.get("callback", None))
  1444. @catch_error_as_json
  1445. @csrf_exempt
  1446. def notes_api(request, note_id_or_ref):
  1447. """
  1448. API for user notes.
  1449. A call to this API with GET returns the list of public notes and private notes belong to the current user on this Ref.
  1450. """
  1451. if request.method == "GET":
  1452. if not note_id_or_ref:
  1453. raise Http404
  1454. oref = Ref(note_id_or_ref)
  1455. cb = request.GET.get("callback", None)
  1456. private = request.GET.get("private", False)
  1457. res = get_notes(oref, uid=request.user.id, public=(not private))
  1458. return jsonResponse(res, cb)
  1459. if request.method == "POST":
  1460. j = request.POST.get("json")
  1461. if not j:
  1462. return jsonResponse({"error": "Missing 'json' parameter in post data."})
  1463. note = json.loads(j)
  1464. if "refs" in note:
  1465. # If data was posted with an array or refs, squish them into one
  1466. # This assumes `refs` are sequential.
  1467. note["ref"] = Ref(note["refs"][0]).to(Ref(note["refs"][-1])).normal()
  1468. del note["refs"]
  1469. func = tracker.update if "_id" in note else tracker.add
  1470. if "_id" in note:
  1471. note["_id"] = ObjectId(note["_id"])
  1472. if not request.user.is_authenticated:
  1473. key = request.POST.get("apikey")
  1474. if not key:
  1475. return jsonResponse({"error": "You must be logged in or use an API key to add, edit or delete links."})
  1476. apikey = db.apikeys.find_one({"key": key})
  1477. if not apikey:
  1478. return jsonResponse({"error": "Unrecognized API key."})
  1479. note["owner"] = apikey["uid"]
  1480. response = format_object_for_client(
  1481. func(apikey["uid"], model.Note, note, method="API")
  1482. )
  1483. else:
  1484. note["owner"] = request.user.id
  1485. @csrf_protect
  1486. def protected_note_post(req):
  1487. resp = format_object_for_client(
  1488. func(req.user.id, model.Note, note)
  1489. )
  1490. return resp
  1491. response = protected_note_post(request)
  1492. if request.POST.get("layer", None):
  1493. layer = Layer().load({"urlkey": request.POST.get("layer")})
  1494. if not layer:
  1495. raise InputError("Layer not found.")
  1496. else:
  1497. # Create notifications for this activity
  1498. path = "/" + note["ref"] + "?layer=" + layer.urlkey
  1499. if ObjectId(response["_id"]) not in layer.note_ids:
  1500. # only notify for new notes, not edits
  1501. for uid in layer.listeners():
  1502. if request.user.id == uid:
  1503. continue
  1504. n = Notification({"uid": uid})
  1505. n.make_discuss(adder_id=request.user.id, discussion_path=path)
  1506. n.save()
  1507. layer.add_note(response["_id"])
  1508. layer.save()
  1509. return jsonResponse(response)
  1510. if request.method == "DELETE":
  1511. if not request.user.is_authenticated:
  1512. return jsonResponse({"error": "You must be logged in to delete notes."})
  1513. return jsonResponse(
  1514. tracker.delete(request.user.id, model.Note, note_id_or_ref)
  1515. )
  1516. return jsonResponse({"error": "Unsupported HTTP method."})
  1517. @catch_error_as_json
  1518. def all_notes_api(request):
  1519. private = request.GET.get("private", False)
  1520. if private:
  1521. if not request.user.is_authenticated:
  1522. res = {"error": "You must be logged in to access you notes."}
  1523. else:
  1524. res = [note.contents(with_string_id=True) for note in NoteSet({"owner": request.user.id}, sort=[("_id", -1)]) ]
  1525. else:
  1526. resr = {"error": "Not implemented."}
  1527. return jsonResponse(res, callback=request.GET.get("callback", None))
  1528. @catch_error_as_json
  1529. def related_api(request, tref):
  1530. """
  1531. Single API to bundle available content related to `tref`.
  1532. """
  1533. oref = model.Ref(tref)
  1534. if request.GET.get("private", False) and request.user.is_authenticated:
  1535. response = {
  1536. "sheets": get_sheets_for_ref(tref, uid=request.user.id),
  1537. "notes": get_notes(oref, uid=request.user.id, public=False)
  1538. }
  1539. elif request.GET.get("private", False) and not request.user.is_authenticated:
  1540. response = {"error": "You must be logged in to access private content."}
  1541. else:
  1542. response = {
  1543. "links": get_links(tref, with_text=False),
  1544. "sheets": get_sheets_for_ref(tref),
  1545. "notes": [], # get_notes(oref, public=True) # Hiding public notes for now
  1546. }
  1547. return jsonResponse(response, callback=request.GET.get("callback", None))
  1548. @catch_error_as_json
  1549. def versions_api(request, tref):
  1550. """
  1551. API for retrieving available text versions list of a ref.
  1552. """
  1553. oref = model.Ref(tref)
  1554. versions = oref.version_list()
  1555. return jsonResponse(versions, callback=request.GET.get("callback", None))
  1556. @catch_error_as_json
  1557. def version_status_api(request):
  1558. res = []
  1559. for v in VersionSet():
  1560. try:
  1561. res.append({
  1562. "id": str(v._id),
  1563. "title": v.title,
  1564. "version": v.versionTitle,
  1565. "language": v.language,
  1566. "categories": v.get_index().categories,
  1567. "wordcount": v.word_count()
  1568. })
  1569. except Exception:
  1570. pass
  1571. return jsonResponse(sorted(res, key = lambda x: x["title"] + x["version"]), callback=request.GET.get("callback", None))
  1572. simplified_toc = {}
  1573. def version_status_tree_api(request, lang=None):
  1574. global simplified_toc
  1575. key = lang or "none"
  1576. if not simplified_toc.get(key):
  1577. def simplify_toc(toc_node, path):
  1578. simple_nodes = []
  1579. for x in toc_node:
  1580. node_name = x.get("category", None) or x.get("title", None)
  1581. node_path = path + [node_name]
  1582. simple_node = {
  1583. "name": node_name,
  1584. "path": node_path
  1585. }
  1586. if "category" in x:
  1587. if "contents" not in x:
  1588. continue
  1589. simple_node["type"] = "category"
  1590. simple_node["children"] = simplify_toc(x["contents"], node_path)
  1591. elif "title" in x:
  1592. query = {"title": x["title"]}
  1593. if lang:
  1594. query["language"] = lang
  1595. simple_node["type"] = "index"
  1596. simple_node["children"] = [{
  1597. "name": u"{} ({})".format(v.versionTitle, v.language),
  1598. "path": node_path + [u"{} ({})".format(v.versionTitle, v.language)],
  1599. "size": v.word_count(),
  1600. "type": "version"
  1601. } for v in VersionSet(query)]
  1602. simple_nodes.append(simple_node)
  1603. return simple_nodes
  1604. simplified_toc[key] = simplify_toc(library.get_toc(), [])
  1605. return jsonResponse({
  1606. "name": "Whole Library" + " ({})".format(lang) if lang else "",
  1607. "path": [],
  1608. "children": simplified_toc[key]
  1609. }, callback=request.GET.get("callback", None))
  1610. def visualize_library(request, lang=None, cats=None):
  1611. template_vars = {"lang": lang or "",
  1612. "cats": json.dumps(cats.replace("_", " ").split("/") if cats else [])}
  1613. return render(request,'visual_library.html', template_vars)
  1614. def visualize_toc(request):
  1615. return render(request,'visual_toc.html', {})
  1616. def visualize_parasha_colors(request):
  1617. return render(request,'visual_parasha_colors.html', {})
  1618. def visualize_links_through_rashi(request):
  1619. level = request.GET.get("level", 1)
  1620. json_file = "../static/files/torah_rashi_torah.json" if level == 1 else "../static/files/tanach_rashi_tanach.json"
  1621. return render(request,'visualize_links_through_rashi.html', {"json_file": json_file})
  1622. def talmudic_relationships(request):
  1623. json_file = "../static/files/talmudic_relationships_data.json"
  1624. return render(request,'talmudic_relationships.html', {"json_file": json_file})
  1625. def sefer_hachinukh_mitzvot(request):
  1626. csv_file = "../static/files/mitzvot.csv"
  1627. return render(request,'sefer_hachinukh_mitzvot.html', {"csv": csv_file})
  1628. @catch_error_as_json
  1629. def set_lock_api(request, tref, lang, version):
  1630. """
  1631. API to set an edit lock on a text segment.
  1632. """
  1633. user = request.user.id if request.user.is_authenticated else 0
  1634. model.set_lock(model.Ref(tref).normal(), lang, version.replace("_", " "), user)
  1635. return jsonResponse({"status": "ok"})
  1636. @catch_error_as_json
  1637. def release_lock_api(request, tref, lang, version):
  1638. """
  1639. API to release the edit lock on a text segment.
  1640. """
  1641. model.release_lock(model.Ref(tref).normal(), lang, version.replace("_", " "))
  1642. return jsonResponse({"status": "ok"})
  1643. @catch_error_as_json
  1644. def check_lock_api(request, tref, lang, version):
  1645. """
  1646. API to check whether a text segment currently has an edit lock.
  1647. """
  1648. locked = model.check_lock(model.Ref(tref).normal(), lang, version.replace("_", " "))
  1649. return jsonResponse({"locked": locked})
  1650. @catch_error_as_json
  1651. def lock_text_api(request, title, lang, version):
  1652. """
  1653. API for locking or unlocking a text as a whole.
  1654. To unlock, include the URL parameter "action=unlock"
  1655. """
  1656. if not request.user.is_staff:
  1657. return jsonResponse({"error": "Only Sefaria Moderators can lock texts."})
  1658. title = title.replace("_", " ")
  1659. version = version.replace("_", " ")
  1660. vobj = Version().load({"title": title, "language": lang, "versionTitle": version})
  1661. if request.GET.get("action", None) == "unlock":
  1662. vobj.status = None
  1663. else:
  1664. vobj.status = "locked"
  1665. vobj.save()
  1666. return jsonResponse({"status": "ok"})
  1667. @catch_error_as_json
  1668. @csrf_exempt
  1669. def flag_text_api(request, title, lang, version):
  1670. """
  1671. API for manipulating attributes of versions.
  1672. versionTitle changes are handled with an attribute called `newVersionTitle`
  1673. Non-Identifying attributes handled:
  1674. versionSource, versionNotes, license, priority, digitizedBySefaria
  1675. `language` attributes are not handled.
  1676. """
  1677. if not request.user.is_authenticated:
  1678. key = request.POST.get("apikey")
  1679. if not key:
  1680. return jsonResponse({"error": "You must be logged in or use an API key to perform this action."})
  1681. apikey = db.apikeys.find_one({"key": key})
  1682. if not apikey:
  1683. return jsonResponse({"error": "Unrecognized API key."})
  1684. user = User.objects.get(id=apikey["uid"])
  1685. if not user.is_staff:
  1686. return jsonResponse({"error": "Only Sefaria Moderators can flag texts."})
  1687. flags = json.loads(request.POST.get("json"))
  1688. title = title.replace("_", " ")
  1689. version = version.replace("_", " ")
  1690. vobj = Version().load({"title": title, "language": lang, "versionTitle": version})
  1691. if flags.get("newVersionTitle"):
  1692. vobj.versionTitle = flags.get("newVersionTitle")
  1693. for flag in vobj.optional_attrs:
  1694. if flag in flags:
  1695. setattr(vobj, flag, flags[flag])
  1696. vobj.save()
  1697. return jsonResponse({"status": "ok"})
  1698. elif request.user.is_staff:
  1699. @csrf_protect
  1700. def protected_post(request, title, lang, version):
  1701. flags = json.loads(request.POST.get("json"))
  1702. title = title.replace("_", " ")
  1703. version = version.replace("_", " ")
  1704. vobj = Version().load({"title": title, "language": lang, "versionTitle": version})
  1705. if flags.get("newVersionTitle"):
  1706. vobj.versionTitle = flags.get("newVersionTitle")
  1707. for flag in vobj.optional_attrs:
  1708. if flag in flags:
  1709. setattr(vobj, flag, flags[flag])
  1710. vobj.save()
  1711. return jsonResponse({"status": "ok"})
  1712. return protected_post(request, title, lang, version)
  1713. else:
  1714. return jsonResponse({"error": "Unauthorized"})
  1715. @catch_error_as_json
  1716. @csrf_exempt
  1717. def category_api(request, path=None):
  1718. """
  1719. API for looking up categories and adding Categories to the Category collection.
  1720. GET takes a category path on the URL. Returns the category specified.
  1721. e.g. "api/category/Tanakh/Torah"
  1722. If the category is not found, it will return "error" in a json object.
  1723. It will also attempt to find the closest parent. If found, it will include "closest_parent" alongside "error".
  1724. POST takes no arguments on the URL. Takes complete category as payload. Category must not already exist. Parent of category must exist.
  1725. """
  1726. if request.method == "GET":
  1727. if not path:
  1728. return jsonResponse({"error": "Please provide category path."})
  1729. cats = path.split("/")
  1730. cat = Category().load({"path": cats})
  1731. if cat:
  1732. return jsonResponse(cat.contents())
  1733. else:
  1734. for i in range(len(cats) - 1, 0, -1):
  1735. cat = Category().load({"path": cats[:i]})
  1736. if cat:
  1737. return jsonResponse({"error": "Category not found", "closest_parent": cat.contents()})
  1738. return jsonResponse({"error": "Category not found"})
  1739. if request.method == "POST":
  1740. def _internal_do_post(request, cat, uid, **kwargs):
  1741. return tracker.add(uid, model.Category, cat, **kwargs).contents()
  1742. if not request.user.is_authenticated:
  1743. key = request.POST.get("apikey")
  1744. if not key:
  1745. return jsonResponse({"error": "You must be logged in or use an API key to add or delete categories."})
  1746. apikey = db.apikeys.find_one({"key": key})
  1747. if not apikey:
  1748. return jsonResponse({"error": "Unrecognized API key."})
  1749. user = User.objects.get(id=apikey["uid"])
  1750. if not user.is_staff:
  1751. return jsonResponse({"error": "Only Sefaria Moderators can add or delete categories."})
  1752. uid = apikey["uid"]
  1753. kwargs = {"method": "API"}
  1754. elif request.user.is_staff:
  1755. uid = request.user.id
  1756. kwargs = {}
  1757. _internal_do_post = csrf_protect(_internal_do_post)
  1758. else:
  1759. return jsonResponse({"error": "Only Sefaria Moderators can add or delete categories."})
  1760. j = request.POST.get("json")
  1761. if not j:
  1762. return jsonResponse({"error": "Missing 'json' parameter in post data."})
  1763. j = json.loads(j)
  1764. if "path" not in j:
  1765. return jsonResponse({"error": "'path' is a required attribute"})
  1766. if Category().load({"path": j["path"]}):
  1767. return jsonResponse({"error": "Category {} already exists.".format(u", ".join(j["path"]))})
  1768. if not Category().load({"path": j["path"][:-1]}):
  1769. return jsonResponse({"error": "No parent category found: {}".format(u", ".join(j["path"][:-1]))})
  1770. return jsonResponse(_internal_do_post(request, j, uid, **kwargs))
  1771. if request.method == "DELETE":
  1772. return jsonResponse({"error": "Unsupported HTTP method."}) # TODO: support this?
  1773. return jsonResponse({"error": "Unsupported HTTP method."})
  1774. @catch_error_as_json
  1775. @csrf_exempt
  1776. def calendars_api(request):
  1777. if request.method == "GET":
  1778. import datetime
  1779. diaspora = request.GET.get("diaspora", "1")
  1780. custom = request.GET.get("custom", None)
  1781. try:
  1782. year = int(request.GET.get("year", None))
  1783. month = int(request.GET.get("month", None))
  1784. day = int(request.GET.get("day", None))
  1785. datetimeobj = datetime.datetime(year, month, day)
  1786. except Exception as e:
  1787. datetimeobj = timezone.localtime(timezone.now())
  1788. if diaspora not in ["0", "1"]:
  1789. return jsonResponse({"error": "'Diaspora' parameter must be 1 or 0."})
  1790. else:
  1791. diaspora = True if diaspora == "1" else False
  1792. calendars = get_all_calendar_items(datetimeobj, diaspora=diaspora, custom=custom)
  1793. return jsonResponse({"date": datetimeobj.date().isoformat(),
  1794. "timezone" : timezone.get_current_timezone_name(),
  1795. "calendar_items": calendars},
  1796. callback=request.GET.get("callback", None))
  1797. @catch_error_as_json
  1798. @csrf_exempt
  1799. def terms_api(request, name):
  1800. """
  1801. API for adding a Term to the Term collection.
  1802. This is mainly to be used for adding hebrew internationalization language for section names, categories and commentators
  1803. """
  1804. if request.method == "GET":
  1805. term = Term().load({'name': name}) or Term().load_by_title(name)
  1806. if term is None:
  1807. return jsonResponse({"error": "Term does not exist."})
  1808. else:
  1809. return jsonResponse(term.contents(), callback=request.GET.get("callback", None))
  1810. if request.method in ("POST", "DELETE"):
  1811. def _internal_do_post(request, uid):
  1812. t = Term().load({'name': name}) or Term().load_by_title(name)
  1813. if request.method == "POST":
  1814. term = request.POST.get("json")
  1815. if not term:
  1816. return {"error": "Missing 'json' parameter in POST data."}
  1817. term = json.loads(term)
  1818. if t and not request.GET.get("update"):
  1819. return {"error": "Term already exists."}
  1820. elif t and request.GET.get("update"):
  1821. term["_id"] = t._id
  1822. func = tracker.update if request.GET.get("update", False) else tracker.add
  1823. return func(uid, model.Term, term, **kwargs).contents()
  1824. elif request.method == "DELETE":
  1825. if not t:
  1826. return {"error": 'Term "%s" does not exist.' % term}
  1827. return tracker.delete(uid, model.Term, t._id)
  1828. if not request.user.is_authenticated:
  1829. key = request.POST.get("apikey")
  1830. if not key:
  1831. return jsonResponse({"error": "You must be logged in or use an API key to add, edit or delete terms."})
  1832. apikey = db.apikeys.find_one({"key": key})
  1833. if not apikey:
  1834. return jsonResponse({"error": "Unrecognized API key."})
  1835. user = User.objects.get(id=apikey["uid"])
  1836. if not user.is_staff:
  1837. return jsonResponse({"error": "Only Sefaria Moderators can add or edit terms."})
  1838. uid = apikey["uid"]
  1839. kwargs = {"method": "API"}
  1840. elif request.user.is_staff:
  1841. uid = request.user.id
  1842. kwargs = {}
  1843. _internal_do_post = csrf_protect(_internal_do_post)
  1844. else:
  1845. return jsonResponse({"error": "Only Sefaria Moderators can add or edit terms."})
  1846. return jsonResponse(_internal_do_post(request, uid))
  1847. return jsonResponse({"error": "Unsupported HTTP method."})
  1848. def get_name_completions(name, limit, ref_only):
  1849. lang = "he" if is_hebrew(name) else "en"
  1850. completer = library.ref_auto_completer(lang) if ref_only else library.full_auto_completer(lang)
  1851. object_data = None
  1852. ref = None
  1853. try:
  1854. ref = Ref(name)
  1855. inode = ref.index_node
  1856. # Find possible dictionary entries. This feels like a messy way to do this. Needs a refactor.
  1857. if inode.is_virtual and inode.parent and getattr(inode.parent, "lexiconName", None) in library._lexicon_auto_completer:
  1858. base_title = inode.parent.full_title()
  1859. lexicon_ac = library.lexicon_auto_completer(inode.parent.lexiconName)
  1860. t = [base_title + u", " + t[1] for t in lexicon_ac.items(inode.word)[:limit or None]]
  1861. completions = list(OrderedDict.fromkeys(t)) # filter out dupes
  1862. else:
  1863. completions = [name.capitalize()] + completer.next_steps_from_node(name)
  1864. if limit == 0 or len(completions) < limit:
  1865. current = {t: 1 for t in completions}
  1866. additional_results = completer.complete(name, limit)
  1867. for res in additional_results:
  1868. if res not in current:
  1869. completions += [res]
  1870. except DictionaryEntryNotFound as e:
  1871. # A dictionary beginning, but not a valid entry
  1872. lexicon_ac = library.lexicon_auto_completer(e.lexicon_name)
  1873. t = [e.base_title + u", " + t[1] for t in lexicon_ac.items(e.word)[:limit or None]]
  1874. completions = list(OrderedDict.fromkeys(t)) # filter out dupes
  1875. except InputError:
  1876. completions = completer.complete(name, limit)
  1877. object_data = completer.get_data(name)
  1878. return {
  1879. "completions": completions,
  1880. "lang": lang,
  1881. "object_data": object_data,
  1882. "ref": ref
  1883. }
  1884. @catch_error_as_json
  1885. def name_api(request, name):
  1886. if request.method != "GET":
  1887. return jsonResponse({"error": "Unsupported HTTP method."})
  1888. # Number of results to return. 0 indicates no limit
  1889. LIMIT = int(request.GET.get("limit", 16))
  1890. ref_only = request.GET.get("ref_only", False)
  1891. completions_dict = get_name_completions(name, LIMIT, ref_only)
  1892. ref = completions_dict["ref"]
  1893. if ref:
  1894. inode = ref.index_node
  1895. d = {
  1896. "lang": completions_dict["lang"],
  1897. "is_ref": True,
  1898. "is_book": ref.is_book_level(),
  1899. "is_node": len(ref.sections) == 0,
  1900. "is_section": ref.is_section_level(),
  1901. "is_segment": ref.is_segment_level(),
  1902. "is_range": ref.is_range(),
  1903. "type": "ref",
  1904. "ref": ref.normal(),
  1905. "url": ref.url(),
  1906. "index": ref.index.title,
  1907. "book": ref.book,
  1908. "internalSections": ref.sections,
  1909. "internalToSections": ref.toSections,
  1910. "sections": ref.normal_sections(), # this switch is to match legacy behavior of parseRef
  1911. "toSections": ref.normal_toSections(),
  1912. # "number_follows": inode.has_numeric_continuation(),
  1913. # "titles_follow": titles_follow,
  1914. "completions": completions_dict["completions"] if LIMIT == 0 else completions_dict["completions"][:LIMIT],
  1915. # todo: ADD textual completions as well
  1916. "examples": []
  1917. }
  1918. if inode.has_numeric_continuation():
  1919. inode = inode.get_default_child() if inode.has_default_child() else inode
  1920. d["sectionNames"] = inode.sectionNames
  1921. d["heSectionNames"] = map(hebrew_term, inode.sectionNames)
  1922. d["addressExamples"] = [t.toStr("en", 3*i+3) for i,t in enumerate(inode._addressTypes)]
  1923. d["heAddressExamples"] = [t.toStr("he", 3*i+3) for i,t in enumerate(inode._addressTypes)]
  1924. else:
  1925. # This is not a Ref
  1926. d = {
  1927. "lang": completions_dict["lang"],
  1928. "is_ref": False,
  1929. "completions": completions_dict["completions"]
  1930. }
  1931. # let's see if it's a known name of another sort
  1932. if completions_dict["object_data"]:
  1933. d["type"] = completions_dict["object_data"]["type"]
  1934. d["key"] = completions_dict["object_data"]["key"]
  1935. return jsonResponse(d)
  1936. @catch_error_as_json
  1937. def dictionary_completion_api(request, word, lexicon=None):
  1938. """
  1939. Given a dictionary, looks up the word in that dictionary
  1940. :param request:
  1941. :param word:
  1942. :param dictionary:
  1943. :return:
  1944. """
  1945. if request.method != "GET":
  1946. return jsonResponse({"error": "Unsupported HTTP method."})
  1947. # Number of results to return. 0 indicates no limit
  1948. LIMIT = int(request.GET.get("limit", 10))
  1949. if lexicon is None:
  1950. ac = library.cross_lexicon_auto_completer()
  1951. rs = ac.complete(word, LIMIT)
  1952. result = [[r, ac.title_trie[ac.normalizer(r)]["key"]] for r in rs]
  1953. else:
  1954. result = library.lexicon_auto_completer(lexicon).items(word)[:LIMIT]
  1955. return jsonResponse(result)
  1956. @catch_error_as_json
  1957. def dictionary_api(request, word):
  1958. """
  1959. Looks for lexicon entries for the given string.
  1960. If the string is more than one word, this will look for substring matches when not finding for the original input
  1961. Optional attributes:
  1962. 'lookup_ref' to fine tune the search
  1963. 'never_split' to limit lookup to only the actual input string
  1964. 'always_split' to look for substring matches regardless of results for original input
  1965. :param request:
  1966. :param word:
  1967. :return:
  1968. """
  1969. kwargs = {}
  1970. for key in ["lookup_ref", "never_split", "always_split"]:
  1971. if request.GET.get(key, None):
  1972. kwargs[key] = request.GET.get(key)
  1973. result = []
  1974. ls = LexiconLookupAggregator.lexicon_lookup(word, **kwargs)
  1975. if ls:
  1976. for l in ls:
  1977. result.append(l.contents())
  1978. if len(result):
  1979. return jsonResponse(result, callback=request.GET.get("callback", None))
  1980. else:
  1981. return jsonResponse({"error": "No information found for given word."})
  1982. @catch_error_as_json
  1983. def updates_api(request, gid=None):
  1984. """
  1985. API for retrieving general notifications.
  1986. """
  1987. if request.method == "GET":
  1988. page = int(request.GET.get("page", 0))
  1989. page_size = int(request.GET.get("page_size", 10))
  1990. notifications = GlobalNotificationSet({},limit=page_size, page=page)
  1991. return jsonResponse({
  1992. "updates": notifications.contents(),
  1993. "page": page,
  1994. "page_size": page_size,
  1995. "count": notifications.count()
  1996. })
  1997. elif request.method == "POST":
  1998. if not request.user.is_authenticated:
  1999. key = request.POST.get("apikey")
  2000. if not key:
  2001. return jsonResponse({"error": "You must be logged in or use an API key to perform this action."})
  2002. apikey = db.apikeys.find_one({"key": key})
  2003. if not apikey:
  2004. return jsonResponse({"error": "Unrecognized API key."})
  2005. user = User.objects.get(id=apikey["uid"])
  2006. if not user.is_staff:
  2007. return jsonResponse({"error": "Only Sefaria Moderators can add announcements."})
  2008. payload = json.loads(request.POST.get("json"))
  2009. try:
  2010. GlobalNotification(payload).save()
  2011. return jsonResponse({"status": "ok"})
  2012. except AssertionError as e:
  2013. return jsonResponse({"error": e.message})
  2014. elif request.user.is_staff:
  2015. @csrf_protect
  2016. def protected_post(request):
  2017. payload = json.loads(request.POST.get("json"))
  2018. try:
  2019. GlobalNotification(payload).save()
  2020. return jsonResponse({"status": "ok"})
  2021. except AssertionError as e:
  2022. return jsonResponse({"error": e.message})
  2023. return protected_post(request)
  2024. else:
  2025. return jsonResponse({"error": "Unauthorized"})
  2026. elif request.method == "DELETE":
  2027. if not gid:
  2028. return jsonResponse({"error": "No post id given for deletion."})
  2029. if request.user.is_staff:
  2030. @csrf_protect
  2031. def protected_post(request):
  2032. GlobalNotification().load_by_id(gid).delete()
  2033. return jsonResponse({"status": "ok"})
  2034. return protected_post(request)
  2035. else:
  2036. return jsonResponse({"error": "Unauthorized"})
  2037. @catch_error_as_json
  2038. def notifications_api(request):
  2039. """
  2040. API for retrieving user notifications.
  2041. """
  2042. if not request.user.is_authenticated:
  2043. return jsonResponse({"error": "You must be logged in to access your notifications."})
  2044. page = int(request.GET.get("page", 0))
  2045. page_size = int(request.GET.get("page_size", 10))
  2046. notifications = NotificationSet().recent_for_user(request.user.id, limit=page_size, page=page)
  2047. return jsonResponse({
  2048. "html": notifications.to_HTML(),
  2049. "page": page,
  2050. "page_size": page_size,
  2051. "count": notifications.count()
  2052. })
  2053. @catch_error_as_json
  2054. def notifications_read_api(request):
  2055. """
  2056. API for marking notifications as read
  2057. Takes JSON in the "notifications" parameter of an array of
  2058. notifcation ids as strings.
  2059. """
  2060. if request.method == "POST":
  2061. notifications = request.POST.get("notifications")
  2062. if not notifications:
  2063. return jsonResponse({"error": "'notifications' post parameter missing."})
  2064. notifications = json.loads(notifications)
  2065. for id in notifications:
  2066. notification = Notification().load_by_id(id)
  2067. if notification.uid != request.user.id:
  2068. # Only allow expiring your own notifications
  2069. continue
  2070. notification.mark_read().save()
  2071. return jsonResponse({
  2072. "status": "ok",
  2073. "unreadCount": unread_notifications_count_for_user(request.user.id)
  2074. })
  2075. else:
  2076. return jsonResponse({"error": "Unsupported HTTP method."})
  2077. @catch_error_as_json
  2078. def messages_api(request):
  2079. """
  2080. API for posting user to user messages
  2081. """
  2082. if not request.user.is_authenticated:
  2083. return jsonResponse({"error": "You must be logged in to access your messages."})
  2084. if request.method == "POST":
  2085. j = request.POST.get("json")
  2086. if not j:
  2087. return jsonResponse({"error": "No post JSON."})
  2088. j = json.loads(j)
  2089. Notification({"uid": j["recipient"]}).make_message(sender_id=request.user.id, message=j["message"]).save()
  2090. return jsonResponse({"status": "ok"})
  2091. elif request.method == "GET":
  2092. return jsonResponse({"error": "Unsupported HTTP method."})
  2093. @catch_error_as_json
  2094. def follow_api(request, action, uid):
  2095. """
  2096. API for following and unfollowing another user.
  2097. """
  2098. if request.method != "POST":
  2099. return jsonResponse({"error": "Unsupported HTTP method."})
  2100. if not request.user.is_authenticated:
  2101. return jsonResponse({"error": "You must be logged in to follow."})
  2102. follow = FollowRelationship(follower=request.user.id, followee=int(uid))
  2103. if action == "follow":
  2104. follow.follow()
  2105. elif action == "unfollow":
  2106. follow.unfollow()
  2107. return jsonResponse({"status": "ok"})
  2108. @catch_error_as_json
  2109. def follow_list_api(request, kind, uid):
  2110. """
  2111. API for retrieving a list of followers/followees for a given user.
  2112. """
  2113. if kind == "followers":
  2114. f = FollowersSet(int(uid))
  2115. elif kind == "followees":
  2116. f = FolloweesSet(int(uid))
  2117. return jsonResponse(annotate_user_list(f.uids))
  2118. @catch_error_as_json
  2119. def texts_history_api(request, tref, lang=None, version=None):
  2120. """
  2121. API for retrieving history information about a given text.
  2122. """
  2123. if request.method != "GET":
  2124. return jsonResponse({"error": "Unsupported HTTP method."})
  2125. tref = model.Ref(tref).normal()
  2126. refRe = '^%s$|^%s:' % (tref, tref)
  2127. if lang and version:
  2128. query = {"ref": {"$regex": refRe }, "language": lang, "version": version.replace("_", " ")}
  2129. else:
  2130. query = {"ref": {"$regex": refRe }}
  2131. history = db.history.find(query)
  2132. summary = {"copiers": Set(), "translators": Set(), "editors": Set(), "reviewers": Set() }
  2133. updated = history[0]["date"].isoformat() if history.count() else "Unknown"
  2134. for act in history:
  2135. if act["rev_type"].startswith("edit"):
  2136. summary["editors"].update([act["user"]])
  2137. elif act["rev_type"] == "review":
  2138. summary["reviewers"].update([act["user"]])
  2139. elif act["version"] == "Sefaria Community Translation":
  2140. summary["translators"].update([act["user"]])
  2141. else:
  2142. summary["copiers"].update([act["user"]])
  2143. # Don't list copiers and translators as editors as well
  2144. summary["editors"].difference_update(summary["copiers"])
  2145. summary["editors"].difference_update(summary["translators"])
  2146. for group in summary:
  2147. uids = list(summary[group])
  2148. names = []
  2149. for uid in uids:
  2150. try:
  2151. user = User.objects.get(id=uid)
  2152. name = "%s %s" % (user.first_name, user.last_name)
  2153. link = user_link(uid)
  2154. except User.DoesNotExist:
  2155. name = "Someone"
  2156. link = user_link(-1)
  2157. u = {
  2158. 'name': name,
  2159. 'link': link
  2160. }
  2161. names.append(u)
  2162. summary[group] = names
  2163. summary["lastUpdated"] = updated
  2164. return jsonResponse(summary, callback=request.GET.get("callback", None))
  2165. @catch_error_as_json
  2166. def reviews_api(request, tref=None, lang=None, version=None, review_id=None):
  2167. if request.method == "GET":
  2168. callback=request.GET.get("callback", None)
  2169. if tref and lang and version:
  2170. nref = model.Ref(tref).normal()
  2171. version = version.replace("_", " ")
  2172. reviews = get_reviews(nref, lang, version)
  2173. last_edit = get_last_edit_date(nref, lang, version)
  2174. score_since_last_edit = get_review_score_since_last_edit(nref, lang, version, reviews=reviews, last_edit=last_edit)
  2175. for r in reviews:
  2176. r["date"] = r["date"].isoformat()
  2177. response = {
  2178. "ref": nref,
  2179. "lang": lang,
  2180. "version": version,
  2181. "reviews": reviews,
  2182. "reviewCount": len(reviews),
  2183. "scoreSinceLastEdit": score_since_last_edit,
  2184. "lastEdit": last_edit.isoformat() if last_edit else None,
  2185. }
  2186. elif review_id:
  2187. response = {}
  2188. return jsonResponse(response, callback)
  2189. elif request.method == "POST":
  2190. if not request.user.is_authenticated:
  2191. return jsonResponse({"error": "You must be logged in to write reviews."})
  2192. j = request.POST.get("json")
  2193. if not j:
  2194. return jsonResponse({"error": "No post JSON."})
  2195. j = json.loads(j)
  2196. response = save_review(j, request.user.id)
  2197. return jsonResponse(response)
  2198. elif request.method == "DELETE":
  2199. if not review_id:
  2200. return jsonResponse({"error": "No review ID given for deletion."})
  2201. return jsonResponse(delete_review(review_id, request.user.id))
  2202. else:
  2203. return jsonResponse({"error": "Unsupported HTTP method."})
  2204. @catch_error_as_json
  2205. def topics_list_api(request):
  2206. """
  2207. API to get data for a particular topic.
  2208. """
  2209. topics = get_topics()
  2210. response = topics.list(sort_by="count")
  2211. response = jsonResponse(response, callback=request.GET.get("callback", None))
  2212. response["Cache-Control"] = "max-age=3600"
  2213. return response
  2214. @catch_error_as_json
  2215. def topics_api(request, topic):
  2216. """
  2217. API to get data for a particular topic.
  2218. """
  2219. topics = get_topics()
  2220. topic = Term.normalize(titlecase(topic))
  2221. response = topics.get(topic).contents()
  2222. response = jsonResponse(response, callback=request.GET.get("callback", None))
  2223. response["Cache-Control"] = "max-age=3600"
  2224. return response
  2225. @catch_error_as_json
  2226. def recommend_topics_api(request, ref_list=None):
  2227. """
  2228. API to receive recommended topics for list of strings `refs`.
  2229. """
  2230. if request.method == "GET":
  2231. refs = [Ref(ref).normal() for ref in ref_list.split("+")] if ref_list else []
  2232. elif request.method == "POST":
  2233. topics = get_topics()
  2234. postJSON = request.POST.get("json")
  2235. if not postJSON:
  2236. return jsonResponse({"error": "No post JSON."})
  2237. refs = json.loads(postJSON)
  2238. topics = get_topics()
  2239. response = {"topics": topics.recommend_topics(refs)}
  2240. response = jsonResponse(response, callback=request.GET.get("callback", None))
  2241. return response
  2242. @ensure_csrf_cookie
  2243. def global_activity(request, page=1):
  2244. """
  2245. Recent Activity page listing all recent actions and contributor leaderboards.
  2246. """
  2247. page = int(page)
  2248. page_size = 100
  2249. if page > 40:
  2250. 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>." }
  2251. return render(request,'static/generic.html', generic_response)
  2252. if "api" in request.GET:
  2253. q = {}
  2254. else:
  2255. q = {"method": {"$ne": "API"}}
  2256. filter_type = request.GET.get("type", None)
  2257. activity, page = get_maximal_collapsed_activity(query=q, page_size=page_size, page=page, filter_type=filter_type)
  2258. next_page = page + 1 if page else None
  2259. next_page = "/activity/%d" % next_page if next_page else None
  2260. next_page = "%s?type=%s" % (next_page, filter_type) if next_page and filter_type else next_page
  2261. email = request.user.email if request.user.is_authenticated else False
  2262. return render(request,'activity.html',
  2263. {'activity': activity,
  2264. 'filter_type': filter_type,
  2265. 'email': email,
  2266. 'next_page': next_page,
  2267. 'he': request.interfaceLang == "hebrew", # to make templates less verbose
  2268. })
  2269. @ensure_csrf_cookie
  2270. def user_activity(request, slug, page=1):
  2271. """
  2272. Recent Activity page for a single user.
  2273. """
  2274. page = int(page) if page else 1
  2275. page_size = 100
  2276. try:
  2277. profile = UserProfile(slug=slug)
  2278. except Exception, e:
  2279. raise Http404
  2280. if page > 40:
  2281. 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>." }
  2282. return render(request,'static/generic.html', generic_response)
  2283. q = {"user": profile.id}
  2284. filter_type = request.GET.get("type", None)
  2285. activity, page = get_maximal_collapsed_activity(query=q, page_size=page_size, page=page, filter_type=filter_type)
  2286. next_page = page + 1 if page else None
  2287. next_page = "/activity/%d" % next_page if next_page else None
  2288. next_page = "%s?type=%s" % (next_page, filter_type) if next_page and filter_type else next_page
  2289. email = request.user.email if request.user.is_authenticated else False
  2290. return render(request,'activity.html',
  2291. {'activity': activity,
  2292. 'filter_type': filter_type,
  2293. 'profile': profile,
  2294. 'for_user': True,
  2295. 'email': email,
  2296. 'next_page': next_page,
  2297. 'he': request.interfaceLang == "hebrew", # to make templates less verbose
  2298. })
  2299. @ensure_csrf_cookie
  2300. def segment_history(request, tref, lang, version, page=1):
  2301. """
  2302. View revision history for the text segment named by ref / lang / version.
  2303. """
  2304. try:
  2305. oref = model.Ref(tref)
  2306. except InputError:
  2307. raise Http404
  2308. page = int(page)
  2309. nref = oref.normal()
  2310. version = version.replace("_", " ")
  2311. version_record = Version().load({"title":oref.index.title, "versionTitle":version, "language":lang})
  2312. if not version_record:
  2313. 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))
  2314. filter_type = request.GET.get("type", None)
  2315. history = text_history(oref, version, lang, filter_type=filter_type, page=page)
  2316. next_page = page + 1 if page else None
  2317. next_page = "/activity/%s/%s/%s/%d" % (nref, lang, version, next_page) if next_page else None
  2318. next_page = "%s?type=%s" % (next_page, filter_type) if next_page and filter_type else next_page
  2319. email = request.user.email if request.user.is_authenticated else False
  2320. return render(request,'activity.html',
  2321. {'activity': history,
  2322. "single": True,
  2323. "ref": nref,
  2324. "lang": lang,
  2325. "version": version,
  2326. "versionTitleInHebrew": getattr(version_record, "versionTitleInHebrew", version_record.versionTitle),
  2327. 'email': email,
  2328. 'filter_type': filter_type,
  2329. 'next_page': next_page,
  2330. 'he': request.interfaceLang == "hebrew", # to make templates less verbose
  2331. })
  2332. @catch_error_as_json
  2333. def revert_api(request, tref, lang, version, revision):
  2334. """
  2335. API for reverting a text segment to a previous revision.
  2336. """
  2337. if not request.user.is_authenticated:
  2338. return jsonResponse({"error": "You must be logged in to revert changes."})
  2339. if request.method != "POST":
  2340. return jsonResponse({"error": "Unsupported HTTP method."})
  2341. revision = int(revision)
  2342. version = version.replace("_", " ")
  2343. oref = model.Ref(tref)
  2344. new_text = text_at_revision(oref.normal(), version, lang, revision)
  2345. tracker.modify_text(request.user.id, oref, version, lang, new_text, type="revert")
  2346. return jsonResponse({"status": "ok"})
  2347. def leaderboard(request):
  2348. return render(request,'leaderboard.html',
  2349. {'leaders': top_contributors(),
  2350. 'leaders30': top_contributors(30),
  2351. 'leaders7': top_contributors(7),
  2352. 'leaders1': top_contributors(1),
  2353. })
  2354. @ensure_csrf_cookie
  2355. def user_profile(request, username, page=1):
  2356. """
  2357. User's profile page.
  2358. """
  2359. try:
  2360. profile = UserProfile(slug=username)
  2361. except Exception, e:
  2362. # Couldn't find by slug, try looking up by username (old style urls)
  2363. # If found, redirect to new URL
  2364. # If we no longer want to support the old URLs, we can remove this
  2365. user = get_object_or_404(User, username=username)
  2366. profile = UserProfile(id=user.id)
  2367. return redirect("/profile/%s" % profile.slug, permanent=True)
  2368. following = profile.followed_by(request.user.id) if request.user.is_authenticated else False
  2369. page_size = 20
  2370. page = int(page) if page else 1
  2371. if page > 40:
  2372. 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>." }
  2373. return render(request,'static/generic.html', generic_response)
  2374. query = {"user": profile.id}
  2375. filter_type = request.GET["type"] if "type" in request.GET else None
  2376. activity, apage= get_maximal_collapsed_activity(query=query, page_size=page_size, page=page, filter_type=filter_type)
  2377. notes, npage = get_maximal_collapsed_activity(query=query, page_size=page_size, page=page, filter_type="add_note")
  2378. contributed = activity[0]["date"] if activity else None
  2379. scores = db.leaders_alltime.find_one({"_id": profile.id})
  2380. score = int(scores["count"]) if scores else 0
  2381. user_texts = scores.get("texts", None) if scores else None
  2382. sheets = db.sheets.find({"owner": profile.id, "status": "public"}, {"id": 1, "datePublished": 1}).sort([["datePublished", -1]])
  2383. next_page = apage + 1 if apage else None
  2384. next_page = "/profile/%s/%d" % (username, next_page) if next_page else None
  2385. return render(request,"profile.html",
  2386. {
  2387. 'profile': profile,
  2388. 'following': following,
  2389. 'activity': activity,
  2390. 'sheets': sheets,
  2391. 'notes': notes,
  2392. 'joined': profile.date_joined,
  2393. 'contributed': contributed,
  2394. 'score': score,
  2395. 'scores': scores,
  2396. 'user_texts': user_texts,
  2397. 'filter_type': filter_type,
  2398. 'next_page': next_page,
  2399. "single": False,
  2400. })
  2401. @catch_error_as_json
  2402. def profile_api(request):
  2403. """
  2404. API for user profiles.
  2405. """
  2406. if not request.user.is_authenticated:
  2407. return jsonResponse({"error": _("You must be logged in to update your profile.")})
  2408. if request.method == "POST":
  2409. profileJSON = request.POST.get("json")
  2410. if not profileJSON:
  2411. return jsonResponse({"error": "No post JSON."})
  2412. profileUpdate = json.loads(profileJSON)
  2413. profile = UserProfile(id=request.user.id)
  2414. profile.update(profileUpdate)
  2415. error = profile.errors()
  2416. #TODO: should validation not need to be called manually? maybe inside the save
  2417. if error:
  2418. return jsonResponse({"error": error})
  2419. else:
  2420. profile.save()
  2421. return jsonResponse(profile.to_DICT())
  2422. return jsonResponse({"error": "Unsupported HTTP method."})
  2423. @catch_error_as_json
  2424. def profile_sync_api(request):
  2425. """
  2426. API for syncing history and settings with your profile
  2427. Required POST fields: settings, last_synce
  2428. POST payload should look like
  2429. {
  2430. settings: {..., time_stamp},
  2431. user_history: [{...},...],
  2432. last_sync: ...
  2433. }
  2434. """
  2435. if not request.user.is_authenticated:
  2436. return jsonResponse({"error": _("You must be logged in to update your profile.")})
  2437. # fields in the POST req which can be synced
  2438. syncable_fields = ["settings", "user_history"]
  2439. if request.method == "POST":
  2440. post = request.POST
  2441. from sefaria.utils.util import epoch_time
  2442. now = epoch_time()
  2443. no_return = request.GET.get("no_return", False)
  2444. profile = UserProfile(id=request.user.id)
  2445. if not no_return:
  2446. # send back items after `last_sync`
  2447. last_sync = json.loads(post.get("last_sync", str(profile.last_sync_web)))
  2448. uhs = UserHistorySet({"uid": request.user.id, "server_time_stamp": {"$gt": last_sync}})
  2449. ret = {"last_sync": now, "user_history": [uh.contents() for uh in uhs.array()]}
  2450. if "last_sync" not in post:
  2451. # request was made from web. update last_sync on profile
  2452. profile.update({"last_sync_web": now})
  2453. profile.save()
  2454. else:
  2455. ret = {}
  2456. # sync items from request
  2457. for field in syncable_fields:
  2458. if field not in post:
  2459. continue
  2460. field_data = json.loads(post[field])
  2461. if field == "settings":
  2462. if field_data["time_stamp"] > profile.attr_time_stamps[field]:
  2463. # this change happened after other changes in the db
  2464. profile.update({
  2465. field: field_data,
  2466. "attr_time_stamps": profile.attr_time_stamps.update({field: field_data["time_stamp"]})
  2467. })
  2468. ret["settings"] = profile.settings
  2469. elif field == "user_history":
  2470. for hist in field_data:
  2471. hist["uid"] = request.user.id
  2472. if "he_ref" not in hist or "book" not in hist:
  2473. oref = Ref(hist["ref"])
  2474. hist["he_ref"] = oref.he_normal()
  2475. hist["book"] = oref.index.title
  2476. hist["server_time_stamp"] = now if "server_time_stamp" not in hist else hist[
  2477. "server_time_stamp"] # DEBUG: helpful to include this field for debugging
  2478. action = hist.pop("action", None)
  2479. saved = True if action == "add_saved" else (False if action == "delete_saved" else hist.get("saved", False))
  2480. uh = UserHistory(hist, load_existing=(action is not None), update_last_place=(action is None), field_updates={
  2481. "saved": saved,
  2482. "server_time_stamp": hist["server_time_stamp"]
  2483. })
  2484. uh.save()
  2485. ret["created"] = uh.contents(for_api=True)
  2486. return jsonResponse(ret)
  2487. return jsonResponse({"error": "Unsupported HTTP method."})
  2488. def profile_get_user_history(request):
  2489. """
  2490. GET API for user history. optional URL params are
  2491. :saved: bool. True if you only want saved items. None if you dont care
  2492. :secondary: bool. True if you only want secondary items. None if you dont care
  2493. :tref: Ref associated with history item
  2494. """
  2495. if not request.user.is_authenticated:
  2496. import urlparse
  2497. recents = json.loads(urlparse.unquote(request.COOKIES.get("recentlyViewed", '[]'))) # for backwards compat
  2498. recents = UserProfile.transformOldRecents(None, recents)
  2499. history = json.loads(urlparse.unquote(request.COOKIES.get("user_history", '[]')))
  2500. return jsonResponse(history + recents)
  2501. if request.method == "GET":
  2502. saved = request.GET.get("saved", None)
  2503. if saved is not None:
  2504. saved = bool(int(saved))
  2505. secondary = request.GET.get("secondary", None)
  2506. if secondary is not None:
  2507. secondary = bool(int(secondary))
  2508. last_place = request.GET.get("last_place", None)
  2509. if last_place is not None:
  2510. last_place = bool(int(last_place))
  2511. tref = request.GET.get("tref", None)
  2512. oref = Ref(tref) if tref else None
  2513. user = UserProfile(id=request.user.id)
  2514. return jsonResponse(user.get_user_history(oref=oref, saved=saved, secondary=secondary, serialized=True, last_place=last_place))
  2515. return jsonResponse({"error": "Unsupported HTTP method."})
  2516. def profile_redirect(request, uid, page=1):
  2517. """"
  2518. Redirect to the profile of the logged in user.
  2519. """
  2520. return redirect("/profile/%s" % uid, permanent=True)
  2521. @login_required
  2522. def my_profile(request):
  2523. """
  2524. Redirect to a user profile
  2525. """
  2526. return redirect("/profile/%s" % UserProfile(id=request.user.id).slug)
  2527. def interrupting_messages_read_api(request, message):
  2528. if not request.user.is_authenticated:
  2529. return jsonResponse({"error": "You must be logged in to use this API."})
  2530. profile = UserProfile(id=request.user.id)
  2531. profile.mark_interrupting_message_read(message)
  2532. return jsonResponse({"status": "ok"})
  2533. @login_required
  2534. @ensure_csrf_cookie
  2535. def edit_profile(request):
  2536. """
  2537. Page for editing a user's profile.
  2538. """
  2539. profile = UserProfile(id=request.user.id)
  2540. sheets = db.sheets.find({"owner": profile.id, "status": "public"}, {"id": 1, "datePublished": 1}).sort([["datePublished", -1]])
  2541. return render(request,'edit_profile.html',
  2542. {
  2543. 'user': request.user,
  2544. 'profile': profile,
  2545. 'sheets': sheets,
  2546. })
  2547. @login_required
  2548. @ensure_csrf_cookie
  2549. def account_settings(request):
  2550. """
  2551. Page for managing a user's account settings.
  2552. """
  2553. profile = UserProfile(id=request.user.id)
  2554. return render(request,'account_settings.html',
  2555. {
  2556. 'user': request.user,
  2557. 'profile': profile,
  2558. })
  2559. @ensure_csrf_cookie
  2560. def home(request):
  2561. """
  2562. Homepage
  2563. """
  2564. recent = request.COOKIES.get("recentlyViewed", None)
  2565. last_place = request.COOKIES.get("user_history", None)
  2566. if (recent or last_place or request.user.is_authenticated) and not "home" in request.GET:
  2567. return redirect("/texts")
  2568. if request.user_agent.is_mobile:
  2569. return mobile_home(request)
  2570. calendar_items = get_keyed_calendar_items(request.diaspora)
  2571. daf_today = calendar_items["Daf Yomi"]
  2572. parasha = calendar_items["Parashat Hashavua"]
  2573. metrics = db.metrics.find().sort("timestamp", -1).limit(1)[0]
  2574. return render(request,'static/home.html',
  2575. {
  2576. "metrics": metrics,
  2577. "daf_today": daf_today,
  2578. "parasha": parasha,
  2579. })
  2580. @ensure_csrf_cookie
  2581. def discussions(request):
  2582. """
  2583. Discussions page.
  2584. """
  2585. discussions = LayerSet({"owner": request.user.id})
  2586. return render(request,'discussions.html',
  2587. {
  2588. "discussions": discussions,
  2589. })
  2590. @catch_error_as_json
  2591. def new_discussion_api(request):
  2592. """
  2593. API for user profiles.
  2594. """
  2595. if not request.user.is_authenticated:
  2596. return jsonResponse({"error": "You must be logged in to start a discussion."})
  2597. if request.method == "POST":
  2598. import uuid
  2599. attempts = 10
  2600. while attempts > 0:
  2601. key = str(uuid.uuid4())[:8]
  2602. if LayerSet({"urlkey": key}).count() > 0:
  2603. attempts -= 1
  2604. continue
  2605. discussion = Layer({
  2606. "urlkey": key,
  2607. "owner": request.user.id,
  2608. })
  2609. discussion.save()
  2610. return jsonResponse(discussion.contents())
  2611. return jsonResponse({"error": "An extremely unlikley event has occurred."})
  2612. return jsonResponse({"error": "Unsupported HTTP method."})
  2613. @ensure_csrf_cookie
  2614. def dashboard(request):
  2615. """
  2616. Dashboard page -- table view of all content
  2617. """
  2618. states = VersionStateSet(
  2619. {},
  2620. proj={"title": 1, "flags": 1, "linksCount": 1, "content._en.percentAvailable": 1, "content._he.percentAvailable": 1}
  2621. ).array()
  2622. flat_toc = library.get_toc_tree().flatten()
  2623. def toc_sort(a):
  2624. try:
  2625. return flat_toc.index(a["title"])
  2626. except:
  2627. return 9999
  2628. states = sorted(states, key=toc_sort)
  2629. return render(request,'dashboard.html',
  2630. {
  2631. "states": states,
  2632. })
  2633. @ensure_csrf_cookie
  2634. def translation_requests(request, completed_only=False, featured_only=False):
  2635. """
  2636. Page listing all outstnading translation requests.
  2637. """
  2638. page = int(request.GET.get("page", 1)) - 1
  2639. page_size = 100
  2640. query = {"completed": False, "section_level": False} if not completed_only else {"completed": True}
  2641. query = {"completed": True, "featured": True} if completed_only and featured_only else query
  2642. requests = TranslationRequestSet(query, limit=page_size, page=page, sort=[["request_count", -1]])
  2643. request_count = TranslationRequestSet({"completed": False, "section_level": False}).count()
  2644. complete_count = TranslationRequestSet({"completed": True}).count()
  2645. featured_complete = TranslationRequestSet({"completed": True, "featured": True}).count()
  2646. next_page = page + 2 if True or requests.count() == page_size else 0
  2647. featured_query = {"featured": True, "featured_until": { "$gt": datetime.now() } }
  2648. featured = TranslationRequestSet(featured_query, sort=[["completed", 1], ["featured_until", 1]])
  2649. today = datetime.today()
  2650. featured_end = today + timedelta(7 - ((today.weekday()+1) % 7)) # This coming Sunday
  2651. featured_end = featured_end.replace(hour=0, minute=0) # At midnight
  2652. current = [d.featured_until <= featured_end for d in featured]
  2653. featured_current = sum(current)
  2654. show_featured = not completed_only and not page and ((request.user.is_staff and featured.count()) or (featured_current))
  2655. return render(request,'translation_requests.html',
  2656. {
  2657. "featured": featured,
  2658. "featured_current": featured_current,
  2659. "show_featured": show_featured,
  2660. "requests": requests,
  2661. "request_count": request_count,
  2662. "completed_only": completed_only,
  2663. "complete_count": complete_count,
  2664. "featured_complete": featured_complete,
  2665. "featured_only": featured_only,
  2666. "next_page": next_page,
  2667. "page_offset": page * page_size
  2668. })
  2669. def completed_translation_requests(request):
  2670. """
  2671. Wrapper for listing completed translations requests.
  2672. """
  2673. return translation_requests(request, completed_only=True)
  2674. def completed_featured_translation_requests(request):
  2675. """
  2676. Wrapper for listing completed translations requests.
  2677. """
  2678. return translation_requests(request, completed_only=True, featured_only=True)
  2679. @catch_error_as_json
  2680. def translation_request_api(request, tref):
  2681. """
  2682. API for requesting a text segment for translation.
  2683. """
  2684. if not request.user.is_authenticated:
  2685. return jsonResponse({"error": "You must be logged in to request a translation."})
  2686. oref = Ref(tref)
  2687. ref = oref.normal()
  2688. if "unrequest" in request.POST:
  2689. TranslationRequest.remove_request(ref, request.user.id)
  2690. response = {"status": "ok"}
  2691. elif "feature" in request.POST:
  2692. if not request.user.is_staff:
  2693. response = {"error": "Only admins can feature requests."}
  2694. else:
  2695. tr = TranslationRequest().load({"ref": ref})
  2696. tr.featured = True
  2697. tr.featured_until = dateutil.parser.parse(request.POST.get("feature"))
  2698. tr.save()
  2699. response = {"status": "ok"}
  2700. elif "unfeature" in request.POST:
  2701. if not request.user.is_staff:
  2702. response = {"error": "Only admins can unfeature requests."}
  2703. else:
  2704. tr = TranslationRequest().load({"ref": ref})
  2705. tr.featured = False
  2706. tr.featured_until = None
  2707. tr.save()
  2708. response = {"status": "ok"}
  2709. else:
  2710. if oref.is_text_translated():
  2711. response = {"error": "Sefaria already has a translation for %s." % ref}
  2712. else:
  2713. tr = TranslationRequest.make_request(ref, request.user.id)
  2714. response = tr.contents()
  2715. return jsonResponse(response)
  2716. @ensure_csrf_cookie
  2717. def translation_flow(request, tref):
  2718. """
  2719. Assign a user a paritcular bit of text to translate within 'ref',
  2720. either a text title or category.
  2721. """
  2722. tref = tref.replace("_", " ")
  2723. generic_response = { "title": "Help Translate %s" % tref, "content": "" }
  2724. categories = model.library.get_text_categories()
  2725. next_text = None
  2726. next_section = None
  2727. # expire old locks before checking for a currently unlocked text
  2728. model.expire_locks()
  2729. try:
  2730. oref = model.Ref(tref)
  2731. except InputError:
  2732. oref = False
  2733. if oref and len(oref.sections) == 0:
  2734. # tref is an exact text Title
  2735. # normalize URL
  2736. if request.path != "/translate/%s" % oref.url():
  2737. return redirect("/translate/%s" % oref.url(), permanent=True)
  2738. # Check for completion
  2739. if oref.get_state_node().get_percent_available("en") == 100:
  2740. 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
  2741. return render(request,'static/generic.html', generic_response)
  2742. if "random" in request.GET:
  2743. # choose a ref from a random section within this text
  2744. if "skip" in request.GET:
  2745. if oref.is_talmud():
  2746. skip = int(daf_to_section(request.GET.get("skip")))
  2747. else:
  2748. skip = int(request.GET.get("skip"))
  2749. else:
  2750. skip = None
  2751. assigned_ref = random_untranslated_ref_in_text(oref.normal(), skip=skip)
  2752. if assigned_ref:
  2753. next_section = model.Ref(assigned_ref).padded_ref().sections[0]
  2754. elif "section" in request.GET:
  2755. # choose the next ref within the specified section
  2756. next_section = int(request.GET["section"])
  2757. assigned_ref = next_untranslated_ref_in_text(oref.normal(), section=next_section)
  2758. else:
  2759. # choose the next ref in this text in order
  2760. assigned_ref = next_untranslated_ref_in_text(oref.normal())
  2761. if not assigned_ref:
  2762. 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)
  2763. return render(request,'static/generic.html', generic_response)
  2764. elif oref and len(oref.sections) > 0:
  2765. # ref is a citation to a particular location in a text
  2766. # for now, send this to the edit_text view
  2767. return edit_text(request, tref)
  2768. elif tref in categories: #todo: Fix me to work with Version State!
  2769. # ref is a text Category
  2770. raise InputError("This function is under repair. Our Apologies.")
  2771. '''
  2772. cat = tref
  2773. # Check for completion
  2774. if get_percent_available(cat) == 100:
  2775. 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
  2776. return render(request,'static/generic.html', generic_response)
  2777. if "random" in request.GET:
  2778. # choose a random text from this cateogory
  2779. skip = int(request.GET.get("skip")) if "skip" in request.GET else None
  2780. text = random_untranslated_text_in_category(cat, skip=skip)
  2781. assigned_ref = next_untranslated_ref_in_text(text)
  2782. next_text = text
  2783. elif "text" in request.GET:
  2784. # choose the next text requested in URL
  2785. oref = model.Ref(request.GET["text"])
  2786. text = oref.normal()
  2787. next_text = text
  2788. if oref.get_state_node().get_percent_available("en") == 100:
  2789. generic_response["content"] = "%s is complete! Work on <a href='/translate/%s'>another text</a>." % (text, tref)
  2790. return render(request,'static/generic.html', generic_response)
  2791. try:
  2792. assigned_ref = next_untranslated_ref_in_text(text)
  2793. except InputError:
  2794. 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)
  2795. return render(request,'static/generic.html', generic_response)
  2796. else:
  2797. # choose the next text in order
  2798. skip = 0
  2799. success = 0
  2800. # TODO -- need an escape valve here
  2801. while not success:
  2802. try:
  2803. text = next_untranslated_text_in_category(cat, skip=skip)
  2804. assigned_ref = next_untranslated_ref_in_text(text)
  2805. skip += 1
  2806. except InputError:
  2807. pass
  2808. else:
  2809. success = 1
  2810. '''
  2811. else:
  2812. # we don't know what this is
  2813. 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)
  2814. return render(request,'static/generic.html', generic_response)
  2815. # get the assigned text
  2816. assigned = TextFamily(Ref(assigned_ref), context=0, commentary=False).contents()
  2817. # Put a lock on this assignment
  2818. user = request.user.id if request.user.is_authenticated else 0
  2819. model.set_lock(assigned_ref, "en", "Sefaria Community Translation", user)
  2820. # if the assigned text is actually empty, run this request again
  2821. # but leave the new lock in place to skip over it
  2822. if "he" not in assigned or not len(assigned["he"]):
  2823. return translation_flow(request, tref)
  2824. # get percentage and remaining counts
  2825. # percent = get_percent_available(assigned["book"])
  2826. translated = StateNode(assigned["book"]).get_translated_count_by_unit(assigned["sectionNames"][-1])
  2827. remaining = StateNode(assigned["book"]).get_untranslated_count_by_unit(assigned["sectionNames"][-1])
  2828. percent = 100 * translated / float(translated + remaining)
  2829. return render(request,'translate_campaign.html',
  2830. {"title": "Help Translate %s" % tref,
  2831. "base_ref": tref,
  2832. "assigned_ref": assigned_ref,
  2833. "assigned_ref_url": model.Ref(assigned_ref).url(),
  2834. "assigned_text": assigned["he"],
  2835. "assigned_segment_name": assigned["sectionNames"][-1],
  2836. "assigned": assigned,
  2837. "translated": translated,
  2838. "remaining": remaining,
  2839. "percent": percent,
  2840. "thanks": "thank" in request.GET,
  2841. "random_param": "&skip={}".format(assigned["sections"][0]) if request.GET.get("random") else "",
  2842. "next_text": next_text,
  2843. "next_section": next_section,
  2844. })
  2845. @ensure_csrf_cookie
  2846. def contest_splash(request, slug):
  2847. """
  2848. Splash page for contest.
  2849. Example of adding a contest record to the DB:
  2850. db.contests.save({
  2851. "contest_start" : datetime.strptime("3/5/14", "%m/%d/%y"),
  2852. "contest_end" : datetime.strptime("3/26/14", "%m/%d/%y"),
  2853. "version" : "Sefaria Community Translation",
  2854. "ref_regex" : "^Shulchan Arukh, Even HaEzer ",
  2855. "assignment_url" : "/translate/Shulchan_Arukh,_Even_HaEzer",
  2856. "title" : "Translate Shulchan Arukh, Even HaEzer",
  2857. "slug" : "shulchan-arukh-even-haezer"
  2858. })
  2859. """
  2860. settings = db.contests.find_one({"slug": slug})
  2861. if not settings:
  2862. raise Http404
  2863. settings["copy_template"] = "static/contest/%s.html" % settings["slug"]
  2864. leaderboard_condition = make_leaderboard_condition( start = settings["contest_start"],
  2865. end = settings["contest_end"],
  2866. version = settings["version"],
  2867. ref_regex = settings["ref_regex"])
  2868. now = datetime.now()
  2869. if now < settings["contest_start"]:
  2870. settings["phase"] = "pre"
  2871. settings["leaderboard"] = None
  2872. settings["time_to_start"] = td_format(settings["contest_start"] - now)
  2873. elif settings["contest_start"] < now < settings["contest_end"]:
  2874. settings["phase"] = "active"
  2875. settings["leaderboard_title"] = "Current Leaders"
  2876. settings["leaderboard"] = make_leaderboard(leaderboard_condition)
  2877. settings["time_to_end"] = td_format(settings["contest_end"] - now)
  2878. elif settings["contest_end"] < now:
  2879. settings["phase"] = "post"
  2880. settings["leaderboard_title"] = "Contest Leaders (Unreviewed)"
  2881. settings["leaderboard"] = make_leaderboard(leaderboard_condition)
  2882. return render(request,"contest_splash.html",
  2883. settings)
  2884. @ensure_csrf_cookie
  2885. def metrics(request):
  2886. """
  2887. Metrics page. Shows graphs of core metrics.
  2888. """
  2889. metrics = db.metrics.find().sort("timestamp", 1)
  2890. metrics_json = dumps(metrics)
  2891. return render(request,'metrics.html',
  2892. {
  2893. "metrics_json": metrics_json,
  2894. })
  2895. @ensure_csrf_cookie
  2896. def digitized_by_sefaria(request):
  2897. """
  2898. Metrics page. Shows graphs of core metrics.
  2899. """
  2900. texts = VersionSet({"digitizedBySefaria": True}, sort=[["title", 1]])
  2901. return render(request,'static/digitized-by-sefaria.html',
  2902. {
  2903. "texts": texts,
  2904. })
  2905. def parashat_hashavua_redirect(request):
  2906. """ Redirects to this week's Parashah"""
  2907. diaspora = request.GET.get("diaspora", "1")
  2908. calendars = get_keyed_calendar_items() # TODO Support israel / customs
  2909. parashah = calendars["Parashat Hashavua"]
  2910. return redirect(iri_to_uri("/" + parashah["url"]), permanent=False)
  2911. def daf_yomi_redirect(request):
  2912. """ Redirects to today's Daf Yomi"""
  2913. calendars = get_keyed_calendar_items()
  2914. daf_yomi = calendars["Daf Yomi"]
  2915. return redirect(iri_to_uri("/" + daf_yomi["url"]), permanent=False)
  2916. def random_ref():
  2917. """
  2918. Returns a valid random ref within the Sefaria library.
  2919. """
  2920. # refs = library.ref_list()
  2921. # ref = choice(refs)
  2922. # picking by text first biases towards short texts
  2923. text = choice(VersionSet().distinct("title"))
  2924. try:
  2925. # ref = choice(VersionStateSet({"title": text}).all_refs()) # check for orphaned texts
  2926. ref = Ref(text).normal()
  2927. except Exception:
  2928. return random_ref()
  2929. return ref
  2930. def random_redirect(request):
  2931. """
  2932. Redirect to a random text page.
  2933. """
  2934. response = redirect(iri_to_uri("/" + random_ref()), permanent=False)
  2935. return response
  2936. def random_text_page(request):
  2937. """
  2938. Page for generating random texts.
  2939. """
  2940. return render(request,'random.html', {})
  2941. def random_text_api(request):
  2942. """
  2943. Return Texts API data for a random ref.
  2944. """
  2945. response = redirect(iri_to_uri("/api/texts/" + random_ref()) + "?commentary=0", permanent=False)
  2946. return response
  2947. def random_by_topic_api(request):
  2948. """
  2949. Returns Texts API data for a random text taken from popular topic tags
  2950. """
  2951. cb = request.GET.get("callback", None)
  2952. topics_filtered = filter(lambda x: x['count'] > 15, get_topics().list())
  2953. if len(topics_filtered) == 0:
  2954. resp = jsonResponse({"ref": None, "topic": None, "url": None}, callback=cb)
  2955. resp['Content-Type'] = "application/json; charset=utf-8"
  2956. return resp
  2957. random_topic = choice(topics_filtered)['tag']
  2958. random_source = choice(get_topics().get(random_topic).contents()['sources'])[0]
  2959. try:
  2960. oref = Ref(random_source)
  2961. tref = oref.normal()
  2962. url = oref.url()
  2963. except Exception:
  2964. return random_by_topic_api(request)
  2965. resp = jsonResponse({"ref": tref, "topic": random_topic, "url": url}, callback=cb)
  2966. resp['Content-Type'] = "application/json; charset=utf-8"
  2967. return resp
  2968. @csrf_exempt
  2969. def dummy_search_api(request):
  2970. # Thou shalt upgrade thine app or thou shalt not glean the results of search thou seeketh
  2971. # this api is meant to information users of the old search.sefaria.org to upgrade their apps to get search to work again
  2972. were_sorry = u"We're sorry, but your version of the app is no longer compatible with our new search. We recommend you upgrade the Sefaria app to fully enjoy all it has to offer <br> עמכם הסליחה, אך גרסת האפליקציה הנמצאת במכשירכם איננה תואמת את מנוע החיפוש החדש. אנא עדכנו את אפליקצית ספריא להמשך שימוש בחיפוש"
  2973. resp = jsonResponse({
  2974. "took": 613,
  2975. "timed_out": False,
  2976. "_shards": {
  2977. "total": 5,
  2978. "successful": 5,
  2979. "skipped": 0,
  2980. "failed": 0
  2981. },
  2982. "hits": {
  2983. "total": 1,
  2984. "max_score": 1234,
  2985. "hits": [
  2986. {
  2987. "_index": "merged-c",
  2988. "_type": "text",
  2989. "_id": "yoyo [he]",
  2990. "_score": 1,
  2991. "_source": {
  2992. "titleVariants": ["Upgrade"],
  2993. "path": "Tanakh/Torah/Genesis",
  2994. "version_priority": 0,
  2995. "content": were_sorry,
  2996. "exact": were_sorry,
  2997. "naive_lemmatizer": were_sorry,
  2998. "comp_date": -1400,
  2999. "categories": ["Tanakh", "Torah"],
  3000. "lang": "he",
  3001. "pagesheetrank": 1,
  3002. "ref": "Genesis 1:1",
  3003. "heRef": u"בראשית א:א",
  3004. "version": None,
  3005. "order":"A00000100220030"
  3006. },
  3007. "highlight": {
  3008. "content": [
  3009. were_sorry
  3010. ],
  3011. "exact": [
  3012. were_sorry
  3013. ],
  3014. "naive_lemmatizer": [
  3015. were_sorry
  3016. ]
  3017. }
  3018. }
  3019. ]
  3020. },
  3021. "aggregations": {
  3022. "category": {
  3023. "buckets": []
  3024. }
  3025. }
  3026. })
  3027. resp['Content-Type'] = "application/json; charset=utf-8"
  3028. return resp
  3029. # def search_api(request):
  3030. # # dict to define request parameters and their default values. None means parameter is required
  3031. # params = {
  3032. # "query": None,
  3033. # "size": 10,
  3034. # "from": 0,
  3035. # "type": None, #
  3036. # "get_filters": False,
  3037. # "applied_filters": [],
  3038. # "field": None,
  3039. # "sort_type": None,
  3040. # "exact": False
  3041. # }
  3042. # param_vals = {}
  3043. # for p in params:
  3044. # param_vals[p] = request.GET.get(p, )
  3045. # query = request.GET.get("q")
  3046. # """
  3047. # query: query string
  3048. # size: size of result set
  3049. # from: from what result to start
  3050. # type: "sheet" or "text"
  3051. # get_filters: if to fetch initial filters
  3052. # applied_filters: filter query by these filters
  3053. # field: field to query in elastic_search
  3054. # sort_type: chonological or relevance
  3055. # exact: if query is exact
  3056. # success: callback on success
  3057. # error: callback on error
  3058. # """
  3059. # size = request.GET.get("size")
  3060. @ensure_csrf_cookie
  3061. def serve_static(request, page):
  3062. """
  3063. Serve a static page whose template matches the URL
  3064. """
  3065. return render(request,'static/%s.html' % page, {})
  3066. @ensure_csrf_cookie
  3067. def explore(request, book1, book2, lang=None):
  3068. """
  3069. Serve the explorer, with the provided deep linked books
  3070. """
  3071. books = []
  3072. for book in [book1, book2]:
  3073. if book:
  3074. books.append(book)
  3075. template_vars = {"books": json.dumps(books)}
  3076. if lang == "he": # Override language settings if 'he' is in URL
  3077. request.contentLang = "hebrew"
  3078. return render(request,'explore.html', template_vars)
  3079. def person_page(request, name):
  3080. person = Person().load({"key": name})
  3081. if not person:
  3082. raise Http404
  3083. assert isinstance(person, Person)
  3084. template_vars = person.contents()
  3085. if request.interfaceLang == "hebrew":
  3086. template_vars["name"] = person.primary_name("he")
  3087. template_vars["bio"]= getattr(person, "heBio", _("Learn about %(name)s - works written, biographies, dates and more.") % {"name": person.primary_name("he")})
  3088. else:
  3089. template_vars["name"] = person.primary_name("en")
  3090. template_vars["bio"]= getattr(person, "enBio", _("Learn about %(name)s - works written, biographies, dates and more.") % {"name": person.primary_name("en")})
  3091. template_vars["primary_name"] = {
  3092. "en": person.primary_name("en"),
  3093. "he": person.primary_name("he")
  3094. }
  3095. template_vars["secondary_names"] = {
  3096. "en": person.secondary_names("en"),
  3097. "he": person.secondary_names("he")
  3098. }
  3099. template_vars["time_period_name"] = {
  3100. "en": person.mostAccurateTimePeriod().primary_name("en"),
  3101. "he": person.mostAccurateTimePeriod().primary_name("he")
  3102. }
  3103. template_vars["time_period"] = {
  3104. "en": person.mostAccurateTimePeriod().period_string("en"),
  3105. "he": person.mostAccurateTimePeriod().period_string("he")
  3106. }
  3107. template_vars["relationships"] = person.get_grouped_relationships()
  3108. template_vars["indexes"] = person.get_indexes()
  3109. template_vars["post_talmudic"] = person.is_post_talmudic()
  3110. template_vars["places"] = person.get_places()
  3111. return render(request,'person.html', template_vars)
  3112. def person_index(request):
  3113. eras = ["GN", "RI", "AH", "CO"]
  3114. template_vars = {
  3115. "eras": []
  3116. }
  3117. for era in eras:
  3118. tp = TimePeriod().load({"symbol": era})
  3119. template_vars["eras"].append(
  3120. {
  3121. "name_en": tp.primary_name("en"),
  3122. "name_he": tp.primary_name("he"),
  3123. "years_en": tp.period_string("en"),
  3124. "years_he": tp.period_string("he"),
  3125. "people": [p for p in PersonSet({"era": era}, sort=[('deathYear', 1)]) if p.has_indexes()]
  3126. }
  3127. )
  3128. return render(request,'people.html', template_vars)
  3129. def talmud_person_index(request):
  3130. gens = TimePeriodSet.get_generations()
  3131. template_vars = {
  3132. "gens": []
  3133. }
  3134. for gen in gens:
  3135. people = gen.get_people_in_generation()
  3136. template_vars["gens"].append({
  3137. "name_en": gen.primary_name("en"),
  3138. "name_he": gen.primary_name("he"),
  3139. "years_en": gen.period_string("en"),
  3140. "years_he": gen.period_string("he"),
  3141. "people": [p for p in people]
  3142. })
  3143. return render(request,'talmud_people.html', template_vars)
  3144. def _get_sheet_tag_garden(tag):
  3145. garden_key = u"sheets.tagged.{}".format(tag)
  3146. g = Garden().load({"key": garden_key})
  3147. if not g:
  3148. g = Garden({"key": garden_key, "title": u"Sources from Sheets Tagged {}".format(tag), "heTitle": u"מקורות מדפים מתויגים:" + u" " + unicode(tag)})
  3149. g.import_sheets_by_tag(tag)
  3150. g.save()
  3151. return g
  3152. def sheet_tag_garden_page(request, key):
  3153. g = _get_sheet_tag_garden(key)
  3154. return garden_page(request, g)
  3155. def sheet_tag_visual_garden_page(request, key):
  3156. g = _get_sheet_tag_garden(key)
  3157. return visual_garden_page(request, g)
  3158. def custom_visual_garden_page(request, key):
  3159. g = Garden().load({"key": "sefaria.custom.{}".format(key)})
  3160. if not g:
  3161. raise Http404
  3162. return visual_garden_page(request, g)
  3163. def _get_search_garden(q):
  3164. garden_key = u"search.query.{}".format(q)
  3165. g = Garden().load({"key": garden_key})
  3166. if not g:
  3167. g = Garden({"key": garden_key, "title": u"Search: {}".format(q), "heTitle": u"חיפוש:" + u" " + unicode(q)})
  3168. g.import_search(q)
  3169. g.save()
  3170. return g
  3171. def search_query_visual_garden_page(request, q):
  3172. g = _get_search_garden(q)
  3173. return visual_garden_page(request, g)
  3174. def garden_page(request, g):
  3175. template_vars = {
  3176. 'title': g.title,
  3177. 'heTitle': g.heTitle,
  3178. 'key': g.key,
  3179. 'stopCount': g.stopSet().count(),
  3180. 'stopsByTime': g.stopsByTime(),
  3181. 'stopsByPlace': g.stopsByPlace(),
  3182. 'stopsByAuthor': g.stopsByAuthor(),
  3183. 'stopsByTag': g.stopsByTag()
  3184. }
  3185. return render(request,'garden.html', template_vars)
  3186. def visual_garden_page(request, g):
  3187. template_vars = {
  3188. 'title': g.title,
  3189. 'heTitle': g.heTitle,
  3190. 'subtitle': getattr(g, "subtitle", ""),
  3191. 'heSubtitle': getattr(g, "heSubtitle", ""),
  3192. 'key': g.key,
  3193. 'stopCount': g.stopSet().count(),
  3194. 'stops': json.dumps(g.stopData()),
  3195. 'places': g.placeSet().asGeoJson(as_string=True),
  3196. 'config': json.dumps(getattr(g, "config", {}))
  3197. }
  3198. return render(request,'visual_garden.html', template_vars)
  3199. @requires_csrf_token
  3200. def custom_server_error(request, template_name='500.html'):
  3201. """
  3202. 500 error handler.
  3203. Templates: `500.html`
  3204. """
  3205. t = get_template(template_name) # You need to create a 500.html template.
  3206. return http.HttpResponseServerError(t.render({'request_path': request.path}, request))