diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java index 06419f12edb23d1f4c510f9f0c8fa7af16943aa1..da5cd899926ddd209470e85aaaf8069f70d19a4f 100644 --- a/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java +++ b/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java @@ -62,7 +62,7 @@ public class ArticleResource { query.criteria("topicModel", new TopicModel(topicModel)); if (word != null && !word.isEmpty()) - query.criteria("words.word.id", word); + query.criteria("words.word", word); final List<ArticleFull> articles = dbArticles.getMultiple(query); diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java index b13dfe1c7aebd30231b1ae00fb51b83f15971d87..b4b7f02a86816283763fdd094a7148e59a20533d 100644 --- a/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java +++ b/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java @@ -50,7 +50,7 @@ public class TopicResource { @Produces(MediaType.APPLICATION_JSON) public Response getTopics(@QueryParam("topicModel") final String topicModel, @QueryParam("skip") final Integer skip, @QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("name") final String sortBy, - @QueryParam("fields") final String fields) { + @QueryParam("fields") final String fields, @QueryParam("word") final String word) { final ResponseWrapper<List<TopicFull>> res = new ResponseWrapper<>(); if (res.hasErrors()) @@ -64,6 +64,9 @@ public class TopicResource { if (topicModel != null && !topicModel.isEmpty()) query.criteria("topicModel", new TopicModel(topicModel)); + if (word != null && !word.isEmpty()) + query.criteria("words.word", word); + final List<TopicFull> topics = dbTopics.getMultiple(query); if ((skip != null && skip > 0) || (limit != null && limit > 0)) diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/text/ProcessedText.java b/vipra-cmd/src/main/java/de/vipra/cmd/text/ProcessedText.java index 8911bcd2e62df83ebe84e51af5d279521034a94a..9d29c9f9c8f392b1fe03d5bd4121316bebee4895 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/text/ProcessedText.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/text/ProcessedText.java @@ -1,6 +1,7 @@ package de.vipra.cmd.text; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map.Entry; @@ -28,6 +29,7 @@ public class ProcessedText { for (final Entry<String, Integer> entry : wordCounts.entrySet()) articleWords.add(new ArticleWord(entry.getKey(), entry.getValue())); this.articleWords = articleWords; + Collections.sort(this.articleWords); } public String[] getWords() { diff --git a/vipra-ui/app/html/about.html b/vipra-ui/app/html/about.html index f13b32d7645015a1873826b59c336383a1aa5834..4865c49cfd93ec603a9d985d73314d4a3ecb1f18 100644 --- a/vipra-ui/app/html/about.html +++ b/vipra-ui/app/html/about.html @@ -1,4 +1,4 @@ -<div class="container" ng-cloak ng-hide="$state.current.name !== 'about'"> +<div class="container" ng-cloak ng-hide="state.name !== 'about'"> <div class="row"> <div class="col-md-12"> <div class="page-header"> @@ -6,17 +6,17 @@ </div> <p><strong>Vipra</strong></p> <p>Created by Eike Cochu</p> - <h3><anchor-link fragment="description" />Description</h3> + <h3>Description</h3> <p>The Vipra application is a topic modeling based search system with a frontend web application, a backend REST service and a maintenance tool for data import and modeling. It attempts to leverage automatically discovered topic informations in document collections to ease collection browsing and organization. The search system relies on ElasticSearch and Apache Lucene.</p> <p>This application was created by Eike Cochu for his master's degree thesis in computer science, 2015-2016 at the Freie Universität in Berlin, Germany.</p> - <h3><anchor-link fragment="license" />License</h3> + <h3>License</h3> <p>The MIT License (MIT)</p> <p>Copyright 2016 Eike Cochu</p> <p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p> <p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p> <p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p> <hr> - <h3><anchor-link fragment="version" />Version</h3> + <h3>Version</h3> <table class="table table-bordered table-fixed"> <tbody> <tr> @@ -33,7 +33,7 @@ </tr> </tbody> </table> - <h3><anchor-link fragment="host" />Host</h3> + <h3>Host</h3> <table class="table table-bordered table-fixed"> <tbody> <tr> @@ -50,7 +50,7 @@ </tr> </tbody> </table> - <h3><anchor-link fragment="vm" />VM</h3> + <h3>VM</h3> <table class="table table-bordered table-fixed"> <tbody> <tr> @@ -63,7 +63,7 @@ </tr> </tbody> </table> - <h3><anchor-link fragment="database" />Database</h3> + <h3>Database</h3> <table class="table table-bordered table-fixed"> <tbody> <tr> @@ -76,7 +76,7 @@ </tr> </tbody> </table> - <h3><anchor-link fragment="constants" />Constants</h3> + <h3>Constants</h3> <table class="table table-bordered table-fixed"> <tbody> <tr> diff --git a/vipra-ui/app/html/articles/index.html b/vipra-ui/app/html/articles/index.html index ab26863ecfcdcf564862ef3434f326f8259bbb52..fd3ac02b48c9e494a6886f48b5cc1414cda5628e 100644 --- a/vipra-ui/app/html/articles/index.html +++ b/vipra-ui/app/html/articles/index.html @@ -1,6 +1,6 @@ -<div class="container" ng-cloak ng-hide="!rootModels.topicModel || $state.current.name !== 'articles'"> +<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'articles'"> <div class="row"> - <div class="col-md-12"> + <div class="col-md-12 text-center"> <pagination total="articlesTotal" page="articlesIndexModels.page" limit="articlesIndexModels.limit" /> </div> </div> @@ -36,7 +36,7 @@ </div> </div> <div class="row"> - <div class="col-md-12"> + <div class="col-md-12 text-center"> <pagination total="articlesTotal" page="articlesIndexModels.page" limit="articlesIndexModels.limit" /> </div> </div> diff --git a/vipra-ui/app/html/articles/show.html b/vipra-ui/app/html/articles/show.html index 6c682de6891a1b0d18bb9083af696de5d0b69f9b..2367cbe1a275b03bd36928438acfb8ed4ee028ff 100644 --- a/vipra-ui/app/html/articles/show.html +++ b/vipra-ui/app/html/articles/show.html @@ -1,85 +1,129 @@ -<div class="container" ng-cloak ng-hide="!rootModels.topicModel || $state.current.name !== 'articles.show'"> - <div class="row"> - <div class="col-md-12"> - <div class="page-header"> - <h1 ng-bind="::article.title"></h1> - <table class="item-actions"> - <tr> - <td> - <a class="btn btn-default" ui-sref="network({type:'articles', id:article.id})"> - <i class="fa fa-sitemap"></i> Network - </a> - </td> - </tr> - </table> - </div> - </div> +<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'articles.show'"> + <div class="page-header no-border"> + <h1 ng-bind="::article.title"></h1> </div> - <div class="row"> - <div class="col-md-8"> - <div class="row"> - <div class="col-md-12"> - <h3><anchor-link fragment="info" />Info</h3> - <table class="table table-bordered table-condensed table-infos"> - <tbody> - <tr> - <th>ID</th> - <td ng-bind="::article.id"></td> - </tr> - <tr> - <th>Date</th> - <td ng-bind="::articleDate"></td> - </tr> - <tr> - <th>URL</th> - <td> - <a ng-href="{{::article.url}}" target="_blank"> - <i class="fa fa-link"></i> - <span ng-bind="::article.url"></span> - </a> - </td> - </tr> - <tr> - <th>Word count</th> - <td ng-bind="::article.stats.wordCount"></td> - </tr> - </tbody> - </table> + <div> + <ul class="nav nav-tabs" role="tablist"> + <li class="active"> + <a data-target=".tab-info" data-toggle="tab"><i class="fa fa-file-text-o"></i></a> + </li> + <li> + <a data-target=".tab-words" data-toggle="tab" bs-tab shown="openTabWords()">Words</a> + </li> + <li> + <a ui-sref="network({type:'articles', id:article.id})"> + <i class="fa fa-sitemap"></i> Network + </a> + </li> + </ul> + <div class="tab-content"> + <div role="tabpanel" class="tab-pane active tab-info"> + <div class="row"> + <div class="col-md-8"> + <h3>Info</h3> + <table class="table table-bordered table-condensed table-infos"> + <tbody> + <tr> + <th class="infocol">ID</th> + <td ng-bind="::article.id"></td> + </tr> + <tr> + <th>Date</th> + <td ng-bind="::articleDate"></td> + </tr> + <tr> + <th>URL</th> + <td> + <a ng-href="{{::article.url}}" target="_blank"> + <i class="fa fa-link"></i> + <span ng-bind="::article.url"></span> + </a> + </td> + </tr> + <tr> + <th>Word count</th> + <td ng-bind="::article.stats.wordCount"></td> + </tr> + </tbody> + </table> + <h3>Topics</h3> + <table class="table table-bordered table-condensed"> + <thead> + <tr> + <th class="infocol" ng-model="articlesShowModels.topicsSort" sort-by="share">Share</th> + <th ng-model="articlesShowModels.topicsSort" sort-by="name">Name</th> + <th style="width:1px"></th> + </tr> + </thead> + <tbody> + <tr ng-repeat="topic in article.topics | orderBy:articlesShowModels.topicsSort"> + <td class="text-right" ng-bind-template="{{(topic.share*100).toFixed(0)}}%"></td> + <td> + <topic-link topic="topic.topic" /> + </td> + <td> + <span class="colorbox" style="background:{{::topic.color}}"></span> + </td> + </tr> + </tbody> + </table> + <span class="text-muted" ng-hide="article.topics.length > 0">No topics</span> + </div> + <div class="col-md-4"> + <h3>Share</h3> + <div class="pie-chart" id="topic-share" highcharts="topicShare" style="height: 250px;"></div> + </div> </div> + <h3>Similar articles</h3> + <table class="table table-bordered table-condensed"> + <thead> + <tr> + <th class="infocol" ng-model="articlesShowModels.similarSort" sort-by="divergence">Share</th> + <th ng-model="articlesShowModels.similarSort" sort-by="article.title">Title</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="simArticle in article.similarArticles | orderBy:articlesShowModels.similarSort"> + <td class="text-right" ng-bind-template="{{((1-simArticle.divergence)*100).toFixed(0)}}%"></td> + <td> + <a ui-sref="articles.show({id: simArticle.article.id})" ng-attr-title="{{::simArticle.article.title}}" ng-bind="::simArticle.article.title"></a> + </td> + </tr> + </tbody> + </table> + <hr> + <div class="text-justify" ng-bind-html="::article.text"></div> </div> - <div class="row"> - <div class="col-md-12"> - <h3><anchor-link fragment="similar" />Similar articles</h3> - <ul class="list-unstyled"> - <li ng-repeat="simArticle in article.similarArticles | orderBy:'divergence'" class="ellipsis"> - <small class="text-muted percent-align" ng-bind-template="({{((1-simArticle.divergence)*100).toFixed(0)}}%)"></small> - <a ui-sref="articles.show({id: simArticle.article.id})" ng-attr-title="{{::simArticle.article.title}}" ng-bind="::simArticle.article.title"></a> - </li> - <li class="text-muted" ng-show="!article.similarArticles"> - None - </li> - </ul> + <div role="tabpanel" class="tab-pane tab-words"> + <br> + <div class="row"> + <div class="col-md-12"> + <div class="panel panel-default"> + <div class="panel-heading"> + Found + <ng-pluralize count="words.length||0" when="{0:'no words',1:'1 word',other:'{} words'}"></ng-pluralize> in the database. + </div> + <table class="table table-bordered table-condensed"> + <thead> + <tr> + <th ng-model="articlesShowModels.wordsSort" sort-by="word">Word</th> + <th ng-model="articlesShowModels.wordsSort" sort-by="count">Count</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="word in words | orderBy:articlesShowModels.wordsSort"> + <td> + <word-link word="word" /> + </td> + <td ng-bind="word.count"></td> + </tr> + </tbody> + </table> + </div> + </div> </div> </div> </div> - <div class="col-md-4"> - <h3><anchor-link fragment="topics" />Topics</h3> - <ul class="list-unstyled"> - <li ng-repeat="topic in article.topics | orderBy:'share'"> - <small class="text-muted percent-align" ng-bind-template="({{(topic.share*100).toFixed(0)}}%)"></small> - <topic-link topic="topic.topic" /> - </li> - <li class="text-muted" ng-show="!article.topics"> - None - </li> - </ul> - <span class="text-muted" ng-hide="article.topics.length > 0">No topics</span> - <div class="pie-chart" id="topic-share" highcharts="topicShare"></div> - </div> - </div> - <hr> - <div class="row"> - <div class="col-md-12 text-justify" ng-bind-html="::article.text"></div> </div> </div> <div ng-cloak ui-view></div> diff --git a/vipra-ui/app/html/directives/alert.html b/vipra-ui/app/html/directives/alert.html new file mode 100644 index 0000000000000000000000000000000000000000..c703f74d9eb64413bc240f21ab2d2bb34ca6793d --- /dev/null +++ b/vipra-ui/app/html/directives/alert.html @@ -0,0 +1,4 @@ +<div ng-attr-class="{{classes}}" role="alert"> + <button type="button" class="close" data-dismiss="alert" aria-label="Close" ng-if="dismissible"><span aria-hidden="true">×</span></button> + <strong ng-bind="ngModel.title"></strong> <span ng-bind="ngModel.detail"></span> +</div> \ No newline at end of file diff --git a/vipra-ui/app/html/directives/anchor-link.html b/vipra-ui/app/html/directives/anchor-link.html deleted file mode 100644 index 47e17f149e7a873ba64fa601c84b634a18601650..0000000000000000000000000000000000000000 --- a/vipra-ui/app/html/directives/anchor-link.html +++ /dev/null @@ -1,4 +0,0 @@ -<a class="anchor-link" ui-sref="{'#': fragment}" target="_self"> - <i class="fa fa-link"></i> - <span ng-attr-id="{{fragment}}"></span> -</a> \ No newline at end of file diff --git a/vipra-ui/app/html/directives/sequence-dropdown.html b/vipra-ui/app/html/directives/sequence-dropdown.html index 2cbbecb770d5bf0aee830c46eb474f0ccd486742..17e05326a08c4a8a7a014c5ee1e9816fc5f4cc69 100644 --- a/vipra-ui/app/html/directives/sequence-dropdown.html +++ b/vipra-ui/app/html/directives/sequence-dropdown.html @@ -1,4 +1,4 @@ -<ol class="nya-bs-select nya-bs-condensed" ng-model="ngModel" ng-class="{dropup:!ngModel}"> +<ol class="nya-bs-select nya-bs-condensed" ng-model="ngModel" ng-class="{dropup:dropup}"> <li value="{{sequence.id}}" class="nya-bs-option" ng-repeat="sequence in sequences"> <a ng-bind="sequence.label"></a> </li> diff --git a/vipra-ui/app/html/directives/word-link.html b/vipra-ui/app/html/directives/word-link.html new file mode 100644 index 0000000000000000000000000000000000000000..0c2dff7ccf68eafa959c3e130c27b1e41e741286 --- /dev/null +++ b/vipra-ui/app/html/directives/word-link.html @@ -0,0 +1,4 @@ +<span> + <word-menu word="word" /> + <span ng-bind="word.word"></span> +</span> \ No newline at end of file diff --git a/vipra-ui/app/html/directives/word-menu.html b/vipra-ui/app/html/directives/word-menu.html new file mode 100644 index 0000000000000000000000000000000000000000..fe6a7ef3940ebc348eded14b24a8384d20429333 --- /dev/null +++ b/vipra-ui/app/html/directives/word-menu.html @@ -0,0 +1,9 @@ +<div class="dropdown inline-block"> + <a data-toggle="dropdown"> + <i class="fa fa-caret-down"></i> + </a> + <ul class="dropdown-menu" ng-class="{'dropdown-menu-right':dropdownRight}"> + <li><a ui-sref="words.topics({word:word.word})">Topics</a></li> + <li><a ui-sref="words.articles({word:word.word})">Articles</a></li> + </ul> +</div> \ No newline at end of file diff --git a/vipra-ui/app/html/error.html b/vipra-ui/app/html/error.html index 1e41bdb8cba29072891ab596fec7766b0deb0014..2e2c189d99a34877e6f891431ec2fc622d869760 100644 --- a/vipra-ui/app/html/error.html +++ b/vipra-ui/app/html/error.html @@ -1,4 +1,4 @@ -<div class="container" ng-cloak ng-hide="$state.current.name !== 'error'"> +<div class="container" ng-cloak ng-hide="state.name !== 'error'"> <div class="row"> <div class="col-md-3"> <object data="img/exclamation-triangle.svg" type="image/svg+xml" style="width:100%"></object> diff --git a/vipra-ui/app/html/explorer.html b/vipra-ui/app/html/explorer.html index 11344053d783dc548787967149c0f8f60c3b9747..01e44e00ee4cf6fde84e244e175c42a123be5bf6 100644 --- a/vipra-ui/app/html/explorer.html +++ b/vipra-ui/app/html/explorer.html @@ -1,4 +1,4 @@ -<div class="fullsize navpadding explorer" ng-cloak ng-hide="!rootModels.topicModel || $state.current.name !== 'explorer'"> +<div class="fullsize navpadding explorer" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'explorer'"> <div class="sidebar"> <div class="btn-group btn-group-justified" role="group" aria-label="..."> <a tabindex="0" class="btn btn-sm btn-default" ng-click="checkTopics(true)" title="Select all topics">All</a> diff --git a/vipra-ui/app/html/index.html b/vipra-ui/app/html/index.html index eaf87bd581d33395a14c0c6dc65f04544ce01960..3eddb62d5e52b35d4990a1883e91cf2b4f9ac96b 100644 --- a/vipra-ui/app/html/index.html +++ b/vipra-ui/app/html/index.html @@ -1,4 +1,4 @@ -<div class="container" ng-cloak ng-hide="!rootModels.topicModel || $state.current.name !== 'index'"> +<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'index'"> <div class="row" ng-hide="search"> <div class="col-md-12 text-center"> <svg class="logo hover heading" viewBox="0 0 200 120"> diff --git a/vipra-ui/app/html/network.html b/vipra-ui/app/html/network.html index 445a5473d25fb081b5dac52b3d5b0fe1006cbb43..98282f1d1436d57d272c0e1692e5b80aa1093b12 100644 --- a/vipra-ui/app/html/network.html +++ b/vipra-ui/app/html/network.html @@ -1,4 +1,4 @@ -<div ng-cloak ng-hide="!rootModels.topicModel || $state.current.name !== 'network'"> +<div ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'network'"> <div class="fullsize navpadding"> <div class="graph-legend overlay"> <div class="checkbox"> diff --git a/vipra-ui/app/html/topics/articles.html b/vipra-ui/app/html/topics/articles.html index a95e384887f6579839cfe7af9414a6fa70796ec0..0a3a6b30a1decda114683e5053cb76995c656239 100644 --- a/vipra-ui/app/html/topics/articles.html +++ b/vipra-ui/app/html/topics/articles.html @@ -1,4 +1,4 @@ -<div class="container" ng-cloak ng-hide="!rootModels.topicModel || $state.current.name !== 'topics.show.articles'"> +<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'topics.show.articles'"> <div class="row"> <div class="col-md-12"> <div class="page-header"> @@ -14,7 +14,7 @@ </div> </div> <div class="row"> - <div class="col-md-12"> + <div class="col-md-12 text-center"> <pagination total="articlesTotal" page="topicsArticlesModels.page" limit="topicsArticlesModels.limit" change="changePage" /> </div> </div> @@ -50,7 +50,7 @@ </div> </div> <div class="row"> - <div class="col-md-12"> + <div class="col-md-12 text-center"> <pagination total="articlesTotal" page="topicsArticlesModels.page" limit="topicsArticlesModels.limit" change="changePage" /> </div> </div> diff --git a/vipra-ui/app/html/topics/index.html b/vipra-ui/app/html/topics/index.html index b58dd69069addb80201369f2376c70c7f7012fe1..cf277d3968cbc33abe7814a5dd6ca579edafa91b 100644 --- a/vipra-ui/app/html/topics/index.html +++ b/vipra-ui/app/html/topics/index.html @@ -1,6 +1,6 @@ -<div class="container" ng-cloak ng-hide="!rootModels.topicModel || $state.current.name !== 'topics'"> +<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'topics'"> <div class="row"> - <div class="col-md-12"> + <div class="col-md-12 text-center"> <pagination total="topicsTotal" page="topicsIndexModels.page" limit="topicsIndexModels.limit" /> </div> </div> @@ -35,7 +35,7 @@ </div> </div> <div class="row"> - <div class="col-md-12"> + <div class="col-md-12 text-center"> <pagination total="topicsTotal" page="topicsIndexModels.page" limit="topicsIndexModels.limit" /> </div> </div> diff --git a/vipra-ui/app/html/topics/show.html b/vipra-ui/app/html/topics/show.html index c94f2b3799e2cee2252ccea04611b99d3356716b..63bcc290af0bb689d3858f853c6389cb605a0b09 100644 --- a/vipra-ui/app/html/topics/show.html +++ b/vipra-ui/app/html/topics/show.html @@ -1,152 +1,142 @@ -<div class="container" ng-cloak ng-hide="!rootModels.topicModel || $state.current.name !== 'topics.show'"> - <div class="row"> - <div class="col-md-12"> - <div class="page-header"> - <h1> - <div ng-bind="topic.name" ng-hide="isRename"></div> - <div class="input-group input-group-lg" ng-show="isRename"> - <input type="text" class="form-control" ng-model="topic.name" id="topicName" ng-keyup="keyup($event)"> - <div class="input-group-btn"> - <button class="btn btn-success" ng-click="endRename(true)"> - <span class="glyphicon glyphicon-ok"></span> - </button> - <button class="btn btn-danger" ng-click="endRename(false)"> - <span class="glyphicon glyphicon-remove"></span> - </button> - </div> - </div> - </h1> - <table class="item-actions"> - <tr> - <td> - <div class="dropdown"> - <button class="btn btn-default dropdown-toggle" type="button" id="actionsDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> - Actions - <i class="fa fa-caret-down"></i> - </button> - <ul class="dropdown-menu" aria-labelledby="actionsDropdown"> - <li><a ng-click="startRename()">Rename</a></li> - </ul> - </div> - </td> - <td> - <a class="btn btn-default" ui-sref="network({type:'topics', id:topic.id})"><i class="fa fa-sitemap"></i> Network</a> - </td> - <td> - <a class="btn btn-default" ui-sref="topics.show.articles({id:topic.id})">Articles</a> - </td> - </tr> - </table> - </div> - </div> - </div> - <div class="row"> - <div class="col-md-12"> - <h3><anchor-link fragment="info" />Info</h3> - <table class="table table-bordered table-condensed table-fixed table-infos"> - <tbody> - <tr> - <th>ID</th> - <td ng-bind="::topic.id"></td> - </tr> - </tbody> - </table> - </div> - </div> - <div class="row"> - <div class="col-md-12"> - <h3><anchor-link fragment="relevance" />Relevance</h3> - <div class="panel panel-default"> - <div class="panel-heading"> - <small>Values:</small> - <div class="btn-group"> - <a class="btn btn-sm btn-default" ng-model="topicsShowModels.relSeqstyle" bs-radio="'absolute'">Absolute</a> - <a class="btn btn-sm btn-default" ng-model="topicsShowModels.relSeqstyle" bs-radio="'relative'">Relative</a> - </div> - - <small>Chart:</small> - <div class="btn-group"> - <a class="btn btn-sm btn-default" ng-model="topicsShowModels.relChartstyle" bs-radio="'areaspline'">Area</a> - <a class="btn btn-sm btn-default" ng-model="topicsShowModels.relChartstyle" bs-radio="'spline'">Line</a> - </div> - <div class="pull-right"> - <a tabindex="0" class="btn btn-sm btn-default" ng-click="resetRelZoom()">Reset zoom</a> - </div> - </div> - <div class="panel-body"> - <div class="chart area-chart" id="topicRelChart" highcharts="topicSeq"></div> +<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'topics.show'"> + <div class="page-header no-border"> + <h1> + <div ng-bind="topic.name" ng-hide="isRename"></div> + <div class="input-group input-group-lg" ng-show="isRename"> + <input type="text" class="form-control" ng-model="topic.name" id="topicName" ng-keyup="keyup($event)"> + <div class="input-group-btn"> + <button class="btn btn-success" ng-click="endRename(true)"> + <span class="glyphicon glyphicon-ok"></span> + </button> + <button class="btn btn-danger" ng-click="endRename(false)"> + <span class="glyphicon glyphicon-remove"></span> + </button> </div> </div> - </div> + </h1> </div> - <div class="row"> - <div class="col-md-12"> - <h3><anchor-link fragment="evolution" />Word evolution</h3> - <div class="panel panel-default"> - <div class="panel-heading"> - <small>Values:</small> - <div class="btn-group"> - <a class="btn btn-sm btn-default" ng-model="topicsShowModels.wordSeqstyle" bs-radio="'absolute'">Absolute</a> - <a class="btn btn-sm btn-default" ng-model="topicsShowModels.wordSeqstyle" bs-radio="'relative'">Relative</a> - </div> - - <small>Chart:</small> - <div class="btn-group"> - <a class="btn btn-sm btn-default" ng-model="topicsShowModels.wordChartstyle" bs-radio="'areaspline'">Area</a> - <a class="btn btn-sm btn-default" ng-model="topicsShowModels.wordChartstyle" bs-radio="'spline'">Line</a> + <div> + <ul class="nav nav-tabs" role="tablist"> + <li class="dropdown pull-right"> + <a class="dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"> + Actions <span class="caret"></span> + </a> + <ul class="dropdown-menu"> + <li><a ng-click="startRename()">Rename</a></li> + </ul> + </li> + <li class="active"> + <a data-target=".tab-info" data-toggle="tab"><i class="fa fa-file-text-o"></i></a> + </li> + <li> + <a data-target=".tab-sequences" data-toggle="tab">Sequences</a> + </li> + <li> + <a ui-sref="topics.show.articles({id:topic.id})"> + Articles + </a> + </li> + <li> + <a ui-sref="network({type:'topics', id:topic.id})"> + <i class="fa fa-sitemap"></i> Network + </a> + </li> + </ul> + <div class="tab-content"> + <div role="tabpanel" class="tab-pane active tab-info"> + <h3>Relevance</h3> + <div class="panel panel-default"> + <div class="panel-heading"> + <small>Values:</small> + <div class="btn-group"> + <a class="btn btn-sm btn-default" ng-model="topicsShowModels.relSeqstyle" bs-radio="'absolute'">Absolute</a> + <a class="btn btn-sm btn-default" ng-model="topicsShowModels.relSeqstyle" bs-radio="'relative'">Relative</a> + </div> + + <small>Chart:</small> + <div class="btn-group"> + <a class="btn btn-sm btn-default" ng-model="topicsShowModels.relChartstyle" bs-radio="'areaspline'">Area</a> + <a class="btn btn-sm btn-default" ng-model="topicsShowModels.relChartstyle" bs-radio="'spline'">Line</a> + </div> + <div class="pull-right"> + <a tabindex="0" class="btn btn-sm btn-default" ng-click="resetRelZoom()">Reset zoom</a> + </div> </div> - <div class="pull-right"> - <a tabindex="0" class="btn btn-sm btn-default" ng-click="resetWordZoom()" ng-show="wordsSelected">Reset zoom</a> + <div class="panel-body"> + <div class="chart area-chart" id="topicRelChart" highcharts="topicSeq"></div> </div> </div> - <div class="panel-body"> - <div class="row row-full"> - <div class="col-md-2"> - <ul class="list-unstyled"> - <li ng-repeat="word in topic.words"> - <div class="checkbox checkbox-condensed" ng-class="{selected:word.selected}"> - <input tabindex="0" type="checkbox" ng-model="word.selected" ng-attr-id="{{::word.word}}" ng-change="redrawWordEvolutionChart()"> - <label class="check" ng-attr-for="{{::word.word}}" ng-bind="::word.word"></label> - </div> - </li> - </ul> + <h3>Word evolution</h3> + <div class="panel panel-default"> + <div class="panel-heading"> + <small>Values:</small> + <div class="btn-group"> + <a class="btn btn-sm btn-default" ng-model="topicsShowModels.wordSeqstyle" bs-radio="'absolute'">Absolute</a> + <a class="btn btn-sm btn-default" ng-model="topicsShowModels.wordSeqstyle" bs-radio="'relative'">Relative</a> + </div> + + <small>Chart:</small> + <div class="btn-group"> + <a class="btn btn-sm btn-default" ng-model="topicsShowModels.wordChartstyle" bs-radio="'areaspline'">Area</a> + <a class="btn btn-sm btn-default" ng-model="topicsShowModels.wordChartstyle" bs-radio="'spline'">Line</a> </div> - <div class="col-md-10 message-container"> - <div class="chart area-chart" id="topicWordChart" highcharts="topicWord"></div> - <div class="message text-muted" ng-hide="wordsSelected">No words selected</div> + <div class="pull-right"> + <a tabindex="0" class="btn btn-sm btn-default" ng-click="resetWordZoom()" ng-show="wordsSelected">Reset zoom</a> + </div> + </div> + <div class="panel-body"> + <div class="row row-full"> + <div class="col-md-2"> + <ul class="list-unstyled"> + <li ng-repeat="word in topic.words"> + <div class="checkbox checkbox-condensed" ng-class="{selected:word.selected}"> + <input tabindex="0" type="checkbox" ng-model="word.selected" ng-attr-id="{{::word.word}}" ng-change="redrawWordEvolutionChart()"> + <label class="check" ng-attr-for="{{::word.word}}"> + <word-link word="word" /> + </label> + </div> + </li> + </ul> + </div> + <div class="col-md-10 message-container"> + <div class="chart area-chart" id="topicWordChart" highcharts="topicWord"></div> + <div class="message text-muted" ng-hide="wordsSelected">No words selected</div> + </div> </div> </div> </div> </div> - </div> - </div> - <div class="row"> - <div class="col-md-12"> - <h3><anchor-link fragment="sequences" />Sequences</h3> - <div class="panel panel-default"> - <div class="panel-heading"> - <small>Sequence:</small> - <sequence-dropdown ng-model="sequenceId" sequences="topic.sequences"></sequence-dropdown> - </div> - <table class="table table-condensed table-bordered table-hover" ng-show="sequence"> - <thead> - <tr> - <th ng-model="topicsShowModels.seqSortWords" sort-by="word">Word</th> - <th ng-model="topicsShowModels.seqSortWords" sort-by="probability">Probability</th> - </tr> - </thead> - <tbody> - <tr ng-repeat="word in sequence.words | orderBy:topicsShowModels.seqSortWords"> - <td ng-bind="word.word"></td> - <td ng-bind="word.probability.toFixed(4)"></td> - </tr> - </tbody> - </table> - <div class="panel-footer" ng-show="sequence"> - <ng-pluralize count="sequence.words.length||0" when="{0:'No words',1:'Top word',other:'Top {} words'}"></ng-pluralize> + <div role="tabpanel" class="tab-pane tab-sequences"> + <h3>Sequences</h3> + <div class="panel panel-default"> + <div class="panel-heading"> + <small>Sequence:</small> + <sequence-dropdown ng-model="sequenceId" sequences="topic.sequences"></sequence-dropdown> + </div> + <table class="table table-condensed table-bordered table-hover" ng-show="sequence"> + <thead> + <tr> + <th ng-model="topicsShowModels.seqSortWords" sort-by="word">Word</th> + <th ng-model="topicsShowModels.seqSortWords" sort-by="probability">Probability</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="word in sequence.words | orderBy:topicsShowModels.seqSortWords"> + <td> + <word-menu word="word"/> + <span ng-bind="::word.word"></span> + </td> + <td ng-bind="word.probability.toFixed(4)"></td> + </tr> + </tbody> + </table> + <div class="panel-footer" ng-show="sequence"> + <ng-pluralize count="sequence.words.length||0" when="{0:'No words',1:'Top word',other:'Top {} words'}"></ng-pluralize> + </div> </div> </div> </div> </div> </div> + <div ng-cloak ui-view></div> diff --git a/vipra-ui/app/html/words/articles.html b/vipra-ui/app/html/words/articles.html new file mode 100644 index 0000000000000000000000000000000000000000..03dce56951193bb886e94512579c39a675f4f5e5 --- /dev/null +++ b/vipra-ui/app/html/words/articles.html @@ -0,0 +1,58 @@ +<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'words.articles'"> + <div class="row"> + <div class="col-md-12"> + <div class="page-header"> + <h1 ng-bind-template="Articles for word '{{::word}}'"></h1> + <table class="item-actions"> + <tr> + <td> + <a class="btn btn-default" ng-click="goBack()" ng-show="oldState.name && oldState.name !== state.name">Back</a> + </td> + </tr> + </table> + </div> + </div> + </div> + <div class="row"> + <div class="col-md-12 text-center"> + <pagination total="articlesTotal" page="wordsArticlesModels.page" limit="wordsArticlesModels.limit" /> + </div> + </div> + <div class="row"> + <div class="col-md-12"> + <div class="panel panel-default"> + <div class="panel-heading"> + Found + <ng-pluralize count="articlesTotal||0" when="{0:'no articles',1:'1 article',other:'{} articles'}"></ng-pluralize> in the database. + <span ng-show="articlesTotal"> + Sort by + <ol class="nya-bs-select nya-bs-condensed" ng-model="wordsArticlesModels.sortkey"> + <li value="title" class="nya-bs-option"><a>Title</a></li> + <li value="date" class="nya-bs-option"><a>Date</a></li> + <li value="created" class="nya-bs-option"><a>Added</a></li> + </ol> + <sort-dir ng-model="wordsArticlesModels.sortdir" /> + </span> + </div> + <table class="table table-hover table-condensed"> + <tbody> + <tr ng-repeat="article in articles"> + <td> + <a ui-sref="articles.show({id: article.id})" ng-bind="::article.title"></a> + </td> + </tr> + </tbody> + </table> + <div class="panel-footer"> + Page <span ng-bind="wordsArticlesModels.page||1"></span> of <span ng-bind="maxPage||1"></span> + </div> + </div> + </div> + </div> + <div class="row"> + <div class="col-md-12 text-center"> + <pagination total="articlesTotal" page="wordsArticlesModels.page" limit="wordsArticlesModels.limit" /> + </div> + </div> +</div> +<div ng-cloak ui-view></div> \ No newline at end of file diff --git a/vipra-ui/app/html/words/topics.html b/vipra-ui/app/html/words/topics.html new file mode 100644 index 0000000000000000000000000000000000000000..46a20f18289a83eeeaf1dae8f614fa67aac1f987 --- /dev/null +++ b/vipra-ui/app/html/words/topics.html @@ -0,0 +1,57 @@ +<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'words.topics'"> + <div class="row"> + <div class="col-md-12"> + <div class="page-header"> + <h1 ng-bind-template="Topics for word '{{::word}}'"></h1> + <table class="item-actions"> + <tr> + <td> + <a class="btn btn-default" ng-click="goBack()" ng-show="oldState.name && oldState.name !== state.name">Back</a> + </td> + </tr> + </table> + </div> + </div> + </div> + <div class="row"> + <div class="col-md-12 text-center"> + <pagination total="topicsTotal" page="wordsTopicsModels.page" limit="wordsTopicsModels.limit" /> + </div> + </div> + <div class="row"> + <div class="col-md-12"> + <div class="panel panel-default"> + <div class="panel-heading"> + Found + <ng-pluralize count="topicsTotal||0" when="{0:'no topics',1:'1 topic',other:'{} topics'}"></ng-pluralize> in the database. + <span ng-show="topicsTotal"> + Sort by + <ol class="nya-bs-select nya-bs-condensed" ng-model="wordsTopicsModels.sortkey"> + <li value="name" class="nya-bs-option"><a>Name</a></li> + <li value="created" class="nya-bs-option"><a>Added</a></li> + </ol> + <sort-dir ng-model="wordsTopicsModels.sortdir" /> + </span> + </div> + <table class="table table-hover table-condensed"> + <tbody> + <tr ng-repeat="topic in topics"> + <td> + <topic-link topic="topic" /> + </td> + </tr> + </tbody> + </table> + <div class="panel-footer"> + Page <span ng-bind="wordsTopicsModels.page||1"></span> of <span ng-bind="maxPage||1"></span> + </div> + </div> + </div> + </div> + <div class="row"> + <div class="col-md-12 text-center"> + <pagination total="topicsTotal" page="wordsTopicsModels.page" limit="wordsTopicsModels.limit" /> + </div> + </div> +</div> +<div ng-cloak ui-view></div> diff --git a/vipra-ui/app/index.html b/vipra-ui/app/index.html index 03eab34ccb1a05e106cc80b5ad4834d1603cf79f..3e9d161a5c6212113e1a668983c591d24ab4fe2d 100644 --- a/vipra-ui/app/index.html +++ b/vipra-ui/app/index.html @@ -65,18 +65,16 @@ <a tabindex="0" ui-sref="topics"><span class="mnemonic">T</span>opics</a> </li> </ul> - <form class="navbar-form navbar-left" role="search" ng-hide="$state.current.name === 'index'"> + <form class="navbar-form navbar-left" role="search" ng-hide="state.name === 'index'"> <div class="form-group has-feedback"> <input tabindex="0" type="text" class="form-control" placeholder="Search..." ng-model="rootModels.search" ng-enter="menubarSearch(rootModels.search)" id="menuSearchBox"> <i class="form-control-feedback glyphicon glyphicon-search text-muted"></i> </div> </form> - <ul class="nav navbar-nav"> + <ul class="nav navbar-nav navbar-right"> <li ng-class="{'text-italic active':rootModels.topicModel}"> <a tabindex="0" ng-click="chooseTopicModel()" ng-bind-template="{{rootModels.topicModel ? rootModels.topicModel.id : 'Models'}}" ng-attr-title="{{rootModels.topicModel.modelConfig.description}}"></a> </li> - </ul> - <ul class="nav navbar-nav navbar-right"> <li ui-sref-active="active"> <a tabindex="0" ui-sref="about"> About @@ -134,5 +132,8 @@ </div> </div> </div> + <div class="alerts"> + <bs-alert ng-model="alert" type="alert.type" ng-repeat="alert in alerts"/> + </div> </body> </html> \ No newline at end of file diff --git a/vipra-ui/app/js/app.js b/vipra-ui/app/js/app.js index d67c058af5ca1d70c4100e6e1de4cc3c9d6b2035..94aefbb11a67505e3cabaf212b6a8e24960174e5 100644 --- a/vipra-ui/app/js/app.js +++ b/vipra-ui/app/js/app.js @@ -88,6 +88,26 @@ controller: 'TopicsArticlesController' }); + // states: words + + $stateProvider.state('words', { + abstract: true, + url: '/words/:word', + template: '<ui-view/>' + }); + + $stateProvider.state('words.topics', { + url: '/topics', + templateUrl: 'html/words/topics.html', + controller: 'WordsTopicsController' + }); + + $stateProvider.state('words.articles', { + url: '/articles', + templateUrl: 'html/words/articles.html', + controller: 'WordsArticlesController' + }); + // states: errors $stateProvider.state('error', { @@ -98,7 +118,7 @@ // http interceptors - $httpProvider.interceptors.push(function($q, $injector, $rootScope) { + $httpProvider.interceptors.push(function($q, $rootScope) { var requestIncrement = function(config) { $rootScope.loading.requests = ++$rootScope.loading.requests || 1; $rootScope.loading[config.method] = ++$rootScope.loading[config.method] || 1; @@ -129,13 +149,14 @@ responseError: function(rejection) { requestDecrement(rejection.config); - $rootScope.error = rejection; - if (rejection.status >= 400 && rejection.status < 600) { - $injector.get('$state').transitionTo('error', { - code: rejection.status - }, { - location: 'replace' - }); + if(rejection.data) { + if(angular.isArray(rejection.data)) { + for(var i = 0; i < rejection.data.length; i++) { + $rootScope.alerts.push(angular.extend({type:'danger'}, rejection.data[i])); + } + } else { + $rootScope.alerts.push(angular.extend({type:'danger'}, rejection.data)); + } } return $q.reject(rejection); } @@ -159,6 +180,15 @@ }); }); + $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) { + $rootScope.oldState = fromState; + $rootScope.state = toState; + }); + + $rootScope.$on('$stateChangeStart', function() { + $rootScope.alerts = []; + }); + }]); })(); \ No newline at end of file diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js index 13e979f25afd3730705bba1516b6ae61f99a3587..bdcb50498c99363194c421b2293fa96c65d8e53b 100644 --- a/vipra-ui/app/js/controllers.js +++ b/vipra-ui/app/js/controllers.js @@ -9,10 +9,9 @@ var app = angular.module('vipra.controllers', []); - app.controller('RootController', ['$scope', '$state', '$location', 'hotkeys', 'TopicModelFactory', - function($scope, $state, $location, hotkeys, TopicModelFactory) { + app.controller('RootController', ['$scope', '$state', '$location', '$window', 'hotkeys', 'TopicModelFactory', + function($scope, $state, $location, $window, hotkeys, TopicModelFactory) { - $scope.$state = $state; $scope.rootModels = { topicModel: null, search: null @@ -23,8 +22,6 @@ fields: '_all' }, function(data) { $scope.topicModels = data; - }, function(err) { - $scope.errors = err; }); }; @@ -49,6 +46,10 @@ }); }; + $scope.goBack = function() { + $window.history.back(); + }; + $scope.$on('$stateChangeSuccess', function() { $scope.rootModels.search = null; }); @@ -126,8 +127,6 @@ sort: '-created' }, function(data) { $scope.latestArticles = data; - }, function(err) { - $scope.errors = err; }); TopicFactory.query({ @@ -136,8 +135,6 @@ sort: '-created' }, function(data) { $scope.latestTopics = data; - }, function(err) { - $scope.errors = err; }); }); @@ -161,8 +158,6 @@ }, function(data) { $scope.searching = false; $scope.searchResults = data; - }, function(err) { - $scope.errors = err; }); }; @@ -180,10 +175,7 @@ $scope.buildDate = Vipra.formatDateTime(moment($scope.info.app.builddate, 'YYMMDD_HHmm').toDate()); $scope.startTime = Vipra.formatDateTime(moment($scope.info.vm.starttime, 'x').toDate()); $scope.upTime = moment.duration($scope.info.vm.uptime).humanize(); - }, function(err) { - $scope.errors = err; }); - } ]); @@ -273,8 +265,6 @@ // take topic model from node if (!angular.isObject($scope.rootModels.topicModel)) $scope.rootModels.topicModel = data.topicModel; - }, function(err) { - $scope.errors = err; }); var newNode = function(title, type, show, dbid, color, shape) { @@ -371,8 +361,6 @@ data.topics[i] = data.topics[i].topic; constructor(data.topics, node, topicNode); } - }, function(err) { - $scope.errors = err; }); } else if (node.type === 'topic') { // node is topic, load topic to get articles @@ -381,8 +369,6 @@ id: node.dbid }, function(data) { constructor(data, node, articleNode); - }, function(err) { - $scope.errors = err; }); } $scope.nodes.update(node); @@ -452,8 +438,6 @@ } risingDecayMin = Math.abs(risingDecayMin); risingDecayMax = risingDecayMin + Math.abs(risingDecayMax); - }, function(err) { - $scope.errors = err; }); }); @@ -600,8 +584,6 @@ $scope.articles = data; $scope.articlesTotal = headers("V-Total"); $scope.maxPage = Math.ceil($scope.articlesTotal / $scope.articlesIndexModels.limit); - }, function(err) { - $scope.errors = err; }); }); @@ -614,6 +596,12 @@ app.controller('ArticlesShowController', ['$scope', '$state', '$stateParams', '$timeout', 'ArticleFactory', function($scope, $state, $stateParams, $timeout, ArticleFactory) { + $scope.articlesShowModels = { + topicsSort: 'share', + similarSort: 'divergence', + wordsSort: '-count' + }; + ArticleFactory.get({ id: $stateParams.id }, function(data) { @@ -631,9 +619,6 @@ if ($scope.article.topics) { var topicShareSeries = [], topics = $scope.article.topics, - maximum = { - y: 0 - }, colors = randomColor({ count: $scope.article.topics.length }); @@ -645,15 +630,9 @@ }; topicShareSeries.push(d); - if (d.y > maximum.y) - maximum = d; - $scope.article.topics[i].color = colors[i]; } - // preselect biggest value - maximum.selected = maximum.sliced = true; - $timeout(function() { // highcharts data $scope.topicShare = topicShareChart([{ @@ -663,14 +642,23 @@ }]); }, 0); } - }, function(err) { - $scope.errors = err; }); $scope.$watch('rootModels.topicModel', function(newVal) { if ($scope.article && $scope.article.topicModel.id !== newVal.id) $state.transitionTo('index'); }); + + $scope.openTabWords = function() { + if($scope.words) return; + + ArticleFactory.get({ + id: $stateParams.id, + fields: 'words' + }, function(data) { + $scope.words = data.words; + }); + }; } ]); @@ -707,8 +695,6 @@ $scope.topics = data; $scope.topicsTotal = headers("V-Total"); $scope.maxPage = Math.ceil($scope.topicsTotal / $scope.topicsIndexModels.limit); - }, function(err) { - $scope.errors = err; }); }); @@ -718,16 +704,15 @@ /** * Topic Show route */ - app.controller('TopicsShowController', ['$scope', '$state', '$stateParams', '$timeout', 'TopicFactory', 'SequenceFactory', - function($scope, $state, $stateParams, $timeout, TopicFactory, SequenceFactory) { + app.controller('TopicsShowController', ['$scope', '$state', '$stateParams', '$timeout', 'hotkeys', 'TopicFactory', 'SequenceFactory', + function($scope, $state, $stateParams, $timeout, hotkeys, TopicFactory, SequenceFactory) { $scope.topicsShowModels = { relSeqstyle: 'absolute', relChartstyle: 'areaspline', wordSeqstyle: 'absolute', wordChartstyle: 'spline', - topicSortwords: '-probability', - seqSortwords: '-probability' + seqSortWords: '-probability' }; TopicFactory.get({ @@ -747,12 +732,14 @@ $scope.topic.words[i].selected = true; } + // preselect first sequence + if($scope.topic.sequences && $scope.topic.sequences.length) + $scope.sequenceId = $scope.topic.sequences[0].id; + $timeout(function() { $scope.redrawRelevanceGraph(); $scope.redrawWordEvolutionChart(); }, 0); - }, function(err) { - $scope.errors = err; }); $scope.redrawRelevanceGraph = function() { @@ -840,8 +827,6 @@ } } } - }, function(err) { - $scope.errors = err; }); } else { $scope.isRename = false; @@ -862,15 +847,13 @@ $scope.$watch('topicsShowModels.wordSeqstyle', $scope.redrawWordEvolutionChart); $scope.$watch('topicsShowModels.wordChartstyle', $scope.redrawWordEvolutionChart); - $scope.$watchGroup(['sequenceId'], function() { + $scope.$watch('sequenceId', function() { if (!$scope.sequenceId) return; SequenceFactory.get({ id: $scope.sequenceId }, function(data) { $scope.sequence = data; - }, function(err) { - $scope.errors = err; }); }); @@ -878,6 +861,14 @@ if ($scope.topic && $scope.topic.topicModel.id !== newVal.id) $state.transitionTo('index'); }); + + hotkeys.add({ + combo: 'r', + description: 'Rename topic', + callback: function() { + $scope.startRename(); + } + }); } ]); @@ -904,26 +895,94 @@ $scope.articles = data; $scope.articlesTotal = headers("V-Total"); $scope.maxPage = Math.ceil($scope.articlesTotal / $scope.topicsArticlesModels.limit); - }, function(err) { - $scope.errors = err; }); }); } ]); - /** - * Errors route - */ + /**************************************************************************** + * Word Controllers + ****************************************************************************/ + + app.controller('WordsTopicsController', ['$scope', '$state', '$stateParams', 'TopicFactory', + function($scope, $state, $stateParams, TopicFactory) { + + $scope.word = $stateParams.word; + + // page was reloaded, choose topic model + if (!$scope.rootModels.topicModel && $state.current.name === 'words.topics') + $scope.chooseTopicModel(); + + $scope.wordsTopicsModels = { + sortkey: 'name', + sortdir: true, + page: 1, + limit: 100 + }; + + $scope.$watchGroup(['wordsTopicsModels.page', 'wordsTopicsModels.sortkey', 'wordsTopicsModels.sortdir', 'rootModels.topicModel'], function() { + if (!$scope.rootModels.topicModel) return; + + TopicFactory.query({ + topicModel: $scope.rootModels.topicModel.id, + skip: ($scope.wordsTopicsModels.page - 1) * $scope.wordsTopicsModels.limit, + limit: $scope.wordsTopicsModels.limit, + sort: ($scope.wordsTopicsModels.sortdir ? '' : '-') + $scope.wordsTopicsModels.sortkey, + word: $stateParams.word + }, function(data, headers) { + $scope.topics = data; + $scope.topicsTotal = headers("V-Total"); + $scope.maxPage = Math.ceil($scope.topicsTotal / $scope.wordsTopicsModels.limit); + }); + }); + + } + ]); + + app.controller('WordsArticlesController', ['$scope', '$state', '$stateParams', 'ArticleFactory', + function($scope, $state, $stateParams, ArticleFactory) { + + $scope.word = $stateParams.word; + + // page was reloaded, choose topic model + if (!$scope.rootModels.topicModel && $state.current.name === 'words.articles') + $scope.chooseTopicModel(); + + $scope.wordsArticlesModels = { + sortkey: 'date', + sortdir: true, + page: 1, + limit: 100 + }; + + $scope.$watchGroup(['wordsArticlesModels.page', 'wordsArticlesModels.sortkey', 'wordsArticlesModels.sortdir', 'rootModels.topicModel'], function() { + if (!$scope.rootModels.topicModel) return; + + ArticleFactory.query({ + skip: ($scope.wordsArticlesModels.page - 1) * $scope.wordsArticlesModels.limit, + limit: $scope.wordsArticlesModels.limit, + sort: ($scope.wordsArticlesModels.sortdir ? '' : '-') + $scope.wordsArticlesModels.sortkey, + topicModel: $scope.rootModels.topicModel.id, + word: $stateParams.word + }, function(data, headers) { + $scope.articles = data; + $scope.articlesTotal = headers("V-Total"); + $scope.maxPage = Math.ceil($scope.articlesTotal / $scope.wordsArticlesModels.limit); + }); + }); + + } + ]); + + /**************************************************************************** + * Error Controllers + ****************************************************************************/ + app.controller('ErrorsController', ['$scope', '$state', '$stateParams', function($scope, $state, $stateParams) { - if ($scope.error) { - $scope.code = $scope.error.status; - $scope.title = $scope.error.statusText; - } else { - $scope.code = $stateParams.code; - $scope.title = Vipra.statusMsg($scope.code); - } + $scope.code = $stateParams.code; + $scope.title = Vipra.statusMsg($scope.code); } ]); @@ -1045,8 +1104,8 @@ type: 'pie', spacingBottom: 0, spacingTop: 0, - spacingLeft: 20, - spacingRight: 20, + spacingLeft: 0, + spacingRight: 0, }, credits: { enabled: false diff --git a/vipra-ui/app/js/directives.js b/vipra-ui/app/js/directives.js index 5638c5691a07065ad67920c8746cbb3d19aa52be..68ff167810d98352abda5eb2d3db9ac0f8a8e0ae 100644 --- a/vipra-ui/app/js/directives.js +++ b/vipra-ui/app/js/directives.js @@ -23,6 +23,18 @@ }; }]); + app.directive('wordLink', [function() { + return { + scope: { + word: '=' + }, + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'html/directives/word-link.html' + }; + }]); + app.directive('articleLink', [function() { return { scope: { @@ -117,13 +129,58 @@ } ]); + app.directive('bsTab', [function() { + return { + link: function($scope, $elem, $attrs) { + $elem.on('shown.bs.tab', function() { + if($attrs.shown) { + $scope.$eval($attrs.shown); + } + }); + } + }; + }]); + + app.directive('bsAlert', [function() { + return { + scope: { + ngModel: '=', + type: '@', + dismissible: '@' + }, + replace: true, + restrict: 'E', + link: function($scope) { + var classes = ['alert']; + $scope.dismissible = $scope.dismissible !== 'false'; + if($scope.dismissible) { + classes.push('alert-dismissible'); + } + switch($scope.type) { + case 'success': + case 'info': + case 'warning': + classes.push('alert-' + $scope.type); + break; + case 'danger': + default: + classes.push('alert-danger'); + } + $scope.classes = classes.join(' '); + }, + templateUrl: 'html/directives/alert.html' + } + }]); + app.directive('sequenceDropdown', [function() { return { scope: { ngModel: '=', - sequences: '=' + sequences: '=', + dropup: '@' }, link: function($scope) { + $scope.dropup = $scope.dropup === 'true'; $scope.$watch('sequences', function(newValue) { if (newValue) { for (var i = 0, s; i < $scope.sequences.length; i++) { @@ -161,6 +218,11 @@ $scope.showCaret = function() { return $scope.ngModel === $scope.sortBy || $scope.ngModel === '-' + $scope.sortBy; }; + + if($scope.ngModel === $scope.sortBy) + $scope.reverse = false; + else if($scope.ngModel === '-' + $scope.sortBy) + $scope.reverse = true; }, transclude: true, template: '<span ng-transclude></span> <i class="fa" ng-class="{\'fa-caret-down\':!reverse, \'fa-caret-up\':reverse}" ng-show="showCaret()"></i>' @@ -203,36 +265,28 @@ }; }]); - app.directive('sortDir', [function() { + app.directive('wordMenu', [function() { return { scope: { - ngModel: '=' + word: '=', + right: '@' }, restrict: 'E', - replace: true, - templateUrl: 'html/directives/sort-dir.html' + templateUrl: 'html/directives/word-menu.html', + link: function($scope) { + $scope.dropdownRight = $scope.right === 'true'; + } }; }]); - app.directive('anchorLink', ['$timeout', '$location', function($timeout, $location) { + app.directive('sortDir', [function() { return { scope: { - fragment: '@' + ngModel: '=' }, restrict: 'E', replace: true, - templateUrl: 'html/directives/anchor-link.html', - link: function($scope) { - $timeout(function() { - var hash = $location.hash(); - if (hash === $scope.fragment) { - var elem = $('#' + hash); - if (elem.length) { - window.scrollTo(0, elem.offset().top); - } - } - }, 10); - } + templateUrl: 'html/directives/sort-dir.html' }; }]); diff --git a/vipra-ui/app/less/app.less b/vipra-ui/app/less/app.less index 1cad561ea7ec2f42de3fb795712798720a04887e..0b4abd01f536f78250167c8d8a4c9b1d3b701f5f 100644 --- a/vipra-ui/app/less/app.less +++ b/vipra-ui/app/less/app.less @@ -340,12 +340,6 @@ a:hover { width: 100% !important; } -.table-infos { - th { - white-space: nowrap; - } -} - .percent-align { width: 45px; text-align: right; @@ -374,40 +368,11 @@ a:hover { color: #333; } -topic-menu { +topic-menu, +word-menu { display: inline-block; } -[bs-list] > li { - .pointer; - -} - -.anchor-link { - font-size: 12px; - display: inline-block; - margin-left: -19px; - width: 19px; - padding: 5px 0; - vertical-align: middle; - position: relative; - - > i { - visibility: hidden; - } - - > span { - display: block; - position: absolute; - top: -@topbarSpace; - } - - *:hover > & > i, - &:hover > i { - visibility: visible; - } -} - .text-italic { font-style: italic; } @@ -432,6 +397,29 @@ topic-menu { } } +.infocol { + width: 100px; +} + +.page-header.no-border { + border-color: transparent; +} + +.alerts { + position: fixed; + bottom: 20px; + left: 20px; + right: 20px; + + .alert { + margin: 0; + } + + .alert + .alert { + margin-top: 5px; + } +} + @-moz-keyframes spin { 100% { -moz-transform: rotateY(360deg); diff --git a/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java b/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java index 8e4c184a6ea1d490cddbffbc373292d72842f6bb..6b6015ecec2b904d10e809b11fce9dd03e96f1ab 100644 --- a/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java +++ b/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java @@ -6,6 +6,7 @@ import java.util.Date; import java.util.List; import org.bson.types.ObjectId; +import org.mongodb.morphia.annotations.Embedded; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; import org.mongodb.morphia.annotations.Index; @@ -38,6 +39,7 @@ public class TopicFull implements Model<ObjectId>, Serializable { @QueryIgnore(multi = true) private List<Sequence> sequences; + @Embedded @QueryIgnore(multi = true) private List<TopicWord> words;