Skip to content

Commit 6b7bd4b

Browse files
committed
Allow upload of judgments
+ Add new endpoint to bmi_fcgi to check if a document id exists + allow session deletion + clean judgment modal source fields + Dynamically show some form fields
1 parent c0f8513 commit 6b7bd4b

File tree

21 files changed

+493
-102
lines changed

21 files changed

+493
-102
lines changed

CALEngine/README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ Error Response:
230230
#### Get logs from a session
231231

232232
```
233-
DELETE /log
233+
GET /log
234234
Data Params:
235235
session_id: [string]
236236
@@ -242,3 +242,20 @@ Error Response:
242242
Code: 404
243243
Content: {'error': 'session not found'}
244244
```
245+
246+
#### Check if docid exits
247+
248+
```
249+
GET /docid_exists
250+
Data Params:
251+
session_id: [string]
252+
docid: [string]
253+
254+
Success Response:
255+
Code: 200
256+
Content: {'exists': true/false}
257+
258+
Error Response:
259+
Code: 404
260+
Content: {'error': 'session not found'}
261+
```

CALEngine/src/bmi_fcgi.cc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,32 @@ void log_view(const FCGX_Request & request, const vector<pair<string, string>> &
315315
write_response(request, 200, "text/plain", SESSIONS[session_id]->get_log());
316316
}
317317

318+
// Handler for /docid_exists
319+
void docid_exists_view(const FCGX_Request & request, const vector<pair<string, string>> &params){
320+
string session_id, doc_id;
321+
322+
for(auto kv: params){
323+
if(kv.first == "session_id"){
324+
session_id = kv.second;
325+
}else if(kv.first == "doc_id"){
326+
doc_id = kv.second;
327+
}
328+
}
329+
330+
if(SESSIONS.find(session_id) == SESSIONS.end()){
331+
write_response(request, 404, "application/json", "{\"error\": \"session not found\"}");
332+
return;
333+
}
334+
335+
const unique_ptr<BMI> &bmi = SESSIONS[session_id];
336+
337+
if(bmi->get_dataset()->get_index(doc_id) == bmi->get_dataset()->NPOS){
338+
write_response(request, 200, "application/json", "{\"exists\": false}");
339+
return;
340+
}
341+
write_response(request, 200, "application/json", "{\"exists\": true}");
342+
}
343+
318344
// Handler for /judge
319345
void judge_view(const FCGX_Request & request, const vector<pair<string, string>> &params){
320346
string session_id, doc_id;
@@ -384,6 +410,10 @@ void process_request(const FCGX_Request & request) {
384410
if(method == "GET"){
385411
get_docs_view(request, params);
386412
}
413+
}else if(action == "docid_exists"){
414+
if(method == "GET"){
415+
docid_exists_view(request, params);
416+
}
387417
}else if(action == "judge"){
388418
if(method == "POST"){
389419
judge_view(request, params);

HiCALWeb/hicalweb/CAL/templates/CAL/CAL.html

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ <h5 class="card-title text-secondary mt-4">Highlighted terms <span class="badge
280280
"",
281281
"<span class='text-danger'>Server returned empty list.</span>",
282282
"",
283-
"Something went wrong.. Please contact the study coordinators.",
283+
"Something went wrong..",
284284
""
285285
);
286286
}
@@ -445,10 +445,7 @@ <h5 class="card-title text-secondary mt-4">Highlighted terms <span class="badge
445445
'highlyRelevant': highlyRelevant,
446446
'nonrelevant': notrelevant,
447447
'relevant': relevant,
448-
'isFromCAL': true,
449-
'isFromSearch': false,
450-
'isFromSearchModal': false,
451-
'isFromIterative': false,
448+
'source': "CAL",
452449
'client_time': now,
453450
'fromMouse': isMouse,
454451
'fromKeyboard': isKeyboard,

HiCALWeb/hicalweb/CAL/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def get_ajax(self, request, *args, **kwargs):
8181
doc['doc_id'], doc['para_id'] = doc_id.split('.')
8282
doc_ids_hack.append(doc)
8383

84-
if self.request.user.current_task.strategy == 'doc':
84+
if 'doc' in self.request.user.current_task.strategy:
8585
documents = DocEngine.get_documents(docs_ids_to_judge,
8686
self.request.user.current_task.topic.seed_query)
8787
else:
@@ -93,7 +93,7 @@ def get_ajax(self, request, *args, **kwargs):
9393
error_dict = {u"message": u"Timeout error. Please check status of servers."}
9494
return self.render_timeout_request_response(error_dict)
9595
except CALError as e:
96-
error_dict = {u"message": u"Error occurred. Please inform study coordinators"}
96+
error_dict = {u"message": u"Error occurred."}
9797

9898
# TODO: add proper http response for CAL errors
9999
return self.render_timeout_request_response(error_dict)

HiCALWeb/hicalweb/archive/templates/archive/home.html

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% extends "base.html" %}
2-
{% load static %}
2+
{% load static crispy_forms_tags %}
33
{% block pagetitle %}Archive{% endblock %}
44

