1. 1 : /**
  2. 2 : * The Documents tool shows a table of the documents in the corpus and includes functionality for modifying the corpus.
  3. 3 : *
  4. 4 : * @example
  5. 5 : *
  6. 6 : * let config = {
  7. 7 : * "columns": null,
  8. 8 : * "dir": null,
  9. 9 : * "docId": null,
  10. 10 : * "docIndex": null,
  11. 11 : * "query": null,
  12. 12 : * "sort": null,
  13. 13 : * };
  14. 14 : *
  15. 15 : * loadCorpus("austen").tool("documents", config);
  16. 16 : *
  17. 17 : * @class Documents
  18. 18 : * @tutorial documents
  19. 19 : * @memberof Tools
  20. 20 : */
  21. 21 : Ext.define('Voyant.panel.Documents', {
  22. 22 : extend: 'Ext.grid.Panel',
  23. 23 : mixins: ['Voyant.panel.Panel','Voyant.util.Downloadable'],
  24. 24 : alias: 'widget.documents',
  25. 25 : isConsumptive: true,
  26. 26 : statics: {
  27. 27 : i18n: {
  28. 28 : newCorpusError: 'There was an error creating the new the corpus. You may not have permission to do this.'
  29. 29 : },
  30. 30 : api: {
  31. 31 : /**
  32. 32 : * @memberof Tools.Documents
  33. 33 : * @instance
  34. 34 : * @property {query}
  35. 35 : */
  36. 36 : query: undefined,
  37. 37 :
  38. 38 : /**
  39. 39 : * @memberof Tools.Documents
  40. 40 : * @instance
  41. 41 : * @property {docIndex}
  42. 42 : */
  43. 43 : docIndex: undefined,
  44. 44 :
  45. 45 : /**
  46. 46 : * @memberof Tools.Documents
  47. 47 : * @instance
  48. 48 : * @property {docId}
  49. 49 : */
  50. 50 : docId: undefined,
  51. 51 :
  52. 52 : /**
  53. 53 : * @memberof Tools.Documents
  54. 54 : * @instance
  55. 55 : * @property {columns} columns 'title', 'author', 'pubDate', 'publisher', 'pubPlace', 'keyword', 'collection', 'tokensCount-lexical', 'typesCount-lexical', 'typeTokenRatio-lexical', 'averageWordsPerSentence', 'language'
  56. 56 : */
  57. 57 : columns: undefined,
  58. 58 :
  59. 59 : /**
  60. 60 : * @memberof Tools.Documents
  61. 61 : * @instance
  62. 62 : * @property {sort}
  63. 63 : */
  64. 64 : sort: undefined,
  65. 65 :
  66. 66 : /**
  67. 67 : * @memberof Tools.Documents
  68. 68 : * @instance
  69. 69 : * @property {dir}
  70. 70 : */
  71. 71 : dir: undefined
  72. 72 : },
  73. 73 : glyph: 'xf0ce@FontAwesome'
  74. 74 : },
  75. 75 :
  76. 76 : MODE_EDITING: 'editing',
  77. 77 : MODE_NORMAL: 'normal',
  78. 78 : config: {
  79. 79 : options: [{
  80. 80 : xtype: 'stoplistoption'
  81. 81 : },
  82. 82 : {xtype: 'categoriesoption'}
  83. 83 : ],
  84. 84 : mode: this.MODE_NORMAL
  85. 85 : },
  86. 86 :
  87. 87 : constructor: function(config) {
  88. 88 :
  89. 89 : var store = Ext.create("Voyant.data.store.Documents", {
  90. 90 : selModel: {pruneRemoved: false},
  91. 91 : proxy: {
  92. 92 : extraParams: {
  93. 93 : forTool: 'documents'
  94. 94 : }
  95. 95 : }
  96. 96 : });
  97. 97 :
  98. 98 : var dockedItemsItems = [{
  99. 99 : xtype: 'querysearchfield'
  100. 100 : }, {
  101. 101 : xtype: 'totalpropertystatus'
  102. 102 : }]
  103. 103 :
  104. 104 : var me = this;
  105. 105 :
  106. 106 : if (!config || config.mode!=this.MODE_EDITING) {
  107. 107 : dockedItemsItems.push({
  108. 108 : text: this.localize("modify"),
  109. 109 : tooltip: this.localize("modifyTip"),
  110. 110 : glyph: 'xf044@FontAwesome',
  111. 111 : scope: this,
  112. 112 : itemId: 'modifyButton',
  113. 113 : handler: function(btn) {
  114. 114 : var win = Ext.create('Ext.window.Window', {
  115. 115 : title: this.localize("title"),
  116. 116 : modal: true,
  117. 117 : width: "80%",
  118. 118 : minWidth: 300,
  119. 119 : minHeight: 200,
  120. 120 : height: "80%",
  121. 121 : layout: 'fit',
  122. 122 : frame: true,
  123. 123 : border: true,
  124. 124 : items: {
  125. 125 : xtype: 'documents',
  126. 126 : mode: this.MODE_EDITING,
  127. 127 : corpus: this.getStore().getCorpus(),
  128. 128 : header: false,
  129. 129 : viewConfig: {
  130. 130 : plugins:{
  131. 131 : ptype:'gridviewdragdrop'
  132. 132 : },
  133. 133 : listeners: {
  134. 134 : beforedrop: function(node, data, overModel, dropPosition, dropHandlers) {
  135. 135 : if (this.getStore().getCount()<this.getStore().getCorpus().getDocumentsCount()) {
  136. 136 : var panel = this.up("panel");
  137. 137 : Ext.Msg.show({
  138. 138 : title: panel.localize('error'),
  139. 139 : message: panel.localize('reorderFilteredError'),
  140. 140 : buttons: Ext.Msg.OK,
  141. 141 : icon: Ext.Msg.ERROR
  142. 142 : });
  143. 143 : return false;
  144. 144 : }
  145. 145 : return true;
  146. 146 : }
  147. 147 : }
  148. 148 : }
  149. 149 : },
  150. 150 : buttons: [{
  151. 151 : text: this.localize('add'),
  152. 152 : tooltip: this.localize("addTip"),
  153. 153 : glyph: 'xf067@FontAwesome',
  154. 154 : handler: function(btn) {
  155. 155 : btn.up("window").close();
  156. 156 : Ext.create('Ext.window.Window', {
  157. 157 : header: false,
  158. 158 : modal: true,
  159. 159 : layout: 'fit',
  160. 160 : items: {
  161. 161 : xtype: 'corpuscreator',
  162. 162 : corpus: this.getStore().getCorpus(),
  163. 163 : listeners: {
  164. 164 : boxready: function(cmp) {
  165. 165 : cmp.setStyle({borderColor: '#f5f5f5'});
  166. 166 : }
  167. 167 : }
  168. 168 : }
  169. 169 : }).show();
  170. 170 : },
  171. 171 : scope: this
  172. 172 : }, {
  173. 173 : text: this.localize('remove'),
  174. 174 : tooltip: this.localize("removeTip"),
  175. 175 : glyph: 'xf05e@FontAwesome',
  176. 176 : hidden: this.getStore().getCorpus().getDocumentsCount()==1,
  177. 177 : handler: this.keepRemoveReorderHandler,
  178. 178 : itemId: 'remove',
  179. 179 : scope: this
  180. 180 : }, {
  181. 181 : text: this.localize('keep'),
  182. 182 : tooltip: this.localize("keepTip"),
  183. 183 : glyph: 'xf00c@FontAwesome',
  184. 184 : hidden: this.getStore().getCorpus().getDocumentsCount()==1,
  185. 185 : handler: this.keepRemoveReorderHandler,
  186. 186 : itemId: 'keep',
  187. 187 : scope: this
  188. 188 : }, {
  189. 189 : text: this.localize('reorder'),
  190. 190 : tooltip: this.localize("reorderTip"),
  191. 191 : glyph: 'xf0dc@FontAwesome',
  192. 192 : hidden: this.getStore().getCorpus().getDocumentsCount()==1,
  193. 193 : handler: this.keepRemoveReorderHandler,
  194. 194 : itemId: 'reorder',
  195. 195 : scope: this
  196. 196 : },{
  197. 197 : text: 'Cancel',
  198. 198 : glyph: 'xf00d@FontAwesome',
  199. 199 : handler: function(btn) {
  200. 200 : btn.up("window").close();
  201. 201 : }
  202. 202 : }]
  203. 203 : }).show();
  204. 204 :
  205. 205 : }
  206. 206 : }, {
  207. 207 : text: this.localize('downloadButton'),
  208. 208 : glyph: 'xf019@FontAwesome',
  209. 209 : itemId: 'downloadButton',
  210. 210 : handler: function() {
  211. 211 : me.downloadFromCorpusId(me.getStore().getCorpus().getId())
  212. 212 : }
  213. 213 : })
  214. 214 : }
  215. 215 :
  216. 216 : Ext.apply(this, {
  217. 217 : title: this.localize('title'),
  218. 218 : emptyText: this.localize("emptyText"),
  219. 219 : columns:[
  220. 220 : {
  221. 221 : xtype: 'rownumberer',
  222. 222 : renderer: function(value, metaData, record) {return record.getIndex()+1},
  223. 223 : sortable: false
  224. 224 : },{
  225. 225 : text: this.localize('documentTitle'),
  226. 226 : dataIndex: 'title',
  227. 227 : sortable: true,
  228. 228 : renderer: function(val, metadata, record) {return record.getTitle();},
  229. 229 : flex: 3
  230. 230 : },{
  231. 231 : text: this.localize('documentAuthor'),
  232. 232 : dataIndex: 'author',
  233. 233 : sortable: true,
  234. 234 : hidden: true,
  235. 235 : renderer: function(val, metadata, record) {return record.getAuthor();},
  236. 236 : flex: 2
  237. 237 : },{
  238. 238 : text: this.localize('documentPubDate'),
  239. 239 : dataIndex: 'pubDate',
  240. 240 : sortable: true,
  241. 241 : hidden: true,
  242. 242 : renderer: function(val, metadata, record) {return record.getPubDate();},
  243. 243 : flex: 2
  244. 244 : },{
  245. 245 : text: this.localize('documentPublisher'),
  246. 246 : dataIndex: 'publisher',
  247. 247 : sortable: false,
  248. 248 : hidden: true,
  249. 249 : renderer: function(val, metadata, record) {return record.getPublisher();},
  250. 250 : flex: 2
  251. 251 : },{
  252. 252 : text: this.localize('documentPubPlace'),
  253. 253 : dataIndex: 'pubPlace',
  254. 254 : sortable: false,
  255. 255 : hidden: true,
  256. 256 : renderer: function(val, metadata, record) {return record.getPubPlace();},
  257. 257 : flex: 2
  258. 258 : },{
  259. 259 : text: this.localize('documentKeyword'),
  260. 260 : dataIndex: 'keyword',
  261. 261 : sortable: false,
  262. 262 : hidden: true,
  263. 263 : renderer: function(val, metadata, record) {return record.getKeyword();},
  264. 264 : flex: 2
  265. 265 : },{
  266. 266 : text: this.localize('documentCollection'),
  267. 267 : dataIndex: 'collection',
  268. 268 : sortable: false,
  269. 269 : hidden: true,
  270. 270 : renderer: function(val, metadata, record) {return record.getCollection();},
  271. 271 : flex: 2
  272. 272 : },{
  273. 273 : text: this.localize('tokensCountLexical'),
  274. 274 : dataIndex: 'tokensCount-lexical',
  275. 275 : renderer: Ext.util.Format.numberRenderer('0,000'),
  276. 276 : sortable: true,
  277. 277 : width: 'autoSize'
  278. 278 : },{
  279. 279 : text: this.localize('typesCountLexical'),
  280. 280 : dataIndex: 'typesCount-lexical',
  281. 281 : renderer: Ext.util.Format.numberRenderer('0,000'),
  282. 282 : width: 'autoSize'
  283. 283 : },{
  284. 284 : text: this.localize('typeTokenRatioLexical'),
  285. 285 : dataIndex: 'typeTokenRatio-lexical',
  286. 286 : renderer: function(val) {return Ext.util.Format.percent(val)},
  287. 287 : width: 'autoSize'
  288. 288 : },{
  289. 289 : text: this.localize('averageWordsPerSentence'),
  290. 290 : dataIndex: 'averageWordsPerSentence',
  291. 291 : renderer: Ext.util.Format.numberRenderer('0,000.0'),
  292. 292 : tooltip: this.localize("averageWordsPerSentenceTip"),
  293. 293 : width: 'autoSize'
  294. 294 : },{
  295. 295 : text: this.localize('language'),
  296. 296 : dataIndex: 'language',
  297. 297 : hidden: true,
  298. 298 : renderer: function(val, metaData, record, rowIndex, colIndex, store, view) {return view.ownerCt.getLanguage(val);},
  299. 299 : width: 'autoSize'
  300. 300 : }
  301. 301 : ],
  302. 302 :
  303. 303 : store: store,
  304. 304 :
  305. 305 : selModel: {
  306. 306 : type: 'rowmodel',
  307. 307 : mode: 'MULTI',
  308. 308 : listeners: {
  309. 309 : selectionchange: {
  310. 310 : fn: function(sm, selections) {
  311. 311 : this.getApplication().dispatchEvent('documentsClicked', this, selections, this.getStore().getCorpus());
  312. 312 : },
  313. 313 : scope: this
  314. 314 : }
  315. 315 : }
  316. 316 : },
  317. 317 :
  318. 318 : dockedItems: [{
  319. 319 : dock: 'bottom',
  320. 320 : xtype: 'toolbar',
  321. 321 : overflowHandler: 'scroller',
  322. 322 : items: dockedItemsItems
  323. 323 : }]
  324. 324 : });
  325. 325 :
  326. 326 : this.callParent(arguments);
  327. 327 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  328. 328 :
  329. 329 : // create a listener for corpus loading (defined here, in case we need to load it next)
  330. 330 : this.on('loadedCorpus', function(src, corpus) {
  331. 331 :
  332. 332 : this.store.setCorpus(corpus);
  333. 333 :
  334. 334 : if (this.isVisible()) {
  335. 335 : this.store.load({params: this.getApiParams()});
  336. 336 : } else {
  337. 337 : this.on('afterrender', function() {
  338. 338 : this.store.load({params: this.getApiParams()});
  339. 339 : }, this);
  340. 340 : }
  341. 341 :
  342. 342 : if (this.hasModifyCorpusAccess(corpus) === false) {
  343. 343 : this.queryById('modifyButton').hide();
  344. 344 : this.queryById('downloadButton').hide();
  345. 345 : }
  346. 346 : });
  347. 347 :
  348. 348 : this.on("activate", function() { // load after tab activate (if we're in a tab panel)
  349. 349 : if (this.getStore().getCorpus()) {
  350. 350 : this.getStore().load({params: this.getApiParams()});
  351. 351 : }
  352. 352 : }, this);
  353. 353 :
  354. 354 : // create a listener for corpus loading (defined here, in case we need to load it next)
  355. 355 : this.on('query', function(src, query) {
  356. 356 : this.setApiParam('query', query);
  357. 357 : this.store.load({params: this.getApiParams()});
  358. 358 : })
  359. 359 :
  360. 360 : if (config.embedded) {
  361. 361 : if (Ext.getClass(config.embedded).getName() == "Voyant.data.model.Corpus") {
  362. 362 : config.corpus = config.embedded
  363. 363 : }
  364. 364 : else if (Ext.getClass(config.embedded).getName() == "Voyant.data.store.Documents") {
  365. 365 : this.store.setRecords(config.embedded.getData())
  366. 366 : config.corpus = config.embedded.getCorpus()
  367. 367 : }
  368. 368 :
  369. 369 : }
  370. 370 :
  371. 371 : // if we have a corpus, load it
  372. 372 : if (config.corpus) {
  373. 373 : this.fireEvent('loadedCorpus', this, config.corpus)
  374. 374 : }
  375. 375 : },
  376. 376 :
  377. 377 : keepRemoveReorderHandler: function(btn) {
  378. 378 : // we're not sure which scope we're in, so ensure we're talking about this buttons panel
  379. 379 : var panel = btn.up("window").down("documents");
  380. 380 : var selection = panel.getSelection();
  381. 381 : var docs = panel.getStore().getCorpus().getDocumentsCount();
  382. 382 : var btnMode = btn.getItemId();
  383. 383 : // if reordering, check to make sure that we're not looking at a subset
  384. 384 : if (btnMode=='reorder') {
  385. 385 : if (panel.getStore().getCount()<docs) {
  386. 386 : return Ext.Msg.show({
  387. 387 : title: this.localize('error'),
  388. 388 : message: this.localize('reorderFilteredError'),
  389. 389 : buttons: Ext.Msg.OK,
  390. 390 : icon: Ext.Msg.ERROR
  391. 391 : });
  392. 392 : }
  393. 393 : else {
  394. 394 : docIndex = [];
  395. 395 : panel.getStore().each(function(doc) {
  396. 396 : docIndex.push(doc.getIndex())
  397. 397 : }, this);
  398. 398 : for (var i=1; i<docIndex.length; i++) {
  399. 399 : if (docIndex[i-1]>docIndex[i]) {
  400. 400 : return Ext.Msg.confirm(panel.localize('newCorpus'), new Ext.Template(panel.localize(btnMode+'Documents')).applyTemplate([selection.length]), function(confirmBtn){
  401. 401 : if (confirmBtn==='yes') {
  402. 402 : docIndex = [];
  403. 403 : this.getStore().each(function(doc) {
  404. 404 : docIndex.push(doc.getIndex())
  405. 405 : }, this);
  406. 406 : var params = {docIndex: docIndex};
  407. 407 : params[btnMode+"Documents"] = true;
  408. 408 : this.editCorpus(params)
  409. 409 : }
  410. 410 : }, panel);
  411. 411 : }
  412. 412 : }
  413. 413 : // if we get here it's because nothing's been reordered
  414. 414 : return Ext.Msg.show({
  415. 415 : title: this.localize('error'),
  416. 416 : message: this.localize('reorderOriginalError'),
  417. 417 : buttons: Ext.Msg.OK,
  418. 418 : icon: Ext.Msg.ERROR
  419. 419 : });
  420. 420 : }
  421. 421 :
  422. 422 : }
  423. 423 :
  424. 424 : if (selection.length>0) {
  425. 425 : if (selection.length==docs) {
  426. 426 : if (docs==1) {
  427. 427 : return Ext.Msg.show({
  428. 428 : title: this.localize('error'),
  429. 429 : message: this.localize('onlyOneError'),
  430. 430 : buttons: Ext.Msg.OK,
  431. 431 : icon: Ext.Msg.ERROR
  432. 432 : });
  433. 433 : }
  434. 434 : else {
  435. 435 : return Ext.Msg.show({
  436. 436 : title: this.localize('error'),
  437. 437 : message: this.localize('allSelectedError'),
  438. 438 : buttons: Ext.Msg.OK,
  439. 439 : icon: Ext.Msg.ERROR
  440. 440 : });
  441. 441 : }
  442. 442 : }
  443. 443 : else {
  444. 444 : return Ext.Msg.confirm(this.localize('newCorpus'), new Ext.Template(this.localize(btnMode+'SelectedDocuments')).applyTemplate([selection.length]), function(confirmBtn){
  445. 445 : if (confirmBtn==='yes') {
  446. 446 : docIndex = [];
  447. 447 : selection.forEach(function(doc){
  448. 448 : docIndex.push(doc.getIndex())
  449. 449 : })
  450. 450 : var params = {docIndex: docIndex};
  451. 451 : params[btnMode+"Documents"] = true;
  452. 452 : this.editCorpus(params)
  453. 453 : }
  454. 454 : }, panel);
  455. 455 : }
  456. 456 : }
  457. 457 : else if (panel.getApiParam("query") && panel.getStore().getCount()<docs) {
  458. 458 : return Ext.Msg.confirm(this.localize('newCorpus'), new Ext.Template(this.localize(btnMode+'FilteredDocuments')).applyTemplate([selection.length]), function(confirmBtn){
  459. 459 : if (confirmBtn==='yes') {
  460. 460 : docIndex = [];
  461. 461 : this.getStore().each(function(doc) {
  462. 462 : docIndex.push(doc.getIndex())
  463. 463 : }, this);
  464. 464 : var params = {docIndex: docIndex};
  465. 465 : params[btnMode+"Documents"] = true;
  466. 466 : this.editCorpus(params)
  467. 467 : }
  468. 468 : }, panel);
  469. 469 : }
  470. 470 : else {
  471. 471 : return Ext.Msg.show({
  472. 472 : title: this.localize('error'),
  473. 473 : message: this.localize('selectOrFilterError'),
  474. 474 : buttons: Ext.Msg.OK,
  475. 475 : icon: Ext.Msg.ERROR
  476. 476 : });
  477. 477 : }
  478. 478 : },
  479. 479 :
  480. 480 : editCorpus: function(params) {
  481. 481 :
  482. 482 : Ext.apply(params, {
  483. 483 : tool: 'corpus.CorpusManager',
  484. 484 : corpus: this.getStore().getCorpus().getId()
  485. 485 : })
  486. 486 :
  487. 487 : // mask main viewport while we create a new corpus
  488. 488 : var app = this.getApplication();
  489. 489 : var view = app.getViewport();
  490. 490 : view.mask(this.localize("Creating new corpus…"));
  491. 491 : Ext.Ajax.request({
  492. 492 : url: this.getApplication().getTromboneUrl(),
  493. 493 : method: 'POST',
  494. 494 : params: params,
  495. 495 : success: function(response) {
  496. 496 : view.unmask();
  497. 497 : var obj = Ext.decode(response.responseText);
  498. 498 : app.openUrl(app.getBaseUrl()+"?corpus="+obj.corpus.id);
  499. 499 : // view.mask("Loading new corpus…")
  500. 500 : // new Voyant.data.model.Corpus({corpus: obj.corpus.id}).then(function(corpus) {
  501. 501 : // view.unmask();
  502. 502 : // app.openUrl(app.getBaseUrl()+"/?corpus="+obj.corpus.id)
  503. 503 : // app.dispatchEvent('loadedCorpus', app, corpus);
  504. 504 : // }).fail(function(message, response) {
  505. 505 : // view.unmask();
  506. 506 : // app.showErrorResponse({message: message}, response);
  507. 507 : // });
  508. 508 : },
  509. 509 : failure: function(response) {
  510. 510 : view.unmask();
  511. 511 : Ext.Msg.show({
  512. 512 : title: this.localize('error'),
  513. 513 : message: this.localize('newCorpusError'),
  514. 514 : buttons: Ext.Msg.OK,
  515. 515 : icon: Ext.Msg.ERROR
  516. 516 : });
  517. 517 : },
  518. 518 : scope: this
  519. 519 : });
  520. 520 :
  521. 521 : // close editing window if we're in modal mode, should happen asynchronously while new corpus is created
  522. 522 : var win = this.up("window");
  523. 523 : if (win && win.isFloating()) {win.close()}
  524. 524 : }
  525. 525 : })