Skip to content

Commit

Permalink
Merge branch 'release/v1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
kylase committed May 30, 2018
2 parents 41c26e9 + d3df812 commit 745aacf
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 77 deletions.
27 changes: 24 additions & 3 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
from flask import Flask, Blueprint, render_template
from flask_restful import Api
from flask_restful_swagger_2 import Api, get_swagger_blueprint
from flask_swagger_ui import get_swaggerui_blueprint
from app.resources.documents import Compare

def create_app(config):
app = Flask(__name__)
app.config.from_object(config)

API_DOC_PATH = '/api/docs'
SWAGGER_PATH = '/api/swagger'

api_bp = Blueprint('api', __name__)
api = Api(api_bp)
api = Api(api_bp, add_api_spec_resource=False)
api.add_resource(Compare, '/api/documents/compare')
app.register_blueprint(api_bp)

docs = []
docs.append(api.get_swagger_doc())

swagger_ui_blueprint = get_swaggerui_blueprint(
API_DOC_PATH,
SWAGGER_PATH + '.json',
config={
'app_name': 'TopicDiff API'
}
)

app.register_blueprint(api.blueprint)
app.register_blueprint(get_swagger_blueprint(docs, SWAGGER_PATH,
title='TopicDiff API',
api_version='1.0',
base_path='/'))
app.register_blueprint(swagger_ui_blueprint, url_prefix=API_DOC_PATH)

@app.errorhandler(404)
def not_found(error):
Expand Down
4 changes: 4 additions & 0 deletions app/common/topic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ def _sanitize(self, text):

def infer(self, text):
"""
First, convert text to vector using gensim's Dictionary doc2bow after simple
sanitisation.
Return the inferred topics
"""
tf = self.tf.doc2bow(self._sanitize(text))

Expand Down
61 changes: 51 additions & 10 deletions app/resources/documents.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,62 @@
import os
from collections import defaultdict
from flask_restful import Resource, reqparse
from flask_restful import reqparse
from flask_restful_swagger_2 import swagger, Resource
from flask import current_app
from app.resources.schemas import CompareRequestBody
from app.common.topic_model import TopicModelPipeline

parser = reqparse.RequestParser()
class Compare(Resource):
parser = reqparse.RequestParser()
parser.add_argument('content', action='append', required=True)
parser.add_argument('model', type=str, choices=('wikipedia',), trim=True, default='wikipedia')
parser.add_argument('threshold', type=float, default=0.01)

parser.add_argument('content', action='append', required=True)
parser.add_argument('model', type=str, choices=('wikipedia',), trim=True, default='wikipedia')
parser.add_argument('threshold', type=float, default=0.01)
@swagger.doc({
'tags': ['Documents'],
'description': 'Infer the topics which the document(s) is/are associated to.',
'parameters': [
{
'description': 'Content that is/are going to be parsed and inferred by the topic model with certain threshold.',
'in': 'body',
'name': 'body',
'required': True,
'schema': CompareRequestBody
}
],
'responses': {
'200': {
'description': 'Successfully parsed the document(s) and inferred the topics.',
'examples': {
'application/json': {
"data": {
"1": {
"0": 0.17226679623126984,
"1": 0.08550900220870972
},
"10": {
"0": 0.08550900220870972
},
"77": {
"0": 0.07097268104553223
}
},
"model": {
"name": "wikipedia",
"total_topics": 100,
"threshold": 0.05
}
}
}
},
'400': {
'description': 'Body is not valid, such as no content is included in the body or model is not valid or threshold is out of range.'
}
}
})

class Compare(Resource):
def post(self):
"""
Submit 1 or more `content`
"""
args = parser.parse_args()
args = self.parser.parse_args()
model_type = args.get('model')
threshold = args.get('threshold')

Expand Down
19 changes: 19 additions & 0 deletions app/resources/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from flask_restful_swagger_2 import Schema

