udata 9.1.5.dev31391__py2.py3-none-any.whl → 9.2.2.dev31715__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of udata might be problematic. Click here for more details.

Files changed (32) hide show
  1. tasks/__init__.py +1 -1
  2. udata/__init__.py +1 -1
  3. udata/api/commands.py +10 -2
  4. udata/api/oauth2.py +4 -3
  5. udata/api_fields.py +5 -1
  6. udata/core/dataservices/models.py +2 -2
  7. udata/core/dataset/csv.py +4 -16
  8. udata/core/discussions/api.py +24 -1
  9. udata/core/discussions/csv.py +22 -0
  10. udata/core/owned.py +16 -0
  11. udata/core/reuse/api.py +3 -2
  12. udata/core/reuse/models.py +1 -1
  13. udata/cors.py +9 -4
  14. udata/static/chunks/{5.3aa55302c802f48db900.js → 5.9049c2001a2f21930f78.js} +2 -2
  15. udata/static/chunks/5.9049c2001a2f21930f78.js.map +1 -0
  16. udata/static/chunks/{6.0db5d3ff22944de7edd5.js → 6.ad092769b0983a6eec2a.js} +19 -19
  17. udata/static/chunks/6.ad092769b0983a6eec2a.js.map +1 -0
  18. udata/static/common.js +1 -1
  19. udata/static/common.js.map +1 -1
  20. udata/tests/api/test_auth_api.py +88 -0
  21. udata/tests/api/test_reuses_api.py +105 -0
  22. udata/tests/test_cors.py +37 -1
  23. udata/tests/test_discussions.py +61 -0
  24. udata/tests/test_owned.py +100 -1
  25. {udata-9.1.5.dev31391.dist-info → udata-9.2.2.dev31715.dist-info}/METADATA +18 -3
  26. {udata-9.1.5.dev31391.dist-info → udata-9.2.2.dev31715.dist-info}/RECORD +30 -29
  27. udata/static/chunks/5.3aa55302c802f48db900.js.map +0 -1
  28. udata/static/chunks/6.0db5d3ff22944de7edd5.js.map +0 -1
  29. {udata-9.1.5.dev31391.dist-info → udata-9.2.2.dev31715.dist-info}/LICENSE +0 -0
  30. {udata-9.1.5.dev31391.dist-info → udata-9.2.2.dev31715.dist-info}/WHEEL +0 -0
  31. {udata-9.1.5.dev31391.dist-info → udata-9.2.2.dev31715.dist-info}/entry_points.txt +0 -0
  32. {udata-9.1.5.dev31391.dist-info → udata-9.2.2.dev31715.dist-info}/top_level.txt +0 -0
