views.py 151 KB

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