class CompareRequestBody(Schema):
type = 'object'
properties = {
'content': {
'type': 'array',
'format':'string'
},
'model': {
'type': 'string',
'enum': ['wikipedia']
},
'threshold': {
'type': 'number',
'minimum': 1e-8,
'maximum': 1.0
}
}
Binary file added app/static/favicon.ico
Binary file not shown.
121 changes: 65 additions & 56 deletions app/static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,69 +34,78 @@
headers: {
"Content-Type": "application/json; charset=UTF-8"
}
}).catch((err) => {
console.log(err);
}).catch((error) => {
if (error) {
$("button").text("Please enter some content for at least one of the textboxes above and submit again")
.removeClass("btn-dark")
.addClass("btn-danger");
}
}).then((response) => {
$(".viz").prepend(
"<h3>Topic Cloud based on model trained using <mark>" + response.model.name + "</mark> with <mark>" + response.model.total_topics + "</mark> topics </h3>" +
'<p>Topics with <span class="both">purple</span> shade means they are common to both documents. ' +
'Topics with <span class="left">red</span> and <span class="right">blue</span> shade means they are exclusive to each respective document (denoted by the colour of the border of the text boxes).</p>'
);
if (response) {
$(".viz").prepend(
'<h3>Topic Cloud <small class="text-muted"> based on model trained using <mark>' + response.model.name + '</mark> with <mark>' + response.model.total_topics + '</mark> topics that have at least <mark>' + response.model.threshold * 100 + '%</mark> association</small></h3>' +
'<p>Topics with <span class="both">purple</span> shade means they are common to both documents. ' +
'Topics with <span class="left">red</span> and <span class="right">blue</span> shade means they are exclusive to each respective document (denoted by the colour of the border of the text boxes).</p>'
);

let margin = {top: 40, right: 30, bottom: 40, left: 30},
number_per_row = 20,
box_size = (content.height - (margin.top + margin.bottom)) / (response.model.total_topics / number_per_row);
let margin = { top: 40, right: 30, bottom: 40, left: 30 },
number_per_row = 20,
box_size = (content.height - (margin.top + margin.bottom)) / (response.model.total_topics / number_per_row);

let topics_groups = svg.selectAll("g")
.data(Object.entries(response["data"]))
.enter()
.append("g");
let topics_groups = svg.selectAll("g")
.data(Object.entries(response["data"]))
.enter()
.append("g");

let labels = topics_groups.append("text")
.attr("class", "topic-label")
.attr("x", d => {
let canvas_width = content.width - (margin.left + margin.right);
return ((parseInt(d[0]) + 1) / number_per_row - Math.floor(parseInt(d[0]) / number_per_row)) * canvas_width
})
.attr("y", d => {
let canvas_height = content.height - (margin.top + margin.bottom);
return Math.floor(parseInt(d[0]) / number_per_row) * canvas_height / (response.model.total_topics / number_per_row) + margin.top
})
.text(d => {
return d[0]
});
let labels = topics_groups.append("text")
.attr("class", "topic-label")
.attr("x", d => {
let canvas_width = content.width - (margin.left + margin.right);
return ((parseInt(d[0]) + 1) / number_per_row - Math.floor(parseInt(d[0]) / number_per_row)) * canvas_width
})
.attr("y", d => {
let canvas_height = content.height - (margin.top + margin.bottom);
return Math.floor(parseInt(d[0]) / number_per_row) * canvas_height / (response.model.total_topics / number_per_row) + margin.top
})
.text(d => {
return d[0]
});

topics_groups.insert("rect", ":first-child")
.attr("class", d => {
const sum = (a, b) => a + b + 2;
const idx = Object.keys(d[1]).map(Number).reduce(sum);
switch (idx) {
case 0:
return "topic-label-box " + "left"
break;
case 1:
return "topic-label-box " + "right"
break;
case 3:
return "topic-label-box " + "both"
break;
}
})
.attr("x", function (d) {
const x = d3.select(this.parentNode).select(".topic-label").node().getBBox().x;
const width = d3.select(this.parentNode).select(".topic-label").node().getBBox().width;
topics_groups.insert("rect", ":first-child")
.attr("class", d => {
const sum = (a, b) => a + b + 2;
const idx = Object.keys(d[1]).map(Number).reduce(sum);
switch (idx) {
case 0:
return "topic-label-box " + "left"
break;
case 1:
return "topic-label-box " + "right"
break;
case 3:
return "topic-label-box " + "both"
break;
}
})
.attr("x", function (d) {
const x = d3.select(this.parentNode).select(".topic-label").node().getBBox().x;
const width = d3.select(this.parentNode).select(".topic-label").node().getBBox().width;

return x + width / 2 - box_size / 2
})
.attr("y", function (d) {
const y = d3.select(this.parentNode).select(".topic-label").node().getBBox().y;
const height = d3.select(this.parentNode).select(".topic-label").node().getBBox().height;
return y + height / 2 - box_size / 2
})
.attr("width", box_size)
.attr("height", box_size);
return x + width / 2 - box_size / 2
})
.attr("y", function (d) {
const y = d3.select(this.parentNode).select(".topic-label").node().getBBox().y;
const height = d3.select(this.parentNode).select(".topic-label").node().getBBox().height;
return y + height / 2 - box_size / 2
})
.attr("width", box_size)
.attr("height", box_size);

$("button").text("Re-submit");
$("button").text("You can change the content and re-submit again")
.removeClass("btn-danger")
.removeClass("btn-dark")
.addClass("btn-success");
}
})
event.preventDefault();
})
Expand Down
22 changes: 16 additions & 6 deletions app/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" type= "text/css" href= "{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB"
crossorigin="anonymous">
Expand All @@ -13,18 +14,27 @@
<title>{% block title %}{% endblock %}</title>
</head>
<body>
<nav class="navbar fixed-top navbar-dark bg-dark ">
<nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-dark">
<a class="navbar-brand" href="/">TopicDiff</a>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="https://github.com/kylase/cs-topic-app">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<div class="navbar-nav ml-auto">
<a class="nav-item nav-link" href="/api/docs">API</a>
<a class="nav-item nav-link" href="https://github.com/kylase/cs-topic-app">
<i class="fab fa-github"></i>
Github
</a>
</li>
</ul>
</div>
</div>
</nav>
<main role="main" class="container">
{% block body %}{% endblock %}
</main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T"
crossorigin="anonymous"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion app/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ <h2>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary btn-dark btn-lg btn-block">Submit and look at the results below</button>
<button type="submit" class="btn btn-primary btn-dark btn-lg btn-block">Submit and the topic cloud will be shown below</button>
</form>
<div class="viz"></div>
<script type=text/javascript src="{{url_for('static', filename='js/jquery-3.3.1.min.js') }}"></script>
Expand Down
4 changes: 3 additions & 1 deletion requirements/prod.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
Flask==1.0.2
gunicorn==19.8.1
gensim==3.4.0
flask_restful==0.3.6
flask_restful==0.3.6
flask-restful-swagger-2==0.35
flask-swagger-ui==3.6.0
7 changes: 7 additions & 0 deletions tests/test_swagger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import pytest

def test_swagger(client):
assert client.get('/api/swagger.json').status_code == 200

def test_api_documentation(client):
assert client.get('/api/docs').status_code == 301

0 comments on commit 745aacf

Please sign in to comment.