udata/static/common.js CHANGED
@@ -1,2 +1,2 @@
1
- !function(e){function __webpack_require__(a){if(c[a])return c[a].exports;var r=c[a]={exports:{},id:a,loaded:!1};return e[a].call(r.exports,r,r.exports,__webpack_require__),r.loaded=!0,r.exports}var a=window.webpackJsonp;window.webpackJsonp=function(b,f){for(var t,d,_=0,n=[];_<b.length;_++)d=b[_],r[d]&&n.push.apply(n,r[d]),r[d]=0;for(t in f){var i=f[t];switch(typeof i){case"object":e[t]=function(a){var c=a.slice(1),r=a[0];return function(a,b,f){e[r].apply(this,[a,b,f].concat(c))}}(i);break;case"function":e[t]=i;break;default:e[t]=e[i]}}for(a&&a(b,f);n.length;)n.shift().call(null,__webpack_require__);if(f[0])return c[0]=0,__webpack_require__(0)};var c={},r={31:0};__webpack_require__.e=function(e,a){if(0===r[e])return a.call(null,__webpack_require__);if(void 0!==r[e])r[e].push(a);else{r[e]=[a];var c=document.getElementsByTagName("head")[0],b=document.createElement("script");b.type="text/javascript",b.charset="utf-8",b.async=!0,b.src=__webpack_require__.p+"chunks/"+e+"."+{0:"285786eca84d7b7471ac",1:"765e28a442160395e71a",2:"f6372652ce6895fe1fad",3:"31f4515503a814e0e3a3",4:"ef9b48820b09477986b5",5:"3aa55302c802f48db900",6:"0db5d3ff22944de7edd5",7:"b2e2b3f88a04f6b296dd",8:"292c6a157d71f0b21b6f",9:"8ad948dd393d38f07a7a",10:"c1c9496ebfc8949f3de2",11:"18f1cc16362b76373c40",12:"f6b6e770dfe4a5949444",13:"066b4d36814efa6c485f",14:"72a6fc22c9b0cc8bcd14",15:"7de380008f4ec6ea4b5b",16:"cddb601c07a2df6564b2",17:"26a29ec18e773c2be9b2",18:"b7098bfe5401f75973fa",19:"2061dca8438f415029a3",20:"17e9e766625991aa0a7b",21:"2b48c7e5fe59775d5eb3",22:"2a919b7f6014457264e8",23:"18bb4231e440e17f29a2",24:"bb29ce250a1b77491420",25:"6cb865e768a122d52b4d",26:"2bd61452982a0d4de0a3",27:"1872ae081edb64216589",28:"1e240391cc0358805408",29:"556152a9334761b0d972",30:"a2eb870c6d29644ed9b5",32:"92a026dcad120ccdf15f",33:"6636c1fd8aec6fc072b8"}[e]+".js",c.appendChild(b)}},__webpack_require__.m=e,__webpack_require__.c=c,__webpack_require__.p="/static/"}(function(e){for(var a in e)if(Object.prototype.hasOwnProperty.call(e,a))switch(typeof e[a]){case"function":break;case"object":e[a]=function(a){var c=a.slice(1),r=e[a[0]];return function(e,a,b){r.apply(this,[e,a,b].concat(c))}}(e[a]);break;default:e[a]=e[e[a]]}return e}([]));
1
+ !function(e){function __webpack_require__(a){if(c[a])return c[a].exports;var r=c[a]={exports:{},id:a,loaded:!1};return e[a].call(r.exports,r,r.exports,__webpack_require__),r.loaded=!0,r.exports}var a=window.webpackJsonp;window.webpackJsonp=function(b,f){for(var t,_,n=0,d=[];n<b.length;n++)_=b[n],r[_]&&d.push.apply(d,r[_]),r[_]=0;for(t in f){var i=f[t];switch(typeof i){case"object":e[t]=function(a){var c=a.slice(1),r=a[0];return function(a,b,f){e[r].apply(this,[a,b,f].concat(c))}}(i);break;case"function":e[t]=i;break;default:e[t]=e[i]}}for(a&&a(b,f);d.length;)d.shift().call(null,__webpack_require__);if(f[0])return c[0]=0,__webpack_require__(0)};var c={},r={31:0};__webpack_require__.e=function(e,a){if(0===r[e])return a.call(null,__webpack_require__);if(void 0!==r[e])r[e].push(a);else{r[e]=[a];var c=document.getElementsByTagName("head")[0],b=document.createElement("script");b.type="text/javascript",b.charset="utf-8",b.async=!0,b.src=__webpack_require__.p+"chunks/"+e+"."+{0:"285786eca84d7b7471ac",1:"765e28a442160395e71a",2:"f6372652ce6895fe1fad",3:"31f4515503a814e0e3a3",4:"ef9b48820b09477986b5",5:"9049c2001a2f21930f78",6:"ad092769b0983a6eec2a",7:"b2e2b3f88a04f6b296dd",8:"292c6a157d71f0b21b6f",9:"8ad948dd393d38f07a7a",10:"c1c9496ebfc8949f3de2",11:"18f1cc16362b76373c40",12:"f6b6e770dfe4a5949444",13:"066b4d36814efa6c485f",14:"72a6fc22c9b0cc8bcd14",15:"7de380008f4ec6ea4b5b",16:"cddb601c07a2df6564b2",17:"26a29ec18e773c2be9b2",18:"b7098bfe5401f75973fa",19:"2061dca8438f415029a3",20:"17e9e766625991aa0a7b",21:"2b48c7e5fe59775d5eb3",22:"2a919b7f6014457264e8",23:"18bb4231e440e17f29a2",24:"bb29ce250a1b77491420",25:"6cb865e768a122d52b4d",26:"2bd61452982a0d4de0a3",27:"1872ae081edb64216589",28:"1e240391cc0358805408",29:"556152a9334761b0d972",30:"a2eb870c6d29644ed9b5",32:"92a026dcad120ccdf15f",33:"6636c1fd8aec6fc072b8"}[e]+".js",c.appendChild(b)}},__webpack_require__.m=e,__webpack_require__.c=c,__webpack_require__.p="/static/"}(function(e){for(var a in e)if(Object.prototype.hasOwnProperty.call(e,a))switch(typeof e[a]){case"function":break;case"object":e[a]=function(a){var c=a.slice(1),r=e[a[0]];return function(e,a,b){r.apply(this,[e,a,b].concat(c))}}(e[a]);break;default:e[a]=e[e[a]]}return e}([]));
2
2
  //# sourceMappingURL=common.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["webpack:///common.js","webpack:///webpack/bootstrap aae90f74dc2315755797"],"names":["modules","__webpack_require__","moduleId","installedModules","exports","module","id","loaded","call","parentJsonpFunction","window","chunkIds","moreModules","chunkId","i","callbacks","length","installedChunks","push","apply","_m","args","slice","templateId","a","b","c","this","concat","shift","31","e","callback","undefined","head","document","getElementsByTagName","script","createElement","type","charset","async","src","p","0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","32","33","appendChild","m","Object","prototype","hasOwnProperty","fn"],"mappings":"CAAS,SAAUA,GCwDnB,QAAAC,qBAAAC,GAGA,GAAAC,EAAAD,GACA,MAAAC,GAAAD,GAAAE,OAGA,IAAAC,GAAAF,EAAAD,IACAE,WACAE,GAAAJ,EACAK,QAAA,EAUA,OANAP,GAAAE,GAAAM,KAAAH,EAAAD,QAAAC,IAAAD,QAAAH,qBAGAI,EAAAE,QAAA,EAGAF,EAAAD,QA3EA,GAAAK,GAAAC,OAAA,YACAA,QAAA,sBAAAC,EAAAC,GAIA,IADA,GAAAV,GAAAW,EAAAC,EAAA,EAAAC,KACQD,EAAAH,EAAAK,OAAoBF,IAC5BD,EAAAF,EAAAG,GACAG,EAAAJ,IACAE,EAAAG,KAAAC,MAAAJ,EAAAE,EAAAJ,IACAI,EAAAJ,GAAA,CAEA,KAAAX,IAAAU,GAAA,CACA,GAAAQ,GAAAR,EAAAV,EAGA,cAAAkB,IACA,aAEApB,EAAAE,GAAA,SAAAkB,GACA,GAAAC,GAAAD,EAAAE,MAAA,GAAAC,EAAAH,EAAA,EACA,iBAAAI,EAAAC,EAAAC,GACA1B,EAAAuB,GAAAJ,MAAAQ,MAAAH,EAAAC,EAAAC,GAAAE,OAAAP,MAEMD,EACN,MACA,gBAEApB,EAAAE,GAAAkB,CACA,MACA,SAEApB,EAAAE,GAAAF,EAAAoB,IAKA,IADAX,KAAAE,EAAAC,GACAG,EAAAC,QACAD,EAAAc,QAAArB,KAAA,KAAAP,oBACA,IAAAW,EAAA,GAEA,MADAT,GAAA,KACAF,oBAAA,GAKA,IAAAE,MAKAc,GACAa,GAAA,EA6BA7B,qBAAA8B,EAAA,SAAAlB,EAAAmB,GAEA,OAAAf,EAAAJ,GACA,MAAAmB,GAAAxB,KAAA,KAAAP,oBAGA,IAAAgC,SAAAhB,EAAAJ,GACAI,EAAAJ,GAAAK,KAAAc,OACI,CAEJf,EAAAJ,IAAAmB,EACA,IAAAE,GAAAC,SAAAC,qBAAA,WACAC,EAAAF,SAAAG,cAAA,SACAD,GAAAE,KAAA,kBACAF,EAAAG,QAAA,QACAH,EAAAI,OAAA,EAEAJ,EAAAK,IAAAzC,oBAAA0C,EAAA,UAAA9B,EAAA,KAAsE+B,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,wBAAk5B/D,GAAA,MACx9BqB,EAAA2C,YAAAxC,KAKApC,oBAAA6E,EAAA9E,EAGAC,oBAAAyB,EAAAvB,EAGAF,oBAAA0C,EAAA,YDIW,SAAS3C,GAEnB,IAAI,GAAIc,KAAKd,GACZ,GAAG+E,OAAOC,UAAUC,eAAezE,KAAKR,EAASc,GAChD,aAAcd,GAAQc,IACtB,IAAK,WAAY,KACjB,KAAK,SAEJd,EAAQc,GAAM,SAASM,GACtB,GAAIC,GAAOD,EAAGE,MAAM,GAAI4D,EAAKlF,EAAQoB,EAAG,GACxC,OAAO,UAAUI,EAAEC,EAAEC,GACpBwD,EAAG/D,MAAMQ,MAAOH,EAAEC,EAAEC,GAAGE,OAAOP,MAE9BrB,EAAQc,GACV,MACD,SAECd,EAAQc,GAAKd,EAAQA,EAAQc,IAKhC,MAAOd","file":"common.js","sourcesContent":["/******/ (function(modules) { // webpackBootstrap\n/******/ \t// install a JSONP callback for chunk loading\n/******/ \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n/******/ \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules) {\n/******/ \t\t// add \"moreModules\" to the modules object,\n/******/ \t\t// then flag all \"chunkIds\" as loaded and fire callback\n/******/ \t\tvar moduleId, chunkId, i = 0, callbacks = [];\n/******/ \t\tfor(;i < chunkIds.length; i++) {\n/******/ \t\t\tchunkId = chunkIds[i];\n/******/ \t\t\tif(installedChunks[chunkId])\n/******/ \t\t\t\tcallbacks.push.apply(callbacks, installedChunks[chunkId]);\n/******/ \t\t\tinstalledChunks[chunkId] = 0;\n/******/ \t\t}\n/******/ \t\tfor(moduleId in moreModules) {\n/******/ \t\t\tvar _m = moreModules[moduleId];\n/******/\n/******/ \t\t\t// Check if module is deduplicated\n/******/ \t\t\tswitch(typeof _m) {\n/******/ \t\t\tcase \"object\":\n/******/ \t\t\t\t// Module can be created from a template\n/******/ \t\t\t\tmodules[moduleId] = (function(_m) {\n/******/ \t\t\t\t\tvar args = _m.slice(1), templateId = _m[0];\n/******/ \t\t\t\t\treturn function (a,b,c) {\n/******/ \t\t\t\t\t\tmodules[templateId].apply(this, [a,b,c].concat(args));\n/******/ \t\t\t\t\t};\n/******/ \t\t\t\t}(_m));\n/******/ \t\t\t\tbreak;\n/******/ \t\t\tcase \"function\":\n/******/ \t\t\t\t// Normal module\n/******/ \t\t\t\tmodules[moduleId] = _m;\n/******/ \t\t\t\tbreak;\n/******/ \t\t\tdefault:\n/******/ \t\t\t\t// Module is a copy of another module\n/******/ \t\t\t\tmodules[moduleId] = modules[_m];\n/******/ \t\t\t\tbreak;\n/******/ \t\t\t}\n/******/ \t\t}\n/******/ \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);\n/******/ \t\twhile(callbacks.length)\n/******/ \t\t\tcallbacks.shift().call(null, __webpack_require__);\n/******/ \t\tif(moreModules[0]) {\n/******/ \t\t\tinstalledModules[0] = 0;\n/******/ \t\t\treturn __webpack_require__(0);\n/******/ \t\t}\n/******/ \t};\n/******/\n/******/ \t// The module cache\n/******/ \tvar installedModules = {};\n/******/\n/******/ \t// object to store loaded and loading chunks\n/******/ \t// \"0\" means \"already loaded\"\n/******/ \t// Array means \"loading\", array contains callbacks\n/******/ \tvar installedChunks = {\n/******/ \t\t31:0\n/******/ \t};\n/******/\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(installedModules[moduleId])\n/******/ \t\t\treturn installedModules[moduleId].exports;\n/******/\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = installedModules[moduleId] = {\n/******/ \t\t\texports: {},\n/******/ \t\t\tid: moduleId,\n/******/ \t\t\tloaded: false\n/******/ \t\t};\n/******/\n/******/ \t\t// Execute the module function\n/******/ \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n/******/\n/******/ \t\t// Flag the module as loaded\n/******/ \t\tmodule.loaded = true;\n/******/\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/\n/******/ \t// This file contains only the entry chunk.\n/******/ \t// The chunk loading function for additional chunks\n/******/ \t__webpack_require__.e = function requireEnsure(chunkId, callback) {\n/******/ \t\t// \"0\" is the signal for \"already loaded\"\n/******/ \t\tif(installedChunks[chunkId] === 0)\n/******/ \t\t\treturn callback.call(null, __webpack_require__);\n/******/\n/******/ \t\t// an array means \"currently loading\".\n/******/ \t\tif(installedChunks[chunkId] !== undefined) {\n/******/ \t\t\tinstalledChunks[chunkId].push(callback);\n/******/ \t\t} else {\n/******/ \t\t\t// start chunk loading\n/******/ \t\t\tinstalledChunks[chunkId] = [callback];\n/******/ \t\t\tvar head = document.getElementsByTagName('head')[0];\n/******/ \t\t\tvar script = document.createElement('script');\n/******/ \t\t\tscript.type = 'text/javascript';\n/******/ \t\t\tscript.charset = 'utf-8';\n/******/ \t\t\tscript.async = true;\n/******/\n/******/ \t\t\tscript.src = __webpack_require__.p + \"chunks/\" + chunkId + \".\" + {\"0\":\"285786eca84d7b7471ac\",\"1\":\"765e28a442160395e71a\",\"2\":\"f6372652ce6895fe1fad\",\"3\":\"31f4515503a814e0e3a3\",\"4\":\"ef9b48820b09477986b5\",\"5\":\"3aa55302c802f48db900\",\"6\":\"0db5d3ff22944de7edd5\",\"7\":\"b2e2b3f88a04f6b296dd\",\"8\":\"292c6a157d71f0b21b6f\",\"9\":\"8ad948dd393d38f07a7a\",\"10\":\"c1c9496ebfc8949f3de2\",\"11\":\"18f1cc16362b76373c40\",\"12\":\"f6b6e770dfe4a5949444\",\"13\":\"066b4d36814efa6c485f\",\"14\":\"72a6fc22c9b0cc8bcd14\",\"15\":\"7de380008f4ec6ea4b5b\",\"16\":\"cddb601c07a2df6564b2\",\"17\":\"26a29ec18e773c2be9b2\",\"18\":\"b7098bfe5401f75973fa\",\"19\":\"2061dca8438f415029a3\",\"20\":\"17e9e766625991aa0a7b\",\"21\":\"2b48c7e5fe59775d5eb3\",\"22\":\"2a919b7f6014457264e8\",\"23\":\"18bb4231e440e17f29a2\",\"24\":\"bb29ce250a1b77491420\",\"25\":\"6cb865e768a122d52b4d\",\"26\":\"2bd61452982a0d4de0a3\",\"27\":\"1872ae081edb64216589\",\"28\":\"1e240391cc0358805408\",\"29\":\"556152a9334761b0d972\",\"30\":\"a2eb870c6d29644ed9b5\",\"32\":\"92a026dcad120ccdf15f\",\"33\":\"6636c1fd8aec6fc072b8\"}[chunkId] + \".js\";\n/******/ \t\t\thead.appendChild(script);\n/******/ \t\t}\n/******/ \t};\n/******/\n/******/ \t// expose the modules object (__webpack_modules__)\n/******/ \t__webpack_require__.m = modules;\n/******/\n/******/ \t// expose the module cache\n/******/ \t__webpack_require__.c = installedModules;\n/******/\n/******/ \t// __webpack_public_path__\n/******/ \t__webpack_require__.p = \"/static/\";\n/******/ })\n/************************************************************************/\n/******/ ((function(modules) {\n\t// Check all modules for deduplicated modules\n\tfor(var i in modules) {\n\t\tif(Object.prototype.hasOwnProperty.call(modules, i)) {\n\t\t\tswitch(typeof modules[i]) {\n\t\t\tcase \"function\": break;\n\t\t\tcase \"object\":\n\t\t\t\t// Module can be created from a template\n\t\t\t\tmodules[i] = (function(_m) {\n\t\t\t\t\tvar args = _m.slice(1), fn = modules[_m[0]];\n\t\t\t\t\treturn function (a,b,c) {\n\t\t\t\t\t\tfn.apply(this, [a,b,c].concat(args));\n\t\t\t\t\t};\n\t\t\t\t}(modules[i]));\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t// Module is a copy of another module\n\t\t\t\tmodules[i] = modules[modules[i]];\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\treturn modules;\n}([])));\n\n\n/** WEBPACK FOOTER **\n ** common.js\n **/"," \t// install a JSONP callback for chunk loading\n \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules) {\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, callbacks = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId])\n \t\t\t\tcallbacks.push.apply(callbacks, installedChunks[chunkId]);\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tvar _m = moreModules[moduleId];\n\n \t\t\t// Check if module is deduplicated\n \t\t\tswitch(typeof _m) {\n \t\t\tcase \"object\":\n \t\t\t\t// Module can be created from a template\n \t\t\t\tmodules[moduleId] = (function(_m) {\n \t\t\t\t\tvar args = _m.slice(1), templateId = _m[0];\n \t\t\t\t\treturn function (a,b,c) {\n \t\t\t\t\t\tmodules[templateId].apply(this, [a,b,c].concat(args));\n \t\t\t\t\t};\n \t\t\t\t}(_m));\n \t\t\t\tbreak;\n \t\t\tcase \"function\":\n \t\t\t\t// Normal module\n \t\t\t\tmodules[moduleId] = _m;\n \t\t\t\tbreak;\n \t\t\tdefault:\n \t\t\t\t// Module is a copy of another module\n \t\t\t\tmodules[moduleId] = modules[_m];\n \t\t\t\tbreak;\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);\n \t\twhile(callbacks.length)\n \t\t\tcallbacks.shift().call(null, __webpack_require__);\n \t\tif(moreModules[0]) {\n \t\t\tinstalledModules[0] = 0;\n \t\t\treturn __webpack_require__(0);\n \t\t}\n \t};\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// \"0\" means \"already loaded\"\n \t// Array means \"loading\", array contains callbacks\n \tvar installedChunks = {\n \t\t31:0\n \t};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n \t// This file contains only the entry chunk.\n \t// The chunk loading function for additional chunks\n \t__webpack_require__.e = function requireEnsure(chunkId, callback) {\n \t\t// \"0\" is the signal for \"already loaded\"\n \t\tif(installedChunks[chunkId] === 0)\n \t\t\treturn callback.call(null, __webpack_require__);\n\n \t\t// an array means \"currently loading\".\n \t\tif(installedChunks[chunkId] !== undefined) {\n \t\t\tinstalledChunks[chunkId].push(callback);\n \t\t} else {\n \t\t\t// start chunk loading\n \t\t\tinstalledChunks[chunkId] = [callback];\n \t\t\tvar head = document.getElementsByTagName('head')[0];\n \t\t\tvar script = document.createElement('script');\n \t\t\tscript.type = 'text/javascript';\n \t\t\tscript.charset = 'utf-8';\n \t\t\tscript.async = true;\n\n \t\t\tscript.src = __webpack_require__.p + \"chunks/\" + chunkId + \".\" + {\"0\":\"285786eca84d7b7471ac\",\"1\":\"765e28a442160395e71a\",\"2\":\"f6372652ce6895fe1fad\",\"3\":\"31f4515503a814e0e3a3\",\"4\":\"ef9b48820b09477986b5\",\"5\":\"3aa55302c802f48db900\",\"6\":\"0db5d3ff22944de7edd5\",\"7\":\"b2e2b3f88a04f6b296dd\",\"8\":\"292c6a157d71f0b21b6f\",\"9\":\"8ad948dd393d38f07a7a\",\"10\":\"c1c9496ebfc8949f3de2\",\"11\":\"18f1cc16362b76373c40\",\"12\":\"f6b6e770dfe4a5949444\",\"13\":\"066b4d36814efa6c485f\",\"14\":\"72a6fc22c9b0cc8bcd14\",\"15\":\"7de380008f4ec6ea4b5b\",\"16\":\"cddb601c07a2df6564b2\",\"17\":\"26a29ec18e773c2be9b2\",\"18\":\"b7098bfe5401f75973fa\",\"19\":\"2061dca8438f415029a3\",\"20\":\"17e9e766625991aa0a7b\",\"21\":\"2b48c7e5fe59775d5eb3\",\"22\":\"2a919b7f6014457264e8\",\"23\":\"18bb4231e440e17f29a2\",\"24\":\"bb29ce250a1b77491420\",\"25\":\"6cb865e768a122d52b4d\",\"26\":\"2bd61452982a0d4de0a3\",\"27\":\"1872ae081edb64216589\",\"28\":\"1e240391cc0358805408\",\"29\":\"556152a9334761b0d972\",\"30\":\"a2eb870c6d29644ed9b5\",\"32\":\"92a026dcad120ccdf15f\",\"33\":\"6636c1fd8aec6fc072b8\"}[chunkId] + \".js\";\n \t\t\thead.appendChild(script);\n \t\t}\n \t};\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/static/\";\n\n\n\n/** WEBPACK FOOTER **\n ** webpack/bootstrap aae90f74dc2315755797\n **/"],"sourceRoot":""}
