Skip to content
Snippets Groups Projects
Commit a159589b authored by Eike Cochu's avatar Eike Cochu
Browse files

using word id, simplified ui routes

ui: simplified routes, removed .index routes
ui: added ui-view to route bases
ui: moved network graph link to actions bar
ui: added alert directive to display alerts
ui: added alert to topic renaming
backend: removed word attribute from word model, using id instead
backend: moved topics query from single word query to subpath /topics
backend: removed uuid from apierror, unused
backend: removed wrapper from resource put requests, serializing to model directly
backend: fixed deserializing topicword/word when receiving from frontend
parent 4ffb57b2
Branches
No related tags found
No related merge requests found
Showing
with 221 additions and 150 deletions
package de.vipra.rest.model;
import java.util.UUID;
import javax.ws.rs.core.Response.Status;
public class APIError {
private final String id = UUID.randomUUID().toString();
private String status;
private String code;
private String title;
......@@ -25,10 +22,6 @@ public class APIError {
this(Integer.toString(status.getStatusCode()), status.getReasonPhrase(), title, detail);
}
public String getId() {
return id;
}
public String getStatus() {
return status;
}
......
......@@ -156,9 +156,9 @@ public class ArticleResource {
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("{id}")
public Response replaceArticle(@PathParam("id") String id, Wrapper<ArticleFull> wrapper) {
ArticleFull article = wrapper.getData();
public Response replaceArticle(@PathParam("id") String id, ArticleFull article) {
Wrapper<ArticleFull> res = new Wrapper<>();
try {
dbArticles.updateSingle(article);
return res.ok(article);
......
......@@ -119,8 +119,10 @@ public class TopicResource {
Wrapper<List<ArticleFull>> res = new Wrapper<>();
try {
Topic topic = new Topic(MongoUtils.objectId(id));
List<ArticleFull> articles = dbArticles.getMultiple(
QueryBuilder.builder().criteria("topics.topic", topic).fields(true, StringUtils.getFields(fields)));
QueryBuilder query = QueryBuilder.builder().criteria("topics.topic", topic);
if (fields != null && !fields.isEmpty())
query.fields(true, StringUtils.getFields(fields));
List<ArticleFull> articles = dbArticles.getMultiple(query);
return res.ok(articles);
} catch (Exception e) {
e.printStackTrace();
......@@ -133,15 +135,15 @@ public class TopicResource {
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("{id}")
public Response replaceTopic(@PathParam("id") String id, Wrapper<TopicFull> wrapper) {
TopicFull topic = wrapper.getData();
public Response replaceTopic(@PathParam("id") String id, TopicFull topic) {
Wrapper<TopicFull> res = new Wrapper<>();
try {
dbTopics.updateSingle(topic);
return res.ok(topic);
} catch (DatabaseException e) {
e.printStackTrace();
res = new Wrapper<>(new APIError(Response.Status.INTERNAL_SERVER_ERROR, "item could not be updated",
res.addError(new APIError(Response.Status.INTERNAL_SERVER_ERROR, "item could not be updated",
"item could not be updated due to an internal server error"));
return res.serverError();
}
......
......@@ -47,7 +47,7 @@ public class WordResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getWords(@QueryParam("skip") Integer skip, @QueryParam("limit") Integer limit,
@QueryParam("sort") @DefaultValue("word") String sortBy, @QueryParam("fields") String fields) {
@QueryParam("sort") @DefaultValue("id") String sortBy, @QueryParam("fields") String fields) {
Wrapper<List<Word>> res = new Wrapper<>();
if (skip != null && limit != null)
......@@ -94,16 +94,32 @@ public class WordResource {
}
if (word != null) {
List<TopicFull> topics = dbTopics.getMultiple(
QueryBuilder.builder().fields(false, "index", "created", "modified").criteria("words.word", word));
word.setTopics(topics);
return res.ok(word);
} else {
String msg = String.format(Messages.NOT_FOUND, "word", id);
String msg = String.format(Messages.NOT_FOUND, "id", id);
res.addError(new APIError(Response.Status.NOT_FOUND, "Resource not found", msg));
return res.notFound();
}
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Path("{id}/topics")
public Response getWordTopics(@PathParam("id") String id, @QueryParam("fields") String fields) {
Wrapper<List<TopicFull>> res = new Wrapper<>();
try {
Word word = new Word(id);
QueryBuilder query = QueryBuilder.builder().fields(true, "id", "name").criteria("words.word", word);
if (fields != null && !fields.isEmpty())
query.fields(true, StringUtils.getFields(fields));
List<TopicFull> topics = dbTopics.getMultiple(query);
return res.ok(topics);
} catch (Exception e) {
e.printStackTrace();
res.addError(new APIError(Response.Status.BAD_REQUEST, "Error", e.getMessage()));
return res.badRequest();
}
}
}
<div ui-view>
<div class="well">
Found <span ng-bind="articlesMeta.total"></span> articles in the database <query-time/>.<br>
</div>
......@@ -9,3 +10,4 @@
</ul>
<pagination total="articlesMeta.total" page="page" limit="limit" change="changePage"/>
</div>
\ No newline at end of file
......@@ -14,7 +14,7 @@
</tr>
<tr>
<th>Date</th>
<td ng-bind="::article.date"></td>
<td ng-bind="::articleDate"></td>
</tr>
<tr>
<th>URL</th>
......@@ -22,11 +22,11 @@
</tr>
<tr>
<th>Created</th>
<td ng-bind="::article.created"></td>
<td ng-bind="::articleCreated"></td>
</tr>
<tr>
<th>Last modified</th>
<td ng-bind="::article.modified"></td>
<td ng-bind="::articleModified"></td>
</tr>
<tr>
<th>Word count</th>
......
<div ng-attr-class="{{classes}}" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close" ng-show="dismissible">
<span aria-hidden="true">&times;</span>
</button>
<ng-transclude/>
</div>
\ No newline at end of file
<div ui-view>
<div class="well">
Found <span ng-bind="topicsMeta.total"></span> topics in the database <query-time/>.
</div>
......@@ -9,3 +10,4 @@
</ul>
<pagination total="topicsMeta.total" page="page" limit="limit" change="changePage"/>
</div>
\ No newline at end of file
......@@ -14,9 +14,22 @@
</div>
</h1>
<bs-alert type="danger" ng-if="renameErrors">
<span ng-bind-html="renameErrors"></span>
</bs-alert>
<table class="item-actions">
<tr>
<td>
<bs-dropdown label="Actions">
<li><a ng-click="startRename()">Rename</a></li>
</bs-dropdown>
</td>
<td>
<a class="btn btn-default" ui-sref="network({type:'topics', id:topic.id})">Network graph</a>
</td>
</tr>
</table>
</div>
<h3>Info <hide-link target="#info"/></h3>
......@@ -35,17 +48,11 @@
</tr>
<tr>
<th>Created</th>
<td ng-bind="::topic.created"></td>
<td ng-bind="::topicCreated"></td>
</tr>
<tr>
<th>Last modified</th>
<td ng-bind="::topic.modified"></td>
</tr>
<tr>
<th>Links</th>
<td>
<a ui-sref="network({type:'topics', id:topic.id})">Network graph</a>
</td>
<td ng-bind="::topicModified"></td>
</tr>
</tbody>
</table>
......
<div ui-view>
<div class="well">
Found <span ng-bind="wordsMeta.total"></span> words in the database <query-time/>.
</div>
......@@ -27,3 +28,4 @@
</div>
<pagination total="wordsMeta.total" page="page" limit="limit" change="changePage"/>
</div>
\ No newline at end of file
......@@ -10,7 +10,7 @@
<tbody>
<tr>
<th>Created</th>
<td ng-bind="::word.created"></td>
<td ng-bind="::wordCreated"></td>
</tr>
</tbody>
</table>
......@@ -22,7 +22,7 @@
<div class="row" id="topics">
<div class="col-md-12">
<ul class="list-unstyled">
<li ng-repeat="topic in ::word.topics">
<li ng-repeat="topic in ::topics">
<topic-link topic="topic"/>
</li>
</ul>
......
......@@ -51,9 +51,9 @@
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="vipra-navbar-collapse-1">
<ul class="nav navbar-nav">
<li ng-class="{active:$state.includes('articles')}"><a ui-sref="articles.index">Articles</a></li>
<li ng-class="{active:$state.includes('topics')}"><a ui-sref="topics.index">Topics</a></li>
<li ng-class="{active:$state.includes('words')}"><a ui-sref="words.index">Words</a></li>
<li ng-class="{active:$state.includes('articles')}"><a ui-sref="articles">Articles</a></li>
<li ng-class="{active:$state.includes('topics')}"><a ui-sref="topics">Topics</a></li>
<li ng-class="{active:$state.includes('words')}"><a ui-sref="words">Words</a></li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
......
......@@ -38,13 +38,7 @@
// states: articles
$stateProvider.state('articles', {
url: '/articles',
abstract: true,
template: '<ui-view/>'
});
$stateProvider.state('articles.index', {
url: '?page',
url: '/articles?page',
templateUrl: tplBase + '/articles/index.html',
controller: 'ArticlesIndexController',
reloadOnSearch: false
......@@ -59,13 +53,7 @@
// states: topics
$stateProvider.state('topics', {
url: '/topics',
abstract: true,
template: '<ui-view/>'
});
$stateProvider.state('topics.index', {
url: '?page',
url: '/topics?page',
templateUrl: tplBase + '/topics/index.html',
controller: 'TopicsIndexController',
reloadOnSearch: false
......@@ -80,13 +68,7 @@
// states: words
$stateProvider.state('words', {
url: '/words',
abstract: true,
template: '<ui-view/>'
});
$stateProvider.state('words.index', {
url: '?page',
url: '/words?page',
templateUrl: tplBase + '/words/index.html',
controller: 'WordsIndexController',
reloadOnSearch: false
......
......@@ -286,16 +286,17 @@
app.controller('ArticlesShowController', ['$scope', '$stateParams', 'ArticleFactory',
function($scope, $stateParams, ArticleFactory, testService) {
$scope.topicSort = $scope.topicSort || 'topic.share';
$scope.topicSortRev = typeof $scope.topicSortRev === 'undefined' ? false : $scope.topicSortRev;
ArticleFactory.get({id: $stateParams.id}, function(response) {
$scope.article = response.data;
$scope.article.text = createInitial($scope.article.text);
$scope.article.date = formatDate($scope.article.date);
$scope.article.created = formatDateTime($scope.article.created);
$scope.article.modified = formatDateTime($scope.article.modified);
$scope.articleDate = formatDate($scope.article.date);
$scope.articleCreated = formatDateTime($scope.article.created);
$scope.articleModified = formatDateTime($scope.article.modified);
$scope.articleMeta = response.meta;
$scope.queryTime = response.$queryTime;
$scope.topicSort = $scope.topicSort || 'topic.share';
$scope.topicSortRev = typeof $scope.topicSortRev === 'undefined' ? false : $scope.topicSortRev;
// calculate percentage share
var topicShareSeries = [],
......@@ -323,7 +324,6 @@
};
$scope.topicShare = topicShare;
});
}]);
......@@ -370,14 +370,16 @@
app.controller('TopicsShowController', ['$scope', '$stateParams', '$timeout', 'TopicFactory',
function($scope, $stateParams, $timeout, TopicFactory) {
$scope.wordSort = $scope.wordSort || 'likeliness';
$scope.wordSortRev = typeof $scope.wordSortRev === 'undefined' ? true : $scope.wordSortRev;
TopicFactory.get({id: $stateParams.id}, function(response) {
$scope.topic = response.data;
$scope.topic.created = formatDateTime($scope.topic.created);
$scope.topic.modified = formatDateTime($scope.topic.modified);
$scope.topicCreated = formatDateTime($scope.topic.created);
$scope.topicModified = formatDateTime($scope.topic.modified);
$scope.topicMeta = response.meta;
$scope.queryTime = response.$queryTime;
$scope.wordSort = $scope.wordSort || 'likeliness';
$scope.wordSortRev = typeof $scope.wordSortRev === 'undefined' ? true : $scope.wordSortRev;
});
$scope.startRename = function() {
$scope.origName = $scope.topic.name;
......@@ -388,10 +390,17 @@
};
$scope.endRename = function(save) {
$scope.isRename = false;
delete $scope.renameErrors;
if(save) {
// TODO implement
TopicFactory.update({id:$scope.topic.id}, $scope.topic, function(response) {
$scope.topic = response.data;
$scope.isRename = false;
}, function(response) {
if(response.data)
$scope.renameErrors = getErrors(response.data.errors);
});
} else {
$scope.isRename = false;
$scope.topic.name = $scope.origName;
}
};
......@@ -402,7 +411,6 @@
$event.preventDefault();
}
};
});
}]);
......@@ -418,7 +426,7 @@
$scope.page = Math.max($stateParams.page || 1, 1);
$scope.limit = 300;
$scope.sort = Store('sortwords') || 'word';
$scope.sort = Store('sortwords') || 'id';
$scope.order = Store('orderwords') || '';
$scope.reload = function() {
......@@ -451,11 +459,15 @@
WordFactory.get({id: $stateParams.id}, function(response) {
$scope.word = response.data;
$scope.word.created = formatDateTime($scope.word.created);
$scope.wordCreated = formatDateTime($scope.word.created);
$scope.wordMeta = response.meta;
$scope.queryTime = response.$queryTime;
});
WordFactory.topics({id: $stateParams.id}, function(response) {
$scope.topics = response.data;
});
}]);
/****************************************************************************
......
......@@ -155,6 +155,31 @@
};
});
app.directive('bsAlert', function() {
return {
scope: {
type: '@',
dismissible: '@'
},
transclude: true,
replace: true,
templateUrl: 'html/directives/alert.html',
link: function($scope) {
if(!$scope.type) {
console.log('no alert type given');
return;
}
$scope.dismissible = $scope.dismissible !== 'false';
var classes = 'alert alert-' + $scope.type;
if($scope.dismissible)
classes += ' alert-dismissible';
$scope.classes = classes;
}
};
});
app.directive('sortBy', ['Store', function(Store) {
return {
scope: {
......
......@@ -6,30 +6,32 @@
var app = angular.module('vipra.factories', []);
var endpoint = '//' + location.hostname + ':8000/vipra/rest';
var endpoint = '//' + location.hostname + ':8080/vipra/rest';
app.factory('ArticleFactory', ['$resource', function($resource) {
return $resource(endpoint + '/articles/:id', {}, {
query: { isArray: false, cache: true }
query: { isArray: false }
});
}]);
app.factory('TopicFactory', ['$resource', function($resource) {
return $resource(endpoint + '/topics/:id', {}, {
query: { isArray: false, cache: true },
articles: { method: 'GET', isArray: false, url: endpoint + '/topics/:id/articles', cache: true }
query: { isArray: false },
update: { method: 'PUT' },
articles: { url: endpoint + '/topics/:id/articles' }
});
}]);
app.factory('WordFactory', ['$resource', function($resource) {
return $resource(endpoint + '/words/:id', {}, {
query: { isArray: false, cache: true }
query: { isArray: false },
topics: { url: endpoint + '/words/:id/topics' }
});
}]);
app.factory('SearchFactory', ['$resource', function($resource) {
return $resource(endpoint + '/search', {}, {
query: { isArray: false, cache: true }
query: { isArray: false }
});
}]);
......
......@@ -27,6 +27,19 @@
return 'id' + Math.random().toString(36).substring(7);
};
window.console = window.console || {
log: function () {}
};
window.getErrors = function(errors) {
var html = [];
if(errors && errors.length) {
for(var i = 0; i < errors.length; i++)
html.push('<strong>' + errors[i].title + '</strong>: ' + errors[i].detail);
}
return html.join('<br>');
}
String.prototype.ellipsize = function(max) {
max = max || 20;
if(this.length > max) {
......@@ -44,8 +57,4 @@
return this.lastIndexOf(start, 0) === 0;
};
window.console = window.console || {
log: function () {}
};
})();
\ No newline at end of file
......@@ -185,7 +185,13 @@ ul.dashed {
}
.item-actions {
padding: 5px 0 0 10px;
td {
vertical-align: middle;
}
td + td {
padding-left: 10px;
}
}
.row {
......
......@@ -30,7 +30,7 @@ public class WordMap {
this.newWords = new HashSet<>();
List<Word> words = dbWords.getAll();
for (Word word : words)
wordMap.put(word.getWord().toLowerCase(), word);
wordMap.put(word.getId().toLowerCase(), word);
}
public Word get(Object w) {
......
......@@ -5,6 +5,11 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* The ConfigKey field annotates configuration values. The key itself is the
* properties file key under which the value will be stored in the configuration
* file.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ConfigKey {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment