From babcae3ba40ce4453158306174e140409f778f4f Mon Sep 17 00:00:00 2001 From: Eike Cochu <eike@cochu.com> Date: Wed, 30 Mar 2016 21:56:48 +0200 Subject: [PATCH] updated --- .../vipra/rest/resource/ArticleResource.java | 2 +- .../de/vipra/rest/resource/TopicResource.java | 5 +- .../java/de/vipra/cmd/text/ProcessedText.java | 2 + vipra-ui/app/html/about.html | 16 +- vipra-ui/app/html/articles/index.html | 6 +- vipra-ui/app/html/articles/show.html | 196 ++++++++----- vipra-ui/app/html/directives/alert.html | 4 + vipra-ui/app/html/directives/anchor-link.html | 4 - .../html/directives/sequence-dropdown.html | 2 +- vipra-ui/app/html/directives/word-link.html | 4 + vipra-ui/app/html/directives/word-menu.html | 9 + vipra-ui/app/html/error.html | 2 +- vipra-ui/app/html/explorer.html | 2 +- vipra-ui/app/html/index.html | 2 +- vipra-ui/app/html/network.html | 2 +- vipra-ui/app/html/topics/articles.html | 6 +- vipra-ui/app/html/topics/index.html | 6 +- vipra-ui/app/html/topics/show.html | 260 +++++++++--------- vipra-ui/app/html/words/articles.html | 58 ++++ vipra-ui/app/html/words/topics.html | 57 ++++ vipra-ui/app/index.html | 9 +- vipra-ui/app/js/app.js | 46 +++- vipra-ui/app/js/controllers.js | 183 +++++++----- vipra-ui/app/js/directives.js | 92 +++++-- vipra-ui/app/less/app.less | 62 ++--- .../java/de/vipra/util/model/TopicFull.java | 2 + 26 files changed, 670 insertions(+), 369 deletions(-) create mode 100644 vipra-ui/app/html/directives/alert.html delete mode 100644 vipra-ui/app/html/directives/anchor-link.html create mode 100644 vipra-ui/app/html/directives/word-link.html create mode 100644 vipra-ui/app/html/directives/word-menu.html create mode 100644 vipra-ui/app/html/words/articles.html create mode 100644 vipra-ui/app/html/words/topics.html 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 06419f12..da5cd899 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 b13dfe1c..b4b7f02a 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 8911bcd2..9d29c9f9 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 f13b32d7..4865c49c 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 ab26863e..fd3ac02b 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 6c682de6..2367cbe1 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 00000000..c703f74d --- /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 47e17f14..00000000 --- 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 2cbbecb7..17e05326 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 00000000..0c2dff7c --- /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 00000000..fe6a7ef3 --- /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 1e41bdb8..2e2c189d 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 11344053..01e44e00 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 eaf87bd5..3eddb62d 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 445a5473..98282f1d 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 a95e3848..0a3a6b30 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 b58dd690..cf277d39 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 c94f2b37..63bcc290 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 00000000..03dce569 --- /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 00000000..46a20f18 --- /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 03eab34c..3e9d161a 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 d67c058a..94aefbb1 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 13e979f2..bdcb5049 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 5638c569..68ff1678 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 1cad561e..0b4abd01 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 8e4c184a..6b6015ec 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; -- GitLab