1
+ {"version":3,"sources":["webpack:///common.js","webpack:///webpack/bootstrap 47bc9a5c7fac4343d504"],"names":["modules","__webpack_require__","moduleId","installedModules","exports","module","id","loaded","call","parentJsonpFunction","window","chunkIds","moreModules","chunkId","i","callbacks","length","installedChunks","push","apply","_m","args","slice","templateId","a","b","c","this","concat","shift","31","e","callback","undefined","head","document","getElementsByTagName","script","createElement","type","charset","async","src","p","0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","32","33","appendChild","m","Object","prototype","hasOwnProperty","fn"],"mappings":"CAAS,SAAUA,GCwDnB,QAAAC,qBAAAC,GAGA,GAAAC,EAAAD,GACA,MAAAC,GAAAD,GAAAE,OAGA,IAAAC,GAAAF,EAAAD,IACAE,WACAE,GAAAJ,EACAK,QAAA,EAUA,OANAP,GAAAE,GAAAM,KAAAH,EAAAD,QAAAC,IAAAD,QAAAH,qBAGAI,EAAAE,QAAA,EAGAF,EAAAD,QA3EA,GAAAK,GAAAC,OAAA,YACAA,QAAA,sBAAAC,EAAAC,GAIA,IADA,GAAAV,GAAAW,EAAAC,EAAA,EAAAC,KACQD,EAAAH,EAAAK,OAAoBF,IAC5BD,EAAAF,EAAAG,GACAG,EAAAJ,IACAE,EAAAG,KAAAC,MAAAJ,EAAAE,EAAAJ,IACAI,EAAAJ,GAAA,CAEA,KAAAX,IAAAU,GAAA,CACA,GAAAQ,GAAAR,EAAAV,EAGA,cAAAkB,IACA,aAEApB,EAAAE,GAAA,SAAAkB,GACA,GAAAC,GAAAD,EAAAE,MAAA,GAAAC,EAAAH,EAAA,EACA,iBAAAI,EAAAC,EAAAC,GACA1B,EAAAuB,GAAAJ,MAAAQ,MAAAH,EAAAC,EAAAC,GAAAE,OAAAP,MAEMD,EACN,MACA,gBAEApB,EAAAE,GAAAkB,CACA,MACA,SAEApB,EAAAE,GAAAF,EAAAoB,IAKA,IADAX,KAAAE,EAAAC,GACAG,EAAAC,QACAD,EAAAc,QAAArB,KAAA,KAAAP,oBACA,IAAAW,EAAA,GAEA,MADAT,GAAA,KACAF,oBAAA,GAKA,IAAAE,MAKAc,GACAa,GAAA,EA6BA7B,qBAAA8B,EAAA,SAAAlB,EAAAmB,GAEA,OAAAf,EAAAJ,GACA,MAAAmB,GAAAxB,KAAA,KAAAP,oBAGA,IAAAgC,SAAAhB,EAAAJ,GACAI,EAAAJ,GAAAK,KAAAc,OACI,CAEJf,EAAAJ,IAAAmB,EACA,IAAAE,GAAAC,SAAAC,qBAAA,WACAC,EAAAF,SAAAG,cAAA,SACAD,GAAAE,KAAA,kBACAF,EAAAG,QAAA,QACAH,EAAAI,OAAA,EAEAJ,EAAAK,IAAAzC,oBAAA0C,EAAA,UAAA9B,EAAA,KAAsE+B,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,uBAAAC,GAAA,wBAAk5B/D,GAAA,MACx9BqB,EAAA2C,YAAAxC,KAKApC,oBAAA6E,EAAA9E,EAGAC,oBAAAyB,EAAAvB,EAGAF,oBAAA0C,EAAA,YDIW,SAAS3C,GAEnB,IAAI,GAAIc,KAAKd,GACZ,GAAG+E,OAAOC,UAAUC,eAAezE,KAAKR,EAASc,GAChD,aAAcd,GAAQc,IACtB,IAAK,WAAY,KACjB,KAAK,SAEJd,EAAQc,GAAM,SAASM,GACtB,GAAIC,GAAOD,EAAGE,MAAM,GAAI4D,EAAKlF,EAAQoB,EAAG,GACxC,OAAO,UAAUI,EAAEC,EAAEC,GACpBwD,EAAG/D,MAAMQ,MAAOH,EAAEC,EAAEC,GAAGE,OAAOP,MAE9BrB,EAAQc,GACV,MACD,SAECd,EAAQc,GAAKd,EAAQA,EAAQc,IAKhC,MAAOd","file":"common.js","sourcesContent":["/******/ (function(modules) { // webpackBootstrap\n/******/ \t// install a JSONP callback for chunk loading\n/******/ \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n/******/ \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules) {\n/******/ \t\t// add \"moreModules\" to the modules object,\n/******/ \t\t// then flag all \"chunkIds\" as loaded and fire callback\n/******/ \t\tvar moduleId, chunkId, i = 0, callbacks = [];\n/******/ \t\tfor(;i < chunkIds.length; i++) {\n/******/ \t\t\tchunkId = chunkIds[i];\n/******/ \t\t\tif(installedChunks[chunkId])\n/******/ \t\t\t\tcallbacks.push.apply(callbacks, installedChunks[chunkId]);\n/******/ \t\t\tinstalledChunks[chunkId] = 0;\n/******/ \t\t}\n/******/ \t\tfor(moduleId in moreModules) {\n/******/ \t\t\tvar _m = moreModules[moduleId];\n/******/\n/******/ \t\t\t// Check if module is deduplicated\n/******/ \t\t\tswitch(typeof _m) {\n/******/ \t\t\tcase \"object\":\n/******/ \t\t\t\t// Module can be created from a template\n/******/ \t\t\t\tmodules[moduleId] = (function(_m) {\n/******/ \t\t\t\t\tvar args = _m.slice(1), templateId = _m[0];\n/******/ \t\t\t\t\treturn function (a,b,c) {\n/******/ \t\t\t\t\t\tmodules[templateId].apply(this, [a,b,c].concat(args));\n/******/ \t\t\t\t\t};\n/******/ \t\t\t\t}(_m));\n/******/ \t\t\t\tbreak;\n/******/ \t\t\tcase \"function\":\n/******/ \t\t\t\t// Normal module\n/******/ \t\t\t\tmodules[moduleId] = _m;\n/******/ \t\t\t\tbreak;\n/******/ \t\t\tdefault:\n/******/ \t\t\t\t// Module is a copy of another module\n/******/ \t\t\t\tmodules[moduleId] = modules[_m];\n/******/ \t\t\t\tbreak;\n/******/ \t\t\t}\n/******/ \t\t}\n/******/ \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);\n/******/ \t\twhile(callbacks.length)\n/******/ \t\t\tcallbacks.shift().call(null, __webpack_require__);\n/******/ \t\tif(moreModules[0]) {\n/******/ \t\t\tinstalledModules[0] = 0;\n/******/ \t\t\treturn __webpack_require__(0);\n/******/ \t\t}\n/******/ \t};\n/******/\n/******/ \t// The module cache\n/******/ \tvar installedModules = {};\n/******/\n/******/ \t// object to store loaded and loading chunks\n/******/ \t// \"0\" means \"already loaded\"\n/******/ \t// Array means \"loading\", array contains callbacks\n/******/ \tvar installedChunks = {\n/******/ \t\t31:0\n/******/ \t};\n/******/\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(installedModules[moduleId])\n/******/ \t\t\treturn installedModules[moduleId].exports;\n/******/\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = installedModules[moduleId] = {\n/******/ \t\t\texports: {},\n/******/ \t\t\tid: moduleId,\n/******/ \t\t\tloaded: false\n/******/ \t\t};\n/******/\n/******/ \t\t// Execute the module function\n/******/ \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n/******/\n/******/ \t\t// Flag the module as loaded\n/******/ \t\tmodule.loaded = true;\n/******/\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/\n/******/ \t// This file contains only the entry chunk.\n/******/ \t// The chunk loading function for additional chunks\n/******/ \t__webpack_require__.e = function requireEnsure(chunkId, callback) {\n/******/ \t\t// \"0\" is the signal for \"already loaded\"\n/******/ \t\tif(installedChunks[chunkId] === 0)\n/******/ \t\t\treturn callback.call(null, __webpack_require__);\n/******/\n/******/ \t\t// an array means \"currently loading\".\n/******/ \t\tif(installedChunks[chunkId] !== undefined) {\n/******/ \t\t\tinstalledChunks[chunkId].push(callback);\n/******/ \t\t} else {\n/******/ \t\t\t// start chunk loading\n/******/ \t\t\tinstalledChunks[chunkId] = [callback];\n/******/ \t\t\tvar head = document.getElementsByTagName('head')[0];\n/******/ \t\t\tvar script = document.createElement('script');\n/******/ \t\t\tscript.type = 'text/javascript';\n/******/ \t\t\tscript.charset = 'utf-8';\n/******/ \t\t\tscript.async = true;\n/******/\n/******/ \t\t\tscript.src = __webpack_require__.p + \"chunks/\" + chunkId + \".\" + {\"0\":\"285786eca84d7b7471ac\",\"1\":\"765e28a442160395e71a\",\"2\":\"f6372652ce6895fe1fad\",\"3\":\"31f4515503a814e0e3a3\",\"4\":\"ef9b48820b09477986b5\",\"5\":\"9049c2001a2f21930f78\",\"6\":\"ad092769b0983a6eec2a\",\"7\":\"b2e2b3f88a04f6b296dd\",\"8\":\"292c6a157d71f0b21b6f\",\"9\":\"8ad948dd393d38f07a7a\",\"10\":\"c1c9496ebfc8949f3de2\",\"11\":\"18f1cc16362b76373c40\",\"12\":\"f6b6e770dfe4a5949444\",\"13\":\"066b4d36814efa6c485f\",\"14\":\"72a6fc22c9b0cc8bcd14\",\"15\":\"7de380008f4ec6ea4b5b\",\"16\":\"cddb601c07a2df6564b2\",\"17\":\"26a29ec18e773c2be9b2\",\"18\":\"b7098bfe5401f75973fa\",\"19\":\"2061dca8438f415029a3\",\"20\":\"17e9e766625991aa0a7b\",\"21\":\"2b48c7e5fe59775d5eb3\",\"22\":\"2a919b7f6014457264e8\",\"23\":\"18bb4231e440e17f29a2\",\"24\":\"bb29ce250a1b77491420\",\"25\":\"6cb865e768a122d52b4d\",\"26\":\"2bd61452982a0d4de0a3\",\"27\":\"1872ae081edb64216589\",\"28\":\"1e240391cc0358805408\",\"29\":\"556152a9334761b0d972\",\"30\":\"a2eb870c6d29644ed9b5\",\"32\":\"92a026dcad120ccdf15f\",\"33\":\"6636c1fd8aec6fc072b8\"}[chunkId] + \".js\";\n/******/ \t\t\thead.appendChild(script);\n/******/ \t\t}\n/******/ \t};\n/******/\n/******/ \t// expose the modules object (__webpack_modules__)\n/******/ \t__webpack_require__.m = modules;\n/******/\n/******/ \t// expose the module cache\n/******/ \t__webpack_require__.c = installedModules;\n/******/\n/******/ \t// __webpack_public_path__\n/******/ \t__webpack_require__.p = \"/static/\";\n/******/ })\n/************************************************************************/\n/******/ ((function(modules) {\n\t// Check all modules for deduplicated modules\n\tfor(var i in modules) {\n\t\tif(Object.prototype.hasOwnProperty.call(modules, i)) {\n\t\t\tswitch(typeof modules[i]) {\n\t\t\tcase \"function\": break;\n\t\t\tcase \"object\":\n\t\t\t\t// Module can be created from a template\n\t\t\t\tmodules[i] = (function(_m) {\n\t\t\t\t\tvar args = _m.slice(1), fn = modules[_m[0]];\n\t\t\t\t\treturn function (a,b,c) {\n\t\t\t\t\t\tfn.apply(this, [a,b,c].concat(args));\n\t\t\t\t\t};\n\t\t\t\t}(modules[i]));\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t// Module is a copy of another module\n\t\t\t\tmodules[i] = modules[modules[i]];\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\treturn modules;\n}([])));\n\n\n/** WEBPACK FOOTER **\n ** common.js\n **/"," \t// install a JSONP callback for chunk loading\n \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules) {\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, callbacks = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId])\n \t\t\t\tcallbacks.push.apply(callbacks, installedChunks[chunkId]);\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tvar _m = moreModules[moduleId];\n\n \t\t\t// Check if module is deduplicated\n \t\t\tswitch(typeof _m) {\n \t\t\tcase \"object\":\n \t\t\t\t// Module can be created from a template\n \t\t\t\tmodules[moduleId] = (function(_m) {\n \t\t\t\t\tvar args = _m.slice(1), templateId = _m[0];\n \t\t\t\t\treturn function (a,b,c) {\n \t\t\t\t\t\tmodules[templateId].apply(this, [a,b,c].concat(args));\n \t\t\t\t\t};\n \t\t\t\t}(_m));\n \t\t\t\tbreak;\n \t\t\tcase \"function\":\n \t\t\t\t// Normal module\n \t\t\t\tmodules[moduleId] = _m;\n \t\t\t\tbreak;\n \t\t\tdefault:\n \t\t\t\t// Module is a copy of another module\n \t\t\t\tmodules[moduleId] = modules[_m];\n \t\t\t\tbreak;\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);\n \t\twhile(callbacks.length)\n \t\t\tcallbacks.shift().call(null, __webpack_require__);\n \t\tif(moreModules[0]) {\n \t\t\tinstalledModules[0] = 0;\n \t\t\treturn __webpack_require__(0);\n \t\t}\n \t};\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// \"0\" means \"already loaded\"\n \t// Array means \"loading\", array contains callbacks\n \tvar installedChunks = {\n \t\t31:0\n \t};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n \t// This file contains only the entry chunk.\n \t// The chunk loading function for additional chunks\n \t__webpack_require__.e = function requireEnsure(chunkId, callback) {\n \t\t// \"0\" is the signal for \"already loaded\"\n \t\tif(installedChunks[chunkId] === 0)\n \t\t\treturn callback.call(null, __webpack_require__);\n\n \t\t// an array means \"currently loading\".\n \t\tif(installedChunks[chunkId] !== undefined) {\n \t\t\tinstalledChunks[chunkId].push(callback);\n \t\t} else {\n \t\t\t// start chunk loading\n \t\t\tinstalledChunks[chunkId] = [callback];\n \t\t\tvar head = document.getElementsByTagName('head')[0];\n \t\t\tvar script = document.createElement('script');\n \t\t\tscript.type = 'text/javascript';\n \t\t\tscript.charset = 'utf-8';\n \t\t\tscript.async = true;\n\n \t\t\tscript.src = __webpack_require__.p + \"chunks/\" + chunkId + \".\" + {\"0\":\"285786eca84d7b7471ac\",\"1\":\"765e28a442160395e71a\",\"2\":\"f6372652ce6895fe1fad\",\"3\":\"31f4515503a814e0e3a3\",\"4\":\"ef9b48820b09477986b5\",\"5\":\"9049c2001a2f21930f78\",\"6\":\"ad092769b0983a6eec2a\",\"7\":\"b2e2b3f88a04f6b296dd\",\"8\":\"292c6a157d71f0b21b6f\",\"9\":\"8ad948dd393d38f07a7a\",\"10\":\"c1c9496ebfc8949f3de2\",\"11\":\"18f1cc16362b76373c40\",\"12\":\"f6b6e770dfe4a5949444\",\"13\":\"066b4d36814efa6c485f\",\"14\":\"72a6fc22c9b0cc8bcd14\",\"15\":\"7de380008f4ec6ea4b5b\",\"16\":\"cddb601c07a2df6564b2\",\"17\":\"26a29ec18e773c2be9b2\",\"18\":\"b7098bfe5401f75973fa\",\"19\":\"2061dca8438f415029a3\",\"20\":\"17e9e766625991aa0a7b\",\"21\":\"2b48c7e5fe59775d5eb3\",\"22\":\"2a919b7f6014457264e8\",\"23\":\"18bb4231e440e17f29a2\",\"24\":\"bb29ce250a1b77491420\",\"25\":\"6cb865e768a122d52b4d\",\"26\":\"2bd61452982a0d4de0a3\",\"27\":\"1872ae081edb64216589\",\"28\":\"1e240391cc0358805408\",\"29\":\"556152a9334761b0d972\",\"30\":\"a2eb870c6d29644ed9b5\",\"32\":\"92a026dcad120ccdf15f\",\"33\":\"6636c1fd8aec6fc072b8\"}[chunkId] + \".js\";\n \t\t\thead.appendChild(script);\n \t\t}\n \t};\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/static/\";\n\n\n\n/** WEBPACK FOOTER **\n ** webpack/bootstrap 47bc9a5c7fac4343d504\n **/"],"sourceRoot":""}
@@ -59,6 +59,7 @@ def oauth(app, request):
59
59
  name="test-client",
