From b4654c0eafc72925ee9f390a397946116d6d02e2 Mon Sep 17 00:00:00 2001 From: Eike Cochu <eike@cochu.com> Date: Thu, 4 Feb 2016 00:03:49 +0100 Subject: [PATCH] updated indexing indexing now includes topics, had to move indexing to after topic modeling and store processed text in db added pagination directive to ui --- .../main/java/de/vipra/cmd/file/Filebase.java | 6 +- .../java/de/vipra/cmd/file/JGibbFilebase.java | 12 ++-- .../de/vipra/cmd/model/ProcessedArticle.java | 53 ----------------- .../de/vipra/cmd/option/ClearCommand.java | 6 +- .../de/vipra/cmd/option/ImportCommand.java | 58 +++++++++++-------- .../vipra/rest/resource/SearchResource.java | 2 +- vipra-ui/css/app.css | 4 +- vipra-ui/css/app.less | 17 ++++-- vipra-ui/html/articles/index.html | 4 +- vipra-ui/html/articles/show.html | 4 ++ vipra-ui/html/directives/pagination.html | 15 +++++ vipra-ui/html/index.html | 4 +- vipra-ui/html/topics/show.html | 39 +++++++++++++ vipra-ui/js/app.js | 6 +- vipra-ui/js/controllers.js | 45 ++++++++++++-- vipra-ui/js/directives.js | 18 +++++- .../java/de/vipra/util/an/QueryIgnore.java | 2 + .../java/de/vipra/util/model/ArticleFull.java | 28 +++++++++ .../main/java/de/vipra/util/model/Topic.java | 5 ++ .../java/de/vipra/util/model/TopicRef.java | 6 -- .../vipra/util/service/DatabaseService.java | 4 +- 21 files changed, 222 insertions(+), 116 deletions(-) delete mode 100644 vipra-cmd/src/main/java/de/vipra/cmd/model/ProcessedArticle.java create mode 100644 vipra-ui/html/directives/pagination.html diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/file/Filebase.java b/vipra-cmd/src/main/java/de/vipra/cmd/file/Filebase.java index 0bc63735..9730d405 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/file/Filebase.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/file/Filebase.java @@ -5,10 +5,10 @@ import java.io.File; import java.io.IOException; import de.vipra.cmd.ex.FilebaseException; -import de.vipra.cmd.model.ProcessedArticle; import de.vipra.util.Config; import de.vipra.util.Constants; import de.vipra.util.ex.ConfigException; +import de.vipra.util.model.ArticleFull; public abstract class Filebase implements Closeable { @@ -45,7 +45,7 @@ public abstract class Filebase implements Closeable { return vocab; } - public void remove(ProcessedArticle article) throws FilebaseException { + public void remove(ArticleFull article) throws FilebaseException { remove(article.getId().toString()); } @@ -56,7 +56,7 @@ public abstract class Filebase implements Closeable { vocab.close(); } - public abstract void add(ProcessedArticle article) throws FilebaseException; + public abstract void add(ArticleFull article) throws FilebaseException; public abstract void remove(String id) throws FilebaseException; diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/file/JGibbFilebase.java b/vipra-cmd/src/main/java/de/vipra/cmd/file/JGibbFilebase.java index d5ee6273..1fb52be4 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/file/JGibbFilebase.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/file/JGibbFilebase.java @@ -7,15 +7,15 @@ import java.util.ArrayList; import java.util.List; import de.vipra.cmd.ex.FilebaseException; -import de.vipra.cmd.model.ProcessedArticle; import de.vipra.util.ex.NotImplementedException; +import de.vipra.util.model.ArticleFull; public class JGibbFilebase extends Filebase { private final File modelFile; private final FilebaseIndex index; private final FilebaseVocabulary vocab; - private final List<ProcessedArticle> articles; + private final List<ArticleFull> articles; private final int bufferMaxSize = 100; @@ -28,8 +28,8 @@ public class JGibbFilebase extends Filebase { } @Override - public void add(ProcessedArticle article) throws FilebaseException { - String[] words = article.getProcessedText().getText().split("\\s+"); + public void add(ArticleFull article) throws FilebaseException { + String[] words = article.getProcessedText().split("\\s+"); vocab.addVocabulary(words); index.add(article.getId().toString()); articles.add(article); @@ -56,12 +56,12 @@ public class JGibbFilebase extends Filebase { // write articles raf.seek(raf.length()); - for (ProcessedArticle a : articles) { + for (ArticleFull a : articles) { if (linesep) raf.writeBytes(System.lineSeparator()); else linesep = true; - raf.writeBytes(a.getProcessedText().getText()); + raf.writeBytes(a.getProcessedText()); } raf.close(); diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/model/ProcessedArticle.java b/vipra-cmd/src/main/java/de/vipra/cmd/model/ProcessedArticle.java deleted file mode 100644 index e11dd916..00000000 --- a/vipra-cmd/src/main/java/de/vipra/cmd/model/ProcessedArticle.java +++ /dev/null @@ -1,53 +0,0 @@ -package de.vipra.cmd.model; - -import java.util.Date; - -import org.json.simple.JSONObject; -import org.mongodb.morphia.annotations.Entity; -import org.mongodb.morphia.annotations.Transient; - -import de.vipra.cmd.text.ProcessedText; -import de.vipra.util.an.ElasticIndex; - -@SuppressWarnings("serial") -@Entity(value = "articles", noClassnameStored = true) -public class ProcessedArticle extends de.vipra.util.model.ArticleFull { - - @Transient - private ProcessedText processedText; - - public ProcessedText getProcessedText() { - return processedText; - } - - public void setProcessedText(ProcessedText processedText) { - this.processedText = processedText; - } - - @ElasticIndex("title") - public String serializeTitle() { - return super.getTitle(); - } - - @ElasticIndex("date") - public Date serializeDate() { - return super.getDate(); - } - - @ElasticIndex("text") - public String serializeText() { - return processedText.getText(); - } - - public void fromJSON(JSONObject obj) { - if (obj.containsKey("title")) - setTitle(obj.get("title").toString()); - if (obj.containsKey("text")) - setText(obj.get("text").toString()); - if (obj.containsKey("url")) - setUrl(obj.get("url").toString()); - if (obj.containsKey("date")) - setDate(obj.get("date").toString()); - } - -} diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/ClearCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/ClearCommand.java index 1ee15d55..0c03aa63 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/option/ClearCommand.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/ClearCommand.java @@ -9,10 +9,10 @@ import org.apache.logging.log4j.Logger; import org.bson.types.ObjectId; import org.elasticsearch.client.Client; -import de.vipra.cmd.model.ProcessedArticle; import de.vipra.util.Config; import de.vipra.util.ConsoleUtils; import de.vipra.util.ESClient; +import de.vipra.util.model.Article; import de.vipra.util.model.Import; import de.vipra.util.model.TopicFull; import de.vipra.util.model.Word; @@ -25,7 +25,7 @@ public class ClearCommand implements Command { private boolean defaults; private Config config; - private DatabaseService<ProcessedArticle, ObjectId> dbArticles; + private DatabaseService<Article, ObjectId> dbArticles; private DatabaseService<TopicFull, ObjectId> dbTopics; private DatabaseService<Word, String> dbWords; private DatabaseService<Import, ObjectId> dbImports; @@ -37,7 +37,7 @@ public class ClearCommand implements Command { private void clear() throws Exception { config = Config.getConfig(); - dbArticles = DatabaseService.getDatabaseService(config, ProcessedArticle.class); + dbArticles = DatabaseService.getDatabaseService(config, Article.class); dbTopics = DatabaseService.getDatabaseService(config, TopicFull.class); dbWords = DatabaseService.getDatabaseService(config, Word.class); dbImports = DatabaseService.getDatabaseService(config, Import.class); diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java index f6e960bf..4d879875 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java @@ -21,7 +21,6 @@ import org.json.simple.parser.JSONParser; import de.vipra.cmd.file.Filebase; import de.vipra.cmd.file.FilebaseIndex; import de.vipra.cmd.lda.LDAAnalyzer; -import de.vipra.cmd.model.ProcessedArticle; import de.vipra.cmd.text.ProcessedText; import de.vipra.cmd.text.Processor; import de.vipra.util.Config; @@ -35,6 +34,7 @@ import de.vipra.util.Timer; import de.vipra.util.WordMap; import de.vipra.util.ex.DatabaseException; import de.vipra.util.model.Article; +import de.vipra.util.model.ArticleFull; import de.vipra.util.model.ArticleStats; import de.vipra.util.model.Import; import de.vipra.util.model.Topic; @@ -51,7 +51,7 @@ public class ImportCommand implements Command { private ArrayList<File> files = new ArrayList<>(); private JSONParser parser = new JSONParser(); private Config config; - private DatabaseService<ProcessedArticle, ObjectId> dbArticles; + private DatabaseService<ArticleFull, ObjectId> dbArticles; private DatabaseService<TopicFull, ObjectId> dbTopics; private DatabaseService<Word, String> dbWords; private DatabaseService<Import, ObjectId> dbImports; @@ -60,7 +60,7 @@ public class ImportCommand implements Command { private WordMap wordMap; private LDAAnalyzer analyzer; private Client elasticClient; - private ElasticSerializer<ProcessedArticle> elasticSerializer; + private ElasticSerializer<ArticleFull> elasticSerializer; /** * Import command to import articles into the database, do topic modeling @@ -111,15 +111,14 @@ public class ImportCommand implements Command { */ private Article importArticle(JSONObject obj) throws Exception { out.info("importing \"" + obj.get("title") + "\""); - ProcessedArticle article = new ProcessedArticle(); - article.fromJSON(obj); + ArticleFull article = articleFromJSON(obj); // preprocess text and generate text statistics ProcessedText processedText = preprocessor.process(article.getText()); ArticleStats articleStats = ArticleStats.generateFromText(processedText.getText(), wordMap); // add article to mongodb - article.setProcessedText(processedText); + article.setProcessedText(processedText.getText()); article.setStats(articleStats); article = dbArticles.createSingle(article); @@ -132,10 +131,6 @@ public class ImportCommand implements Command { // add article to filebase filebase.add(article); - // index article - Map<String, Object> source = elasticSerializer.serialize(article); - elasticClient.prepareIndex("articles", "article", article.getId().toString()).setSource(source).get(); - // return article reference return new Article(article.getId()); } @@ -169,12 +164,23 @@ public class ImportCommand implements Command { return articles; } + private ArticleFull articleFromJSON(JSONObject obj) { + ArticleFull article = new ArticleFull(); + if (obj.containsKey("title")) + article.setTitle(obj.get("title").toString()); + if (obj.containsKey("text")) + article.setText(obj.get("text").toString()); + if (obj.containsKey("url")) + article.setUrl(obj.get("url").toString()); + if (obj.containsKey("date")) + article.setDate(obj.get("date").toString()); + return article; + } + @Override public void run() throws Exception { config = Config.getConfig(); - @SuppressWarnings("unused") - Config asd = Config.getConfig(); - dbArticles = DatabaseService.getDatabaseService(config, ProcessedArticle.class); + dbArticles = DatabaseService.getDatabaseService(config, ArticleFull.class); dbTopics = DatabaseService.getDatabaseService(config, TopicFull.class); dbWords = DatabaseService.getDatabaseService(config, Word.class); dbImports = DatabaseService.getDatabaseService(config, Import.class); @@ -183,7 +189,7 @@ public class ImportCommand implements Command { wordMap = new WordMap(dbWords); analyzer = LDAAnalyzer.getAnalyzer(config, wordMap); elasticClient = ESClient.getClient(config); - elasticSerializer = new ElasticSerializer<>(ProcessedArticle.class); + elasticSerializer = new ElasticSerializer<>(ArticleFull.class); out.info("using data directory: " + config.getDataDirectory().getAbsolutePath()); out.info("using preprocessor: " + preprocessor.getName()); @@ -222,7 +228,7 @@ public class ImportCommand implements Command { out.info("saving topic definitions"); int batchSize = 100; ConvertStream<TopicFull> topicDefs = analyzer.getTopicDefinitions(); - Map<String, String> topicIndexMap = new HashMap<>(); + Map<String, TopicFull> topicIndexMap = new HashMap<>(); dbTopics.drop(); List<TopicFull> newTopicDefs = new ArrayList<>(batchSize); List<Topic> newTopicRefs = new ArrayList<>(); @@ -232,26 +238,28 @@ public class ImportCommand implements Command { if (newTopicDefs.size() == batchSize || !it.hasNext()) { dbTopics.createMultiple(newTopicDefs); for (TopicFull newTopicDef : newTopicDefs) { - topicIndexMap.put(Integer.toString(newTopicDef.getIndex()), newTopicDef.getId().toString()); + topicIndexMap.put(Integer.toString(newTopicDef.getIndex()), newTopicDef); newTopicRefs.add(new Topic(newTopicDef.getId())); } + newTopicDefs.clear(); } } importOp.setTopics(newTopicRefs); timer.lap("saving topics"); /* - * save topic refs + * save topic refs and index article */ out.info("saving document topics"); ConvertStream<List<TopicRef>> topicStream = analyzer.getTopics(); FilebaseIndex index = filebase.getIndex(); Iterator<String> indexIter = index.iterator(); Iterator<List<TopicRef>> topicRefsListIter = topicStream.iterator(); + elasticClient.admin().indices().prepareDelete("_all").get(); while (indexIter.hasNext() && topicRefsListIter.hasNext()) { // get article from database String id = indexIter.next(); - ProcessedArticle article = dbArticles.getSingle(MongoUtils.objectId(id)); + ArticleFull article = dbArticles.getSingle(MongoUtils.objectId(id)); if (article == null) { log.error("no article found in db for id " + id); continue; @@ -267,10 +275,10 @@ public class ImportCommand implements Command { topicRefsIter.remove(); continue; } - String topicObjectId = topicIndexMap.get(topicRef.getTopicIndex()); - if (topicObjectId != null) - topicRef.setTopicId(topicObjectId); - else + TopicFull topicFull = topicIndexMap.get(topicRef.getTopicIndex()); + if (topicFull != null) { + topicRef.setTopic(new Topic(topicFull.getId(), topicFull.getName())); + } else log.error("no object id for topic index " + topicRef.getTopicIndex()); } @@ -281,10 +289,14 @@ public class ImportCommand implements Command { } catch (DatabaseException e) { log.error("could not update article: " + article.getTitle() + " (" + article.getId() + ")"); } + + // index article + Map<String, Object> source = elasticSerializer.serialize(article); + elasticClient.prepareIndex("articles", "article", article.getId().toString()).setSource(source).get(); } List<Word> importedWords = wordMap.getNewWords(); importOp.setWords(importedWords); - timer.lap("saving topic refs"); + timer.lap("saving topic refs and indexing"); /* * save words diff --git a/vipra-rest/src/main/java/de/vipra/rest/resource/SearchResource.java b/vipra-rest/src/main/java/de/vipra/rest/resource/SearchResource.java index 5df5c56b..faf5f0e7 100644 --- a/vipra-rest/src/main/java/de/vipra/rest/resource/SearchResource.java +++ b/vipra-rest/src/main/java/de/vipra/rest/resource/SearchResource.java @@ -33,7 +33,7 @@ import de.vipra.util.StringUtils; import de.vipra.util.ex.ConfigException; import de.vipra.util.model.ArticleFull; -@Path("/search") +@Path("search") public class SearchResource { @Context diff --git a/vipra-ui/css/app.css b/vipra-ui/css/app.css index c138c2d1..26bd0d5b 100644 --- a/vipra-ui/css/app.css +++ b/vipra-ui/css/app.css @@ -1,2 +1,2 @@ -html{position:relative;min-height:100%}body{padding-bottom:20px;margin-bottom:60px}.heading{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default;font-size:120px;text-align:center;background:transparent url(/img/logo.svg) no-repeat 50% 50%;background-size:contain;height:200px;line-height:200px;margin:35px 0}.search-results{padding:15px}.search-results .search-result{margin-bottom:20px}.search-results .search-result a{font-size:1.5rem}.ellipsize{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.navbar-default .collapse:not(.in) .navbar-nav>.active>a,.navbar-default .collapse:not(.in) .navbar-nav>.active>a:focus,.navbar-default .collapse:not(.in) .navbar-nav>.active>a:hover{border-bottom:3px solid;padding-bottom:12px}.navbar-default .navbar-header{padding:0 10px}.navbar-default .navbar-brand{background:transparent url(/img/logo.svg) no-repeat 50% 50%;background-size:contain}.navbar-default .navbar-brand.spin,.navbar-default .navbar-brand:hover:not(.spin){-webkit-animation:a 4s linear infinite;animation:a 4s linear infinite}.row-spaced{margin-top:15px;margin-bottom:15px}.footer{position:absolute;bottom:0;width:100%;height:50px;border-top-width:1px;border-top-style:solid}.noselect{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}@-webkit-keyframes a{to{-webkit-transform:rotateY(1turn)}}@keyframes a{to{-webkit-transform:rotateY(1turn);transform:rotateY(1turn)}} -/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFwcC5sZXNzIiwiYXBwLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxLQUNFLGtCQUFBLEFBQ0EsZUFBQSxDQ0NELEFERUQsS0FDRSxvQkFBQSxBQUVBLGtCQUFBLENDQUQsQURHRCxTQXlFRSwyQkFBQSxBQUNBLHlCQUFBLEFBQ0Esc0JBQUEsQUFDQSxxQkFBQSxBQUNBLGlCQUFBLEFBQ0EsZUFBQSxBQTVFQSxnQkFBQSxBQUNBLGtCQUFBLEFBQ0EsNERBQUEsQUFDQSx3QkFBQSxBQUNBLGFBQUEsQUFDQSxrQkFBQSxBQUNBLGFBQUEsQ0NJRCxBRERELGdCQUNFLFlBQUEsQ0NHRCxBREpELCtCQUlJLGtCQUFBLENDR0gsQURQRCxpQ0FPTSxnQkFBQSxDQ0dMLEFERUQsV0FDRSxtQkFBQSxBQUNBLGdCQUFBLEFBQ0Esc0JBQUEsQ0NBRCxBRE1LLHVMQUdFLHdCQUFBLEFBQ0EsbUJBQUEsQ0NKUCxBREhELCtCQWFJLGNBQUEsQ0NQSCxBRE5ELDhCQWlCSSw0REFBQSxBQUNBLHVCQUFBLENDUkgsQURTRyxrRkFFRSx1Q0FBQSxBQUVBLDhCQUFBLENDUEwsQURZRCxZQUNFLGdCQUFBLEFBQ0Esa0JBQUEsQ0NWRCxBRGFELFFBQ0Usa0JBQUEsQUFDQSxTQUFBLEFBQ0EsV0FBQSxBQUVBLFlBQUEsQUFDQSxxQkFBQSxBQUNBLHNCQUFBLENDWEQsQURjRCxVQUNFLDJCQUFBLEFBQ0EseUJBQUEsQUFDQSxzQkFBQSxBQUNBLHFCQUFBLEFBQ0EsaUJBQUEsQUFDQSxjQUFBLENDWkQsQURnQkQscUJBQTBCLEdBQU8sZ0NBQUEsQ0NQOUIsQ0FDRixBRE9ELGFBQWtCLEdBQU8saUNBQUEsQUFBb0Msd0JBQUEsQ0NGMUQsQ0FDRiIsImZpbGUiOiJhcHAuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiaHRtbCB7XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgbWluLWhlaWdodDogMTAwJTtcbn1cblxuYm9keSB7XG4gIHBhZGRpbmctYm90dG9tOiAyMHB4O1xuICAvKiBNYXJnaW4gYm90dG9tIGJ5IGZvb3RlciBoZWlnaHQgKi9cbiAgbWFyZ2luLWJvdHRvbTogNjBweDtcbn1cblxuLmhlYWRpbmcge1xuICAubm9zZWxlY3Q7XG4gIGZvbnQtc2l6ZTogMTIwcHg7XG4gIHRleHQtYWxpZ246IGNlbnRlcjtcbiAgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQgdXJsKC9pbWcvbG9nby5zdmcpIG5vLXJlcGVhdCA1MCUgNTAlO1xuICBiYWNrZ3JvdW5kLXNpemU6IGNvbnRhaW47XG4gIGhlaWdodDogMjAwcHg7XG4gIGxpbmUtaGVpZ2h0OiAyMDBweDtcbiAgbWFyZ2luOiAzNXB4IDA7XG59XG5cbi5zZWFyY2gtcmVzdWx0cyB7XG4gIHBhZGRpbmc6IDE1cHg7XG5cbiAgLnNlYXJjaC1yZXN1bHQge1xuICAgIG1hcmdpbi1ib3R0b206IDIwcHg7XG5cbiAgICBhIHtcbiAgICAgIGZvbnQtc2l6ZTogMS41cmVtO1xuICAgIH1cbiAgfVxufVxuXG4uZWxsaXBzaXplIHtcbiAgd2hpdGUtc3BhY2U6IG5vd3JhcDtcbiAgb3ZlcmZsb3c6IGhpZGRlbjtcbiAgdGV4dC1vdmVyZmxvdzogZWxsaXBzaXM7XG59XG5cbi5uYXZiYXItZGVmYXVsdCB7XG4gIC5jb2xsYXBzZTpub3QoLmluKSB7XG4gICAgLm5hdmJhci1uYXYgPiAuYWN0aXZlIHtcbiAgICAgICY+IGEsXG4gICAgICAmPiBhOmhvdmVyLFxuICAgICAgJj4gYTpmb2N1cyB7XG4gICAgICAgIGJvcmRlci1ib3R0b206IDNweCBzb2xpZDtcbiAgICAgICAgcGFkZGluZy1ib3R0b206IDEycHg7XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgLm5hdmJhci1oZWFkZXIge1xuICAgIHBhZGRpbmc6IDAgMTBweDtcbiAgfVxuXG4gIC5uYXZiYXItYnJhbmQge1xuICAgIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50IHVybCgvaW1nL2xvZ28uc3ZnKSBuby1yZXBlYXQgNTAlIDUwJTtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IGNvbnRhaW47XG4gICAgJi5zcGluLFxuICAgICY6aG92ZXI6bm90KC5zcGluKSB7XG4gICAgICAtd2Via2l0LWFuaW1hdGlvbjpzcGluIDRzIGxpbmVhciBpbmZpbml0ZTtcbiAgICAgIC1tb3otYW5pbWF0aW9uOnNwaW4gNHMgbGluZWFyIGluZmluaXRlO1xuICAgICAgYW5pbWF0aW9uOnNwaW4gNHMgbGluZWFyIGluZmluaXRlO1xuICAgIH1cbiAgfVxufVxuXG4ucm93LXNwYWNlZCB7XG4gIG1hcmdpbi10b3A6IDE1cHg7XG4gIG1hcmdpbi1ib3R0b206IDE1cHg7XG59XG5cbi5mb290ZXIge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIGJvdHRvbTogMDtcbiAgd2lkdGg6IDEwMCU7XG4gIC8qIFNldCB0aGUgZml4ZWQgaGVpZ2h0IG9mIHRoZSBmb290ZXIgaGVyZSAqL1xuICBoZWlnaHQ6IDUwcHg7XG4gIGJvcmRlci10b3Atd2lkdGg6IDFweDtcbiAgYm9yZGVyLXRvcC1zdHlsZTogc29saWQ7XG59XG5cbi5ub3NlbGVjdCB7XG4gIC13ZWJraXQtdG91Y2gtY2FsbG91dDogbm9uZTtcbiAgLXdlYmtpdC11c2VyLXNlbGVjdDogbm9uZTtcbiAgLW1vei11c2VyLXNlbGVjdDogbm9uZTtcbiAgLW1zLXVzZXItc2VsZWN0OiBub25lO1xuICB1c2VyLXNlbGVjdDogbm9uZTtcbiAgY3Vyc29yOiBkZWZhdWx0O1xufVxuXG5ALW1vei1rZXlmcmFtZXMgc3BpbiB7IDEwMCUgeyAtbW96LXRyYW5zZm9ybTogcm90YXRlWSgzNjBkZWcpOyB9IH1cbkAtd2Via2l0LWtleWZyYW1lcyBzcGluIHsgMTAwJSB7IC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGVZKDM2MGRlZyk7IH0gfVxuQGtleWZyYW1lcyBzcGluIHsgMTAwJSB7IC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGVZKDM2MGRlZyk7IHRyYW5zZm9ybTpyb3RhdGVZKDM2MGRlZyk7IH0gfSIsImh0bWwge1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gIG1pbi1oZWlnaHQ6IDEwMCU7XG59XG5ib2R5IHtcbiAgcGFkZGluZy1ib3R0b206IDIwcHg7XG4gIC8qIE1hcmdpbiBib3R0b20gYnkgZm9vdGVyIGhlaWdodCAqL1xuICBtYXJnaW4tYm90dG9tOiA2MHB4O1xufVxuLmhlYWRpbmcge1xuICAtd2Via2l0LXRvdWNoLWNhbGxvdXQ6IG5vbmU7XG4gIC13ZWJraXQtdXNlci1zZWxlY3Q6IG5vbmU7XG4gIC1tb3otdXNlci1zZWxlY3Q6IG5vbmU7XG4gIC1tcy11c2VyLXNlbGVjdDogbm9uZTtcbiAgdXNlci1zZWxlY3Q6IG5vbmU7XG4gIGN1cnNvcjogZGVmYXVsdDtcbiAgZm9udC1zaXplOiAxMjBweDtcbiAgdGV4dC1hbGlnbjogY2VudGVyO1xuICBiYWNrZ3JvdW5kOiB0cmFuc3BhcmVudCB1cmwoL2ltZy9sb2dvLnN2Zykgbm8tcmVwZWF0IDUwJSA1MCU7XG4gIGJhY2tncm91bmQtc2l6ZTogY29udGFpbjtcbiAgaGVpZ2h0OiAyMDBweDtcbiAgbGluZS1oZWlnaHQ6IDIwMHB4O1xuICBtYXJnaW46IDM1cHggMDtcbn1cbi5zZWFyY2gtcmVzdWx0cyB7XG4gIHBhZGRpbmc6IDE1cHg7XG59XG4uc2VhcmNoLXJlc3VsdHMgLnNlYXJjaC1yZXN1bHQge1xuICBtYXJnaW4tYm90dG9tOiAyMHB4O1xufVxuLnNlYXJjaC1yZXN1bHRzIC5zZWFyY2gtcmVzdWx0IGEge1xuICBmb250LXNpemU6IDEuNXJlbTtcbn1cbi5lbGxpcHNpemUge1xuICB3aGl0ZS1zcGFjZTogbm93cmFwO1xuICBvdmVyZmxvdzogaGlkZGVuO1xuICB0ZXh0LW92ZXJmbG93OiBlbGxpcHNpcztcbn1cbi5uYXZiYXItZGVmYXVsdCAuY29sbGFwc2U6bm90KC5pbikgLm5hdmJhci1uYXYgPiAuYWN0aXZlID4gYSxcbi5uYXZiYXItZGVmYXVsdCAuY29sbGFwc2U6bm90KC5pbikgLm5hdmJhci1uYXYgPiAuYWN0aXZlID4gYTpob3Zlcixcbi5uYXZiYXItZGVmYXVsdCAuY29sbGFwc2U6bm90KC5pbikgLm5hdmJhci1uYXYgPiAuYWN0aXZlID4gYTpmb2N1cyB7XG4gIGJvcmRlci1ib3R0b206IDNweCBzb2xpZDtcbiAgcGFkZGluZy1ib3R0b206IDEycHg7XG59XG4ubmF2YmFyLWRlZmF1bHQgLm5hdmJhci1oZWFkZXIge1xuICBwYWRkaW5nOiAwIDEwcHg7XG59XG4ubmF2YmFyLWRlZmF1bHQgLm5hdmJhci1icmFuZCB7XG4gIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50IHVybCgvaW1nL2xvZ28uc3ZnKSBuby1yZXBlYXQgNTAlIDUwJTtcbiAgYmFja2dyb3VuZC1zaXplOiBjb250YWluO1xufVxuLm5hdmJhci1kZWZhdWx0IC5uYXZiYXItYnJhbmQuc3Bpbixcbi5uYXZiYXItZGVmYXVsdCAubmF2YmFyLWJyYW5kOmhvdmVyOm5vdCguc3Bpbikge1xuICAtd2Via2l0LWFuaW1hdGlvbjogc3BpbiA0cyBsaW5lYXIgaW5maW5pdGU7XG4gIC1tb3otYW5pbWF0aW9uOiBzcGluIDRzIGxpbmVhciBpbmZpbml0ZTtcbiAgYW5pbWF0aW9uOiBzcGluIDRzIGxpbmVhciBpbmZpbml0ZTtcbn1cbi5yb3ctc3BhY2VkIHtcbiAgbWFyZ2luLXRvcDogMTVweDtcbiAgbWFyZ2luLWJvdHRvbTogMTVweDtcbn1cbi5mb290ZXIge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIGJvdHRvbTogMDtcbiAgd2lkdGg6IDEwMCU7XG4gIC8qIFNldCB0aGUgZml4ZWQgaGVpZ2h0IG9mIHRoZSBmb290ZXIgaGVyZSAqL1xuICBoZWlnaHQ6IDUwcHg7XG4gIGJvcmRlci10b3Atd2lkdGg6IDFweDtcbiAgYm9yZGVyLXRvcC1zdHlsZTogc29saWQ7XG59XG4ubm9zZWxlY3Qge1xuICAtd2Via2l0LXRvdWNoLWNhbGxvdXQ6IG5vbmU7XG4gIC13ZWJraXQtdXNlci1zZWxlY3Q6IG5vbmU7XG4gIC1tb3otdXNlci1zZWxlY3Q6IG5vbmU7XG4gIC1tcy11c2VyLXNlbGVjdDogbm9uZTtcbiAgdXNlci1zZWxlY3Q6IG5vbmU7XG4gIGN1cnNvcjogZGVmYXVsdDtcbn1cbkAtbW96LWtleWZyYW1lcyBzcGluIHtcbiAgMTAwJSB7XG4gICAgLW1vei10cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTtcbiAgfVxufVxuQC13ZWJraXQta2V5ZnJhbWVzIHNwaW4ge1xuICAxMDAlIHtcbiAgICAtd2Via2l0LXRyYW5zZm9ybTogcm90YXRlWSgzNjBkZWcpO1xuICB9XG59XG5Aa2V5ZnJhbWVzIHNwaW4ge1xuICAxMDAlIHtcbiAgICAtd2Via2l0LXRyYW5zZm9ybTogcm90YXRlWSgzNjBkZWcpO1xuICAgIHRyYW5zZm9ybTogcm90YXRlWSgzNjBkZWcpO1xuICB9XG59XG4iXSwic291cmNlUm9vdCI6Ii9zb3VyY2UvIn0= */ +html{position:relative;min-height:100%}body{padding-bottom:20px;margin-bottom:60px}.heading{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default;background:transparent url(/img/logo.svg) no-repeat 50% 50%;background-size:contain;height:125px;margin:25px 0}.search-results{padding:15px}.search-results .search-result{margin-bottom:20px}.search-results .search-result a{font-size:1.5rem}.ellipsize{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.navbar-default .collapse:not(.in) .navbar-nav>.active>a,.navbar-default .collapse:not(.in) .navbar-nav>.active>a:focus,.navbar-default .collapse:not(.in) .navbar-nav>.active>a:hover{border-bottom:3px solid;padding-bottom:12px}.navbar-default .navbar-header{padding:0 10px}.navbar-default .navbar-brand{background:transparent url(/img/logo.svg) no-repeat 50% 50%;background-size:contain}.navbar-default .navbar-brand.spin,.navbar-default .navbar-brand:hover:not(.spin){-webkit-animation:a 4s linear infinite;animation:a 4s linear infinite}.row-spaced{margin-top:15px;margin-bottom:15px}.footer{position:absolute;bottom:0;width:100%;height:50px;border-top-width:1px;border-top-style:solid}.noselect{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}@-webkit-keyframes a{to{-webkit-transform:rotateY(1turn)}}@keyframes a{to{-webkit-transform:rotateY(1turn);transform:rotateY(1turn)}} +/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFwcC5sZXNzIiwiYXBwLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxLQUNFLGtCQUFBLEFBQ0EsZUFBQSxDQ0NELEFERUQsS0FDRSxvQkFBQSxBQUVBLGtCQUFBLENDQUQsQURHRCxTQXNFRSwyQkFBQSxBQUNBLHlCQUFBLEFBQ0Esc0JBQUEsQUFDQSxxQkFBQSxBQUNBLGlCQUFBLEFBQ0EsZUFBQSxBQXpFQSw0REFBQSxBQUNBLHdCQUFBLEFBQ0EsYUFBQSxBQUNBLGFBQUEsQ0NJRCxBRERELGdCQUNFLFlBQUEsQ0NHRCxBREpELCtCQUlJLGtCQUFBLENDR0gsQURQRCxpQ0FPTSxnQkFBQSxDQ0dMLEFERUQsV0FDRSxtQkFBQSxBQUNBLGdCQUFBLEFBQ0Esc0JBQUEsQ0NBRCxBRE1LLHVMQUdFLHdCQUFBLEFBQ0EsbUJBQUEsQ0NKUCxBREhELCtCQWFJLGNBQUEsQ0NQSCxBRE5ELDhCQWlCSSw0REFBQSxBQUNBLHVCQUFBLENDUkgsQURTRyxrRkFFRSx1Q0FBQSxBQUVBLDhCQUFBLENDUEwsQURZRCxZQUNFLGdCQUFBLEFBQ0Esa0JBQUEsQ0NWRCxBRGFELFFBQ0Usa0JBQUEsQUFDQSxTQUFBLEFBQ0EsV0FBQSxBQUVBLFlBQUEsQUFDQSxxQkFBQSxBQUNBLHNCQUFBLENDWEQsQURjRCxVQUNFLDJCQUFBLEFBQ0EseUJBQUEsQUFDQSxzQkFBQSxBQUNBLHFCQUFBLEFBQ0EsaUJBQUEsQUFDQSxjQUFBLENDWkQsQURnQkQscUJBQTBCLEdBQU8sZ0NBQUEsQ0NQOUIsQ0FDRixBRE9ELGFBQWtCLEdBQU8saUNBQUEsQUFBb0Msd0JBQUEsQ0NGMUQsQ0FDRiIsImZpbGUiOiJhcHAuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiaHRtbCB7XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgbWluLWhlaWdodDogMTAwJTtcbn1cblxuYm9keSB7XG4gIHBhZGRpbmctYm90dG9tOiAyMHB4O1xuICAvKiBNYXJnaW4gYm90dG9tIGJ5IGZvb3RlciBoZWlnaHQgKi9cbiAgbWFyZ2luLWJvdHRvbTogNjBweDtcbn1cblxuLmhlYWRpbmcge1xuICAubm9zZWxlY3Q7XG4gIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50IHVybCgvaW1nL2xvZ28uc3ZnKSBuby1yZXBlYXQgNTAlIDUwJTtcbiAgYmFja2dyb3VuZC1zaXplOiBjb250YWluO1xuICBoZWlnaHQ6IDEyNXB4O1xuICBtYXJnaW46IDI1cHggMDtcbn1cblxuLnNlYXJjaC1yZXN1bHRzIHtcbiAgcGFkZGluZzogMTVweDtcblxuICAuc2VhcmNoLXJlc3VsdCB7XG4gICAgbWFyZ2luLWJvdHRvbTogMjBweDtcblxuICAgIGEge1xuICAgICAgZm9udC1zaXplOiAxLjVyZW07XG4gICAgfVxuICB9XG59XG5cbi5lbGxpcHNpemUge1xuICB3aGl0ZS1zcGFjZTogbm93cmFwO1xuICBvdmVyZmxvdzogaGlkZGVuO1xuICB0ZXh0LW92ZXJmbG93OiBlbGxpcHNpcztcbn1cblxuLm5hdmJhci1kZWZhdWx0IHtcbiAgLmNvbGxhcHNlOm5vdCguaW4pIHtcbiAgICAubmF2YmFyLW5hdiA+IC5hY3RpdmUge1xuICAgICAgJj4gYSxcbiAgICAgICY+IGE6aG92ZXIsXG4gICAgICAmPiBhOmZvY3VzIHtcbiAgICAgICAgYm9yZGVyLWJvdHRvbTogM3B4IHNvbGlkO1xuICAgICAgICBwYWRkaW5nLWJvdHRvbTogMTJweDtcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICAubmF2YmFyLWhlYWRlciB7XG4gICAgcGFkZGluZzogMCAxMHB4O1xuICB9XG5cbiAgLm5hdmJhci1icmFuZCB7XG4gICAgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQgdXJsKC9pbWcvbG9nby5zdmcpIG5vLXJlcGVhdCA1MCUgNTAlO1xuICAgIGJhY2tncm91bmQtc2l6ZTogY29udGFpbjtcbiAgICAmLnNwaW4sXG4gICAgJjpob3Zlcjpub3QoLnNwaW4pIHtcbiAgICAgIC13ZWJraXQtYW5pbWF0aW9uOnNwaW4gNHMgbGluZWFyIGluZmluaXRlO1xuICAgICAgLW1vei1hbmltYXRpb246c3BpbiA0cyBsaW5lYXIgaW5maW5pdGU7XG4gICAgICBhbmltYXRpb246c3BpbiA0cyBsaW5lYXIgaW5maW5pdGU7XG4gICAgfVxuICB9XG59XG5cbi5yb3ctc3BhY2VkIHtcbiAgbWFyZ2luLXRvcDogMTVweDtcbiAgbWFyZ2luLWJvdHRvbTogMTVweDtcbn1cblxuLmZvb3RlciB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgYm90dG9tOiAwO1xuICB3aWR0aDogMTAwJTtcbiAgLyogU2V0IHRoZSBmaXhlZCBoZWlnaHQgb2YgdGhlIGZvb3RlciBoZXJlICovXG4gIGhlaWdodDogNTBweDtcbiAgYm9yZGVyLXRvcC13aWR0aDogMXB4O1xuICBib3JkZXItdG9wLXN0eWxlOiBzb2xpZDtcbn1cblxuLm5vc2VsZWN0IHtcbiAgLXdlYmtpdC10b3VjaC1jYWxsb3V0OiBub25lO1xuICAtd2Via2l0LXVzZXItc2VsZWN0OiBub25lO1xuICAtbW96LXVzZXItc2VsZWN0OiBub25lO1xuICAtbXMtdXNlci1zZWxlY3Q6IG5vbmU7XG4gIHVzZXItc2VsZWN0OiBub25lO1xuICBjdXJzb3I6IGRlZmF1bHQ7XG59XG5cbkAtbW96LWtleWZyYW1lcyBzcGluIHsgMTAwJSB7IC1tb3otdHJhbnNmb3JtOiByb3RhdGVZKDM2MGRlZyk7IH0gfVxuQC13ZWJraXQta2V5ZnJhbWVzIHNwaW4geyAxMDAlIHsgLXdlYmtpdC10cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTsgfSB9XG5Aa2V5ZnJhbWVzIHNwaW4geyAxMDAlIHsgLXdlYmtpdC10cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTsgdHJhbnNmb3JtOnJvdGF0ZVkoMzYwZGVnKTsgfSB9IiwiaHRtbCB7XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgbWluLWhlaWdodDogMTAwJTtcbn1cbmJvZHkge1xuICBwYWRkaW5nLWJvdHRvbTogMjBweDtcbiAgLyogTWFyZ2luIGJvdHRvbSBieSBmb290ZXIgaGVpZ2h0ICovXG4gIG1hcmdpbi1ib3R0b206IDYwcHg7XG59XG4uaGVhZGluZyB7XG4gIC13ZWJraXQtdG91Y2gtY2FsbG91dDogbm9uZTtcbiAgLXdlYmtpdC11c2VyLXNlbGVjdDogbm9uZTtcbiAgLW1vei11c2VyLXNlbGVjdDogbm9uZTtcbiAgLW1zLXVzZXItc2VsZWN0OiBub25lO1xuICB1c2VyLXNlbGVjdDogbm9uZTtcbiAgY3Vyc29yOiBkZWZhdWx0O1xuICBiYWNrZ3JvdW5kOiB0cmFuc3BhcmVudCB1cmwoL2ltZy9sb2dvLnN2Zykgbm8tcmVwZWF0IDUwJSA1MCU7XG4gIGJhY2tncm91bmQtc2l6ZTogY29udGFpbjtcbiAgaGVpZ2h0OiAxMjVweDtcbiAgbWFyZ2luOiAyNXB4IDA7XG59XG4uc2VhcmNoLXJlc3VsdHMge1xuICBwYWRkaW5nOiAxNXB4O1xufVxuLnNlYXJjaC1yZXN1bHRzIC5zZWFyY2gtcmVzdWx0IHtcbiAgbWFyZ2luLWJvdHRvbTogMjBweDtcbn1cbi5zZWFyY2gtcmVzdWx0cyAuc2VhcmNoLXJlc3VsdCBhIHtcbiAgZm9udC1zaXplOiAxLjVyZW07XG59XG4uZWxsaXBzaXplIHtcbiAgd2hpdGUtc3BhY2U6IG5vd3JhcDtcbiAgb3ZlcmZsb3c6IGhpZGRlbjtcbiAgdGV4dC1vdmVyZmxvdzogZWxsaXBzaXM7XG59XG4ubmF2YmFyLWRlZmF1bHQgLmNvbGxhcHNlOm5vdCguaW4pIC5uYXZiYXItbmF2ID4gLmFjdGl2ZSA+IGEsXG4ubmF2YmFyLWRlZmF1bHQgLmNvbGxhcHNlOm5vdCguaW4pIC5uYXZiYXItbmF2ID4gLmFjdGl2ZSA+IGE6aG92ZXIsXG4ubmF2YmFyLWRlZmF1bHQgLmNvbGxhcHNlOm5vdCguaW4pIC5uYXZiYXItbmF2ID4gLmFjdGl2ZSA+IGE6Zm9jdXMge1xuICBib3JkZXItYm90dG9tOiAzcHggc29saWQ7XG4gIHBhZGRpbmctYm90dG9tOiAxMnB4O1xufVxuLm5hdmJhci1kZWZhdWx0IC5uYXZiYXItaGVhZGVyIHtcbiAgcGFkZGluZzogMCAxMHB4O1xufVxuLm5hdmJhci1kZWZhdWx0IC5uYXZiYXItYnJhbmQge1xuICBiYWNrZ3JvdW5kOiB0cmFuc3BhcmVudCB1cmwoL2ltZy9sb2dvLnN2Zykgbm8tcmVwZWF0IDUwJSA1MCU7XG4gIGJhY2tncm91bmQtc2l6ZTogY29udGFpbjtcbn1cbi5uYXZiYXItZGVmYXVsdCAubmF2YmFyLWJyYW5kLnNwaW4sXG4ubmF2YmFyLWRlZmF1bHQgLm5hdmJhci1icmFuZDpob3Zlcjpub3QoLnNwaW4pIHtcbiAgLXdlYmtpdC1hbmltYXRpb246IHNwaW4gNHMgbGluZWFyIGluZmluaXRlO1xuICAtbW96LWFuaW1hdGlvbjogc3BpbiA0cyBsaW5lYXIgaW5maW5pdGU7XG4gIGFuaW1hdGlvbjogc3BpbiA0cyBsaW5lYXIgaW5maW5pdGU7XG59XG4ucm93LXNwYWNlZCB7XG4gIG1hcmdpbi10b3A6IDE1cHg7XG4gIG1hcmdpbi1ib3R0b206IDE1cHg7XG59XG4uZm9vdGVyIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBib3R0b206IDA7XG4gIHdpZHRoOiAxMDAlO1xuICAvKiBTZXQgdGhlIGZpeGVkIGhlaWdodCBvZiB0aGUgZm9vdGVyIGhlcmUgKi9cbiAgaGVpZ2h0OiA1MHB4O1xuICBib3JkZXItdG9wLXdpZHRoOiAxcHg7XG4gIGJvcmRlci10b3Atc3R5bGU6IHNvbGlkO1xufVxuLm5vc2VsZWN0IHtcbiAgLXdlYmtpdC10b3VjaC1jYWxsb3V0OiBub25lO1xuICAtd2Via2l0LXVzZXItc2VsZWN0OiBub25lO1xuICAtbW96LXVzZXItc2VsZWN0OiBub25lO1xuICAtbXMtdXNlci1zZWxlY3Q6IG5vbmU7XG4gIHVzZXItc2VsZWN0OiBub25lO1xuICBjdXJzb3I6IGRlZmF1bHQ7XG59XG5ALW1vei1rZXlmcmFtZXMgc3BpbiB7XG4gIDEwMCUge1xuICAgIC1tb3otdHJhbnNmb3JtOiByb3RhdGVZKDM2MGRlZyk7XG4gIH1cbn1cbkAtd2Via2l0LWtleWZyYW1lcyBzcGluIHtcbiAgMTAwJSB7XG4gICAgLXdlYmtpdC10cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTtcbiAgfVxufVxuQGtleWZyYW1lcyBzcGluIHtcbiAgMTAwJSB7XG4gICAgLXdlYmtpdC10cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTtcbiAgICB0cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTtcbiAgfVxufVxuIl0sInNvdXJjZVJvb3QiOiIvc291cmNlLyJ9 */ diff --git a/vipra-ui/css/app.less b/vipra-ui/css/app.less index df90b128..e6440674 100644 --- a/vipra-ui/css/app.less +++ b/vipra-ui/css/app.less @@ -11,13 +11,10 @@ body { .heading { .noselect; - font-size: 120px; - text-align: center; background: transparent url(/img/logo.svg) no-repeat 50% 50%; background-size: contain; - height: 200px; - line-height: 200px; - margin: 35px 0; + height: 125px; + margin: 25px 0; } .search-results { @@ -81,6 +78,16 @@ body { border-top-style: solid; } +.loading { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background rgba(0,0,0,0.2); + +} + .noselect { -webkit-touch-callout: none; -webkit-user-select: none; diff --git a/vipra-ui/html/articles/index.html b/vipra-ui/html/articles/index.html index 1ac91c5c..9ce005c3 100644 --- a/vipra-ui/html/articles/index.html +++ b/vipra-ui/html/articles/index.html @@ -6,4 +6,6 @@ <li ng-repeat="article in articles"> <a ui-sref="articles.show({id: article.id})">{{article.title}}</a> </li> -</ul> \ No newline at end of file +</ul> + +<pagination total="articlesMeta.total" page="page" limit="limit"/> \ No newline at end of file diff --git a/vipra-ui/html/articles/show.html b/vipra-ui/html/articles/show.html index 3800387d..e8ed5e34 100644 --- a/vipra-ui/html/articles/show.html +++ b/vipra-ui/html/articles/show.html @@ -4,6 +4,10 @@ <table class="table table-bordered table-condensed"> <tbody> + <tr> + <th>ID</th> + <td ng-bind="article.id"></td> + </tr> <tr> <th>URL</th> <td><a ng-href="{{article.url}}" ng-bind="article.url"></a></td> diff --git a/vipra-ui/html/directives/pagination.html b/vipra-ui/html/directives/pagination.html new file mode 100644 index 00000000..3947b26b --- /dev/null +++ b/vipra-ui/html/directives/pagination.html @@ -0,0 +1,15 @@ +<nav ng-show="total > limit"> + <ul class="pagination"> + <li ng-class="{disabled:page==1}"> + <a ui-sref="{page:page==2?null:page-1}" ng-show="page>1">«</a> + <span ng-hide="page>1">«</span> + </li> + <li ng-class="{active:p==page}" ng-repeat="p in pages"> + <a ui-sref="{page:p===1?null:p}" ng-bind="p"></a> + </li> + <li ng-class="{disabled:page>=maxPage}"> + <a ui-sref="{page:page+1}" ng-show="page<maxPage">»</a> + <span ng-hide="page<maxPage">»</span> + </li> + </ul> +</nav> \ No newline at end of file diff --git a/vipra-ui/html/index.html b/vipra-ui/html/index.html index b8d0f892..bb5516f3 100644 --- a/vipra-ui/html/index.html +++ b/vipra-ui/html/index.html @@ -2,9 +2,7 @@ <div class="row"> <div class="col-md-12"> - <div class="heading"> - 'vɪprə - </div> + <div class="heading"></div> </div> </div> diff --git a/vipra-ui/html/topics/show.html b/vipra-ui/html/topics/show.html index e69de29b..08b26339 100644 --- a/vipra-ui/html/topics/show.html +++ b/vipra-ui/html/topics/show.html @@ -0,0 +1,39 @@ +<h1 ng-bind="topic.name"></h1> + +<table class="table table-bordered table-condensed"> + <tbody> + <tr> + <th>ID</th> + <td ng-bind="topic.id"></td> + </tr> + <tr> + <th>Index</th> + <td ng-bind="topic.index"></td> + </tr> + <tr> + <th>Created</th> + <td ng-bind="(topic.created | formatDateTime)"></td> + </tr> + <tr> + <th>Last modified</th> + <td ng-bind="(topic.modified | formatDateTime)"></td> + </tr> + </tbody> +</table> + +<h4>Words</h4> + +<table class="table table-bordered table-condensed"> + <thead> + <tr> + <th>Word</th> + <th>Likeliness</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="word in topic.words"> + <td><a ui-sref="words.show({id:word.word})" ng-bind="word.word"></a></td> + <td ng-bind="word.likeliness"></td> + </tr> + </tbody> +</table> \ No newline at end of file diff --git a/vipra-ui/js/app.js b/vipra-ui/js/app.js index a48c4a7e..fcb3999b 100644 --- a/vipra-ui/js/app.js +++ b/vipra-ui/js/app.js @@ -34,7 +34,7 @@ }); $stateProvider.state('articles.index', { - url: '', + url: '?page', templateUrl: tplBase + '/articles/index.html', controller: 'ArticlesIndexController' }); @@ -54,7 +54,7 @@ }); $stateProvider.state('topics.index', { - url: '', + url: '?page', templateUrl: tplBase + '/topics/index.html', controller: 'TopicsIndexController' }); @@ -74,7 +74,7 @@ }); $stateProvider.state('words.index', { - url: '', + url: '?page', templateUrl: tplBase + '/words/index.html', controller: 'WordsIndexController' }); diff --git a/vipra-ui/js/controllers.js b/vipra-ui/js/controllers.js index 14b33503..2572c6ae 100644 --- a/vipra-ui/js/controllers.js +++ b/vipra-ui/js/controllers.js @@ -6,7 +6,9 @@ ]); var latestItemsCount = 3, - searchItemsCount = 10; + searchItemsCount = 10, + pageSize = 100, + paginationPadding = 4; app.controller('IndexController', ['$scope', '$location', 'ArticleFactory', 'TopicFactory', 'WordFactory', 'SearchFactory', function($scope, $location, ArticleFactory, TopicFactory, WordFactory, SearchFactory) { @@ -40,10 +42,16 @@ * ARTICLES */ - app.controller('ArticlesIndexController', ['$scope', 'ArticleFactory', - function($scope, ArticleFactory) { + app.controller('ArticlesIndexController', ['$scope', '$stateParams', 'ArticleFactory', + function($scope, $stateParams, ArticleFactory) { + + $scope.page = Math.max($stateParams.page || 1, 1); + $scope.limit = pageSize; - ArticleFactory.query(function(response) { + ArticleFactory.query({ + skip: ($scope.page-1)*pageSize, + limit: pageSize + }, function(response) { $scope.articles = response.data; $scope.articlesMeta = response.meta; $scope.queryTime = response.$queryTime; @@ -114,4 +122,33 @@ }]); + /* + * DIRECTIVES + */ + + app.controller('PaginationController', ['$scope', + function($scope) { + + $scope.calculatePages = function() { + var pages = [], + max = Math.ceil($scope.total/$scope.limit*1.0), + start = Math.max($scope.page - paginationPadding, 1), + end = Math.min(Math.max($scope.page + paginationPadding, start + paginationPadding * 2), max); + for(var i = start; i <= end; i++) { + pages.push(i); + } + $scope.pages = pages; + $scope.maxPage = max; + }; + + $scope.$watchGroup(['total', 'page', 'limit'], function(newVal, oldVal) { + if(!angular.equals(newVal, oldVal)) { + $scope.calculatePages(); + } + }); + + $scope.calculatePages(); + + }]); + })(); \ No newline at end of file diff --git a/vipra-ui/js/directives.js b/vipra-ui/js/directives.js index e421c434..b0493828 100644 --- a/vipra-ui/js/directives.js +++ b/vipra-ui/js/directives.js @@ -1,6 +1,8 @@ (function() { - var app = angular.module('vipra.directives', []); + var app = angular.module('vipra.directives', [ + 'ui.router' + ]); app.directive('topicLink', function() { return { @@ -22,4 +24,18 @@ }; }); + app.directive('pagination', function() { + return { + restrict: 'E', + replace: true, + scope: { + total: '=', + page: '=', + limit: '=' + }, + controller: 'PaginationController', + templateUrl: 'html/directives/pagination.html' + }; + }); + })(); \ No newline at end of file diff --git a/vipra-util/src/main/java/de/vipra/util/an/QueryIgnore.java b/vipra-util/src/main/java/de/vipra/util/an/QueryIgnore.java index 422439e7..8e2c7df1 100644 --- a/vipra-util/src/main/java/de/vipra/util/an/QueryIgnore.java +++ b/vipra-util/src/main/java/de/vipra/util/an/QueryIgnore.java @@ -13,4 +13,6 @@ public @interface QueryIgnore { public boolean multi() default false; + public boolean all() default false; + } diff --git a/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java b/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java index a55d0f8b..d5dd234f 100644 --- a/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java +++ b/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.io.Serializable; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -26,6 +27,7 @@ import de.vipra.util.FileUtils; import de.vipra.util.MongoUtils; import de.vipra.util.NestedMap; import de.vipra.util.StringUtils; +import de.vipra.util.an.ElasticIndex; import de.vipra.util.an.QueryIgnore; @SuppressWarnings("serial") @@ -38,13 +40,19 @@ public class ArticleFull extends FileModel<ObjectId> implements Serializable { @Id private ObjectId id; + @ElasticIndex("title") private String title; @QueryIgnore(multi = true) private String text; + @ElasticIndex("text") + @QueryIgnore(multi = true) + private String processedText; + private String url; + @ElasticIndex("date") private Date date; @Embedded @@ -95,6 +103,14 @@ public class ArticleFull extends FileModel<ObjectId> implements Serializable { this.text = text; } + public String getProcessedText() { + return processedText; + } + + public void setProcessedText(String processedText) { + this.processedText = processedText; + } + public String getUrl() { return url; } @@ -128,6 +144,18 @@ public class ArticleFull extends FileModel<ObjectId> implements Serializable { this.topics = topics; } + @ElasticIndex("topics") + public String[] serializeTopics() { + List<TopicRef> refs = getTopics(); + if (refs == null) + return new String[0]; + List<String> topics = new ArrayList<>(refs.size()); + for (TopicRef ref : refs) { + topics.add(ref.getTopic().getName()); + } + return topics.toArray(new String[topics.size()]); + } + public ArticleStats getStats() { return stats; } diff --git a/vipra-util/src/main/java/de/vipra/util/model/Topic.java b/vipra-util/src/main/java/de/vipra/util/model/Topic.java index 5f097b90..760510f2 100644 --- a/vipra-util/src/main/java/de/vipra/util/model/Topic.java +++ b/vipra-util/src/main/java/de/vipra/util/model/Topic.java @@ -25,6 +25,11 @@ public class Topic implements Model<ObjectId>, Serializable { this.id = id; } + public Topic(ObjectId id, String name) { + this.id = id; + this.name = name; + } + @Override public ObjectId getId() { return id; diff --git a/vipra-util/src/main/java/de/vipra/util/model/TopicRef.java b/vipra-util/src/main/java/de/vipra/util/model/TopicRef.java index 35d8755e..920abdce 100644 --- a/vipra-util/src/main/java/de/vipra/util/model/TopicRef.java +++ b/vipra-util/src/main/java/de/vipra/util/model/TopicRef.java @@ -6,8 +6,6 @@ import org.mongodb.morphia.annotations.Embedded; import org.mongodb.morphia.annotations.Reference; import org.mongodb.morphia.annotations.Transient; -import de.vipra.util.MongoUtils; - @SuppressWarnings("serial") @Embedded public class TopicRef implements Comparable<TopicRef>, Serializable { @@ -26,10 +24,6 @@ public class TopicRef implements Comparable<TopicRef>, Serializable { this.topicIndex = index; } - public void setTopicId(String id) { - this.topic = new Topic(MongoUtils.objectId(id)); - } - public int getCount() { return count; } diff --git a/vipra-util/src/main/java/de/vipra/util/service/DatabaseService.java b/vipra-util/src/main/java/de/vipra/util/service/DatabaseService.java index d68e1bcc..da1e780a 100644 --- a/vipra-util/src/main/java/de/vipra/util/service/DatabaseService.java +++ b/vipra-util/src/main/java/de/vipra/util/service/DatabaseService.java @@ -35,9 +35,9 @@ public class DatabaseService<Type extends Model<IdType>, IdType> implements Serv for (Field field : fields) { QueryIgnore qi = field.getDeclaredAnnotation(QueryIgnore.class); if (qi != null) { - if (qi.single()) + if (qi.single() || qi.all()) ignoreSingle.add(field.getName()); - if (qi.multi()) + if (qi.multi() || qi.all()) ignoreMulti.add(field.getName()); } } -- GitLab