55

@@ -52,11 +52,46 @@ <h2 class="text-secondary">Judged documents</h2>
5252
<div class="card-block">
5353
<h2 class="text-secondary">Operations</h2>
5454
<hr>
55-
<a id="tocsv" href="#" class="btn btn-outline-secondary btn-sm mb-3">Export to csv</a>
55+
<a id="import_csv" href="#" class="btn btn-outline-secondary btn-sm mb-3 btn-block" data-toggle="modal" data-target="#importModal">Import judgments</a>
56+
<a id="export_csv" href="#" class="btn btn-outline-secondary btn-sm mb-3 btn-block">Export to csv</a>
5657
</div>
5758
</div>
5859
</div>
5960

61+
<!-- Import Modal -->
62+
<div class="modal fade" id="importModal" tabindex="-1" role="dialog">
63+
<div class="modal-dialog" role="document">
64+
<div class="modal-content">
65+
<div class="modal-header">
66+
<h5 class="modal-title" id="exampleModalLabel">Import judgments</h5>
67+
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
68+
<span aria-hidden="true">&times;</span>
69+
</button>
70+
</div>
71+
<form method="POST" enctype="multipart/form-data" id="import_csv_form">
72+
<div class="modal-body">
73+
<span class="">Your document judgment file needs to be in csv format. Please reformat your csv to have two columns, <code>docno</code>, and <code>judgments</code>. For example:</span>
74+
<pre class="small text-muted">
75+
docno, judgment
76+
mydocid1, Relevant,
77+
mydocid2, Nonrelevant,
78+
mydocid3, HighlyRelevant</pre>
79+
80+
{% csrf_token %}
81+
{% crispy upload_form %}
82+
83+
</div>
84+
85+
<div class="modal-footer">
86+
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
87+
<button class="btn btn-outline-secondary" id="import_csv_button" type="submit">Upload</button>
88+
</div>
89+
</form>
90+
</div>
91+
</div>
92+
</div>
93+
94+
<!-- Document Modal -->
6095
<div class="modal fade doc-modal" id="doc-modal" tabindex="-1" role="dialog">
6196
<div class="modal-dialog modal-xlg">
6297
<div class="modal-content">
@@ -145,6 +180,7 @@ <h5 class="gray-text align-left">Highlight content</h5>
145180

146181
{% block extra_scripts %}
147182
<script>
183+
148184
var loaded_docs = {}; // dict of loaded documents to prevent reloading doc content.
149185