60
60
  owner=UserFactory(),
61
61
  redirect_uris=["https://test.org/callback"],
62
+ secret="suchs3cr3t",
62
63
  )
63
64
  kwargs.update(custom_kwargs)
64
65
  return OAuth2Client.objects.create(**kwargs)
@@ -456,6 +457,93 @@ class APIAuthTest:
456
457
  assert response.content_type == "application/json"
457
458
  assert response.json == {"success": True}
458
459
 
460
+ @pytest.mark.oauth(secret=None)
461
+ def test_s256_code_challenge_success_no_client_secret(self, client, oauth):
462
+ """Authenticate through an OAuth client that has no secret associated (public client)"""
463
+ code_verifier = generate_token(48)
464
+ code_challenge = create_s256_code_challenge(code_verifier)
465
+
466
+ client.login()
467
+
468
+ response = client.post(
469
+ url_for(
470
+ "oauth.authorize",
471
+ response_type="code",
472
+ client_id=oauth.client_id,
473
+ code_challenge=code_challenge,
474
+ code_challenge_method="S256",
475
+ ),
476
+ {
477
+ "scope": "default",
478
+ "accept": "",
479
+ },
480
+ )
481
+ assert "code=" in response.location
482
+
483
+ params = dict(url_decode(urlparse.urlparse(response.location).query))
484
+ code = params["code"]
485
+
486
+ response = client.post(
487
+ url_for("oauth.token"),
488
+ {
489
+ "grant_type": "authorization_code",
490
+ "code": code,
491
+ "code_verifier": code_verifier,
492
+ "client_id": oauth.client_id,
493
+ },
494
+ )
495
+
496
+ assert200(response)
497
+ assert response.content_type == "application/json"
498
+ assert "access_token" in response.json
499
+
500
+ token = response.json["access_token"]
501
+
502
+ response = client.post(
503
+ url_for("api.fake"), headers={"Authorization": " ".join(["Bearer", token])}
504
+ )
505
+
506
+ assert200(response)
507
+ assert response.content_type == "application/json"
508
+ assert response.json == {"success": True}
509
+
510
+ def test_s256_code_challenge_missing_client_secret(self, client, oauth):
511
+ """Fail authentication through an OAuth client with missing secret"""
512
+ code_verifier = generate_token(48)
513
+ code_challenge = create_s256_code_challenge(code_verifier)
514
+
515
+ client.login()
516
+
517
+ response = client.post(
518
+ url_for(
519
+ "oauth.authorize",
520
+ response_type="code",
521
+ client_id=oauth.client_id,
522
+ code_challenge=code_challenge,
523
+ code_challenge_method="S256",
524
+ ),
525
+ {
526
+ "scope": "default",
527
+ "accept": "",
528
+ },
529
+ )
530
+ assert "code=" in response.location
531
+
532
+ params = dict(url_decode(urlparse.urlparse(response.location).query))
533
+ code = params["code"]
534
+
535
+ response = client.post(
536
+ url_for("oauth.token"),
537
+ {
538
+ "grant_type": "authorization_code",
539
+ "code": code,
540
+ "code_verifier": code_verifier,
541
+ "client_id": oauth.client_id,
542
+ },
543
+ )
544
+
545
+ assert401(response)
546
+
459
547
  def test_authorization_multiple_grant_token(self, client, oauth):
