Commit cd21d78a authored by Romain Loth's avatar Romain Loth

initial add of annotation app

parent 818c54d5
{
"directory": "static/bower_components"
}
static/bower_components/
{
"globalstrict": true,
"globals": {
"angular": false,
"describe": false,
"it": false,
"expect": false,
"beforeEach": false,
"afterEach": false,
"module": false,
"inject": false
}
}
\ No newline at end of file
set tabstop=4
set shiftwidth=4
set expandtab
set softtabstop=4
# Gargantext Annotations web application
We also use a number of node.js tools to initialize and test. You must have node.js and
its package manager (npm) installed. You can get them from [http://nodejs.org/](http://nodejs.org/).
## Preview only
Activate your virtualenv and run a simple http server
```
workon gargantext
python3 -m http.server
```
or :
```
npm start
```
Now browse to the app at `http://localhost:8000/app/index.html`.
## Install development tools and dependencies
We have two kinds of dependencies in this project: tools and angular framework code. The tools help
us manage and test the application.
* We get the tools we depend upon via `npm`, the [node package manager][npm].
* We get the angular code via `bower`, a [client-side code package manager][bower].
We have preconfigured `npm` to automatically run `bower` so we can simply do:
```
npm install
```
Behind the scenes this will also call `bower install`. You should find that you have two new
folders in your project.
* `node_modules` - contains the npm packages for the tools we need
* `app/bower_components` - contains the angular framework files
*Note that the `bower_components` folder would normally be installed in the root folder but
angular-seed changes this location through the `.bowerrc` file. Putting it in the app folder makes
it easier to serve the files by a webserver.*
## Directory Layout
This will be adapted to fit the django API code as well. For now, the generic layout is :
```
app/ --> all of the source files for the application
app.css --> default stylesheet
components/ --> all app specific modules
view1/ --> the view1 view template and logic
view1.html --> the partial template
view1.js --> the controller logic
view1_test.js --> tests of the controller
app.js --> main application module
index.html --> app layout file (the main html template file of the app)
```
# Conception and workflow documentation
## TODO : à traduire en anglais
Cette API permet d'éditer les mots-clés miamlistés ou stoplistés associé à un document affiché dans un cadre d'une page web permettant de naviguer à travers un ensemble de document d'un corpus.
### Architecture
- Templates : Django et Angular.js ?
- Communication entre les modules : évènements Angular ($emit et $broadcast)
- Pas de routage entre différentes URL, car ici une seule vue principale basée sur le template django corpus.html
- Modèle d'abstraction de données : côté client (Angular Scopes) et côté serveur (Django Model et SQLAlchemy)
- Composants : TODO lister et décrire les composants client et serveur
- Structure de l'application : organisation du client et du serveur
- Style : Bootstrap et un thème spécifique choisi pour Gargantext
- Gestion des dépendances :
- bower, npm pour le développement web et les tests côté client
- pip requirements pour le côté serveur
## Quelles actions execute l'API ?
- afficher le titre, les auteurs, le résumé, la date de publication et le corps d'un document.
- lecture des mots-clés miamlistés associés à un document (dans le texte et hors du texte).
- lecture des mots-clés stoplistés associés à un document (dans le texte et hors du texte).
- lecture des documents ayant le plus de mots-clés miamlistés associés identiques pour afficher une liste de liens vers de nouveaux documents
- lecture du groupe de mots-clés auquel appartient un mot-clé (synonymes, différentes formes)
- modification du groupe de mots-clés auquel appartient un mot-clé donné
On désigne par mot-clé un NGram.
## Schéma de l'API
Liste des endpoints
### Lecture des données
- POST '^api/nodes/(\d+)/children/queries$' : liste des NGrams d'un document avec la possibilité de filtrer par NGrams
- GET '^api/nodes$' : liste des identifiants de mots-clés filtrés par type (NGram ou autre) pour un identifiant de parent (Document ou autre)
- GET '^api/nodes/(\d+)/ngrams$': liste des termes des mots-clés associés à un Document parent, filtrés par termes
- GET ^api/nodes/(\d+)/children/metadata$ : liste des metadata d'un Node, c'est-à-dire :
- pour un document : titre, auteur, etc
- pour un NGram : stoplisté ou miamlisté ?
### Écriture des données
TODO
## Workflow
Nous nous fixons sur cette documentation et spécification de l'API
- en parallèle : développement de l'API et prototypage de l'interface
- le prototypage de l'interface peut modifier l'API si besoin
### Spécifications des fondations de l'interface
- résolutions d'écran
- browsers
- langue: english only
- SEO: aucun ?
- collaboratif : oui, les modifications d'un autre utilisateurs seront notifiées à tous les utilisateurs présent sur le même corpus de documents
- fonctionne offline ?
### Working process
- follow board is updated regularly (https://trello.com/b/96ItkDBS/gargantext-miamlists-and-stoplists)[on Trello]
- calendrier prévisionnel: TODO
- interactions entre les acteurs: emails
- git, branches : branche "elias", `git pull --rebase origin master` réguliers
- prévision des revues de code et de l'interface : TODO
### Plateforme
- Python 3.4
- Django 1.6
- Postgresql 9.3 + HSTORE
- SQLAlchemy
- Bootstrap CSS
- Angular.js
### Outils de qualité de code
- pylint
- jshint (voir .jshintrc)
- indentations : 4 espaces (voir .lvimrc)
- nettoyage automatique des espaces en fin de ligne
## Tests
There are two kinds of tests possible : Unit tests and End to End tests.
- côté client : étudier karma.js et protractor
- définir la stratégie de tests : TODO
## Déploiement
- définir le processus de déploiement
- prévoir un système de monitoring des erreurs du serveur une fois en ligne
- Sentry ?
## Updating the web application
Previously we recommended that you merge in changes to angular-seed into your own fork of the project.
Now that the angular framework library code and tools are acquired through package managers (npm and
bower) you can use these tools instead to update the dependencies.
You can update the tool dependencies by running:
```
npm update
```
This will find the latest versions that match the version ranges specified in the `package.json` file.
You can update the Angular dependencies by running:
```
bower update
```
This will find the latest versions that match the version ranges specified in the `bower.json` file.
### Running the App in Production
This really depends on how complex your app is and the overall infrastructure of your system, but
the general rule is that all you need in production are all the files under the `app/` directory.
Everything else should be omitted.
Angular apps are really just a bunch of static html, css and js files that just need to be hosted
somewhere they can be accessed by browsers.
If your Angular app is talking to the backend server via xhr or other means, you need to figure
out what is the best way to host the static files to comply with the same origin policy if
applicable. Usually this is done by hosting the files by the backend server or through
reverse-proxying the backend server(s) and webserver(s).
##
[AngularJS]: http://angularjs.org/
[git]: http://git-scm.com/
[bower]: http://bower.io
[npm]: https://www.npmjs.org/
[node]: http://nodejs.org
[protractor]: https://github.com/angular/protractor
[jasmine]: http://jasmine.github.io
[karma]: http://karma-runner.github.io
[http-server]: https://github.com/nodeapps/http-server
{
"name": "annotations",
"description": "Annotations for Gargantext",
"version": "0.0.1",
"license": "GPLv3",
"private": true,
"dependencies": {
"angular": "~1.2.x",
"angular-loader": "~1.2.x",
"angular-resource": "~1.2.x",
"bootstrap": "~3.x",
"angular-cookies": "1.2",
"bootstrap-select": "silviomoreto/bootstrap-select#~1.7.3"
},
"resolutions": {
"angular": "~1.2.x"
}
}
{
"name": "gargantext-annotations",
"private": true,
"version": "0.0.1",
"description": "Annotations for gargantext",
"license": "GPLv3",
"devDependencies": {
"karma": "~0.10",
"protractor": "^1.1.1",
"http-server": "^0.6.1",
"bower": "^1.3.1",
"shelljs": "^0.2.6",
"karma-junit-reporter": "^0.2.2"
},
"scripts": {
"postinstall": "bower install",
"prestart": "npm install",
"start": "http-server -a localhost -p 8000 -c-1",
"pretest": "npm install",
"test": "karma start karma.conf.js",
"test-single-run": "karma start karma.conf.js --single-run",
"preupdate-webdriver": "npm install",
"update-webdriver": "webdriver-manager update",
"preprotractor": "npm run update-webdriver",
"protractor": "protractor e2e-tests/protractor.conf.js",
"update-index-async": "node -e \"require('shelljs/global'); sed('-i', /\\/\\/@@NG_LOADER_START@@[\\s\\S]*\\/\\/@@NG_LOADER_END@@/, '//@@NG_LOADER_START@@\\n' + sed(/sourceMappingURL=angular-loader.min.js.map/,'sourceMappingURL=bower_components/angular-loader/angular-loader.min.js.map','static/bower_components/angular-loader/angular-loader.min.js') + '\\n//@@NG_LOADER_END@@', 'templates/annotations/demo.html');\""
}
}
(function () {
'use strict';
var annotationsAppActiveLists = angular.module('annotationsAppActiveLists', []);
annotationsAppActiveLists.controller('ActiveListsController',
['$scope', '$rootScope', '$timeout',
function ($scope, $rootScope, $timeout) {
$scope.activeListsChange = function() {
var selected = $('.selectpicker option:selected').val();
var newActive = {};
$('.selectpicker option:selected').each(function(item, value) {
var id = value.id.split("---", 2)[1];
newActive[id] = value.value;
});
$rootScope.activeLists = newActive;
};
$rootScope.$watchCollection('activeLists', function (newValue, oldValue) {
if (newValue === undefined) return;
$timeout(function() {
$('.selectpicker').selectpicker('refresh');
});
});
$rootScope.$watchCollection('lists', function (newValue, oldValue) {
if (newValue === undefined) return;
// reformat lists to allListsSelect
var allListsSelect = [];
angular.forEach($rootScope.lists, function(value, key) {
this.push({
'id': key,
'label': value
});
// initialize activeLists with the MiamList by default
if (value == 'MiamList') {
$rootScope.activeLists = {};
$rootScope.activeLists[key] = value;
}
}, allListsSelect);
$rootScope.allListsSelect = allListsSelect;
$timeout(function() {
$('.selectpicker').selectpicker();
$('.selectpicker').selectpicker('val', ['MiamList']);
});
});
}]);
})(window);
/* app css stylesheet */
/*
* Class names corresponding to server-side list names
* To display another list name, add a new class under this
*/
.MiamList {
color: black;
background-color: rgba(60, 118, 61, 0.5);
cursor: pointer;
}
.StopList {
color: black;
background-color: rgba(169, 68, 66, 0.2);
cursor: pointer;
}
.delete-keyword, .occurrences {
vertical-align: super;
font-size: 70%;
}
.delete-keyword {
cursor: pointer;
}
.center-block {
display: block;
margin-left: auto;
margin-right: auto;
}
.keyword-inline {
display: inline;
}
.keyword-inline:hover {
text-decoration: none;
}
.nav-tabs {
border-bottom: none;
}
.main-panel, .text-panel, .words-panel {
margin: 10px 0;
}
#annotationsApp {
min-width: 780px;
}
.words-panel {
min-width: 220px;
}
.text-panel {
overflow-y: auto;
min-width: 400px;
}
.words-list {
margin-bottom: 5px;
}
.keyword-text {
word-break: break-all;
}
.keyword-group-item {
display: inline-block;
float: left;
padding: 5px;
margin: .25em;
box-shadow: .2em .2em .1em rgba(0, 0, 0, .125);
}
.words-pagination {
margin: 5px 0;
}
.text-panel p, .text-panel h3 {
-webkit-transition: all 0.25s linear;
-moz-transition: all 0.25s linear;
-ms-transition: all 0.25s linear;
-o-transition: all 0.25s linear;
transition: all 0.25s linear;
}
.selection {
color: #aaa;
}
::selection {
color: black;
background-color: rgba(0, 0, 0, 0.4);
}
.noselection {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.selection-menu {
display: none;
position: absolute;
color: #394141;
background: white;
font-size: 0.8em;
font-weight: 600;
-webkit-box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
-moz-box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
/*.selection-menu:before {
content: '';
position: absolute;
left: -10px;
top: 0px;
border-right: solid white 10px;
border-top: solid transparent 8px;
border-bottom: solid transparent 8px;
}*/
.selection-menu ul {
list-style: none;
margin: 0;
padding: 0;
}
.selection-menu li {
border-bottom: solid thin #CCC;
padding: 10px;
white-space: nowrap;
}
.selection-menu [class*="glyphicon"] {
min-width: 25px;
display: inline-block;
text-align: center;
border-right: solid thin #CCC;
margin-right: 5px;
}
.float-right {
float: right;
}
(function () {
'use strict';
/*
* Django STATIC_URL given to angular to load async resources
*/
var S = window.STATIC_URL;
window.annotationsApp = angular.module('annotationsApp', ['annotationsAppHttp',
'annotationsAppNgramList', 'annotationsAppHighlight', 'annotationsAppDocument',
'annotationsAppActiveLists', 'annotationsAppUtils']);
/*
* Angular Templates must not conflict with Django's
*/
window.annotationsApp.config(function($interpolateProvider) {
$interpolateProvider.startSymbol('{[{');
$interpolateProvider.endSymbol('}]}');
});
/*
* Main function
* GET the document node and all its ngrams
*/
window.annotationsApp.run(function ($rootScope) {
var path = window.location.pathname.match(/\/projects\/(.*)\/corpora\/(.*)\/documents\/(.*)\//);
$rootScope.projectId = path[1];
$rootScope.corpusId = path[2];
$rootScope.docId = path[3];
});
})(window);
(function () {
'use strict';
var annotationsAppDocument = angular.module('annotationsAppDocument', ['annotationsAppHttp']);
annotationsAppDocument.controller('DocController',
['$scope', '$rootScope', '$timeout', 'NgramListHttpService', 'DocumentHttpService',
function ($scope, $rootScope, $timeout, NgramListHttpService, DocumentHttpService) {
// dataLoading = signal pour afficher wait
$scope.dataLoading = true ;
$rootScope.documentResource = DocumentHttpService.get(
{'docId': $rootScope.docId},
function(data, responseHeaders) {
$scope.authors = data.authors;
$scope.journal = data.journal;
$scope.publication_date = data.publication_date;
//$scope.current_page_number = data.current_page_number;
//$scope.last_page_number = data.last_page_number;
$rootScope.title = data.title;
$rootScope.docId = data.id;
$rootScope.full_text = data.full_text;
$rootScope.abstract_text = data.abstract_text;
// GET the annotationss
NgramListHttpService.get(
{
'corpusId': $rootScope.corpusId,
'docId': $rootScope.docId
},
function(data) {
$rootScope.annotations = data[$rootScope.corpusId.toString()][$rootScope.docId.toString()];
$rootScope.lists = data[$rootScope.corpusId.toString()].lists;
$scope.dataLoading = false ;
},
function(data) {
console.error("unable to get the list of ngrams");
}
);
});
// TODO setup article pagination
$scope.onPreviousClick = function () {
DocumentHttpService.get($scope.docId - 1);
};
$scope.onNextClick = function () {
DocumentHttpService.get($scope.docId + 1);
};
}]);
annotationsAppDocument.controller('DocFavoriteController',
['$scope', '$rootScope', 'DocumentHttpService',
function ($scope, $rootScope, DocumentHttpService) {
$scope.onStarClick = function($event) {
console.log("TODO");
};
$scope.isFavorite = false;
}]);
})(window);
This diff is collapsed.
(function () {
'use strict';
var http = angular.module('annotationsAppHttp', ['ngResource', 'ngCookies']);
http.config(['$httpProvider', function($httpProvider){
$httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
$httpProvider.defaults.xsrfCookieName = 'csrftoken';
}]);
/*
* Read Document
*/
http.factory('DocumentHttpService', function($resource) {
return $resource(
window.ANNOTATION_API_URL + "documents/:docId/",
{
docId: '@docId'
},
{
get: {
method: 'GET',
params: {docId: '@docId'}
}
}
);
});
/*
* Read all Ngrams
*/
http.factory('NgramListHttpService', function ($resource) {
return $resource(
window.ANNOTATION_API_URL + 'corpora/:corpusId/documents/:docId',
{
corpusId: '@corpusId',
docId: '@docId'
},
{
get: {
method: 'GET',
params: {}
}
}
);
});
/*
* Create, modify or delete 1 Ngram
*/
// http.factory('NgramHttpService', function ($resource) {
// return $resource(
// window.ANNOTATION_API_URL + 'lists/:listId/ngrams/:ngramId',
// {
// listId: '@listId',
// ngramId: '@id'
// },
// {
// post: {
// method: 'POST',
// params: {'listId': '@listId', 'ngramId': '@ngramId'}
// },
// delete: {
// method: 'DELETE',
// params: {'listId': '@listId', 'ngramId': '@ngramId'}
// }
// }
// );
// });
})(window);
<span ng-click='onDeleteClick()' class="delete-keyword">×</span>
<span data-toggle="tooltip" class="keyword-text {[{keyword.listName}]}">{[{keyword.text}]}</span>
<span class="occurrences" data-keyword-id="{[{keyword.uuid}]}">{[{keyword.occurrences}]}</span>
// include angular loader, which allows the files to load in any order
//@@NG_LOADER_START@@
// You need to run `npm run update-index-async` to inject the angular async code here
//@@NG_LOADER_END@@
// include a third-party async loader library
/*!
* $script.js v1.3
* https://github.com/ded/script.js
* Copyright: @ded & @fat - Dustin Diaz, Jacob Thornton 2011
* Follow our software http://twitter.com/dedfat
* License: MIT
*/
!function(a,b,c){function t(a,c){var e=b.createElement("script"),f=j;e.onload=e.onerror=e[o]=function(){e[m]&&!/^c|loade/.test(e[m])||f||(e.onload=e[o]=null,f=1,c())},e.async=1,e.src=a,d.insertBefore(e,d.firstChild)}function q(a,b){p(a,function(a){return!b(a)})}var d=b.getElementsByTagName("head")[0],e={},f={},g={},h={},i="string",j=!1,k="push",l="DOMContentLoaded",m="readyState",n="addEventListener",o="onreadystatechange",p=function(a,b){for(var c=0,d=a.length;c<d;++c)if(!b(a[c]))return j;return 1};!b[m]&&b[n]&&(b[n](l,function r(){b.removeEventListener(l,r,j),b[m]="complete"},j),b[m]="loading");var s=function(a,b,d){function o(){if(!--m){e[l]=1,j&&j();for(var a in g)p(a.split("|"),n)&&!q(g[a],n)&&(g[a]=[])}}function n(a){return a.call?a():e[a]}a=a[k]?a:[a];var i=b&&b.call,j=i?b:d,l=i?a.join(""):b,m=a.length;c(function(){q(a,function(a){h[a]?(l&&(f[l]=1),o()):(h[a]=1,l&&(f[l]=1),t(s.path?s.path+a+".js":a,o))})},0);return s};s.get=t,s.ready=function(a,b,c){a=a[k]?a:[a];var d=[];!q(a,function(a){e[a]||d[k](a)})&&p(a,function(a){return e[a]})?b():!function(a){g[a]=g[a]||[],g[a][k](b),c&&c(d)}(a.join("|"));return s};var u=a.$script;s.noConflict=function(){a.$script=u;return this},typeof module!="undefined"&&module.exports?module.exports=s:a.$script=s}(this,document,setTimeout)
// load all of the dependencies asynchronously.
var S = window.STATIC_URL;
$script([
S + 'bower_components/angular/angular.min.js',
S + 'bower_components/bootstrap/dist/js/bootstrap.min.js',
S + 'bower_components/bootstrap-select/dist/js/bootstrap-select.min.js',
S + 'bower_components/angular-loader/angular-loader.min.js',
S + 'bower_components/underscore/underscore-1.5.2.js',
//'bower_components/angular-route/angular-route.js',
], function() {
$script([
S + 'bower_components/angular-cookies/angular-cookies.min.js',
S + 'bower_components/angular-resource/angular-resource.min.js'], function() {
$script([S + 'annotations/http.js', S + 'annotations/highlight.js',
S + 'annotations/document.js', S + 'annotations/ngramlist.js',
S + 'annotations/activelists.js', S + 'annotations/ngramlist.js',
S + 'annotations/utils.js', S + 'annotations/app.js'], function() {
// when all is done, execute bootstrap angular application (replace ng-app directive)
angular.bootstrap(document.getElementById("annotationsApp"), ['annotationsApp']);
});
});
});
(function () {
'use strict';
var annotationsAppNgramList = angular.module('annotationsAppNgramList', ['annotationsAppHttp']);
/*
* Controls one Ngram displayed in the flat lists (called "extra-text")
*/
annotationsAppNgramList.controller('NgramController',
['$scope', '$rootScope', 'NgramHttpService', 'NgramListHttpService',
function ($scope, $rootScope, NgramHttpService, NgramListHttpService) {
/*
* Click on the 'delete' cross button
*/
$scope.onDeleteClick = function () {
NgramHttpService.delete({
'listId': $scope.keyword.list_id,
'ngramId': $scope.keyword.uuid
}, function(data) {
// Refresh the annotationss
NgramListHttpService.get(
{
'corpusId': $rootScope.corpusId,
'docId': $rootScope.docId
},
function(data) {
$rootScope.annotations = data[$rootScope.corpusId.toString()][$rootScope.docId.toString()];
$rootScope.refreshDisplay();
},
function(data) {
console.error("unable to refresh the list of ngrams");
}
);
}, function(data) {
console.error("unable to remove the Ngram " + $scope.keyword.text);
});
};
}]);
/*
* Controller for the list panel displaying extra-text ngram
*/
annotationsAppNgramList.controller('NgramListPaginationController',
['$scope', '$rootScope', function ($scope, $rootScope) {
$rootScope.$watchCollection('extraNgramList', function (newValue, oldValue) {
$scope.currentListPage = 0;
$scope.pageSize = 15;
$scope.nextListPage = function() {
$scope.currentListPage = $scope.currentListPage + 1;
};
$scope.previousListPage = function() {
$scope.currentListPage = $scope.currentListPage - 1;
};
$scope.totalListPages = function(listId) {
if ($rootScope.extraNgramList[listId] === undefined) return 0;
return Math.ceil($rootScope.extraNgramList[listId].length / $scope.pageSize);
};
});
}]);
/*
* Template of the ngram element displayed in the flat lists
*/
annotationsAppNgramList.directive('keywordTemplate', function () {
return {
templateUrl: function ($element, $attributes) {
return S + 'annotations/keyword_tpl.html';
}
};
});
/*
* new NGram from the user input
*/
annotationsAppNgramList.controller('NgramInputController',
['$scope', '$rootScope', '$element', 'NgramHttpService', 'NgramListHttpService',
function ($scope, $rootScope, $element, NgramHttpService, NgramListHttpService) {
/*
* Add a new NGram from the user input in the extra-text list
*/
$scope.onListSubmit = function ($event, listId) {
var inputEltId = "#"+ listId +"-input";
if ($event.keyCode !== undefined && $event.keyCode != 13) return;
var value = angular.element(inputEltId).val().trim();
if (value === "") return;
NgramHttpService.post(
{
'listId': listId,
'ngramId': 'create'
},
{
'text': value
},
function(data) {
// on success
if (data) {
angular.element(inputEltId).val("");
// Refresh the annotationss
NgramListHttpService.get(
{
'corpusId': $rootScope.corpusId,
'docId': $rootScope.docId
},
function(data) {
$rootScope.annotations = data[$rootScope.corpusId.toString()][$rootScope.docId.toString()];
$rootScope.refreshDisplay();
},
function(data) {
console.error("unable to get the list of ngrams");
}
);
}
}, function(data) {
// on error
angular.element(inputEltId).parent().addClass("has-error");
console.error("error adding Ngram "+ value);
}
);
};
}]);
})(window);
(function () {
'use strict';
var annotationsAppUtils = angular.module('annotationsAppUtils', []);
/*
* Filter used in lists pagination (extra-text panel)
*/
annotationsAppUtils.filter('startFrom', function () {
return function (input, start) {
if (input === undefined) return;
start = +start; //parse to int
return input.slice(start);
};
});
})(window);
{% load staticfiles %}
<!DOCTYPE html>
<!--[if lt IE 7]> <html lang="en" class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html lang="en" class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html lang="en" class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!-->
<html lang="en" class="no-js">
<!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Gargantext article editor</title>
<meta name="description" content="Gargantext">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{% static 'bower_components/bootstrap/dist/css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'bower_components/bootstrap-select/dist/css/bootstrap-select.min.css' %}">
<link rel="stylesheet" href="{% static 'bower_components/angular/angular-csp.css' %}">
<link rel="stylesheet" href="{% static 'annotations/app.css' %}">
<script src="{% static 'bower_components/jquery/dist/jquery.min.js' %}"></script>
</head>
<body>
<!-- TODO integrate this later into the any other django template -->
<div id="annotationsApp" ng-cloak>
<div class="container-fluid">
<div class="row-fluid main-panel" ng-controller="NGramHighlightController">
<div class="col-md-4 col-xs-4 tabbable words-panel">
<ul class="nav nav-pills nav-justified">
<li ng-repeat="(listId, listName) in activeLists" ng-class="{active: $first == true}">
<a href="#tab-{[{listId}]}" data-toggle="tab">{[{listName}]}</a>
</li>
</ul>
<div class="tab-content">
<div ng-controller="NgramListPaginationController" ng-repeat="(listId, listName) in activeLists" ng-class="{active: $first == true}" class="tab-pane" id="tab-{[{listId}]}">
<div ng-if="extraNgramList[listId].length == 0" class="alert alert-info" role="alert">
Input any keyword you want to link to this article and the list named '{[{listName}]}'
</div>
<ul class="list-group words-list clearfix">
<li ng-repeat="keyword in extraNgramList[listId] | startFrom:currentListPage * pageSize | limitTo:pageSize" class="keyword-group-item">
<div ng-controller="NgramController" keyword-template class="keyword-container"></div>
</li>
</ul>
<nav ng-class="{invisible: totalListPages(listId) - 1 == 0}" class="clearfix">
<ul class="pagination pagination-s pull-right words-pagination">
<li ng-class="{'disabled': currentListPage == 0}"><a ng-click="previousListPage()" class="glyphicon glyphicon-backward"></a></li>
<li ng-class="{'disabled': currentListPage >= totalListPages(listId) - 1}"><a ng-click="nextListPage()" class="glyphicon glyphicon-forward"></a></li>
</ul>
</nav>
<div class="form-group" ng-controller="NgramInputController">
<input autosave="search" maxlength="240" placeholder="Add any text" type="text" class="form-control" id="{[{listId}]}-input" ng-keypress="onListSubmit($event, listId)">
<button type="submit" class="form-control btn btn-default btn-primary" ng-click="onListSubmit($event, listId)">Add to {[{listName}]}</button>
</div>
</div>
</div>
<div>
<h5>Select lists</h5>
<select class="selectpicker" multiple ng-change="activeListsChange()" ng-model="lists" ng-controller="ActiveListsController">
<option ng-repeat="item in allListsSelect" id="list---{[{item.id}]}" ng-disabled="{[{ item.label == 'MiamList' }]}">{[{item.label}]}</option>
</select>
</div>
</div>
<div class="col-md-8 col-xs-8 text-panel" ng-controller="DocController" id="document">
<div class="row-fluid clearfix">
<div class="col-md-10 col-xs-10">
<h3 class="text-container" id="title">{[{title}]}</h3>
</div>
<div class="col-md-2 col-xs-2 clearfix">
<button ng-controller="DocFavoriteController" type="button" class="btn btn-default float-right" ng-click="onStarClick($event)">
<span class="glyphicon" ng-class="{'glyphicon-star-empty': isFavorite == false, 'glyphicon-star': isFavorite == true}"></span>
</button>
<!--<nav>
<ul class="pager">
<li ng-if="current_page_number > 1"><a ng-click="onPreviousClick()" href="#">Previous</a></li>
<li ng-if="current_page_number < last_page_number"><a ng-click="onNextClick()" href="#">Next</a></li>
</ul>
</nav>-->
</div>
</div>
<div class="row-fluid">
<ul class="list-group clearfix">
<li class="list-group-item small"><span class="badge">journal</span>{[{journal}]}</li>
<li class="list-group-item small"><span class="badge">authors</span>{[{authors}]}</li>
<li class="list-group-item small"><span class="badge">date</span>{[{publication_date}]}</li>
</ul>
</div>
<div ng-if="dataLoading">
Loading text...
<br>
<center>
<img width="10%" src="{% static 'img/ajax-loader.gif'%}"></img>
</center>
<br>
</div>
<div ng-if="abstract_text != null">
<span class="badge">abstract</span>
</div>
<p id="abstract-text" class="text-container">
<div ng-if="abstract_text == null" class="alert alert-info small" role="alert">Empty abstract text</div>
</p>
<div ng-if="full_text != null">
<span class="badge">full article</span>
</div>
<p id="full-text" class="text-container">
<div ng-if="full_text == null" class="alert alert-info small" role="alert">Empty full text</div>
</p>
</div>
</div> <!-- end of the main row -->
</div>
<!-- this menu is over the text on mouse selection -->
<div ng-controller="TextSelectionMenuController" id="selection" class="selection-menu">
<ul class="noselection">
<li ng-repeat="item in menuItems" class="{[{item.listName}]}" ng-click="onMenuClick($event, item.action, item.listId)">{[{item.verb}]} {[{item.listName}]}</li>
</ul>
</div>
</div>
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<script type="application/javascript">
/* Constants required for annotations app JS to work */
window.STATIC_URL = "{% static '' %}";
window.ANNOTATION_API_URL = "{{ api_url }}";
window.NODES_API_URL = "{{ nodes_api_url }}";
</script>
<script src="{% static 'annotations/main.js' %}"></script>
</body>
</html>
from django.conf.urls import patterns, url
from annotations import views
# /!\ urls patterns here are *without* the trailing slash
urlpatterns = patterns('',
# json:title,id,authors,journal,
# publication_date
# abstract_text,full_text
url(r'^documents/(?P<doc_id>[0-9]+)$', views.Document.as_view()), # document view
url(r'^corpora/(?P<corpus_id>[0-9]+)/documents/(?P<doc_id>[0-9]+)$', views.NgramList.as_view()), # the list associated with an ngram
# 2016-03-24: refactoring, deactivated NgramEdit and NgramCreate
#
# url(r'^lists/(?P<list_id>[0-9]+)/ngrams/(?P<ngram_ids>[0-9,\+]+)+$', views.NgramEdit.as_view()),
# POST (fixed 2015-12-16)
# url(r'^lists/(?P<list_id>[0-9]+)/ngrams/create$', views.NgramCreate.as_view()), #
)
from urllib.parse import urljoin
import json
import datetime
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.contrib.auth.decorators import login_required
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer
from rest_framework.exceptions import APIException
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
# 2016-03-24: refactoring, new paths
from gargantext.models.ngrams import Node, NodeNgram, Ngram
from gargantext.util.db import session
from gargantext.util.db_cache import cache
from gargantext.util.http import requires_auth
# from ngram.lists import listIds, listNgramIds
# from gargantext_web.db import get_or_create_node
@requires_auth
def main(request, project_id, corpus_id, document_id):
"""
Full page view
"""
return render_to_response('annotations/main.html', {
# TODO use reverse()
'api_url': urljoin(request.get_host(), '/annotations/'),
'nodes_api_url': urljoin(request.get_host(), '/api/'),
}, context_instance=RequestContext(request))
class NgramList(APIView):
"""Read and Write Annotations"""
renderer_classes = (JSONRenderer,)
def get(self, request, corpus_id, doc_id):
"""Get All for a doc id"""
corpus_id = int(corpus_id)
doc_id = int(doc_id)
lists = {}
for list_type in ['MiamList', 'StopList']:
list_id = listIds(user_id=request.user.id, corpus_id=int(corpus_id), typeList=list_type)
lists["%s" % list_id[0][0]] = list_type
# ngrams for the corpus_id (ignoring doc_id for the moment):
doc_ngram_list = listNgramIds(corpus_id=corpus_id, doc_id=doc_id, user_id=request.user.id)
data = { '%s' % corpus_id : {
'%s' % doc_id : [
{
'uuid': ngram_id,
'text': ngram_text,
'occurrences': ngram_occurrences,
'list_id': list_id,
}
for ngram_id, ngram_text, ngram_occurrences, list_id in doc_ngram_list],
'lists': lists
}}
return Response(data)
# 2016-03-24: refactoring, deactivated NgramEdit and NgramCreate
# ------------------------------------
# class NgramEdit(APIView):
# """
# Actions on one existing Ngram in one list
# """
# renderer_classes = (JSONRenderer,)
# authentication_classes = (SessionAuthentication, BasicAuthentication)
#
# def post(self, request, list_id, ngram_ids):
# """
# Edit an existing NGram in a given list
# """
# # implicit global session
# list_id = int(list_id)
# list_node = session.query(Node).filter(Node.id==list_id).first()
# # TODO add 1 for MapList social score ?
# if list_node.type_id == cache.NodeType['MiamList']:
# weight=1.0
# elif list_node.type_id == cache.NodeType['StopList']:
# weight=-1.0
#
# # TODO remove the node_ngram from another conflicting list
# for ngram_id in ngram_ids.split('+'):
# ngram_id = int(ngram_id)
# node_ngram = NodeNgram(node_id=list_id, ngram_id=ngram_id, weight=weight)
# session.add(node_ngram)
#
# session.commit()
#
# # return the response
# return Response({
# 'uuid': ngram_id,
# 'list_id': list_id,
# } for ngram_id in ngram_ids)
#
# def put(self, request, list_id, ngram_ids):
# return Response(None, 204)
#
# def delete(self, request, list_id, ngram_ids):
# """
# Delete a ngram from a list
# """
# # implicit global session
# print("to del",ngram_ids)
# for ngram_id in ngram_ids.split('+'):
# print('ngram_id', ngram_id)
# ngram_id = int(ngram_id)
# (session.query(NodeNgram)
# .filter(NodeNgram.node_id==list_id)
# .filter(NodeNgram.ngram_id==ngram_id).delete()
# )
#
# session.commit()
#
# # [ = = = = del from map-list = = = = ]
# list_id = session.query(Node).filter(Node.id==list_id).first()
# corpus = session.query(Node).filter(Node.id==list_id.parent_id , Node.type_id==cache.NodeType['Corpus'].id).first()
# node_mapList = get_or_create_node(nodetype='MapList', corpus=corpus )
# results = session.query(NodeNgram).filter(NodeNgram.node_id==node_mapList.id ).all()
# ngram_2del = [int(i) for i in ngram_ids.split('+')]
# ngram_2del_ = session.query(NodeNgram).filter(NodeNgram.node_id==node_mapList.id , NodeNgram.ngram_id.in_(ngram_2del) ).all()
# for map_node in ngram_2del_:
# session.delete(map_node)
# session.commit()
#
# node_stopList = get_or_create_node(nodetype='StopList', corpus=corpus )
# for ngram_id in ngram_2del:
# stop_node = NodeNgram( weight=1.0, ngram_id=ngram_id , node_id=node_stopList.id)
# session.add(stop_node)
# session.commit()
# # [ = = = = / del from map-list = = = = ]
#
# return Response(None, 204)
#
# class NgramCreate(APIView):
# """
# Create a new Ngram in one list
# """
# renderer_classes = (JSONRenderer,)
# authentication_classes = (SessionAuthentication, BasicAuthentication)
#
# def post(self, request, list_id):
# """
# create NGram in a given list
#
# example: request.data = {'text': 'phylogeny'}
# """
# # implicit global session
# list_id = int(list_id)
# # format the ngram's text
# ngram_text = request.data.get('text', None)
# if ngram_text is None:
# raise APIException("Could not create a new Ngram without one \
# text key in the json body")
#
# ngram_text = ngram_text.strip().lower()
# ngram_text = ' '.join(ngram_text.split())
# # check if the ngram exists with the same terms
# ngram = session.query(Ngram).filter(Ngram.terms == ngram_text).first()
# if ngram is None:
# ngram = Ngram(n=len(ngram_text.split()), terms=ngram_text)
# else:
# # make sure the n value is correct
# ngram.n = len(ngram_text.split())
#
# session.add(ngram)
# session.commit()
# ngram_id = ngram.id
# # create the new node_ngram relation
# # TODO check existing Node_Ngram ?
# # £TODO ici indexation
# node_ngram = NodeNgram(node_id=list_id, ngram_id=ngram_id, weight=1.0)
# session.add(node_ngram)
# session.commit()
#
# # return the response
# return Response({
# 'uuid': ngram_id,
# 'text': ngram_text,
# 'list_id': list_id,
# })
class Document(APIView):
"""
Read-only Document view, similar to /api/nodes/
"""
renderer_classes = (JSONRenderer,)
def get(self, request, doc_id):
"""Document by ID"""
# implicit global session
node = session.query(Node).filter(Node.id == doc_id).first()
if node is None:
raise APIException('This node does not exist', 404)
try:
pub_date = datetime.datetime.strptime(node.hyperdata.get('publication_date'),
"%Y-%m-%d %H:%M:%S")
pub_date = pub_date.strftime("%x")
except ValueError:
pub_date = node.hyperdata.get('publication_date')
data = {
'title': node.hyperdata.get('title'),
'authors': node.hyperdata.get('authors'),
'journal': node.hyperdata.get('journal'),
'publication_date': pub_date,
'full_text': node.hyperdata.get('full_text'),
'abstract_text': node.hyperdata.get('abstract'),
'id': node.id
}
return Response(data)
......@@ -14,10 +14,18 @@ import gargantext.views.api.urls
import gargantext.views.generated.urls
import gargantext.views.pages.urls
# tempo: unchanged doc-annotations --
from annotations import urls as annotations_urls
from annotations.views import main as annotations_main_view
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^generated/', include(gargantext.views.generated.urls)),
url(r'^api/', include(gargantext.views.api.urls)),
url(r'^', include(gargantext.views.pages.urls)),
# tempo: unchanged doc-annotations routes --
url(r'^projects/(\d+)/corpora/(\d+)/documents/(\d+)/$', annotations_main_view),
url(r'^annotations/', include(annotations_urls))
]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment