Commit 0bb4edbc authored by delanoe's avatar delanoe

Merge branch 'romain-refactoring' into merge

parents adcaf50b c3adae17
......@@ -161,3 +161,9 @@
.float-right {
float: right;
}
.favactive {
/* yellow */
color: #FFF50D;
text-shadow: -1px 0 #777777, 0 1px #777777, 1px 0 #777777, 0 -1px #777777;
}
......@@ -8,7 +8,7 @@
// dataLoading = signal pour afficher wait
$scope.dataLoading = true ;
console.log("annotations.document.DocController.DocumentHttpService.get():before")
$rootScope.documentResource = DocumentHttpService.get(
......@@ -52,12 +52,64 @@
}]);
annotationsAppDocument.controller('DocFavoriteController',
['$scope', '$rootScope', 'DocumentHttpService',
function ($scope, $rootScope, DocumentHttpService) {
['$scope', '$rootScope', 'MainApiFavoritesHttpService',
function ($scope, $rootScope, MainApiFavoritesHttpService) {
$scope.isFavorite = false;
MainApiFavoritesHttpService.get(
{
'corpusId': $rootScope.corpusId,
'docId': $rootScope.docId
},
function(data) {
if (data['favdocs'].length > 0
&& data['favdocs'][0] == $scope.docId) {
$scope.isFavorite = true ;
}
else {
$scope.isFavorite = false ;
}
},
function(data) {
console.error("unable to check if document belongs to favorites");
$scope.isFavorite = false ;
}
) ;
$scope.onStarClick = function($event) {
console.log($scope.isFavorite)
// console.log($scope)
console.log("TODO");
var myAction ;
if (! $scope.isFavorite) {
// PUT api/nodes/574/favorites?docs=576
myAction = MainApiFavoritesHttpService.put
}
else {
// DELETE api/nodes/574/favorites?docs=576
myAction = MainApiFavoritesHttpService.delete
}
// (1) do the action
myAction(
{
'corpusId': $rootScope.corpusId,
'docId': $rootScope.docId
},
// success
function(data) {
// (2) toggle status and refresh
$scope.isFavorite = ! $scope.isFavorite
$rootScope.refreshDisplay();
},
// failure
function(data) {
console.error("unable to change favorite status");
}
);
};
$scope.isFavorite = false;
}]);
})(window);
......@@ -122,4 +122,49 @@
}
);
});
/*
* MainApiFavoritesHttpService: Check/Add/Del Document in favorites
* ============================
* route: api/nodes/574/favorites?docs=576
* /!\ for this route we reach out of this annotation module
* and send directly to the gargantext api route for favs
* (cross origin request with http protocol scheme)
* ------
*
* exemple:
* --------
* {
* "favdocs": [576] // <== if doc is among favs
* "missing": [] // <== if doc is not in favs
* }
*
*/
http.factory('MainApiFavoritesHttpService', function($resource) {
return $resource(
// adding explicit "http://" b/c this a cross origin request
'http://' + window.GARG_ROOT_URL + "/api/nodes/:corpusId/favorites?docs=:docId",
{
corpusId: '@corpusId',
docId: '@docId'
},
{
get: {
method: 'GET',
params: {corpusId: '@corpusId', docId: '@docId'}
},
put: {
method: 'PUT',
params: {corpusId: '@corpusId', docId: '@docId'}
},
delete: {
method: 'DELETE',
params: {corpusId: '@corpusId', docId: '@docId'}
}
}
);
});
})(window);
......@@ -58,9 +58,9 @@
<div class="list-selector">
<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}]}">{[{item.label}]}</option>
<!-- to disallow unchecking MapList add this into <option> element: ng-disabled="{[{ item.label == 'MapList' }]}" -->
</select>
......@@ -73,7 +73,7 @@
</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>
<span class="glyphicon" ng-class="{'glyphicon-star-empty': isFavorite == false, 'glyphicon-star favactive': isFavorite == true}"></span>
</button>
<!--<nav>
<ul class="pager">
......@@ -129,6 +129,7 @@
/* Constants required for annotations app JS to work */
window.STATIC_URL = "{% static '' %}";
window.ANNOTATION_API_URL = "{{ api_url }}";
window.GARG_ROOT_URL = "{{ garg_url }}";
window.NODES_API_URL = "{{ nodes_api_url }}";
</script>
<script src="{% static 'annotations/main.js' %}"></script>
......
......@@ -5,13 +5,14 @@ from annotations import views
# /!\ urls patterns here are *without* the trailing slash
urlpatterns = [
# json:title,id,authors,journal,
# GET [DocumentHttpService]
# 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
# GET:
# GET [NgramListHttpService]
# was : lists ∩ document (ngram_ids intersection if connected to list node_id and doc node_id)
# fixed 2016-01: just lists (because document doesn't get updated by POST create cf. ngram.lists.DocNgram filter commented)
url(r'^corpora/(?P<corpus_id>[0-9]+)/documents/(?P<doc_id>[0-9]+)$', views.NgramList.as_view()), # the list associated with an ngram
......
......@@ -30,6 +30,7 @@ def main(request, project_id, corpus_id, document_id):
return render_to_response('annotations/main.html', {
# TODO use reverse()
'api_url': urljoin(request.get_host(), '/annotations/'),
'garg_url': request.get_host(),
'nodes_api_url': urljoin(request.get_host(), '/api/'),
}, context_instance=RequestContext(request))
......
......@@ -229,11 +229,17 @@ class NodeResource(APIView):
return JsonHttpResponse({'deleted': result.rowcount})
class CorpusFavorites(APIView):
"""Retrieve/update/delete a corpus node's associated favorite docs
"""Retrieve/update/delete one or several docs from a corpus associated favs
(url: GET /api/nodes/<corpus_id>/favorites)
=> lists all favorites
(url: GET /api/nodes/<corpus_id>/favorites?docs[]=doc1,doc2)
=> checks for each doc if it is in favorites
(url: DEL /api/nodes/<corpus_id>/favorites?docs[]=doc1,doc2)
=> removes each doc from favorites
(url: PUT /api/nodes/<corpus_id>/favorites?docs[]=doc1,doc2)
=> add each doc to favorites
"""
def _get_fav_node(self, corpus_id):
......@@ -253,25 +259,57 @@ class CorpusFavorites(APIView):
else:
self.corpus = corpus
fav_node = self.corpus.children('FAVORITES').first()
return fav_node
def get(self, request, corpus_id):
response = {}
"""
2 possibilities with/without param
1) GET http://localhost:8000/api/nodes/2/favorites
(returns the full list of fav docs within corpus 2)
2) GET http://localhost:8000/api/nodes/2/favorites?docs=53,54
(will test if docs 53 and 54 are among the favorites of corpus 2)
(returns the intersection of fav docs with [53,54])
"""
fav_node = self._get_fav_node(corpus_id)
req_params = validate(
get_parameters(request),
{'docs': list, 'default': ""}
)
response = {}
if fav_node == None:
response = {
'warning':'No favorites node is defined for this corpus (\'%s\')'
% self.corpus.name ,
'doc_ids':[]
'favdocs':[]
}
else:
elif 'docs' not in req_params:
# each docnode associated to the favnode of this corpusnode
q = (session
.query(NodeNode.node2_id)
.filter(NodeNode.node1_id==fav_node.id))
doc_ids = [row.node2_id for row in q.all()]
all_doc_ids = [row.node2_id for row in q.all()]
response = {
'doc_ids': doc_ids
'favdocs': all_doc_ids
}
else:
nodeids_to_check = [int(did) for did in req_params['docs'].split(',')]
# each docnode from the input list, if it is associated to the favnode
q = (session
.query(NodeNode.node2_id)
.filter(NodeNode.node1_id==fav_node.id)
.filter(NodeNode.node2_id.in_(nodeids_to_check)))
present_doc_ids = [row.node2_id for row in q.all()]
absent_doc_ids = [did for did in nodeids_to_check if did not in present_doc_ids]
response = {
'favdocs': present_doc_ids,
'missing': absent_doc_ids
}
return JsonHttpResponse(response)
......@@ -281,66 +319,95 @@ class CorpusFavorites(APIView):
DELETE http://localhost:8000/api/nodes/2/favorites?docs=53,54
(will delete docs 53 and 54 from the favorites of corpus 2)
"""
# if not request.user.is_authenticated():
# # can't use @requires_auth because of positional 'self' within class
# return HttpResponse('Unauthorized', status=401)
if not request.user.is_authenticated():
# can't use @requires_auth because of positional 'self' within class
return HttpResponse('Unauthorized', status=401)
# user is ok
fav_node = self._get_fav_node(corpus_id)
req_params = validate(
get_parameters(request),
{'docs': list, 'default': ""}
)
nodeids_to_delete = req_params['docs'].split(',')
# it deletes from favourites but not from DB
result = session.execute(
delete(NodeNode)
.where(NodeNode.node1_id == fav_node.id)
.where(NodeNode.node2_id.in_(nodeids_to_delete))
)
session.commit()
return JsonHttpResponse({'count_removed': result.rowcount})
response = {}
if fav_node == None:
response = {
'warning':'No favorites node is defined for this corpus (\'%s\')'
% self.corpus.name ,
'count_removed': 0
}
else:
req_params = validate(
get_parameters(request),
{'docs': list, 'default': ""}
)
nodeids_to_delete = [int(did) for did in req_params['docs'].split(',')]
# it deletes from favourites but not from DB
result = session.execute(
delete(NodeNode)
.where(NodeNode.node1_id == fav_node.id)
.where(NodeNode.node2_id.in_(nodeids_to_delete))
)
session.commit()
response = {'count_removed': result.rowcount}
return JsonHttpResponse(response)
def put(self, request, corpus_id, check_each_doc=True):
# if not request.user.is_authenticated():
# # can't use @requires_auth because of positional 'self' within class
# return HttpResponse('Unauthorized', status=401)
if not request.user.is_authenticated():
# can't use @requires_auth because of positional 'self' within class
return HttpResponse('Unauthorized', status=401)
# user is ok
fav_node = self._get_fav_node(corpus_id)
req_params = validate(
get_parameters(request),
{'docs': list, 'default': ""}
)
nodeids_to_add = req_params['docs'].split(',')
if check_each_doc:
# verification que ce sont bien des documents du bon corpus
# un peu long => désactiver par défaut ?
known_docs_q = (session
.query(Node.id)
.filter(Node.parent_id==corpus_id)
.filter(Node.typename=='DOCUMENT')
)
lookup = {known_doc.id:True for known_doc in known_docs_q.all()}
rejected_list = []
for doc_node_id in nodeids_to_add:
if (doc_node_id not in lookup):
rejected_list.append(doc_node_id)
if len(rejected_list):
raise ValidationException(
"Error on some requested docs: %s (Only nodes of type 'doc' AND belonging to corpus %i can be added to favorites.)"
% (str(rejected_list), int(corpus_id)))
# add them
bulk_insert(
NodeNode,
('node1_id', 'node2_id', 'score'),
((fav_node.id, doc_node_id, 1.0 ) for doc_node_id in nodeids_to_add)
)
return JsonHttpResponse({'count_added': len(nodeids_to_add)})
response = {}
if fav_node == None:
response = {
'warning':'No favorites node is defined for this corpus (\'%s\')'
% self.corpus.name ,
'count_added':0
}
else:
req_params = validate(
get_parameters(request),
{'docs': list, 'default': ""}
)
nodeids_to_add = [int(did) for did in req_params['docs'].split(',')]
if check_each_doc:
# verification que ce sont bien des documents du bon corpus
# un peu long => désactiver par défaut ?
known_docs_q = (session
.query(Node.id)
.filter(Node.parent_id==corpus_id)
.filter(Node.typename=='DOCUMENT')
)
lookup = {known_doc.id:True for known_doc in known_docs_q.all()}
# debug
# print("lookup hash", lookup)
rejected_list = []
for doc_node_id in nodeids_to_add:
if (doc_node_id not in lookup):
rejected_list.append(doc_node_id)
if len(rejected_list):
raise ValidationException(
"Error on some requested docs: %s (Only nodes of type 'doc' AND belonging to corpus %i can be added to favorites.)"
% (str(rejected_list), int(corpus_id)))
# add them
bulk_insert(
NodeNode,
('node1_id', 'node2_id', 'score'),
((fav_node.id, doc_node_id, 1.0 ) for doc_node_id in nodeids_to_add)
)
# todo count really added (here: counts input param not result)
response = {'count_added': len(nodeids_to_add)}
return JsonHttpResponse(response)
class CorpusFacet(APIView):
"""Loop through a corpus node's docs => do counts by a hyperdata field
......
......@@ -143,6 +143,7 @@ var RecDict={};
var AjaxRecords = []
var Garbage = {}
var countByTitles = {} // useful for title duplicates
var favorites = {}
function getRecord(rec_id) {
return MyTable.data('dynatable').settings.dataset.originalRecords[rec_id];
......@@ -153,6 +154,39 @@ function getRecords() {
return MyTable.data('dynatable').settings.dataset.originalRecords;
}
function favstatusToStar (rec_id, boolFavstatus, boolStrike=false){
var starStr = boolFavstatus ? "glyphicon-star" : "glyphicon-star-empty";
var styleStr = boolStrike ? "style='text-decoration:line-through'" : "";
var htmlStr = "<span class='glyphicon "+starStr+"' "+styleStr ;
htmlStr += " onclick='toggleFavstatus("+rec_id+")'" ;
htmlStr += ">" ;
htmlStr += "</span>" ;
return htmlStr
}
function toggleFavstatus (rec_id) {
var doc_id = AjaxRecords[rec_id]["id"]
var statusBefore = AjaxRecords[rec_id]["isFavorite"]
var myHttpAction = statusBefore ? 'DELETE' : 'PUT'
$.ajax({
url: 'http://localhost:8000/api/nodes/'+corpus_id+'/favorites?docs='+doc_id,
type: myHttpAction,
beforeSend: function(xhr) {
xhr.setRequestHeader("X-CSRFToken", getCookie("csrftoken"));
},
success: function(favdata) {
// it's been toggled in the DB so we toggle locally
if (statusBefore) delete favorites[doc_id]
else favorites[doc_id] = true
AjaxRecords[rec_id]["isFavorite"] = ! statusBefore ;
// and we reprocess table directly (no need for new ajax of other recs)
MyTable.data('dynatable').process();
},
});
}
function transformContent2(rec_id) {
// pr("\t\ttransformContent2: "+rec_id)
var elem = AjaxRecords[rec_id];
......@@ -162,12 +196,14 @@ function transformContent2(rec_id) {
result["id"] = elem["id"]
result["date"] = '<strike>'+elem["date"]+'</strike>'
result["docurl"] = '<strike>'+elem["docurl"]+'</strike>'
result["isFavorite"] = favstatusToStar(rec_id, elem["isFavorite"], boolStrike=true)
result["rawtitle"] = elem["rawtitle"]
result["del"] = '<input id='+rec_id+' onclick="overRide(this)" type="checkbox" checked/>'
} else {
result["id"] = elem["id"]
result["date"] = elem["date"]
result["docurl"] = elem["docurl"]
result["isFavorite"] = favstatusToStar(rec_id, elem["isFavorite"])
result["rawtitle"] = elem["rawtitle"]
result["del"] = '<input id='+rec_id+' onclick="overRide(this)" type="checkbox"/>'
}
......@@ -269,6 +305,9 @@ function Main_test(Data) {
div_table += "\t"+"\t"+'<span class="glyphicon glyphicon-calendar"></span> Date</th>'+"\n"
div_table += "\t"+"\t"+'<th data-dynatable-column="docurl">'+"\n"
div_table += "\t"+"\t"+'<span class="glyphicon glyphicon-text-size"></span> Title</th>'+"\n"
div_table += "\t"+"\t"+'<th data-dynatable-column="isFavorite">'+"\n"
div_table += "\t"+"\t"+'<span class="glyphicon glyphicon-star"></span>'+"\n"
div_table += "\t"+"\t"+'</th>'+"\n"
div_table += "\t"+"\t"+'<th data-dynatable-column="del" data-dynatable-no-sort="true">'+"\n"
div_table += "\t"+"\t"+'<span class="glyphicon glyphicon-trash"></span>'+"\n"
div_table += "\t"+"\t"+'</th>'+"\n"
......@@ -445,7 +484,7 @@ function Main_test(Data) {
},
inputs: {
// our own search which differentiates title vs abstract queries
queries: $('#doubleSearch, #dupFilter')
queries: $('#doubleSearch, #docFilter')
},
writers: {
_rowWriter: ulWriter
......@@ -495,14 +534,16 @@ function Main_test(Data) {
}
// MyTable.data('dynatable').process();
// also append another bound filter for duplicates
// also append another bound filter for duplicates/favorites
MyTable.data('dynatable').queries
.functions['dupFilter'] = function(record,selected) {
return (selected == 'filter_all')||(countByTitles[record.rawtitle] > 1)
.functions['docFilter'] = function(record,opt) {
if (opt == 'filter_all') return true
else if (opt == 'filter_favs') return favorites[record.id]
else if (opt == 'filter_dupl') return (countByTitles[record.rawtitle] > 1)
}
// and set this filter's initial status to 'filter_all'
MyTable.data('dynatable').settings.dataset.queries['dupFilter'] = 'filter_all'
MyTable.data('dynatable').settings.dataset.queries['docFilter'] = 'filter_all'
MyTable.data('dynatable').sorts.functions["rawtitleSort"] = function testSort (rec1,rec2) {
......@@ -531,20 +572,20 @@ function Main_test(Data) {
var dupFlag = false ;
$("#div-table").on("dynatable:queries:added", function(e, qname, qval) {
if (!dupFlag && qname == 'dupFilter' && qval == "filter_dupl") {
MyTable.data('dynatable').queries.remove('dupFilter')
if (!dupFlag && qname == 'docFilter' && qval == "filter_dupl") {
MyTable.data('dynatable').queries.remove('docFilter')
// to avoid recursion when we'll call this filter again in 4 lines
dupFlag = true ;
// sort alphabetically **before** duplicates filter
MyTable.data('dynatable').sorts.clear();
MyTable.data('dynatable').sorts.add('rawtitle', -1) // -1 <==> DESC (ASC doesn't work well ?)
MyTable.data('dynatable').queries.add('dupFilter', 'filter_dupl')
MyTable.data('dynatable').queries.add('docFilter', 'filter_dupl')
MyTable.data('dynatable').process();
}
});
$("#div-table").on("dynatable:queries:removed", function(e, qname) {
if (qname == 'dupFilter') {
if (qname == 'docFilter') {
dupFlag = false ;
}
});
......@@ -556,36 +597,53 @@ $.ajax({
url: '/api/nodes?types[]=DOCUMENT&pagination_limit=-1&parent_id='
+ corpus_id
+'&fields[]=parent_id&fields[]=id&fields[]=name&fields[]=typename&fields[]=hyperdata',
success: function(data){
$("#content_loader").remove();
$.each(data.records, function(i, record){
var orig_id = parseInt(record.id);
var arr_id = parseInt(i)
RecDict[orig_id] = arr_id;
record.rawtitle = record.name;
// trick to have a clickable title in docurl slot, but could be done in transformContent2
record.docurl = '<a target="_blank" href="/projects/' + project_id + '/corpora/'+ corpus_id + '/documents/' + record.id + '">' + record.name + '</a>';
record.date = get_node_date(record);
record.del = false;
});
success: function(maindata){
// unfortunately favorites info is in a separate request (other nodes)
$.ajax({
url: 'http://localhost:8000/api/nodes/'+corpus_id+'/favorites',
success: function(favdata){
// initialize favs lookup
for (var i in favdata['favdocs']) {
doc_id = favdata['favdocs'][i]
favorites[doc_id] = true ;
}
// initialize CountByTitle census
for (var i in data.records) {
ourTitle = data.records[i]['rawtitle'] ;
if (countByTitles.hasOwnProperty(ourTitle)) {
countByTitles[ourTitle] ++ ;
}
else {
countByTitles[ourTitle] = 1 ;
}
}
AjaxRecords = data.records; // backup-ing in global variable!
// now process the nodes data from 1st request
$.each(maindata.records, function(i, record){
var orig_id = parseInt(record.id);
var arr_id = parseInt(i)
RecDict[orig_id] = arr_id;
record.rawtitle = record.name;
var result = Main_test(data.records)
record.isFavorite = false ;
if (favorites[orig_id]) {
record.isFavorite = true ;
}
$("#content_loader").remove()
// trick to have a clickable title in docurl slot, but could be done in transformContent2
record.docurl = '<a target="_blank" href="/projects/' + project_id + '/corpora/'+ corpus_id + '/documents/' + record.id + '">' + record.name + '</a>';
record.date = get_node_date(record);
record.del = false;
});
console.log( result )
// initialize CountByTitle census
for (var i in maindata.records) {
ourTitle = maindata.records[i]['rawtitle'] ;
if (countByTitles.hasOwnProperty(ourTitle)) {
countByTitles[ourTitle] ++ ;
}
else {
countByTitles[ourTitle] = 1 ;
}
}
AjaxRecords = maindata.records; // backup-ing in global variable!
$("#content_loader").remove();
var result = Main_test(maindata.records)
console.log( result )
},
});
},
});
......@@ -73,9 +73,10 @@
<input title="Search in Abstracts" id="searchAB" name="searchAB" type="checkbox">AB
</span>&nbsp;&nbsp;
<span class="glyphicon glyphicon-filter" aria-hidden="true"></span>
<select id="dupFilter" name="dupFilter">
<select id="docFilter" name="docFilter">
<option value="filter_all">All</option>
<option value="filter_dupl">Duplicates by Title</option>
<option value="filter_favs">Favorite documents</option>
<option value="filter_dupl">Duplicates by title</option>
</select>
</div>
</div>
......
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