460
548
  for i in range(3):
461
549
  client.login()
@@ -2,6 +2,7 @@ from datetime import datetime
2
2
 
3
3
  import pytest
4
4
  from flask import url_for
5
+ from werkzeug.test import TestResponse
5
6
 
6
7
  from udata.core.badges.factories import badge_factory
7
8
  from udata.core.dataset.factories import DatasetFactory
@@ -25,6 +26,11 @@ pytestmark = [
25
26
  ]
26
27
 
27
28
 
29
+ def reuse_in_response(response: TestResponse, reuse: Reuse) -> bool:
30
+ only_reuse = [r for r in response.json["data"] if r["id"] == str(reuse.id)]
31
+ return len(only_reuse) > 0
32
+
33
+
28
34
  class ReuseAPITest:
29
35
  modules = []
30
36
 
@@ -88,6 +94,7 @@ class ReuseAPITest:
88
94
  # Keep only featured reuses (if any)
89
95
  data = [reuse for reuse in response.json["data"] if reuse["featured"]]
90
96
  assert len(data) == 0 # It did not return any featured reuse
97
+
91
98
  # filter on topic
92
99
  response = api.get(url_for("api.reuses", topic=topic_reuse.topic))
93
100
  assert200(response)
@@ -118,6 +125,104 @@ class ReuseAPITest:
118
125
  response = api.get(url_for("api.reuses", organization="org-id"))
