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 43a83f7ad6b834d3636c9301c5c3522a8313c3bd..436e729b4da1742fffa97896c89587233d5cec8e 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 @@ -3,13 +3,16 @@ package de.vipra.rest.resource; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.servlet.ServletContext; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; +import javax.ws.rs.FormParam; import javax.ws.rs.GET; +import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @@ -31,6 +34,7 @@ import de.vipra.util.StringUtils; import de.vipra.util.ex.ConfigException; import de.vipra.util.model.ArticleFull; import de.vipra.util.model.TextEntityFull; +import de.vipra.util.model.Topic; import de.vipra.util.model.TopicModel; import de.vipra.util.model.TopicModelFull; import de.vipra.util.model.WordFull; @@ -56,22 +60,29 @@ public class ArticleResource { dbTopicModels = MongoService.getDatabaseService(config, TopicModelFull.class); } - /** - * @param topicModel - * @param skip - * @param limit - * @param sortBy - * @param fields - * @param word - * @return - */ @GET @Produces(MediaType.APPLICATION_JSON) public Response getArticles(@QueryParam("topicModel") final String topicModel, @QueryParam("skip") final Integer skip, @QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("date") final String sortBy, @QueryParam("fields") final String fields, @QueryParam("word") final String word, @QueryParam("entity") final String entity, @QueryParam("excerpt") final String excerpt, @QueryParam("char") final String startChar, @QueryParam("contains") final String contains, - @QueryParam("from") final Long fromDate, @QueryParam("to") final Long toDate) { + @QueryParam("from") final Long fromDate, @QueryParam("to") final Long toDate, @QueryParam("topics") final List<String> topics) { + return processArticles(topicModel, skip, limit, sortBy, fields, word, entity, excerpt, startChar, contains, fromDate, toDate, topics); + } + + @POST + @Produces(MediaType.APPLICATION_JSON) + public Response postArticles(@FormParam("topicModel") final String topicModel, @FormParam("skip") final Integer skip, + @FormParam("limit") final Integer limit, @FormParam("sort") @DefaultValue("date") final String sortBy, + @FormParam("fields") final String fields, @FormParam("word") final String word, @FormParam("entity") final String entity, + @FormParam("excerpt") final String excerpt, @FormParam("char") final String startChar, @FormParam("contains") final String contains, + @FormParam("from") final Long fromDate, @FormParam("to") final Long toDate, @FormParam("topics") final List<String> topics) { + return processArticles(topicModel, skip, limit, sortBy, fields, word, entity, excerpt, startChar, contains, fromDate, toDate, topics); + } + + private Response processArticles(final String topicModel, final Integer skip, final Integer limit, final String sortBy, final String fields, + final String word, final String entity, final String excerpt, final String startChar, final String contains, final Long fromDate, + final Long toDate, final List<String> topics) { final ResponseWrapper<List<ArticleFull>> res = new ResponseWrapper<>(); if (topicModel == null || topicModel.trim().isEmpty()) { @@ -114,6 +125,14 @@ public class ArticleResource { if (toDate != null) query.lte("date", new Date(toDate)); + if (topics != null && !topics.isEmpty()) { + final List<Topic> ids = new ArrayList<>(topics.size()); + for (final String s : topics) + if (ObjectId.isValid(s)) + ids.add(new Topic(new ObjectId(s))); + query.in("topics.topic", ids); + } + final List<ArticleFull> articles = dbArticles.getMultiple(query); if ((skip != null && skip > 0) || (limit != null && limit > 0)) diff --git a/vipra-ui/app/html/articles/index.html b/vipra-ui/app/html/articles/index.html index fb929327cd0615ef90f9d9a32568cd1897afc161..43276e8928371e99c4ab58414aaa9f7ddf09e08e 100644 --- a/vipra-ui/app/html/articles/index.html +++ b/vipra-ui/app/html/articles/index.html @@ -82,6 +82,10 @@ </span> </div> </div> + <div class="form-group"> + <label class="control-label">Topics</label> + <topic-chooser ng-model="articlesIndexModels.topics" /> + </div> </div> </div> </div> diff --git a/vipra-ui/app/html/directives/article-menu.html b/vipra-ui/app/html/directives/article-menu.html index 4ae1d1143eba6caf251f6659f95ac3f00d25e246..362bdf1c4adc8886ffa22ba8d85537c5dcdf2f71 100644 --- a/vipra-ui/app/html/directives/article-menu.html +++ b/vipra-ui/app/html/directives/article-menu.html @@ -3,6 +3,7 @@ <i class="fa fa-caret-down"></i> </a> <ul class="dropdown-menu" ng-class="{'dropdown-menu-right':dropdownRight}"> + <li class="header">article</li> <li><a ui-sref="articles.show({id:article.id})">Show</a></li> <li><a ui-sref="articles.show.entities({id:article.id})">Entities</a></li> <li><a ui-sref="articles.show.words({id:article.id})">Words</a></li> diff --git a/vipra-ui/app/html/directives/entity-menu.html b/vipra-ui/app/html/directives/entity-menu.html index 11e89f94b4a5d1a56fc2b27d1715b478a0edc2a7..6bc02a74a769c1e5c6c518bbe5250c3b002dae8e 100644 --- a/vipra-ui/app/html/directives/entity-menu.html +++ b/vipra-ui/app/html/directives/entity-menu.html @@ -3,6 +3,7 @@ <i class="fa fa-caret-down"></i> </a> <ul class="dropdown-menu" ng-class="{'dropdown-menu-right':dropdownRight}"> + <li class="header">entity</li> <li><a ui-sref="entities.show({id:id})">Show</a></li> <li ng-if="entity.isWord"><a ui-sref="words.show({id:id})">Show word</a></li> <li><a ui-sref="entities.show.articles({id:id})">Articles</a></li> diff --git a/vipra-ui/app/html/directives/topic-chooser.html b/vipra-ui/app/html/directives/topic-chooser.html new file mode 100644 index 0000000000000000000000000000000000000000..762b40f57e879f3ff1ac80109a92afbcf87a903a --- /dev/null +++ b/vipra-ui/app/html/directives/topic-chooser.html @@ -0,0 +1,20 @@ +<div class="form-control-box"> + <div class="btn-group btn-group-justified"> + <input type="text" class="form-control" ng-model="filter" placeholder="Filter..." /> + <span class="glyphicon glyphicon-remove-circle searchclear" ng-click="filter=''"></span> + </div> + <div class="scroll-area message-container"> + <div class="checkbox checkbox-condensed" ng-repeat="topic in topics | filter:{name:filter} track by topic.id"> + <input tabindex="0" type="checkbox" ng-attr-id="{{::topic.id}}" ng-attr-ng-true-value="'{{::topic.id}}'" ng-false-value="undefined" ng-model="selectedTopics[$index]"> + <label class="check" ng-attr-for="{{::topic.id}}" analytics-on analytics-event="Filter Topic Choice" analytics-category="Filter actions"> + <span class="ellipsis menu-padding padding" ng-attr-title="{{topic.name}}"> + <span class="title" ng-bind="topic.name"></span> + </span> + <topic-menu topic="topic" class="menu-button" /> + </label> + <span class="colorbox" style="background:{{::topic.color}}"></span> + </div> + <span class="message text-muted" ng-if="!topics.length">No Topics</span> + </div> + <button type="button" class="btn btn-default btn-block" ng-click="selectedTopics=[]">All</button> +</div> \ No newline at end of file diff --git a/vipra-ui/app/html/directives/topic-menu.html b/vipra-ui/app/html/directives/topic-menu.html index e6984e1fb0eadb9b993d4b0ce8ec059595383954..45f5c379832d07be7ef1ca4742ccfa1726b98c44 100644 --- a/vipra-ui/app/html/directives/topic-menu.html +++ b/vipra-ui/app/html/directives/topic-menu.html @@ -3,6 +3,7 @@ <i class="fa fa-caret-down"></i> </a> <ul class="dropdown-menu" ng-class="{'dropdown-menu-right':dropdownRight}"> + <li class="header">topic</li> <li><a ui-sref="topics.show({id:topic.id})">Show</a></li> <li><a ui-sref="topics.show.sequences({id:topic.id})">Sequences</a></li> <li><a ui-sref="topics.show.articles({id:topic.id})">Articles</a></li> diff --git a/vipra-ui/app/html/directives/word-menu.html b/vipra-ui/app/html/directives/word-menu.html index cab3e5aae755297a5de6f80047e1c4c02433abf6..d90a6430dc93e29e6e3ee63e3e3942ce77e19641 100644 --- a/vipra-ui/app/html/directives/word-menu.html +++ b/vipra-ui/app/html/directives/word-menu.html @@ -3,6 +3,7 @@ <i class="fa fa-caret-down"></i> </a> <ul class="dropdown-menu" ng-class="{'dropdown-menu-right':dropdownRight}"> + <li class="header">word</li> <li><a ui-sref="words.show({id:id})">Show</a></li> <li ng-if="word.isEntity"><a ui-sref="entities.show({id:id})">Show entity</a></li> <li><a ui-sref="words.show.topics({id:id})">Topics</a></li> diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js index f14631f66cf28b8c2462ee70eee00f7c01430f9b..18e01eb32771ddf01a8a77859dd9f1f93a962342 100644 --- a/vipra-ui/app/js/controllers.js +++ b/vipra-ui/app/js/controllers.js @@ -1169,13 +1169,18 @@ sortkey: 'date', sortdir: true, page: 1, - limit: 50 + limit: 50, + topics: [] }; $scope.reloadArticles = function() { $scope.loadingArticles = true; - ArticleFactory.query({ + var topics = null; + if($scope.articlesIndexModels.topics && $scope.articlesIndexModels.topics.length) + topics = $scope.articlesIndexModels.topics; + + (topics ? ArticleFactory.queryPOST : ArticleFactory.query)({ skip: ($scope.articlesIndexModels.page - 1) * $scope.articlesIndexModels.limit, limit: $scope.articlesIndexModels.limit, sort: ($scope.articlesIndexModels.sortdir ? '' : '-') + $scope.articlesIndexModels.sortkey, @@ -1183,7 +1188,8 @@ char: $scope.articlesIndexModels.startChar, contains: $scope.articlesIndexModels.contains, from: $scope.articlesIndexModels.fromDate ? $scope.articlesIndexModels.fromDate.getTime() : null, - to: $scope.articlesIndexModels.toDate ? $scope.articlesIndexModels.toDate.getTime() : null + to: $scope.articlesIndexModels.toDate ? $scope.articlesIndexModels.toDate.getTime() : null, + topics: topics }, function(data, headers) { $scope.articles = data; $scope.articlesTotal = headers("V-Total"); @@ -1194,7 +1200,7 @@ }); }; - $scope.$watchGroup(['articlesIndexModels.page', 'articlesIndexModels.sortkey', 'articlesIndexModels.sortdir', 'articlesIndexModels.fromDate', 'articlesIndexModels.toDate'], function() { + $scope.$watchGroup(['articlesIndexModels.page', 'articlesIndexModels.sortkey', 'articlesIndexModels.sortdir', 'articlesIndexModels.fromDate', 'articlesIndexModels.toDate', 'articlesIndexModels.topics'], function() { $scope.reloadArticles(); }); diff --git a/vipra-ui/app/js/directives.js b/vipra-ui/app/js/directives.js index b917830c3116fd3257685be425abfaf31a33b90b..d7b15828cfe12cbe9a6d8145805c6c2c85bcc720 100644 --- a/vipra-ui/app/js/directives.js +++ b/vipra-ui/app/js/directives.js @@ -612,4 +612,29 @@ }; }]); + app.directive('topicChooser', ['TopicFactory', function(TopicFactory) { + return { + scope: { + ngModel: '=' + }, + templateUrl: 'html/directives/topic-chooser.html', + link: function($scope) { + $scope.selectedTopics = []; + + TopicFactory.query({ + topicModel: $scope.$parent.rootModels.topicModel.id + }, function(data) { + $scope.topics = data; + }); + + $scope.$watch('selectedTopics', function() { + $scope.ngModel = []; + for(var i = 0; i < $scope.selectedTopics.length; i++) + if($scope.selectedTopics[i]) + $scope.ngModel.push($scope.selectedTopics[i]); + }, true); + } + }; + }]); + })(); \ No newline at end of file diff --git a/vipra-ui/app/js/factories.js b/vipra-ui/app/js/factories.js index 49c0546dfed7b99b76585eaadc8bad979e37b5df..8f5c16dcf20c400d7a2e1f41d66828942f06eda1 100644 --- a/vipra-ui/app/js/factories.js +++ b/vipra-ui/app/js/factories.js @@ -39,7 +39,28 @@ }]); app.factory('ArticleFactory', ['$myResource', function($myResource) { - return $myResource(Vipra.config.restUrl + '/articles/:id'); + return $myResource(Vipra.config.restUrl + '/articles/:id', {}, { + queryPOST: { + method: 'POST', + isArray: true, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + transformRequest: function (data) { + var str = []; + for (var d in data) + if(data[d] !== undefined && data[d] !== null) { + if(angular.isArray(data[d])) { + for(var i = 0; i < data[d].length; i++) + str.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d][i])); + } else { + str.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d])); + } + } + return str.join('&'); + } + } + }); }]); app.factory('TopicFactory', ['$myResource', function($myResource) { diff --git a/vipra-ui/app/less/app.less b/vipra-ui/app/less/app.less index d4280b0b9850e4c0e88c849cb5a1471e307cf4cb..b9a05d2bcdf4784b14508d2e35e1a247f0b6ee6c 100644 --- a/vipra-ui/app/less/app.less +++ b/vipra-ui/app/less/app.less @@ -271,6 +271,9 @@ td { width: 100%; vertical-align: middle; position: relative; + &.padding { + padding-right: 7px; + } } .valuebar { @@ -1137,6 +1140,55 @@ entity-menu { } } +.scroll-area { + overflow-y: auto; + height: 150px; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075); + -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; + padding: 3px 0 0px 3px; + + .checkbox { + padding-bottom: 2px; + } + + .menu-padding { + padding-left: 7px; + } +} + +.form-control-box { + > * + *, + > * + * .form-control { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-top: 0; + } + > *:not(:last-child), + > *:not(:last-child) .form-control { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } +} + +.dropdown-menu { + .header { + padding: 3px 20px; + color: #fff; + background: #555; + font-weight: bold; + margin-top: -5px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + font-variant: small-caps; + margin-bottom: 5px; + } +} + [ng\:cloak], [ng-cloak], .ng-cloak { display: none !important; }