150186
function load_doc(docid) {
@@ -319,7 +355,7 @@ <h5 class="gray-text align-left">Highlight content</h5>
319355

320356

321357
// EXPORT TO CSV
322-
const rows = [["DOCID", "JUDGMENT"]];
358+
const rows = [["docno", "judgment"]];
323359
{% for row in judgments %}
324360
rows.push(["{{ row.doc_id }}", "{% if row.highlyRelevant %}HighlyRelevant{% elif row.relevant %}Relevant{% else %}NonRelevant{% endif %}"])
325361
{% endfor %}
@@ -329,7 +365,7 @@ <h5 class="gray-text align-left">Highlight content</h5>
329365
csvContent += row + "\n";
330366
});
331367
var encodedUri = encodeURI(csvContent);
332-
$('#tocsv').attr('href', encodedUri).attr("download", "{{ request.user.current_task.topic.number }}" + ".txt");
368+
$('#export_csv').attr('href', encodedUri).attr("download", "{{ request.user.current_task.topic.number }}" + ".csv");
333369

334370
</script>
335371
{% endblock %}

HiCALWeb/hicalweb/archive/views.py

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1+
import csv
2+
import io
13
import logging
24

35
from braces import views
4-
from django.views import generic
6+
from django.contrib import messages
57
from django.db.models import Q
8+
from django.http import HttpResponseRedirect
9+
from django.urls import reverse_lazy
10+
from django.views import generic
611

12+
from hicalweb.interfaces.CAL import functions as CALFunctions
13+
from hicalweb.judgment.forms import UploadForm
714
from hicalweb.judgment.models import Judgement
15+
from hicalweb.CAL.exceptions import CALError
816

917
logger = logging.getLogger(__name__)
1018

@@ -23,8 +31,96 @@ def get_context_data(self, **kwargs):
2331
Q(nonrelevant=True)
2432
))
2533
context["judgments"] = judgments
26-
34+
context['upload_form'] = UploadForm()
2735
return context
2836

37+
def post(self, request, *args, **kwargs):
38+
try:
39+
csv_file = request.FILES['csv_file']
40+
train_model = request.POST.get('train_model')
41+
update_existing = request.POST.get('update_existing')
42+
except KeyError:
43+
messages.error(request, 'Ops! Something wrong happened. '
44+
'Could not upload judgments.')
45+
return HttpResponseRedirect(reverse_lazy('archive:main'))
46+
if not csv_file.name.endswith('.csv'):
47+
messages.error(request, 'Please upload a file ending with .csv extension.')
48+
return HttpResponseRedirect(reverse_lazy('archive:main'))
49+
50+
train_model = train_model == "on"
51+
update_existing = update_existing == "on"
52+
try:
53+
data = csv_file.read().decode('UTF-8')
54+
except UnicodeEncodeError:
55+
messages.error(request, 'Ops! Something wrong happened while encoding file.')
56+
return HttpResponseRedirect(reverse_lazy('archive:main'))
57+
58+
try:
59+
io_string = io.StringIO(data)
60+
reader = csv.DictReader(io_string)
61+
except csv.Error:
62+
messages.error(request, 'Ops! Please make sure you upload a valid csv file.')
63+
return HttpResponseRedirect(reverse_lazy('archive:main'))
64+
65+
new, updated, failed = 0, 0, 0
66+
for row in reader:
67+
try:
68+
docno, rel = row['docno'], row['judgment'].lower()
69+
except KeyError:
70+
messages.error(request, 'Ops! Please make sure you upload a valid csv file.')
71+
return HttpResponseRedirect(reverse_lazy('archive:main'))
72+
rel = 1 if rel == "relevant" else -1 if rel == "nonrelevant" else 2
73+
# Check if docid is valid
74+
if not CALFunctions.check_docid_exists(self.request.user.current_task.uuid,
75+
docno):
76+
failed += 1
77+
continue
78+
79+
# check if judged
80+
judged = Judgement.objects.filter(user=self.request.user,
81+
doc_id=docno,
82+
task=self.request.user.current_task)
83+
if train_model:
84+
try:
85+
CALFunctions.send_judgment(self.request.user.current_task.uuid,
86+
docno,
87+
1 if rel > 0 else -1)
88+
except (TimeoutError, CALError):
89+
failed += 1
90+
continue
91+
92+
if judged.exists():
93+
if update_existing:
94+
judged = judged.first()
95+
judged_rel = 2 if judged.highlyRelevant else 1 if judged.relevant else -1
96+
if judged_rel != rel:
97+
judged.highlyRelevant = rel == 2
98+
judged.relevant = rel == 1
99+
judged.nonrelevant = rel == -1
100+
judged.source = "uploaded"
101+
judged.save()
102+
updated += 1
103+
else:
104+
Judgement.objects.create(
105+
user=self.request.user,
106+
doc_id=docno,
107+
task=self.request.user.current_task,
108+
highlyRelevant=rel == 2,
109+
relevant=rel == 1,
110+
nonrelevant=rel == -1,
111+
source="uploaded",
112+
)
113+
new += 1
114+
115+
if failed:
116+
messages.error(request, 'Ops! {} judgments were not recorded.'.format(failed))
117+
118+
messages.success(request,
119+
("Added {} new judgments. ".format(new) if new else "") +
120+
("{} judgments were updated".format(updated) if updated else ""),
121+
)
122+
123+
return HttpResponseRedirect(reverse_lazy('archive:main'))
124+
29125
def get(self, request, *args, **kwargs):
30126
return super(HomePageView, self).get(self, request, *args, **kwargs)