119
126
  assert400(response)
120
127
 
128
+ def test_reuse_api_list_filter_private(self, api) -> None:
129
+ """Should filters reuses results based on the `private` filter"""
130
+ user = UserFactory()
131
+ public_reuse: Reuse = ReuseFactory()
132
+ private_reuse: Reuse = ReuseFactory(private=True, owner=user)
133
+
134
+ # Only public reuses for non-authenticated user.
135
+ response: TestResponse = api.get(url_for("api.reuses"))
136
+ assert200(response)
137
+ assert len(response.json["data"]) == 1
138
+ assert reuse_in_response(response, public_reuse)
139
+
140
+ # With an authenticated user.
141
+ api.login(user)
142
+ # all the reuses (by default)
143
+ response = api.get(url_for("api.reuses"))
144
+ assert200(response)
145
+ assert len(response.json["data"]) == 2 # Return everything
146
+ assert reuse_in_response(response, public_reuse)
147
+ assert reuse_in_response(response, private_reuse)
148
+
149
+ # only public
150
+ response = api.get(url_for("api.reuses", private="false"))
151
+ assert200(response)
152
+ assert len(response.json["data"]) == 1 # Don't return the private reuse
153
+ assert reuse_in_response(response, public_reuse)
154
+
155
+ # only private
156
+ response = api.get(url_for("api.reuses", private="true"))
157
+ assert200(response)
158
+ assert len(response.json["data"]) == 1 # Return only the private
159
+ assert reuse_in_response(response, private_reuse)
160
+
161
+ def test_reuse_api_list_filter_private_only_owned_by_user(self, api) -> None:
162
+ """Should only return private reuses that are owned."""
163
+ user = UserFactory()
164
+ member = Member(user=user, role="editor")
165
+ org = OrganizationFactory(members=[member])
166
+ private_owned: Reuse = ReuseFactory(private=True, owner=user)
167
+ private_owned_through_org: Reuse = ReuseFactory(private=True, organization=org)
168
+ private_not_owned: Reuse = ReuseFactory(private=True)
169
+
170
+ # Only public reuses for non-authenticated user.
171
+ response: TestResponse = api.get(url_for("api.reuses"))
172
+ assert200(response)
173
+ assert len(response.json["data"]) == 0
174
+
175
+ # With an authenticated user.
176
+ api.login(user)
177
+ response = api.get(url_for("api.reuses"))
178
+ assert200(response)
179
+ assert len(response.json["data"]) == 2 # Only the owned reuses
180
+ assert reuse_in_response(response, private_owned)
181
+ assert reuse_in_response(response, private_owned_through_org)
182
+ assert not reuse_in_response(response, private_not_owned)
183
+
184
+ # Still no private returned if `private=False`
185
+ response = api.get(url_for("api.reuses", private=False))
186
+ assert200(response)
187
+ assert len(response.json["data"]) == 0
188
+
189
+ # Still only return owned private reuses
190
+ response = api.get(url_for("api.reuses", private=True))
191
+ assert200(response)
192
+ assert len(response.json["data"]) == 2 # Only the owned reuses
193
+ assert reuse_in_response(response, private_owned)
194
+ assert reuse_in_response(response, private_owned_through_org)
195
+ assert not reuse_in_response(response, private_not_owned)
196
+
197
+ def test_reuse_api_list_filter_private_only_owned_by_user_no_user(self, api) -> None:
198
+ """Shouldn't return any private reuses for non logged in users."""
199
+ user = UserFactory()
200
+ member = Member(user=user, role="editor")
201
+ org = OrganizationFactory(members=[member])
202
+ public_owned: Reuse = ReuseFactory(owner=user)
203
+ public_not_owned: Reuse = ReuseFactory()
204
+ _private_owned: Reuse = ReuseFactory(private=True, owner=user)
205
+ _private_owned_through_org: Reuse = ReuseFactory(private=True, organization=org)
206
+ _private_not_owned: Reuse = ReuseFactory(private=True)
207
+
208
+ response: TestResponse = api.get(url_for("api.reuses"))
209
+ assert200(response)
210
+ assert len(response.json["data"]) == 2
211
+ assert reuse_in_response(response, public_owned)
212
+ assert reuse_in_response(response, public_not_owned)
213
+
214
+ # Still no private returned if `private=False`
215
+ response = api.get(url_for("api.reuses", private=False))
216
+ assert200(response)
217
+ assert len(response.json["data"]) == 2
218
+ assert reuse_in_response(response, public_owned)
219
+ assert reuse_in_response(response, public_not_owned)
220
+
221
+ # Still no private returned if `private=True`
222
+ response = api.get(url_for("api.reuses", private=True))
223
+ assert200(response)
224
+ assert len(response.json["data"]) == 0
225
+
121
226
  def test_reuse_api_get(self, api):
