repo_pull_request.html 63 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690
  1. {% extends "repo_master.html" %}
  2. {% from "_formhelper.html" import show_comment, show_pr_initial_comment, render_bootstrap_field %}
  3. {% from "_repo_renderdiff.html" import repo_renderdiff %}
  4. {% block title %}
  5. {%- if pull_request -%}
  6. PR#{{ requestid }}: {{ pull_request.title | noJS(ignore="img") | safe }}
  7. {%- endif
  8. %} - {{ repo.url_path }}
  9. {% endblock %}
  10. {% set tag = "home" %}
  11. {% block header %}
  12. <link type="text/css" rel="stylesheet" nonce="{{ g.nonce }}" href="{{
  13. url_for('static', filename='vendor/emojione/emojione.sprites.css') }}?version={{ g.version}}"/>
  14. <link type="text/css" rel="stylesheet" nonce="{{ g.nonce }}" href="{{
  15. url_for('static', filename='vendor/selectize/selectize.bootstrap3.css') }}?version={{ g.version}}"/>
  16. <link type="text/css" rel="stylesheet" nonce="{{ g.nonce }}" href="{{
  17. url_for('static', filename='vendor/jquery.atwho/jquery.atwho.css') }}?version={{ g.version}}"/>
  18. {% endblock %}
  19. {% block repo %}
  20. <div class="d-flex align-items-start">
  21. <h4 class="ml-1">
  22. <div>
  23. {% if pull_request.status == 'Open' %}
  24. <span class="fa fa-fw text-success fa-arrow-circle-down pt-1"></span>
  25. <span class="text-success font-weight-bold">#{{requestid}}</span>
  26. {% elif pull_request.status == 'Merged' %}
  27. <span class="fa fa-fw text-info fa-arrow-circle-down pt-1"></span>
  28. <span class="text-info font-weight-bold">#{{requestid}}</span>
  29. {% elif pull_request.status == 'Closed' %}
  30. <span class="fa fa-fw text-danger fa-arrow-circle-down pt-1"></span>
  31. <span class="text-danger font-weight-bold">#{{requestid}}</span>
  32. {% endif %}
  33. <span class="font-weight-bold">
  34. {{ pull_request.title | noJS(ignore="img") | safe}}
  35. </span>
  36. {% if g.authenticated and (g.fas_user.username == pull_request.user.username
  37. or g.repo_committer) and pull_request.status == 'Open'%}
  38. <a class="btn btn-sm btn-outline-secondary border-0"
  39. href="{{ url_for(
  40. 'ui_ns.request_pull_edit',
  41. repo=repo.name,
  42. username=repo.user.user if repo.is_fork else None,
  43. namespace=repo.namespace,
  44. requestid=requestid) }}"
  45. title="Update title"><i class="fa fa-pencil"></i></a>
  46. {% endif %}
  47. </div>
  48. <div>
  49. <small>
  50. {% if pull_request.status == 'Open' %}
  51. <span data-toggle="tooltip" title="{{pull_request.date_created | format_datetime}}">
  52. <span class="text-success font-weight-bold">Opened</span> {{ pull_request.date_created |humanize }}
  53. </span>
  54. <span title="{{ pull_request.user.html_title }}"> by {{ pull_request.user.user }}.</span>
  55. <span class="text-muted" data-toggle="tooltip" title="{{pull_request.updated_on | format_datetime}}">
  56. Modified {{ pull_request.updated_on |humanize }}
  57. </span>
  58. {% elif pull_request.status == 'Merged' %}
  59. <span data-toggle="tooltip" title="{{pull_request.closed_at | format_datetime}}">
  60. <span class="text-info font-weight-bold">Merged</span> {{ pull_request.closed_at |humanize }}
  61. </span>
  62. by
  63. <span title="{{ pull_request.closed_by.html_title }}">{{ pull_request.closed_by.user }}.</span>
  64. <span class="text-muted" data-toggle="tooltip" title="{{pull_request.date_created | format_datetime}}">
  65. <span class="font-weight-bold">Opened</span> {{ pull_request.date_created |humanize }}
  66. </span>
  67. <span class="text-muted" title="{{ pull_request.user.html_title }}">by {{ pull_request.user.user }}.</span>
  68. {% elif pull_request.status == 'Closed' %}
  69. <span data-toggle="tooltip" title="{{pull_request.closed_at | format_datetime}}">
  70. <span class="text-danger font-weight-bold">Closed</span> {{ pull_request.closed_at |humanize }}
  71. </span>
  72. by
  73. <span title="{{ pull_request.closed_by.html_title }}">{{ pull_request.closed_by.user }}.</span>
  74. <span class="text-muted" data-toggle="tooltip" title="{{pull_request.date_created | format_datetime}}">
  75. <span class="font-weight-bold">Opened</span> {{ pull_request.date_created |humanize }}
  76. </span>
  77. <span class="text-muted" title="{{ pull_request.user.html_title }}">by {{ pull_request.user.user }}.</span>
  78. {% endif %}
  79. </small>
  80. </div>
  81. <div class="mt-2">
  82. <small>
  83. {% if pull_request.remote_git or pull_request.project_from.is_fork %}
  84. <span class="badge badge-light badge-pill border border-secondary font-1em">
  85. {% if pull_request.remote_git %}
  86. <i class="fa fa-globe"></i>
  87. {{pull_request.remote_git}}
  88. {% elif pull_request.project_from.is_fork %}
  89. <i class="fa fa-code-fork"></i>
  90. {% if pull_request.project_from.namespace %}
  91. {{pull_request.project_from.namespace}}/
  92. {% endif %}
  93. {% if pull_request.project_from.is_fork -%}
  94. {{ pull_request.project_from.user.user }}/
  95. {%- endif -%}
  96. {{pull_request.project_from.name}}
  97. {% endif %}
  98. </span>
  99. {% endif %}
  100. <a href="{{ url_for('ui_ns.view_tree',
  101. repo=pull_request.project_from.name,
  102. username=pull_request.project_from.user.user
  103. if pull_request.project_from.is_fork else None,
  104. namespace=repo.namespace,
  105. identifier=pull_request.branch_from)
  106. }}"
  107. class="badge badge-secondary badge-pill border border-secondary font-1em">
  108. <span class="fa fa-random"></span>
  109. {{ pull_request.branch_from }}
  110. </a>
  111. &nbsp;into&nbsp;
  112. <a href="{{ url_for('ui_ns.view_tree',
  113. repo=pull_request.project.name,
  114. username=pull_request.project.user.user
  115. if pull_request.project.is_fork else None,
  116. namespace=repo.namespace,
  117. identifier=pull_request.branch)
  118. }}"
  119. class="badge badge-secondary badge-pill border border-secondary font-1em">
  120. <i class="fa fa-random"></i>
  121. {{ pull_request.branch }}
  122. </a>
  123. </small>
  124. </div>
  125. </h4>
  126. <div class="ml-auto d-flex">
  127. {% if g.authenticated and (g.fas_user.username == pull_request.user.username
  128. or g.repo_committer) and pull_request.status == 'Open'%}
  129. {% if pull_request.status == 'Open' and g.authenticated and
  130. (g.repo_committer or g.fas_user.username == pull_request.user.username) %}
  131. {% if mergeform and pull_request.remote %}
  132. <form class="inline" action="{{ url_for(
  133. 'ui_ns.refresh_request_pull',
  134. username=repo.user.user if repo.is_fork else None,
  135. namespace=repo.namespace,
  136. repo=repo.name, requestid=requestid) }}" method="POST">
  137. <button type="submit" value="Refresh" id="refresh_pr"
  138. class="btn btn-outline-primary btn-sm" title="Refresh the remote pull request">
  139. <span class="fa fa-refresh"></span> Refresh
  140. </button>
  141. {{ mergeform.csrf_token }}
  142. </form>
  143. {% endif %}
  144. <form class="inline" action="{{ url_for(
  145. 'ui_ns.close_request_pull',
  146. username=repo.user.user if repo.is_fork else None,
  147. namespace=repo.namespace,
  148. repo=repo.name, requestid=requestid) }}" method="POST">
  149. {% endif %}
  150. {% if pull_request.status == 'Open' and g.authenticated and
  151. (g.repo_committer or g.fas_user.username == pull_request.user.username) %}
  152. {{ mergeform.csrf_token }}
  153. <button type="submit" value="Close" id="close_pr"
  154. class="btn btn-outline-danger btn-sm" title="Close PR without merging it" data-toggle="tooltip">
  155. <span class="fa fa-times"></span> Close
  156. </button>
  157. {% endif %}
  158. {% if pull_request.status == 'Open' and g.authenticated and
  159. (g.repo_committer or g.fas_user.username == pull_request.user.username) %}
  160. </form>
  161. {% endif %}
  162. {% endif %}
  163. {% if pull_request.status == 'Open' %}
  164. <div class="dropdown float-right ml-1">
  165. <span class="dropdown dropdown-btn">
  166. <a href="#" id="merge_dropdown_btn"
  167. class="btn btn-outline-secondary btn-sm disabled dropdown-toggle" data-toggle="dropdown">
  168. <span class="fa fa-circle-o-notch fa-spin fa-fw"></span> <span id="merge-status-text">Merge</span>
  169. </a>
  170. <div id="merge-alert" class="text-xs-center dropdown-menu dropdown-menu-right p-0">
  171. <div class="alert text-center mb-0">
  172. {% if pull_request.status == 'Open' and g.repo_committer and pull_request.allow_rebase %}
  173. <small id="merge-alert-message"></small>
  174. <form action="{{ url_for('ui_ns.merge_request_pull',
  175. repo=repo.name,
  176. username=repo.user.user if repo.is_fork else None,
  177. namespace=repo.namespace,
  178. requestid=requestid)
  179. }}" method="POST" id="merge_pr_form">
  180. {{ mergeform.csrf_token }}
  181. <button id="merge_btn" type="submit"
  182. class="btn btn-block my-2">Merge</button>
  183. {% if can_delete_branch %}
  184. <div class="small">
  185. {{ mergeform.delete_branch }} {{ mergeform.delete_branch.label }}
  186. </div>
  187. {% endif %}
  188. </form>
  189. <button id="rebase_btn" type="submit"
  190. class="btn btn-block my-2">Rebase</button>
  191. {% else %}
  192. <small id="merge-alert-message"></small>
  193. {% endif %}
  194. </div>
  195. </div>
  196. </span>
  197. {% if g.authenticated and trigger_ci %}
  198. <span class="dropdown dropdown-btn">
  199. <a href="#" id="ci_dropdown_btn"
  200. class="btn btn-outline-primary btn-sm btn-info dropdown-toggle" data-toggle="dropdown">
  201. Rerun CI
  202. </a>
  203. <div class="text-xs-center dropdown-menu dropdown-menu-right p-0">
  204. <form action="{{ url_for('ui_ns.ci_trigger_request_pull',
  205. repo=repo.name,
  206. username=repo.username if repo.is_fork else None,
  207. namespace=repo.namespace,
  208. requestid=requestid)
  209. }}" method="POST" id="ci_pr_trigger_form">
  210. {{ trigger_ci_pr_form.csrf_token }}
  211. {{ trigger_ci_pr_form.comment(id="ci_pr_comment", hidden=True) }}
  212. {% for comment, meta in trigger_ci|dictsort %}
  213. <a class="dropdown-item trigger-ci-btn" href="#" title="{{ meta["description"] }}"
  214. data-comment="{{ comment }}">{{ meta["name"] }}</a>
  215. {% endfor %}
  216. </form>
  217. </div>
  218. </span>
  219. {% endif %}
  220. </div>
  221. {% endif %}
  222. {% if pull_request.status == 'Closed' and g.authenticated and
  223. (g.repo_committer or g.fas_user.username == pull_request.user.username) %}
  224. <form action="{{ url_for(
  225. 'ui_ns.reopen_request_pull',
  226. username=repo.user.user if repo.is_fork else None,
  227. namespace=repo.namespace,
  228. repo=repo.name, requestid=requestid) }}" method="POST">
  229. {{ mergeform.csrf_token }}
  230. <button type="submit" value="Reopen" id="reopen_pr"
  231. class="btn btn-sm btn-outline-danger" title="Reopen PR">
  232. Reopen Pull Request
  233. </button>
  234. </form>
  235. {% endif %}
  236. </div>
  237. </div>
  238. <div class="row" id="pr-wrapper">
  239. <div class="col-md-12">
  240. <ul class="nav nav-tabs nav-small my-4 border-bottom" role="tablist" id="pr-tabs">
  241. <li class="nav-item">
  242. <a class="nav-link active" data-toggle="tab" role="tab" href="#comments">
  243. Comments
  244. </a>
  245. </li>
  246. <li class="nav-item">
  247. <a class="nav-link {% if not pull_request %}active{%
  248. endif %}" data-toggle="tab" role="tab" href="#request_diff">
  249. <span>Files Changed&nbsp;</span>
  250. <span class="badge badge-secondary badge-pill">
  251. {{ diff|length if diff else 0}}
  252. </span>
  253. </a>
  254. </li>
  255. <li class="nav-item">
  256. <a class="nav-link" data-toggle="tab" role="tab" href="#commit_list">
  257. <span>Commits&nbsp;</span>
  258. <span class="badge badge-secondary badge-pill">
  259. {{ diff_commits|length }}
  260. </span>
  261. </a>
  262. </li>
  263. <li class="nav-item ml-auto">
  264. <a class="nav-link" href="{{ url_for(
  265. 'ui_ns.request_pull_patch',
  266. repo=repo.name,
  267. username=repo.user.user if repo.is_fork else None,
  268. namespace=repo.namespace,
  269. requestid=requestid) }}">
  270. <span class="hidden-sm-down">Patch</span>
  271. </a>
  272. </li>
  273. </ul>
  274. <div class="tab-content">
  275. <div class="tab-pane" role="tabpanel" id="commit_list">
  276. <div class="list-group">
  277. {% for commit in diff_commits %}
  278. {% if pull_request.status and pull_request.project_from.is_fork %}
  279. {% set commit_link = url_for(
  280. 'ui_ns.view_commit',
  281. repo=pull_request.project_from.name,
  282. username=pull_request.project_from.user.user,
  283. namespace=repo.namespace,
  284. commitid=commit.oid.hex)%}
  285. {% set tree_link = url_for(
  286. 'ui_ns.view_tree', username=pull_request.project_from.user.user, namespace=repo.namespace,
  287. repo=repo.name, identifier=commit.hex) %}
  288. {% elif pull_request.remote %}
  289. {% set commit_link = None %}
  290. {% else %}
  291. {% set commit_link = url_for('ui_ns.view_commit',
  292. repo=repo.name,
  293. username=repo.user.user if repo.is_fork else None,
  294. namespace=repo.namespace,
  295. commitid=commit.oid.hex) %}
  296. {% set tree_link = url_for(
  297. 'ui_ns.view_tree',
  298. username=repo.user.user if repo.is_fork else None,
  299. namespace=repo.namespace,
  300. repo=repo.name, identifier=commit.hex) %}
  301. {% endif %}
  302. <div class="list-group-item">
  303. <div class="row align-items-center">
  304. <div class="col">
  305. {% if commit_link %}
  306. <a class="notblue" href="{{commit_link}}">
  307. {% endif %}
  308. <strong>{{ commit.message.strip().split('\n')[0] }}</strong>
  309. {% if commit_link %}
  310. </a>
  311. {% endif %}
  312. <div>
  313. {{commit.author|author2user_commits(
  314. link=url_for('ui_ns.view_commits',
  315. repo=repo.name,
  316. branchname=branchname,
  317. username=repo.user.user if repo.is_fork else None,
  318. namespace=repo.namespace,
  319. author=commit.author.email),
  320. cssclass="notblue")|safe}}
  321. <span class="commitdate"
  322. title="{{ commit.commit_time|format_ts }}"> &bull;
  323. {{ commit.commit_time|humanize }}</span>&nbsp;&nbsp;
  324. </div>
  325. </div>
  326. <div class="col-xs-auto pr-3 text-right">
  327. <div class="btn-group">
  328. <a href="{{ commit_link }}"
  329. class="btn btn-outline-primary font-weight-bold {{'disabled' if not commit_link}}">
  330. <code>{{ commit.hex|short }}</code>
  331. </a>
  332. <a class="btn btn-outline-primary font-weight-bold {{'disabled' if not commit_link}}" href="{{tree_link}}"><span class="fa fa-file-code-o fa-fw"></span></a>
  333. </div>
  334. </div>
  335. </div>
  336. </div>
  337. {% else %}
  338. <p class="error"> No commits found </p>
  339. {% endfor %}
  340. </div>
  341. </div>
  342. <div class="tab-pane" role="tabpanel" id="request_diff">
  343. {% if g.authenticated %}
  344. <form action="{{ url_for('ui_ns.pull_request_drop_comment',
  345. repo=repo.name,
  346. username=repo.user.user if repo.is_fork else None,
  347. namespace=repo.namespace,
  348. requestid=requestid)
  349. }}" method="post" class="icon form_pr_drop_comment">
  350. {% endif %}
  351. {{repo_renderdiff(diff=diff,
  352. diff_commits=diff_commits,
  353. pull_request=pull_request,
  354. repo=repo,
  355. username=username,
  356. namespace=namespace)}}
  357. {% if g.authenticated %}
  358. {{ mergeform.csrf_token }}
  359. </form>
  360. {% endif %}
  361. </div>
  362. <div class="tab-pane active" role="tabpanel" id="comments">
  363. <div class="row">
  364. <div class="col-md-8">
  365. {{ show_pr_initial_comment(pull_request, repo, form, username) }}
  366. <section class="request_comment" >
  367. <form action="{{ url_for('ui_ns.pull_request_drop_comment',
  368. repo=repo.name,
  369. username=repo.user.user if repo.is_fork else None,
  370. namespace=repo.namespace,
  371. requestid=requestid)
  372. }}" method="post" id="request_comment" class="form_pr_drop_comment">
  373. {% if pull_request.comments %}
  374. {% for comment in pull_request.comments %}
  375. {% if comment.commit_id %}
  376. {{ show_comment(comment, comment.id, repo, username,
  377. requestid, form, PRinline=True) }}
  378. {% elif comment.notification %}
  379. <div class="d-flex align-items-center px-3 py-2 mb-3">
  380. <div class="">
  381. {{ comment.user.default_email | avatar(24) | safe }}
  382. </div>
  383. <span class="font-size-09 autogenerated-comment pl-4">{{ comment.comment | markdown | noJS | safe }}</span>
  384. <div class="text-muted ml-auto">
  385. <span title="{{ comment.date_created | format_datetime }}">{{
  386. comment.date_created | humanize }}</span>
  387. </div>
  388. </div>
  389. {% else %}
  390. {{ show_comment(comment, comment.id, repo, username,
  391. requestid, form) }}
  392. {% endif %}
  393. {% endfor %}
  394. {{ mergeform.csrf_token }}
  395. {% endif %}
  396. </form>
  397. </section>
  398. {% if g.authenticated and mergeform %}
  399. <div class="card mt-5">
  400. {% if g.authenticated %}
  401. <div class="card-header pb-0 pt-1 bg-light">
  402. <div class="row">
  403. <div class="col align-self-center">
  404. <span><strong>Add new comment</strong></span>
  405. </div>
  406. <div class="col">
  407. <ul class="nav nav-tabs float-right">
  408. <li class="nav-item">
  409. <a class="nav-link pointer" id="previewinmarkdown">Preview</a>
  410. </li>
  411. <li class="nav-item">
  412. <a class="nav-link active pointer" id="editinmarkdown">Edit</a>
  413. </li>
  414. </ul>
  415. {% if repo.quick_replies %}
  416. {% include "quick_reply.html" %}
  417. {% endif %}
  418. </div>
  419. </div>
  420. </div>
  421. <form action="{{ url_for(
  422. 'ui_ns.pull_request_add_comment',
  423. repo=repo.name,
  424. username=repo.user.user if repo.is_fork else None,
  425. namespace=repo.namespace,
  426. requestid=requestid) }}"
  427. method="post" class="form_pr_add_comment">
  428. {{ mergeform.csrf_token }}
  429. <div class="card-body">
  430. <textarea class="form-control" rows=8 id="comment" name="comment"
  431. placeholder="Enter your comment here" tabindex=1></textarea>
  432. <div id="preview" class="p-1">
  433. </div>
  434. </div>
  435. <div class="d-flex align-items-center card-footer bg-light">
  436. <small>Comments use <a href="https://docs.pagure.org/pagure/usage/markdown.html"
  437. target="_blank" rel="noopener noreferrer" class="notblue">Markdown Syntax</a></small>
  438. <div class="ml-auto">
  439. <input type="submit" class="btn btn-primary"
  440. value="Submit Comment" tabindex=2 />
  441. </div>
  442. </div>
  443. </form>
  444. {% endif %}
  445. </div>
  446. <div class="small">
  447. <p>
  448. Pull this pull-request locally
  449. <a href="#" id="local_pull_info_btn">
  450. <span class="fa fa-arrow-circle-down fa-fw fa-1x">
  451. </span>
  452. </a>
  453. </p>
  454. <pre id="local_pull_info" class="hidden">git fetch {{ config.get('GIT_URL_GIT') }}{{ repo.fullname }}.git refs/pull/{{ pull_request.id }}/head:pr{{ pull_request.id }}</pre>
  455. </div>
  456. {% endif %}
  457. </div>
  458. <div class="col-md-4">
  459. <div>
  460. <div class="mb-4">
  461. <h5 class="d-flex align-items-center font-weight-bold border-bottom">
  462. <div class="py-2 text-uppercase font-size-09">Metadata</div>
  463. {% if g.authenticated and mergeform
  464. and (g.repo_user
  465. or g.fas_user.username == pull_request.user.user) %}
  466. <div class="ml-auto">
  467. <a class="btn btn-outline-primary border-0 btn-sm issue-metadata-display editmetadatatoggle pointer" ><i class="fa fa-fw fa-pencil"></i></a>
  468. <a class="btn btn-outline-secondary border-0 btn-sm issue-metadata-form hidden editmetadatatoggle pointer" ><i class="fa fa-fw fa-times"></i></a>
  469. </div>
  470. {% endif %}
  471. </h5>
  472. {% if g.authenticated and mergeform and g.repo_user %}
  473. <form method="POST" action="{{ url_for('ui_ns.update_pull_requests',
  474. repo=repo.name,
  475. username=repo.user.user if repo.is_fork else None,
  476. namespace=repo.namespace,
  477. requestid=requestid) }}">
  478. <fieldset class="form-group issue-metadata-form hidden">
  479. <label><strong>Assignee</strong></label>
  480. <div>
  481. <input value="{{ pull_request.assignee.username or '' }}"
  482. name="user" id="assignee" placeholder="username" >
  483. {{ mergeform.csrf_token }}
  484. </div>
  485. </fieldset>
  486. {% endif %}
  487. <fieldset class="form-group issue-metadata-display ml-1">
  488. <label class="mb-1 pl-1"> <i class="fa fa-fw fa-user-plus"></i> <strong>Assignee</strong></label>
  489. <div id="assignee_plain">
  490. <div class="ml-2" title="{{ pull_request.assignee.html_title if pull_request.assignee else '' }}">
  491. {% if pull_request.assignee.username %}
  492. <div class="mt-1">{{pull_request.assignee.username| avatar(size=24) | safe}}
  493. <a href="{{ url_for(
  494. 'ui_ns.request_pulls',
  495. repo=repo.name,
  496. username=username,
  497. namespace=repo.namespace,
  498. assignee=pull_request.assignee.username)
  499. }}" title="{{ pull_request.assignee.html_title }}">
  500. {{ pull_request.assignee.username }}
  501. </a>
  502. {% if g.authenticated and (pull_request.assignee.username == g.fas_user.username) %}
  503. &mdash; <a class="pointer" id="drop-btn"
  504. title="drop the assignment of this pull-request">
  505. Drop
  506. </a>
  507. {% endif %}
  508. </div>
  509. {% else %}
  510. <div class="text-muted">
  511. <span class="text-muted">None</span>
  512. {% if g.authenticated and (g.repo_user or g.fas_user.username == pull_request.user.user) and pull_request.status|lower == 'open'
  513. and (not pull_request.assignee or pull_request.assignee.username != g.fas_user.username)
  514. and not repo.settings.get('pull_request_tracker_read_only', False) %}
  515. &mdash; <a class="pointer" id="take-btn"
  516. title="assign this pull_request to you"> Take </a>
  517. {% endif %}
  518. </div>
  519. {% endif %}
  520. </div>
  521. </div>
  522. </fieldset>
  523. {% if g.authenticated and (
  524. g.repo_user
  525. or g.fas_user.username == pull_request.user.user) %}
  526. <fieldset class="form-group issue-metadata-form hidden">
  527. <label class="mb-1"><i class="fa fa-fw fa-tag"></i> <strong>Tags</strong></label>
  528. <input id="tag" type="text" placeholder="tag1, tag2" name="tag"
  529. title="comma separated list of tags"
  530. value="{{ pull_request.tags_text | join(',') }}" />
  531. </fieldset>
  532. {% endif%}
  533. <fieldset class="form-group issue-metadata-display ml-1">
  534. <label class="mb-0"><strong>Tags</strong></label>
  535. {% if pull_request.tags %}
  536. <h4 id="taglist" class="ml-2">
  537. {% for tag in pull_request.tags %}
  538. <a id="tag-{{ tag.tag }}" title="{{ tag.tag_description }}"
  539. data-bg-color="{{ tag.tag_color }}"
  540. class="badge badge-primary text-left my-1 p-2 badge-tag"
  541. href="{{ url_for('ui_ns.request_pulls',
  542. repo=repo.name,
  543. username=repo.user.user if repo.is_fork else None,
  544. namespace=repo.namespace,
  545. tags=tag.tag) }}">
  546. {{ tag.tag }}
  547. </a>
  548. {% endfor %}
  549. </h4>
  550. {% else %}
  551. <div class="text-muted">No Tags</div>
  552. {% endif %}
  553. </fieldset>
  554. {% if g.authenticated and mergeform
  555. and (g.repo_user
  556. or g.fas_user.username == pull_request.user.user) %}
  557. <input type="submit" class="btn btn-primary issue-metadata-form hidden" value="Update">
  558. </form>
  559. {% endif %}
  560. </div>
  561. {% if pull_request.flags %}
  562. <div class="mb-4">
  563. <h5 class="d-flex align-items-center font-weight-bold border-bottom">
  564. <div class="py-2 text-uppercase font-size-09">Flags</div>
  565. </h5>
  566. <div class="list-group list-group-flush">
  567. {% for flag in pull_request.flags %}
  568. <a href="{{ flag.url }}" class="list-group-item list-group-item-action border-0 pl-2 pr-2">
  569. <div>
  570. <span class="font-weight-bold">
  571. {{ flag.username }}
  572. </span>
  573. <div class="float-right">
  574. <span class="badge {{ flag | flag2label }} font-size-09">
  575. {{ flag.status }}
  576. {%- if flag.percent %} ({{ flag.percent }}%) {%- endif %}
  577. </span>
  578. </div>
  579. </div>
  580. <small><div class="clearfix">
  581. <span>{{ flag.comment }}</span>
  582. <div title="{% if
  583. flag.date_created == flag.date_updated -%}
  584. Created at {% else -%} Updated at {% endif -%}
  585. {{ flag.date_updated }}" class="float-right">
  586. {{ flag.date_updated | humanize }}</div>
  587. </div>
  588. </small>
  589. </a>
  590. {% endfor%}
  591. </div>
  592. </div>
  593. {% endif %}
  594. {% if g.authenticated %}
  595. <div class="mt-3">
  596. <h5 class="d-flex align-items-center font-weight-bold border-bottom">
  597. <div class="py-2 text-uppercase font-size-09">
  598. Subscribers
  599. <span class="badge badge-secondary badge-pill font-size-09 ml-1" id="subscribers-count">{{subscribers|count}}</span>
  600. </div>
  601. <div class="ml-auto">
  602. <a href="#" class="btn btn-sm btn-link" id="subcribe-btn"
  603. {% if g.fas_user.username in subscribers -%}
  604. title="Unsubscribe from this pull-request">Unsubscribe
  605. {%- else -%}
  606. title="Subscribe to this pull-request">Subscribe
  607. {%- endif -%}
  608. </a>
  609. </div>
  610. </h5>
  611. {% if subscribers %}
  612. <div id="subscribers_list" class="p-2">
  613. {% for subscriber in subscribers %}
  614. <a href="{{ url_for('ui_ns.view_user', username=subscriber)
  615. }}" title="{{ subscriber }}" id="sub-avatar-{{subscriber}}">{{
  616. subscriber |avatar(size=30, css_class="pb-1") | safe
  617. }}</a>
  618. {% endfor %}
  619. </div>
  620. {% endif %}
  621. </div>
  622. {% endif %}
  623. </div>
  624. {% if diff %}
  625. <div class="mt-3">
  626. <h5 class="d-flex align-items-center font-weight-bold border-bottom">
  627. <div class="py-2 text-uppercase font-size-09">
  628. Changes Summary
  629. <span class="badge badge-secondary badge-pill font-size-09 ml-1">{{ diff|length if diff else 0}}</span>
  630. </div>
  631. </h5>
  632. {% macro changeschangedfile(filepath, added, removed, diffanchor) -%}
  633. <a href="#_{{diffanchor}}" class="list-group-item list-group-item-action pl-2 pr-2 border-0">
  634. <div class="clearfix">
  635. <div class="float-right">
  636. <span class="font-size-09 badge badge-success">+{{added}}</span>
  637. <span class="font-size-09 badge badge-danger">-{{removed}}</span>
  638. </div>
  639. <div class="pull-xs-left pr-changes-description">
  640. <strong>file <span class="text-muted">changed</span></strong>
  641. </div>
  642. </div>
  643. <div class="ellipsis pr-changes-description">
  644. <small>{{filepath}}</small>
  645. </div>
  646. </a>
  647. {%- endmacro %}
  648. {% macro changesrenamedfile(oldfilepath, newfilepath, added, removed, diffanchor) -%}
  649. <a href="#_{{diffanchor}}" class="list-group-item list-group-item-action pl-2 pr-2 border-0">
  650. <div class="clearfix">
  651. <div class="float-right"><span class="font-size-09 badge badge-success">+{{added}}</span> <span class="font-size-09 badge badge-danger">-{{removed}}</span></div>
  652. <div class="pull-xs-left pr-changes-description"><strong>file <span class="text-warning">renamed</span></strong></div>
  653. </div>
  654. <div class="ellipsis pr-changes-description">
  655. <strike class="text-muted">{{oldfilepath}}</strike> <br/>
  656. <small>{{newfilepath}}</small>
  657. </div>
  658. </a>
  659. {%- endmacro %}
  660. {% macro changesdeletedfile(filepath, added, removed, diffanchor) -%}
  661. <a href="#_{{diffanchor}}" class="list-group-item list-group-item-action pl-2 pr-2 border-0">
  662. <div class="clearfix">
  663. <div class="float-right"><span class="font-size-09 badge badge-danger">-{{removed}}</span></div>
  664. <div class="pull-xs-left pr-changes-description"><strong>file <span class="text-danger">removed</span></strong></div>
  665. </div>
  666. <div class="ellipsis pr-changes-description">
  667. <small>{{filepath}}</small>
  668. </div>
  669. </a>
  670. {%- endmacro %}
  671. {% macro changesaddedfile(filepath, added, removed, diffanchor) -%}
  672. <a href="#_{{diffanchor}}" class="list-group-item list-group-item-action pl-2 pr-2 border-0">
  673. <div class="clearfix">
  674. <div class="float-right"><span class="font-size-09 badge badge-success">+{{added}}</span></div>
  675. <div class="pull-xs-left pr-changes-description"><strong>file <span class="text-success">added</span></strong></div>
  676. </div>
  677. <div class="ellipsis pr-changes-description">
  678. <small>{{ filepath | unicode }}</small>
  679. </div>
  680. </a>
  681. {%- endmacro %}
  682. <div class="list-group list-group-flush">
  683. {% for patch in diff %}
  684. {% set patchstats = (patch | patch_stats) %}
  685. {%- if patchstats["status"] == 'D' -%}
  686. {{ changesdeletedfile(patchstats["new_path"], patchstats["lines_added"], patchstats["lines_removed"], loop.index) }}
  687. {%-elif patchstats["status"] == 'A' -%}
  688. {{ changesaddedfile(patchstats["new_path"], patchstats["lines_added"], patchstats["lines_removed"], loop.index) }}
  689. {%-elif patchstats["status"] == 'M' -%}
  690. {{ changeschangedfile(patchstats["new_path"], patchstats["lines_added"], patchstats["lines_removed"], loop.index) }}
  691. {%- else -%}
  692. {{changesrenamedfile(patchstats["old_path"], patchstats["new_path"], patchstats["lines_added"], patchstats["lines_removed"], loop.index) }}
  693. {%-endif-%}
  694. {% endfor %}
  695. </div>
  696. </div>
  697. {% endif %}
  698. </div>
  699. </div>
  700. </div>
  701. </div> <!-- tab content-->
  702. </div>
  703. </div>
  704. {% endblock %}
  705. {% block jscripts %}
  706. {{ super() }}
  707. <script type="text/javascript" nonce="{{ g.nonce }}" src="{{
  708. url_for('static', filename='vendor/jquery.textcomplete/jquery.textcomplete.min.js') }}?version={{ g.version}}"></script>
  709. <script type="text/javascript" nonce="{{ g.nonce }}" src="{{
  710. url_for('static', filename='vendor/emojione/emojione.min.js') }}?version={{ g.version}}"></script>
  711. <script type="text/javascript" nonce="{{ g.nonce }}" src="{{
  712. url_for('static', filename='emoji/emojicomplete.js') }}?version={{ g.version}}"></script>
  713. <script type="text/javascript" nonce="{{ g.nonce }}" src="{{
  714. url_for('static', filename='vendor/selectize/selectize.min.js') }}?version={{ g.version}}"> </script>
  715. <script type="text/javascript" nonce="{{ g.nonce }}" src="{{
  716. url_for('static', filename='vendor/jquery.caret/jquery.caret.min.js') }}?version={{ g.version}}"></script>
  717. <script type="text/javascript" nonce="{{ g.nonce }}" src="{{
  718. url_for('static', filename='vendor/jquery.atwho/jquery.atwho.min.js') }}?version={{ g.version}}"></script>
  719. <script type="text/javascript" nonce="{{ g.nonce }}" src="{{
  720. url_for('static', filename='request_ev.js') }}?version={{ g.version}}"></script>
  721. <script type="text/javascript" nonce="{{ g.nonce }}">
  722. function cancel_edit_btn() {
  723. $(".cancel").unbind();
  724. $(".cancel").click(
  725. function() {
  726. var item = $(this).closest('section');
  727. $(item.parent().find('.issue_comment')).show();
  728. $(item.parent().find('.issue_actions')).show();
  729. var form = item.find('.pr_comment_form');
  730. {# regular comments have the .pr_comment_form within the closest section #}
  731. if (form.length){
  732. $(form).remove();
  733. } else {
  734. {# inline comments have the section within the .pr_comment_form #}
  735. var form = $(this).closest('.pr_comment_form').parent();
  736. $(form).remove();
  737. }
  738. return false;
  739. }
  740. );
  741. };
  742. function setup_edit_btns() {
  743. $(".edit_btn").unbind();
  744. $(".edit_btn").click(function() {
  745. var commentid = $( this ).attr('data-comment');
  746. var _url = '{{ request.base_url }}' + '/comment/' + commentid + '/edit';
  747. $.ajax({
  748. url: _url + '?js=1',
  749. type: 'GET',
  750. dataType: 'html',
  751. success: function(res) {
  752. var el = $('#comment-' + commentid);
  753. var sec = el.parent().find('.issue_comment');
  754. $(sec).hide();
  755. el.parent().find('.issue_actions').hide();
  756. $(sec).after(res);
  757. cancel_edit_btn();
  758. },
  759. error: function() {
  760. alert('Could not make edit work');
  761. }
  762. });
  763. return false;
  764. });
  765. };
  766. function setup_reply_btns() {
  767. $(".reply").unbind();
  768. $( ".reply" ).click(
  769. function() {
  770. var _section = $(this).closest('.card');
  771. var _comment = _section.find('.comment_body');
  772. var _text = _comment.text().split("\n");
  773. var _output = new Array();
  774. for (var cnt=0; cnt < _text.length; cnt++) {
  775. _output[cnt] = '> ' + _text[cnt];
  776. }
  777. var _prev = $.trim($( "#comment" ).val());
  778. if (_prev.length > 0){
  779. _prev += "\n\n";
  780. }
  781. $( "#comment" ).val(_prev + _output.join("\n"));
  782. }
  783. );
  784. };
  785. function showTab(){
  786. $('#pr-tabs a[href="#request_diff"]').tab('show')
  787. }
  788. {% if pull_request %}
  789. function show_merge_status(){
  790. function process_response(res) {
  791. $('#spinner').hide();
  792. $('#merge_dropdown_btn').removeClass("disabled");
  793. $('#merge_dropdown_btn span.fa').removeClass("fa-spin");
  794. if (res.code == 'FFORWARD'){
  795. $('#merge_dropdown_btn').toggleClass("btn-outline-secondary btn-success");
  796. $('#merge_dropdown_btn span.fa').toggleClass("fa-circle-o-notch fa-check");
  797. $('#merge_btn').addClass("btn-success");
  798. $('#merge-alert .alert').addClass("alert-success");
  799. $('#merge-alert-message').text(res.message);
  800. $('#merge-alert #rebase_btn').hide();
  801. $('#merge-alert div.small').show();
  802. $('#merge_btn').show();
  803. }
  804. else if (res.code == 'MERGE') {
  805. $('#merge_dropdown_btn').toggleClass("btn-outline-secondary btn-warning");
  806. $('#merge_dropdown_btn span.fa').toggleClass("fa-circle-o-notch fa-check");
  807. $('#merge_btn').addClass("btn-warning");
  808. $('#merge-alert .alert').addClass("alert-warning");
  809. $('#merge-alert-message').text(res.message);
  810. $('#merge-alert div.small').show();
  811. $('#merge_btn').show();
  812. }
  813. else if (res.code == 'NEEDSREBASE') {
  814. $('#merge_dropdown_btn').toggleClass("btn-outline-secondary btn-warning");
  815. $('#merge_dropdown_btn span.fa').toggleClass("fa-circle-o-notch fa-times");
  816. $('#merge_btn').hide();
  817. $('#merge-alert .alert').addClass("alert-warning");
  818. $('#merge-alert-message').text(res.message);
  819. $('#merge-alert div.small').hide();
  820. }
  821. else if (res.code == 'CONFLICTS') {
  822. $('#merge_dropdown_btn').toggleClass("btn-outline-secondary btn-danger");
  823. $('#merge_dropdown_btn span.fa').toggleClass("fa-circle-o-notch fa-times");
  824. $('#merge_btn').hide();
  825. $('#merge-alert .alert').addClass("alert-danger");
  826. $('#merge-alert-message').text(res.message);
  827. $('#merge-alert div.small').hide();
  828. $('#merge-alert #rebase_btn').hide();
  829. }
  830. else if (res.code == 'NO_CHANGE') {
  831. $('#merge_btn').hide();
  832. $('#merge_dropdown_btn span.fa').toggleClass("fa-circle-o-notch fa-times");
  833. $('#merge-alert .alert').addClass("alert-secondary");
  834. $('#merge-alert-message').text(res.message);
  835. $('#merge-alert div.small').hide();
  836. $('#merge-alert #rebase_btn').hide();
  837. }
  838. };
  839. $('#spinner').show();
  840. function sleep(ms) {
  841. return new Promise(resolve => setTimeout(resolve, ms));
  842. }
  843. $.ajax({
  844. url: '{{ url_for("internal_ns.mergeable_request_pull") }}' ,
  845. beforeSend: function(){sleep(8000); return true;},
  846. type: 'POST',
  847. data: {
  848. requestid: "{{ pull_request.uid }}",
  849. csrf_token: "{{ mergeform.csrf_token.current_token }}",
  850. },
  851. dataType: 'json',
  852. success: function(res) {
  853. process_response(res)
  854. },
  855. error: function(res) {
  856. process_response(res.responseJSON);
  857. $('#merge_btn').attr("disabled", "disabled");
  858. }
  859. });
  860. return false;
  861. }
  862. {% endif %}
  863. $(document).ready(function() {
  864. $('#merge_btn').click(function() {
  865. return confirm('Confirm merging this pull-request');
  866. });
  867. $('.trigger-ci-btn').click(function() {
  868. $('#ci_pr_comment').val($(this).attr("data-comment"));
  869. $('#ci_pr_trigger_form').submit();
  870. });
  871. $('.inline_comment_link_btn').click(function() { showTab() });
  872. $('.delete_comment_btn').click(function() {
  873. return confirm('Do you really want to remove this comment?');
  874. });
  875. $('#rebase_btn').click(function(){
  876. var _conf = confirm('Confirm rebasing this pull-request');
  877. if (_conf === false){
  878. return false;
  879. }
  880. $('#merge_dropdown_btn span.fa').removeClass(
  881. "fa-circle-o-notch fa-times fa-check").addClass(
  882. "fa-circle-o-notch fa-fw");
  883. $('#merge_btn').removeClass("btn-success btn-warning btn-danger");
  884. $('#merge-alert .alert').removeClass("alert-success alert-warning alert-danger");
  885. $('#merge_dropdown_btn').addClass("disabled");
  886. $('#merge_dropdown_btn').removeClass(
  887. "btn-outline-secondary btn-danger btn-warning btn-success").addClass(
  888. "btn btn-outline-secondary btn-sm disabled dropdown-toggle");
  889. $('#merge_dropdown_btn span.fa').addClass("fa-spin");
  890. $.ajax({
  891. url: '{{ url_for('api_ns.api_pull_request_rebase',
  892. repo=repo.name,
  893. username=username,
  894. namespace=repo.namespace,
  895. requestid=requestid)
  896. }}' ,
  897. type: 'POST',
  898. data: {
  899. csrf_token: "{{ mergeform.csrf_token.current_token }}",
  900. },
  901. dataType: 'json',
  902. success: function(res) {
  903. show_merge_status()
  904. },
  905. error: function(res) {
  906. $('#merge_dropdown_btn').removeClass("disabled");
  907. $('#merge_dropdown_btn span.fa').removeClass("fa-spin");
  908. $('#merge_dropdown_btn').toggleClass("btn-outline-secondary btn-danger");
  909. $('#merge_dropdown_btn span.fa').toggleClass("fa-circle-o-notch fa-times");
  910. $('#merge_btn').hide();
  911. $('#merge-alert #rebase_btn').hide();
  912. $('#merge-alert .alert').addClass("alert-danger");
  913. $('#merge-alert-message').text('Failed to rebase this PR');
  914. $('#merge-alert div.small').hide();
  915. }
  916. });
  917. });
  918. $( ".commit_msg_txt" ).hide();
  919. $( ".commit_msg_btn" ).click(function() {
  920. var msgid = $( this ).attr('data-id');
  921. $( '#commit_msg_' + msgid).toggle();
  922. });
  923. var folder = '{{url_for("static", filename="emoji/png/") }}?version={{ g.version}}';
  924. var json_url = '{{ url_for("static", filename="vendor/emojione/emoji_strategy.json") }}?version={{ g.version}}';
  925. var branchselect = $('#branch_select').selectize({
  926. create: false,
  927. sortField: 'text',
  928. allowEmptyOption: false,
  929. onChange: function(value) {
  930. if (value != ""){
  931. var sel = $('#branch_select');
  932. var final_url = "{{ url_for('ui_ns.new_request_pull',
  933. username=repo.user.user if repo.is_fork else None,
  934. namespace=repo.namespace, repo=repo.name,
  935. branch_from=branch_from, branch_to='--', project_to=project_to) }}";
  936. final_url = final_url.replace('--', sel.val());
  937. window.location.href = final_url;
  938. }
  939. }
  940. });
  941. var branchselect = $('#branch_from_select').selectize({
  942. create: false,
  943. sortField: 'text',
  944. allowEmptyOption: false,
  945. onChange: function(value) {
  946. if (value != ""){
  947. var sel = $('#branch_from_select');
  948. var final_url = "{{ url_for('ui_ns.new_request_pull',
  949. username=repo.user.user if repo.is_fork else None,
  950. namespace=repo.namespace, repo=repo.name,
  951. branch_from='--', branch_to=branch_to, project_to=project_to) }}";
  952. final_url = final_url.replace('--', sel.val());
  953. console.log(final_url);
  954. //return false;
  955. window.location.href = final_url;
  956. }
  957. }
  958. });
  959. $('.form_pr_drop_comment').submit(function() {
  960. return try_async_comment(this, null);
  961. });
  962. $('.form_pr_add_comment').submit(function() {
  963. return try_async_comment($this, false);
  964. })
  965. {% if pull_request %}
  966. {# These lines are only for existing pull-requests, not new ones #}
  967. emoji_complete(json_url, folder);
  968. $('#local_pull_info_btn').click(function(){
  969. var _el = $('#local_pull_info');
  970. if (! _el.is(':visible')){
  971. _el.show();
  972. $('#local_pull_info_btn').html('<span class="fa fa-arrow-circle-up fa-fw fa-1x">');
  973. } else {
  974. _el.hide();
  975. $('#local_pull_info_btn').html('<span class="fa fa-arrow-circle-down fa-fw fa-1x">');
  976. }
  977. return false;
  978. });
  979. $('#close_pr').click(function(){
  980. return window.confirm("Are you sure you want to close this requested pull?");
  981. });
  982. $('#reopen_pr').click(function(){
  983. return window.confirm("Are you sure you want to reopen this requested pull?");
  984. });
  985. {% if g.authenticated %}
  986. $( ".code_table tr" ).hover(
  987. function() {
  988. $( this ).find( ".prc_img" ).show().width(13);
  989. }, function() {
  990. $( this ).find( ".prc_img" ).hide();
  991. }
  992. );
  993. $( ".prc" ).click(
  994. function() {
  995. var disabled = $('.prc').attr('disabled');
  996. if (disabled === true || disabled == "disabled") {
  997. return false;
  998. }
  999. $(".prc").attr("disabled","disabled");
  1000. var row = $( this ).attr('data-row');
  1001. var commit = $( this ).attr('data-commit');
  1002. var filename = $( this ).attr('data-filename');
  1003. var tree_id = $( this ).attr('data-tree');
  1004. var url = "{{ url_for(
  1005. 'ui_ns.pull_request_add_comment',
  1006. repo=repo.name,
  1007. username=repo.user.user if repo.is_fork else None,
  1008. namespace=repo.namespace,
  1009. requestid=requestid, commit='', filename='', row='') }}".slice(0, -2);
  1010. url = url + commit + '/' + filename + '/' + row
  1011. if (tree_id) {
  1012. url += '?tree_id=' + tree_id;
  1013. }
  1014. var rowid = $(this).prev().find('a').attr('id');
  1015. var table = $( this ).parent().parent();
  1016. var nextid = rowid.replace('_' + row, '_' + (Number(row) + 1));
  1017. var next_row = table.find('#' + nextid).parent().parent();
  1018. {# If we're at the last row, we won't be able to find the next_row
  1019. therefore we need to add it manually #}
  1020. if (next_row.length == 0) {
  1021. table.find("tr:last").after(
  1022. '<tr><td><a id="' + nextid + '"></a></td></tr>');
  1023. next_row = table.find('#' + nextid).parent().parent();
  1024. }
  1025. if (next_row.prev().find('.pr_comment_form').length == 0){
  1026. $.get( url , function( data ) {
  1027. next_row.before(
  1028. '<tr><td></td><td colspan="2" class="pr_comment_form"> \
  1029. <div class="card m-x-1"><div class="card-block">'
  1030. + data + '</div></div></td></tr>' );
  1031. cancel_edit_btn();
  1032. $(".prc").removeAttr("disabled");
  1033. emoji_complete(json_url, folder);
  1034. });
  1035. } else {
  1036. next_row.prev().find('.pr_comment_form').parent().remove();
  1037. $(".prc").removeAttr("disabled");
  1038. }
  1039. }
  1040. );
  1041. setup_edit_btns();
  1042. setup_reply_btns();
  1043. $(".comment_body").each(function(ind, obj) {
  1044. var source = $(obj).html();
  1045. var preview = emojione.toImage(source);
  1046. $(obj).html(preview);
  1047. });
  1048. $(".pr_comment").each(function(ind, obj) {
  1049. var source = $(obj).html();
  1050. var preview = emojione.toImage(source);
  1051. $(obj).html(preview);
  1052. });
  1053. {% endif %}
  1054. {% if pull_request.status == 'Open' %}
  1055. show_merge_status()
  1056. {% endif %}
  1057. {% endif %}
  1058. });
  1059. {% if g.authenticated and pull_request %}
  1060. $('#assignee').selectize({
  1061. valueField: 'user',
  1062. labelField: 'user',
  1063. searchField: 'user',
  1064. maxItems: 1,
  1065. create: false,
  1066. load: function(query, callback) {
  1067. if (!query.length) return callback();
  1068. $.getJSON(
  1069. "{{ url_for('api_ns.api_users') }}", {
  1070. pattern: "*"+query+"*"
  1071. },
  1072. function( data ) {
  1073. callback( data.users.map(function(x) { return { user: x }; }) );
  1074. }
  1075. );
  1076. }
  1077. });
  1078. $( ".editmetadatatoggle" ).click(
  1079. function() {
  1080. $( ".issue-metadata-form" ).toggle();
  1081. $( ".issue-metadata-display" ).toggle();
  1082. }
  1083. );
  1084. function set_ui_for_comment(setting){
  1085. if (setting == false) {
  1086. $(document.body).find('input[type="submit"]').removeAttr("disabled");
  1087. document.body.style.cursor = 'default';
  1088. } else {
  1089. $(document.body).find('input[type="submit"]').attr("disabled", "disabled");
  1090. document.body.style.cursor = 'wait';
  1091. }
  1092. }
  1093. $("#merge_pr_form").submit( function() {
  1094. set_ui_for_comment(true);
  1095. var _c = $("#comment");
  1096. if (_c.val()) {
  1097. $('<input />').attr('type', 'hidden')
  1098. .attr('name', "comment")
  1099. .attr('value', _c.val())
  1100. .appendTo(this);
  1101. }
  1102. return true;
  1103. });
  1104. function try_async_comment(form, inline) {
  1105. set_ui_for_comment(true);
  1106. var _data = $(form).serialize();
  1107. var btn = $(document.activeElement);
  1108. if (btn[0].name == 'drop_comment'){
  1109. _data += '&drop_comment=' + btn[0].value;
  1110. set_ui_for_comment(false);
  1111. return true;
  1112. }
  1113. var _url = form.action;
  1114. if (_url.indexOf('?') != -1){
  1115. _url += "&js=1";
  1116. } else {
  1117. _url += "?js=1";
  1118. }
  1119. /* Keep some variable in memory before sending them in case the SSE is down */
  1120. var _update = false;
  1121. var _comment = inline ? $(form).find('#inline-comment').val() : $(form).find('#comment').val();
  1122. var _comment_id = null;
  1123. if (!_comment && form.update_comment) {
  1124. _update = true;
  1125. _comment_id = $(form.edit_comment).val();
  1126. _comment = $(form).find('#update_comment').val();
  1127. }
  1128. var _commit_id = null;
  1129. var _line = null;
  1130. var _token = "{{ mergeform.csrf_token.current_token }}";
  1131. var _base_url = _url.split('?')[0];
  1132. if (!_base_url.match(/comment$/)){
  1133. _commit_id = _url.split('/comment/')[1];
  1134. _commit_id = _commit_id.split('/')[0];
  1135. items = _url.split('/');
  1136. _line = items[items.length -1];
  1137. }
  1138. $.post( _url, $(form).serialize() )
  1139. .done(function(data) {
  1140. if(data == 'ok') {
  1141. $('#comment').val('');
  1142. $('#preview').html('');
  1143. $('#previewinmarkdown').addClass('inactive');
  1144. $('#previewinmarkdown').removeClass('active');
  1145. $('#preview').hide();
  1146. $('#comment').show();
  1147. /* We have submitted the comment correctly */
  1148. var item = $('.pr_comment_form').closest('tr');
  1149. if (!$(item.parent().children()[1]).is(':visible')){
  1150. $(item.parent().children()[1]).show()
  1151. }
  1152. item.remove();
  1153. if (!sse) {
  1154. if (!_comment){
  1155. // Make the browser submit the form sync
  1156. form.submit();
  1157. }
  1158. console.log('no sse, adding the comment manually');
  1159. $.ajax({
  1160. url: "{{ url_for('ui_ns.markdown_preview') }}" ,
  1161. type: 'POST',
  1162. data: {
  1163. content: _comment,
  1164. csrf_token: _token,
  1165. },
  1166. dataType: 'html',
  1167. success: function(res) {
  1168. var _comment = emojione.toImage(res);
  1169. if (_update) {
  1170. var data = {
  1171. comment_updated: _comment,
  1172. comment_user: "{{ g.fas_user.username }}",
  1173. comment_date: Date.now(),
  1174. comment_id: _comment_id,
  1175. avatar_url: "{{ g.fas_user.email | avatar_url(16) }}",
  1176. }
  1177. } else {
  1178. var data = {
  1179. comment_added: _comment,
  1180. comment_user: "{{ g.fas_user.username }}",
  1181. comment_date: Date.now(),
  1182. avatar_url: "{{ g.fas_user.email | avatar_url(16) }}",
  1183. commit_id: _commit_id,
  1184. line: _line,
  1185. }
  1186. }
  1187. process_event(
  1188. data,
  1189. "{{ request.uid }}",
  1190. "{{ g.fas_user.username if g.authenticated or '' }}");
  1191. set_ui_for_comment(false);
  1192. setup_reply_btns()
  1193. return false;
  1194. }
  1195. });
  1196. return false;
  1197. } else {
  1198. set_ui_for_comment(false);
  1199. }
  1200. } else {
  1201. // Make the browser submit the form sync
  1202. form.submit();
  1203. }
  1204. })
  1205. .fail(function() {
  1206. // Make the browser submit the form sync
  1207. form.submit();
  1208. })
  1209. return false;
  1210. };
  1211. $(".add_comment_form").submit(function(event) {
  1212. return try_async_comment(this, true);
  1213. })
  1214. {% endif %}
  1215. </script>
  1216. <script type="text/javascript" nonce="{{ g.nonce }}">
  1217. var cur_hash = null;
  1218. function color_tags() {
  1219. $(".badge-tag").each(function(ind, obj) {
  1220. $(obj).css('background-color', $(obj).attr('data-bg-color'));
  1221. });
  1222. }
  1223. function highlight_comment() {
  1224. var _hash = window.location.hash;
  1225. if (_hash != cur_hash) {
  1226. $( cur_hash ).css(
  1227. "background", "linear-gradient(to bottom, #ededed 0%, #fff 100%)"
  1228. );
  1229. };
  1230. cur_hash = _hash;
  1231. if ( _hash ) {
  1232. $( _hash ).css(
  1233. "background", "linear-gradient(to bottom, #eded98 0%, #fff 100%)"
  1234. );
  1235. };
  1236. return false;
  1237. };
  1238. function updateHighlight(onload) {
  1239. var cls = "highlighted-line";
  1240. $('.' + cls).removeClass(cls)
  1241. if (location.hash === '') {
  1242. // Display comments when the hash is removed.
  1243. $('#pr-tabs .nav-item a.nav-link, #pr-wrapper .tab-pane').removeClass('active');
  1244. $('#comments, [href="#comments"]').addClass('active');
  1245. return
  1246. }
  1247. if (location.hash.indexOf("comment-") > -1) {
  1248. highlight_comment();
  1249. } else {
  1250. if (onload) {
  1251. // Hide all tabs, and then show the one pointed to by the hash.
  1252. // This is neccessary to handle 'Back' button presses in the browser,
  1253. // which otherwise break the tabs view .
  1254. $('#pr-tabs .nav-item a.nav-link').removeClass('active');
  1255. $('#pr-wrapper .tab-pane').removeClass('active');
  1256. // When the hash points to 'Files Changed' tab, or a highlight.
  1257. if (location.hash.indexOf("request_diff") > -1 ||
  1258. location.hash.indexOf("_") === 1 ||
  1259. location.hash.indexOf("c-") === 1) {
  1260. $('[href="#request_diff"]').addClass('active');
  1261. $('#request_diff').addClass('active');
  1262. }
  1263. // When the hash points to 'Commits' tab.
  1264. else if (location.hash.indexOf("commit_list") > -1) {
  1265. $('[href="#commit_list"]').addClass('active');
  1266. $('#commit_list').addClass('active');
  1267. }
  1268. // If neither, then show the 'Comments' tab by default.
  1269. else {
  1270. $('#comments').addClass('active');
  1271. $('[href="#comments"]').addClass('active');
  1272. }
  1273. }
  1274. var file = parseInt(location.hash.substr(2).split('__')[0], 10);
  1275. var lines = location.hash.split('__')[1].split('-').map(function (x) { return parseInt(x, 10) });
  1276. for (var i = lines[lines.length - 1]; i >= lines[0]; i--) {
  1277. $('#' + '_' + file + '__' + i).closest('tr').addClass(cls);
  1278. }
  1279. }
  1280. }
  1281. $(document).ready(function () {
  1282. color_tags();
  1283. {% if g.authenticated and pull_request %}
  1284. function set_up_subcribed() {
  1285. $("#subcribe-btn").click(function(){
  1286. var _url = "{{ url_for(
  1287. 'api_ns.api_subscribe_pull_request',
  1288. repo=repo.name,
  1289. username=repo.user.user if repo.is_fork else None,
  1290. namespace=repo.namespace,
  1291. requestid=pull_request.id
  1292. ) }}";
  1293. var _btn = $("#subcribe-btn");
  1294. var _data = {};
  1295. if (_btn.text() == 'Unsubscribe'){
  1296. _data.status = false;
  1297. } else {
  1298. _data.status = true;
  1299. }
  1300. console.log(_data);
  1301. $.post( _url, _data ).done(
  1302. function(data) {
  1303. var _btn = $("#subcribe-btn");
  1304. var _countlabel = $("#subscribers-count")
  1305. var _count = parseInt(_countlabel.text())
  1306. if (_btn.text() == 'Subscribe'){
  1307. _btn.text('Unsubscribe');
  1308. _countlabel.text(_count+1)
  1309. var _html = '<a href="/user/' + data.user + '"'
  1310. + 'title="'+data.user+'" id="sub-avatar-'+data.user+'">'
  1311. + '<img src="'+data.avatar_url+'" class="pb-1"></a>';
  1312. $('#subscribers_list').prepend(_html);
  1313. } else {
  1314. _btn.text('Subscribe');
  1315. _countlabel.text(_count-1);
  1316. $('#sub-avatar-'+data.user).remove();
  1317. }
  1318. return false;
  1319. }
  1320. )
  1321. return false;
  1322. });
  1323. };
  1324. set_up_subcribed();
  1325. {% endif %}
  1326. updateHighlight(true)
  1327. {% if form or pull_request %}
  1328. $( "#preview" ).hide();
  1329. $( "#previewinmarkdown" ).click(
  1330. function(event, ui) {
  1331. {% if form %}
  1332. var _el = $( "#initial_comment" );
  1333. var _token = "{{ form.csrf_token.current_token }}";
  1334. {% else %}
  1335. var _el = $( "#comment" );
  1336. var _token = "{{ mergeform.csrf_token.current_token }}";
  1337. {% endif %}
  1338. var _text = _el.val();
  1339. var _url = "{{ url_for('ui_ns.markdown_preview') }}";
  1340. $.ajax({
  1341. url: _url ,
  1342. type: 'POST',
  1343. data: {
  1344. content: _text,
  1345. csrf_token: _token,
  1346. },
  1347. dataType: 'html',
  1348. success: function(res) {
  1349. var preview = emojione.toImage(res);
  1350. $( "#preview" ).html(preview);
  1351. $( "#previewinmarkdown" ).addClass("active");
  1352. $( "#editinmarkdown" ).removeClass("active");
  1353. _el.hide();
  1354. $( "#preview" ).show();
  1355. },
  1356. error: function() {
  1357. alert('Unable to generate preview!'+error);
  1358. }
  1359. });
  1360. return false;
  1361. }
  1362. );
  1363. $( "#editinmarkdown" ).click(
  1364. function(event, ui) {
  1365. {% if form %}
  1366. var _el = $( "#initial_comment" );
  1367. var _token = "{{ form.csrf_token.current_token }}";
  1368. {% else %}
  1369. var _el = $( "#comment" );
  1370. var _token = "{{ mergeform.csrf_token.current_token }}";
  1371. {% endif %}
  1372. $( "#editinmarkdown" ).addClass("active");
  1373. $( "#previewinmarkdown" ).removeClass("active");
  1374. _el.show();
  1375. $( "#preview" ).hide();
  1376. }
  1377. );
  1378. {% endif %}
  1379. $.get("{{ url_for('api_ns.api_users') }}", {
  1380. pattern: '*'
  1381. }).done(function(resp) {
  1382. var userConfig = {
  1383. at: '@',
  1384. data: resp['mention'],
  1385. insertTpl: '@${username}',
  1386. displayTpl: "<li><img src=\"${image}\"> ${username} <small>${name}</small></li>",
  1387. searchKey: "username"
  1388. }
  1389. $("#comment").atwho(userConfig);
  1390. $("#initial_comment").atwho(userConfig);
  1391. });
  1392. $.when(
  1393. {%- if g.issues_enabled %}
  1394. $.get("{{ url_for('api_ns.api_view_issues',
  1395. repo=repo.name,
  1396. username=repo.user.user if repo.is_fork else None,
  1397. namespace=repo.namespace,
  1398. status='all') }}"),
  1399. {%- else %}
  1400. {},
  1401. {%- endif %}
  1402. {%- if repo.settings.get('pull_requests', True) %}
  1403. $.get("{{ url_for('api_ns.api_pull_request_views',
  1404. repo=repo.name,
  1405. username=repo.user.user if repo.is_fork else None,
  1406. namespace=repo.namespace,
  1407. status='all') }}")
  1408. {%- else %}
  1409. {}
  1410. {%- endif %}
  1411. ).done(function(issuesResp, prResp) {
  1412. // 0 is the api response
  1413. var issuesAndPrs = [];
  1414. if (typeof issuesResp[0] !== 'undefined') {
  1415. issuesAndPrs = issuesAndPrs.concat(issuesResp[0]['issues']);
  1416. }
  1417. if (typeof prResp[0] !== 'undefined') {
  1418. issuesAndPrs = issuesAndPrs.concat(prResp[0]['requests']);
  1419. }
  1420. var data = $.map(issuesAndPrs, function(ticket, idx) {
  1421. return {
  1422. name: ticket.id.toString(),
  1423. title: $('<div>').text(ticket.title).html()
  1424. }
  1425. });
  1426. var issueAndPrConfig = {
  1427. at: '#',
  1428. data: data,
  1429. insertTpl: '#${name}',
  1430. displayTpl: "<li>#${name}<small> ${title}</small></li>",
  1431. }
  1432. $("#comment").atwho(issueAndPrConfig);
  1433. $("#initial_comment").atwho(issueAndPrConfig);
  1434. });
  1435. var available_tags = [];
  1436. {%for tog in tag_list %}
  1437. available_tags.push("{{tog.tag}}");
  1438. {%endfor%}
  1439. var items = available_tags.map(function(x) { return { item: x }; });
  1440. $('#tag').selectize({
  1441. delimiter: ',',
  1442. options: items,
  1443. persist: false,
  1444. create: false,
  1445. labelField: "item",
  1446. valueField: "item",
  1447. searchField: ["item"],
  1448. });
  1449. } );
  1450. $(window).on('hashchange', updateHighlight);
  1451. var selected = [];
  1452. $("[data-line-number]").click(function (ev) {
  1453. var line = $(this).attr('data-line-number');
  1454. var file = $(this).attr('data-file-number');
  1455. if (ev.shiftKey) {
  1456. selected = selected.slice(-1).concat(line);
  1457. } else {
  1458. selected = [line];
  1459. }
  1460. var hash = '_' + file + '__' + selected[0];
  1461. if (selected.length === 2) {
  1462. hash = '_' + file + '__' + Math.min(selected[0], selected[1]) + '-' + Math.max(selected[0], selected[1]);
  1463. }
  1464. window.location.hash = hash;
  1465. return false;
  1466. });
  1467. // Update hash links in the addressbar according to which tab is clicked
  1468. // on the PR page.
  1469. $(document).on('click', '#pr-tabs a', function() {
  1470. if ($(this).text().trim() == 'Comments' || $(this).text().trim() == 'Patch'){
  1471. window.location.hash = '';
  1472. } else {
  1473. window.location.hash = $(this).attr('href');
  1474. }
  1475. });
  1476. // Show an icon to open the changed file, when the user hovers over the
  1477. // @@ -x,y +x,y @@ line in the diff. Clicking this icon opens the file (at the
  1478. // relevant line number) in a new tab.
  1479. $(document).on("mouseenter", "td.cell2", function(){
  1480. $(this).find("a.open_changed_file_icon_wrap").css('visibility', 'visible');
  1481. });
  1482. $(document).on("mouseleave", "td.cell2", function() {
  1483. $(this).find("a.open_changed_file_icon_wrap").css('visibility', 'hidden');
  1484. });
  1485. {% if g.authenticated and (g.repo_user or pull_request.user.user == g.fas_user.username or open_access) %}
  1486. function take_issue(){
  1487. var _url = "{{ url_for('api_ns.api_pull_request_assign',
  1488. repo=repo.name, namespace=repo.namespace, username=username,
  1489. requestid=requestid) }}";
  1490. var _data = {assignee: "{{ g.fas_user.username }}"};
  1491. $.post (_url, _data ).done(
  1492. function(data) {
  1493. var _user_url = '\n<div class="ml-2"><div class="mt-1">{{g.fas_user.username| avatar(size=24) | safe}} '
  1494. + '<a href="{{ url_for("ui_ns.request_pulls", repo=repo.name, username=username, namespace=repo.namespace) }}'
  1495. + '?assignee={{ g.fas_user.username }}">'
  1496. + '{{ g.fas_user.username }}</a>'
  1497. + ' &mdash; <a class="pointer" id="drop-btn" title="drop the assignment of this pull-request">Drop</a></div></div>';
  1498. $('#assignee_plain').html(_user_url);
  1499. $('#assignee').val("{{ g.fas_user.username }}");
  1500. setup_btn_take_drop();
  1501. }
  1502. ).fail(function() {
  1503. alert( "An error occured, could not assign this pull-request to you." );
  1504. })
  1505. return false;
  1506. }
  1507. {% endif %}
  1508. {% if g.authenticated and (
  1509. g.repo_user
  1510. or pull_request.user.user == g.fas_user.username
  1511. or pull_request.assignee.user == g.fas_user.username) %}
  1512. function drop_issue(){
  1513. var _url = "{{ url_for('api_ns.api_pull_request_assign',
  1514. repo=repo.name, namespace=repo.namespace, username=username,
  1515. requestid=requestid) }}";
  1516. var _data = {assignee: ""};
  1517. $.post( _url, _data ).done(
  1518. function(data) {
  1519. var _user_url = '<div class="ml-2">\n<span class="text-muted">None</span>'
  1520. + ' &mdash; <a class="pointer" id="take-btn" title="assign this pull-request to you">Take</a></div>';
  1521. $('#assignee_plain').html(_user_url);
  1522. $('#assignee').val("");
  1523. setup_btn_take_drop();
  1524. }
  1525. ).fail(function() {
  1526. alert( "An error occured, could not drop the current assignee." );
  1527. })
  1528. return false;
  1529. }
  1530. {% endif %}
  1531. function setup_btn_take_drop(){
  1532. {% if g.authenticated and g.repo_user %}
  1533. $("#take-btn").click(take_issue)
  1534. {% endif %}
  1535. {% if g.authenticated and (
  1536. g.repo_user
  1537. or pull_request.user.user == g.fas_user.username
  1538. or pull_request.assignee.user == g.fas_user.username) %}
  1539. $("#drop-btn").click(drop_issue);
  1540. {% endif %}
  1541. }
  1542. {% if g.authenticated and (
  1543. g.repo_user
  1544. or pull_request.user.user == g.fas_user.username
  1545. or pull_request.assignee.user == g.fas_user.username) %}
  1546. setup_btn_take_drop();
  1547. {% endif %}
  1548. </script>
  1549. <script type="text/javascript" nonce="{{ g.nonce }}">
  1550. var source = null;
  1551. var sse = true;
  1552. {% if config['EVENTSOURCE_SOURCE'] and pull_request %}
  1553. if (!!window.EventSource) {
  1554. source = new EventSource('{{ config["EVENTSOURCE_SOURCE"]
  1555. + request.script_root + request.path }}');
  1556. source.addEventListener('error', function(e) {
  1557. sse = false;
  1558. }, false);
  1559. }
  1560. window.onbeforeunload = function() {
  1561. source.close()
  1562. };
  1563. source.addEventListener('message', function(e) {
  1564. console.log(e.data);
  1565. var data = $.parseJSON(e.data);
  1566. process_event(
  1567. data,
  1568. "{{ request.uid }}",
  1569. "{{ g.fas_user.username if g.authenticated or '' }}");
  1570. setup_edit_btns();
  1571. setup_reply_btns();
  1572. }, false);
  1573. {% else %}
  1574. sse = false;
  1575. {% endif %}
  1576. </script>
  1577. {% if repo.quick_replies %}
  1578. <script type="text/javascript" src="{{ url_for('static', filename='quick_reply.js') }}?version={{ g.version}}"></script>
  1579. {% endif %}
  1580. <script type="text/javascript" src="{{ url_for('static', filename='reactions.js') }}?version={{ g.version}}"></script>
  1581. {% endblock %}