HiCALWeb/hicalweb/interfaces/CAL/functions.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@
1212

1313

1414
def send_judgment(session, doc_id, rel, next_batch_size=5):
15+
"""
16+
Send judgment to CAL server for training
17+
:param session:
18+
:param doc_id:
19+
:param rel:
20+
:param next_batch_size:
21+
:return:
22+
"""
1523
h = httplib2.Http()
1624
url = "http://{}:{}/CAL/judge"
1725

@@ -32,6 +40,31 @@ def send_judgment(session, doc_id, rel, next_batch_size=5):
3240
raise CALServerError(resp['status'])
3341

3442

43+
def check_docid_exists(session, doc_id):
44+
"""
45+
Checks if a docid exists in collection
46+
:param session:
47+
:param doc_id:
48+
:return:
49+
"""
50+
h = httplib2.Http()
51+
url = "http://{}:{}/CAL/docid_exists?"
52+
53+
parameters = {
54+
'session_id': str(session),
55+
'doc_id': doc_id}
56+
parameters = urllib.parse.urlencode(parameters)
57+
resp, content = h.request(url.format(CAL_SERVER_IP,
58+
CAL_SERVER_PORT) + parameters,
59+
method="GET")
60+
61+
if resp and resp['status'] == '200':
62+
content = json.loads(content.decode('utf-8'))
63+
return content["exists"]
64+
else:
65+
raise CALServerError(resp['status'])
66+
67+
3568
def add_session(session, seed_query, mode):
3669
"""
3770
Adds session to CAL backend server
@@ -60,6 +93,29 @@ def add_session(session, seed_query, mode):
6093
raise CALServerError(resp['status'])
6194

6295

96+
def delete_session(session):
97+
"""
98+
Removes session from CAL memory
99+
:param session:
100+
:return:
101+
"""
102+
h = httplib2.Http()
103+
url = "http://{}:{}/CAL/delete_session"
104+
105+
body = {'session_id': str(session)}
106+
body = urllib.parse.urlencode(body)
107+
resp, content = h.request(url.format(CAL_SERVER_IP,
108+
CAL_SERVER_PORT),
109+
body=body,
110+
headers={'Content-Type': 'application/json; charset=UTF-8'},
111+
method="DELETE")
112+
113+
if resp and resp['status'] == '200':
114+
return True
115+
else:
116+
raise CALServerError(resp['status'])
117+
118+
63119
def get_documents(session, num_docs, query):
64120
"""
65121
:param session: current session

0 commit comments

Comments
 (0)