122
227
  """It should fetch a reuse from the API"""
123
228
  reuse = ReuseFactory()
udata/tests/test_cors.py CHANGED
@@ -2,7 +2,8 @@ from datetime import datetime
2
2
 
3
3
  from flask import url_for
4
4
 
5
- from udata.core.dataset.factories import DatasetFactory
5
+ from udata import assets
6
+ from udata.core.dataset.factories import DatasetFactory, ResourceFactory
6
7
  from udata.tests.api import APITestCase
7
8
  from udata.tests.helpers import assert_status
8
9
 
@@ -10,6 +11,41 @@ from udata.tests.helpers import assert_status
10
11
  class CorsTest(APITestCase):
11
12
  modules = []
12
13
 
14
+ def test_cors_on_allowed_routes(self):
15
+ cors_headers = {
16
+ "Origin": "http://localhost",
17
+ "Access-Control-Request-Method": "GET",
18
+ }
19
+
20
+ dataset = DatasetFactory(resources=[ResourceFactory()])
21
+
22
+ # API Swagger
23
+ response = self.get(url_for("api.specs"), headers=cors_headers)
24
+ assert_status(response, 200)
25
+ assert "Access-Control-Allow-Origin" in response.headers
26
+
27
+ # API Dataset
28
+ response = self.get(url_for("api.dataset", dataset=dataset.id), headers=cors_headers)
29
+ assert_status(response, 200)
30
+ assert "Access-Control-Allow-Origin" in response.headers
31
+
32
+ # Resource permalink
33
+ response = self.get(f"/fr/datasets/r/{dataset.resources[0].id}", headers=cors_headers)
34
+ assert_status(response, 404) # The route is defined in udata-front
35
+ assert "Access-Control-Allow-Origin" in response.headers
36
+
37
+ # Oauth
38
+ response = self.get("/oauth/", headers=cors_headers)
39
+ assert_status(response, 404) # Oauth is defined in udata-front
40
+ assert "Access-Control-Allow-Origin" in response.headers
41
+
42
+ # Static
43
+ response = self.get(
44
+ assets.cdn_for("static", filename="my_static.css"), headers=cors_headers
45
+ )
46
+ assert_status(response, 404) # Not available in APITestCase
47
+ assert "Access-Control-Allow-Origin" in response.headers
48
+
13
49
  def test_cors_redirects(self):
14
50
  dataset = DatasetFactory(title="Old title")
15
51
  old_slug = dataset.slug
@@ -2,8 +2,11 @@ from datetime import datetime
2
2
 
3
3
  import pytest
4
4
  from flask import url_for
5
+ from werkzeug.test import TestResponse
5
6
 
7
+ from udata.core.dataservices.factories import DataserviceFactory
6
8
  from udata.core.dataset.factories import DatasetFactory
9
+ from udata.core.discussions.factories import DiscussionFactory
7
10
  from udata.core.discussions.metrics import update_discussions_metric # noqa
8
11
  from udata.core.discussions.models import Discussion, Message
9
12
  from udata.core.discussions.notifications import discussions_notifications
@@ -19,8 +22,11 @@ from udata.core.discussions.tasks import (
19
22
  notify_new_discussion_comment,
20
23
  )
21
24
  from udata.core.organization.factories import OrganizationFactory
25
+ from udata.core.organization.models import Organization
26
+ from udata.core.reuse.factories import ReuseFactory
22
27
  from udata.core.spam.signals import on_new_potential_spam
23
28
  from udata.core.user.factories import AdminFactory, UserFactory
29
+ from udata.core.user.models import User
24
30
  from udata.models import Dataset, Member
25
31
  from udata.tests.helpers import capture_mails
26
32
  from udata.utils import faker
@@ -358,6 +364,61 @@ class DiscussionsTest(APITestCase):
358
364
 
359
365
  self.assertEqual(len(response.json["data"]), len(discussions))
360
366
 
367
+ def assertIdIn(self, json_data: dict, id_: str) -> None:
368
+ for item in json_data:
369
+ if item["id"] == id_:
370
+ return
371
+ self.fail(f"id {id_} not in {json_data}")
372
+
373
+ def test_list_discussions_org_does_not_exist(self) -> None:
374
+ response: TestResponse = self.get(url_for("api.discussions", org="bad org id"))
375
+ self.assert404(response)
376
+
377
+ def test_list_discussions_org(self) -> None:
378
+ organization: Organization = OrganizationFactory()
379
+ user: User = UserFactory()
380
+ _discussion: Discussion = DiscussionFactory(user=user)
381
+ dataset = DatasetFactory(organization=organization)
382
+ dataservice = DataserviceFactory(organization=organization)
383
+ reuse = ReuseFactory(organization=organization)
384
+ discussion_for_dataset: Discussion = DiscussionFactory(subject=dataset, user=user)
385
+ discussion_for_dataservice: Discussion = DiscussionFactory(subject=dataservice, user=user)
386
+ discussion_for_reuse: Discussion = DiscussionFactory(subject=reuse, user=user)
387
+
388
+ response: TestResponse = self.get(url_for("api.discussions", org=organization.id))
389
+ self.assert200(response)
390
+ self.assertEqual(len(response.json["data"]), 3)
391
+ self.assertIdIn(response.json["data"], str(discussion_for_dataset.id))
392
+ self.assertIdIn(response.json["data"], str(discussion_for_dataservice.id))
393
+ self.assertIdIn(response.json["data"], str(discussion_for_reuse.id))
394
+
395
+ def test_list_discussions_sort(self) -> None:
396
+ user: User = UserFactory()
397
+ sorting_keys_dict: dict = {
398
+ "title": ["aaa", "bbb"],
399
+ "created": ["2023-12-12", "2024-01-01"],
400
+ "closed": ["2023-12-12", "2024-01-01"],
401
+ }
402
+ for sorting_key, values in sorting_keys_dict.items():
403
+ discussion1: Discussion = DiscussionFactory(user=user, **{sorting_key: values[0]})
404
+ discussion2: Discussion = DiscussionFactory(user=user, **{sorting_key: values[1]})
405
+
406
+ response: TestResponse = self.get(url_for("api.discussions", sort=sorting_key))
407
+ self.assert200(response)
408
+ self.assertEqual(len(response.json["data"]), 2)
409
+ self.assertEqual(response.json["data"][0]["id"], str(discussion1.id))
410
+ self.assertEqual(response.json["data"][1]["id"], str(discussion2.id))
411
+
412
+ # Reverse sort
413
+ response: TestResponse = self.get(url_for("api.discussions", sort="-" + sorting_key))
414
+ self.assert200(response)
415
+ self.assertEqual(len(response.json["data"]), 2)
416
+ self.assertEqual(response.json["data"][0]["id"], str(discussion2.id))
417
+ self.assertEqual(response.json["data"][1]["id"], str(discussion1.id))
418
+
419
+ # Clean slate
420
+ Discussion.objects.delete()
421
+
361
422
  def test_list_discussions_user(self):
362
423
  dataset = DatasetFactory()
363
424
  discussions = []
udata/tests/test_owned.py CHANGED
@@ -3,14 +3,25 @@ from mongoengine import post_save
3
3
  import udata.core.owned as owned
4
4
  from udata.core.organization.factories import OrganizationFactory
5
5
  from udata.core.organization.models import Organization
6
- from udata.core.user.factories import UserFactory
6
+ from udata.core.user.factories import AdminFactory, UserFactory
7
7
  from udata.core.user.models import User
8
+ from udata.models import Member
8
9
  from udata.mongo import db
9
10
  from udata.tests import DBTestMixin, TestCase
10
11
 
11
12
 
13
+ class CustomQuerySet(owned.OwnedQuerySet):
14
+ def visible(self):
15
+ return self(private__ne=True)
16
+
17
+
12
18
  class Owned(owned.Owned, db.Document):
13
19
  name = db.StringField()
20
+ private = db.BooleanField()
21
+
22
+ meta = {
23
+ "queryset_class": CustomQuerySet,
24
+ }
14
25
 
15
26
 
16
27
  class OwnedPostSave(owned.Owned, db.Document):
@@ -166,3 +177,91 @@ class OwnedQuerysetTest(DBTestMixin, TestCase):
166
177
 
167
178
  for owned_ in excluded:
168
179
  self.assertNotIn(owned_, result)
180
+
181
+ def test_visible_by_user(self) -> None:
182
+ admin: User = AdminFactory()
183
+ user: User = UserFactory()
184
+ member = Member(user=user, role="editor")
185
+ other_user: User = UserFactory()
186
+ org: Organization = OrganizationFactory(members=[member])
187
+ other_org: Organization = OrganizationFactory()
188
+ owned_by_user: Owned = Owned.objects.create(owner=user, name="owned_by_user")
189
+ owned_by_org: Owned = Owned.objects.create(organization=org, name="owned_by_org")
190
+ owned_by_other_user: Owned = Owned.objects.create(
191
+ owner=other_user, name="owned_by_other_user"
192
+ )
193
+ owned_by_other_org: Owned = Owned.objects.create(
194
+ organization=other_org, name="owned_by_other_org"
195
+ )
196
+ private_owned_by_user: Owned = Owned.objects.create(
197
+ owner=user, private=True, name="private_owned_by_user"
198
+ )
199
+ private_owned_by_org: Owned = Owned.objects.create(
200
+ organization=org, private=True, name="private_owned_by_org"
201
+ )
202
+ private_owned_by_other_user: Owned = Owned.objects.create(
203
+ owner=other_user, private=True, name="private_owned_by_other_user"
204
+ )
205
+ private_owned_by_other_org: Owned = Owned.objects.create(
206
+ organization=other_org, private=True, name="private_owned_by_other_org"
207
+ )
208
+
209
+ visible_by_user: list[Owned] = [
210
+ owned_by_user,
211
+ owned_by_org,
212
+ owned_by_other_user,
213
+ owned_by_other_org,
214
+ private_owned_by_user,
215
+ private_owned_by_org,
216
+ ]
217
+ visible_by_other_user: list[Owned] = [
218
+ private_owned_by_other_user,
219
+ private_owned_by_other_org,
220
+ ]
221
+
222
+ # Admin can view everything.
223
+ result: owned.OwnedQuerySet = Owned.objects.visible_by_user(
224
+ admin, Owned.objects.visible()._query_obj
225
+ )
226
+ # 4 public + 1 private owned by user + 1 private owned by the user's org.
227
+ self.assertEqual(len(result), 8)
228
+ for owned_ in visible_by_user + visible_by_other_user:
229
+ self.assertIn(owned_, result)
230
+
231
+ result = Owned.objects.visible_by_user(user, Owned.objects.visible()._query_obj)
232
+ # 4 public + 1 private owned by user + 1 private owned by the user's org.
233
+ self.assertEqual(len(result), 6)
234
+ for owned_ in visible_by_user:
235
+ self.assertIn(owned_, result)
236
+
237
+ # `.visible_by_user` does not reset other queries.
238
+ result = Owned.objects(name="owned_by_user").visible_by_user(
239
+ user, Owned.objects.visible()._query_obj
240
+ )
241
+ self.assertEqual(len(result), 1)
242
+ self.assertIn(owned_by_user, result)
243
+ result = Owned.objects.visible_by_user(user, Owned.objects.visible()._query_obj).filter(
244
+ name="owned_by_user"
245
+ )
246
+ self.assertEqual(len(result), 1)
247
+ self.assertIn(owned_by_user, result)
248
+
249
+ result = Owned.objects(name="private_owned_by_user").visible_by_user(
250
+ user, Owned.objects.visible()._query_obj
251
+ )
252
+ self.assertEqual(len(result), 1)
253
+ self.assertIn(private_owned_by_user, result)
254
+ result = Owned.objects.visible_by_user(user, Owned.objects.visible()._query_obj).filter(
255
+ name="private_owned_by_user"
256
+ )
257
+ self.assertEqual(len(result), 1)
258
+ self.assertIn(private_owned_by_user, result)
259
+
260
+ result = Owned.objects(name="private_owned_by_other_user").visible_by_user(
261
+ user, Owned.objects.visible()._query_obj
262
+ )
263
+ self.assertEqual(len(result), 0)
264
+ result = Owned.objects.visible_by_user(user, Owned.objects.visible()._query_obj).filter(
265
+ name="private_owned_by_other_user"
266
+ )
267
+ self.assertEqual(len(result), 0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: udata
3
- Version: 9.1.5.dev31391
3
+ Version: 9.2.2.dev31715
4
4
  Summary: Open data portal
5
5
  Home-page: https://github.com/opendatateam/udata
6
6
  Author: Opendata Team
@@ -140,7 +140,24 @@ It is collectively taken care of by members of the
140
140
 
141
141
  ## Current (in progress)
142
142
 
143
+ - Add a filter on organization and document sort parameters in the `/discussions` endpoint [#3147](https://github.com/opendatateam/udata/pull/3147)
144
+ - Move discussion catalog creation and add fields [#3152](https://github.com/opendatateam/udata/pull/3152) and [#3154](https://github.com/opendatateam/udata/pull/3154)
145
+ - Add resources formats and harvest remote_url on dataset catalog [#3159](https://github.com/opendatateam/udata/pull/3159)
146
+
147
+ ## 9.2.1 (2024-09-23)
148
+
149
+ - Enable basic search on dataservices [#3148](https://github.com/opendatateam/udata/pull/3148)
150
+
151
+ ## 9.2.0 (2024-09-13)
152
+
153
+ - Allow OAuth clients without secrets [#3138](https://github.com/opendatateam/udata/pull/3138)
154
+ - Add a `archived` button for datasets and reuses on frontend admin [#3104](https://github.com/opendatateam/udata/pull/3104)
155
+ - **breaking change** Return all the reuses available to a user on the /reuses endpoint, including the private and deleted ones they own [#3140](https://github.com/opendatateam/udata/pull/3140).
143
156
  - Fix undelete reuse and dataservices [#3141](https://github.com/opendatateam/udata/pull/3141)
157
+ - Add a minimal publiccode.yml [#3144](https://github.com/opendatateam/udata/pull/3144)
158
+ - Fix the boolean filters in the API for the "new system" endpoints [#3139](https://github.com/opendatateam/udata/pull/3139)
159
+ - Update authlib dependency from 0.14.3 to 1.3.1 [#3135](https://github.com/opendatateam/udata/pull/3135)
160
+ - Add CORS on resource redirect [#3145](https://github.com/opendatateam/udata/pull/3145)
144
161
 
145
162
  ## 9.1.4 (2024-08-26)
146
163
 
@@ -156,8 +173,6 @@ It is collectively taken care of by members of the
156
173
  - Always add Vary even for non CORS requests [#3132](https://github.com/opendatateam/udata/pull/3132)
157
174
  - Add acronym in organization csv catalog [#3134](https://github.com/opendatateam/udata/pull/3134)
158
175
  - Limit the number of user suggestions [#3131](https://github.com/opendatateam/udata/pull/3131)
159
- - Update authlib dependency from 0.14.3 to 1.3.1 [#3135](https://github.com/opendatateam/udata/pull/3135)
160
- - Fix the boolean filters in the API for the "new system" endpoints [#3139](https://github.com/opendatateam/udata/pull/3139)
161
176
 
162
177
  ## 9.1.3 (2024-08-01)
163
178