mirror of
https://github.com/Findus23/se-simulator.git
synced 2024-09-19 15:53:45 +02:00
many improvements (votes, filter, header, etc.)
This commit is contained in:
parent
18fc22de4d
commit
8d89773539
13 changed files with 226 additions and 77 deletions
|
@ -46,6 +46,7 @@ class Answer(BaseModel):
|
|||
text = TextField()
|
||||
upvotes = IntegerField(default=0)
|
||||
downvotes = IntegerField(default=0)
|
||||
datetime = DateTimeField()
|
||||
question = ForeignKeyField(Question, null=True)
|
||||
user = ForeignKeyField(User)
|
||||
site = ForeignKeyField(Site)
|
||||
|
|
105
server.py
105
server.py
|
@ -1,3 +1,5 @@
|
|||
from datetime import datetime
|
||||
|
||||
import sass
|
||||
from flask import render_template, send_from_directory, abort, session, jsonify, make_response
|
||||
from flask_limiter import Limiter
|
||||
|
@ -20,23 +22,22 @@ limiter = Limiter(
|
|||
key_func=get_remote_address,
|
||||
headers_enabled=True
|
||||
)
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('peewee')
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
select = """
|
||||
*,
|
||||
((upvotes + 1.9208) / (upvotes + downvotes) -
|
||||
1.96 * SQRT((upvotes * downvotes) / (upvotes + downvotes) + 0.9604) /
|
||||
(upvotes + downvotes)) / (1 + 3.8416 / (upvotes + downvotes))
|
||||
AS ci_lower_bound
|
||||
"""
|
||||
query = Question.select(SQL(select)).order_by(SQL("ci_lower_bound DESC, random"))
|
||||
@app.route('/s/<string:site>')
|
||||
def index(site=None):
|
||||
query = Question.select(Question, User, Site, Title, SQL(utils.rating_sql)).join(Site).switch(Question).join(
|
||||
User).switch(
|
||||
Question).join(
|
||||
Title)
|
||||
if site:
|
||||
query = query.where(Site.url == site)
|
||||
site_element = Site.select().where(Site.url == site).get()
|
||||
else:
|
||||
site_element = utils.get_fallback_site()
|
||||
query = query.order_by(SQL("ci_lower_bound DESC, random"))
|
||||
# return jsonify(model_to_dict(query.get()))
|
||||
paginated_query = PaginatedQuery(query, paginate_by=10, check_bounds=True)
|
||||
pagearray = utils.create_pagination(paginated_query.get_page_count(), paginated_query.get_page())
|
||||
return render_template(
|
||||
|
@ -44,52 +45,81 @@ def index():
|
|||
pagearray=pagearray,
|
||||
num_pages=paginated_query.get_page_count(),
|
||||
page=paginated_query.get_page(),
|
||||
questions=paginated_query.get_object_list()
|
||||
questions=paginated_query.get_object_list(),
|
||||
site=site_element
|
||||
)
|
||||
|
||||
|
||||
@app.route('/q/<string:slug>')
|
||||
def question(slug):
|
||||
query = Question.select().join(Title).where(Title.slug == slug)
|
||||
query = Question.select(Question, Title, User, Site) \
|
||||
.join(Title).switch(Question) \
|
||||
.join(User).switch(Question) \
|
||||
.join(Site).where(Title.slug == slug)
|
||||
question = get_object_or_404(query)
|
||||
answers = Answer.select().where(Answer.question == question) # TODO: Sort by score
|
||||
answers = Answer.select(Answer, User, SQL(utils.rating_sql)) \
|
||||
.join(User).where(Answer.question == question) \
|
||||
.order_by(SQL("ci_lower_bound DESC"))
|
||||
return render_template(
|
||||
"detail.html",
|
||||
debug=model_to_dict(question),
|
||||
question=question,
|
||||
answers=answers
|
||||
)
|
||||
|
||||
|
||||
@app.route('/api/sites')
|
||||
def sites():
|
||||
sites = Site.select().where(Site.last_download.is_null(False))
|
||||
data = {}
|
||||
for site in sites:
|
||||
data[site.url] = (model_to_dict(site))
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@app.route('/test')
|
||||
def sdfdsfds():
|
||||
user = User.select().get()
|
||||
|
||||
for question in Question.select():
|
||||
question.upvotes = 1
|
||||
question.downvotes = 1
|
||||
question.save()
|
||||
return jsonify(
|
||||
model_to_dict(Answer.select().where((Answer.question.is_null())).get()))
|
||||
|
||||
|
||||
@app.route('/api/vote/<int:id>/<string:type>', methods=["POST"])
|
||||
@app.route('/api/vote/<string:type>/<int:id>/<string:vote>', methods=["POST"])
|
||||
@limiter.limit("10 per minute")
|
||||
def vote(id, type):
|
||||
def vote(type, id, vote):
|
||||
if "voted" not in session:
|
||||
voted = []
|
||||
else:
|
||||
voted = session["voted"]
|
||||
print(voted)
|
||||
if id in voted:
|
||||
if (type, id) in voted:
|
||||
abort(403)
|
||||
if type == "up":
|
||||
query = Question.update(upvotes=Question.upvotes + 1).where(Question.id == id)
|
||||
elif type == "down":
|
||||
query = Question.update(downvotes=Question.downvotes + 1).where(Question.id == id)
|
||||
if type == "question":
|
||||
if vote == "up":
|
||||
query = Question.update(upvotes=Question.upvotes + 1).where(Question.id == id)
|
||||
elif vote == "down":
|
||||
query = Question.update(downvotes=Question.downvotes + 1).where(Question.id == id)
|
||||
else:
|
||||
return abort(404)
|
||||
elif type == "answer":
|
||||
if vote == "up":
|
||||
query = Answer.update(upvotes=Answer.upvotes + 1).where(Answer.id == id)
|
||||
elif vote == "down":
|
||||
query = Answer.update(downvotes=Answer.downvotes + 1).where(Answer.id == id)
|
||||
else:
|
||||
return abort(404)
|
||||
else:
|
||||
return abort(404)
|
||||
voted.append(id)
|
||||
voted.append((type, id))
|
||||
session["voted"] = voted
|
||||
query.execute()
|
||||
query = Question.select(Question.upvotes, Question.downvotes).where(Question.id == id).get()
|
||||
|
||||
if type == "question":
|
||||
query = Question.select(Question.upvotes, Question.downvotes).where(Question.id == id).get()
|
||||
else:
|
||||
query = Answer.select(Answer.upvotes, Answer.downvotes).where(Answer.id == id).get()
|
||||
return jsonify({
|
||||
"upvotes": query.upvotes,
|
||||
"downvotes": query.downvotes
|
||||
|
@ -98,21 +128,22 @@ def vote(id, type):
|
|||
|
||||
@app.errorhandler(429)
|
||||
def ratelimit_handler(e):
|
||||
return make_response(
|
||||
jsonify(error="ratelimit exceeded {}".format(e.description))
|
||||
, 429
|
||||
)
|
||||
return make_response(jsonify(error="ratelimit exceeded {}".format(e.description)), 429)
|
||||
|
||||
|
||||
@app.errorhandler(403)
|
||||
def ratelimit_handler(e):
|
||||
return make_response(
|
||||
jsonify(error="access denied")
|
||||
, 403
|
||||
)
|
||||
return make_response(jsonify(error="access denied"), 403)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('peewee')
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
|
||||
|
||||
@app.route('/static/js/<path:path>')
|
||||
def send_js(path):
|
||||
return send_from_directory('web/static/js', path)
|
||||
|
|
|
@ -15,4 +15,6 @@
|
|||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/awesomplete.min.js') }}"></script>
|
||||
|
||||
</body>
|
||||
|
|
|
@ -1,27 +1,21 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'macros.html' import siteheader %}
|
||||
{% block body %}
|
||||
<header class="siteheader"
|
||||
style="background-color: {{ question.site.tag_background_color if question.site.tag_background_color!="#FFF" }};
|
||||
color: {{ question.site.link_color }}">
|
||||
<img src="{{ question.site.icon_url }}" width="30" height="30">
|
||||
<span>{{ question.site.name }}</span>
|
||||
<a class="gotolink" href="https://{{ question.site.url }}" target="_blank" rel="noopener">Go to site</a>
|
||||
</header>
|
||||
|
||||
{{ siteheader(question.site) }}
|
||||
<h1>{{ question.title.text }}</h1>
|
||||
|
||||
<div class="question">
|
||||
<div class="vote">
|
||||
<div class="content question">
|
||||
<div class="vote" data-id="{{ question.id }}" data-type="question">
|
||||
<a class="up"></a>
|
||||
<div>{{ question.upvotes - question.downvotes }}</div>
|
||||
<a class="down"></a>
|
||||
</div>
|
||||
<div class="questionbox">
|
||||
<div class="contentbox">
|
||||
|
||||
{% for paragraph in question.text.split("\n") %}
|
||||
<p>{{ paragraph }}</p>
|
||||
{% endfor %}
|
||||
<div class="questionfooter">
|
||||
<div class="contentfooter">
|
||||
<div class="authorbox">
|
||||
asked {{ prettydate(question.datetime) }}
|
||||
<br>
|
||||
|
@ -30,19 +24,26 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="answerheader">{{ answers|length }} Answers</h2>
|
||||
{% for answer in answers %}
|
||||
<hr>
|
||||
<div class="answer">
|
||||
<div class="vote">
|
||||
<div class="content answer">
|
||||
<div class="vote" data-id="{{ answer.id }}" data-type="answer" data-ranking="{{ answer.ci_lower_bound }}">
|
||||
<a class="up"></a>
|
||||
<div>{{ answer.upvotes - answer.downvotes }}</div>
|
||||
<a class="down"></a>
|
||||
</div>
|
||||
{% for paragraph in answer.text.split("\n") %}
|
||||
<p>{{ paragraph }}</p>
|
||||
{% endfor %}
|
||||
|
||||
<div class="contentbox">
|
||||
{% for paragraph in answer.text.split("\n") %}
|
||||
<p>{{ paragraph }}</p>
|
||||
{% endfor %}
|
||||
<div class="contentfooter">
|
||||
<div class="authorbox">
|
||||
answered {{ prettydate(answer.datetime) }}
|
||||
<br>
|
||||
{{ answer.user.username }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<pre>{{ debug|pprint(True) }}</pre>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'macros.html' import pagination %}
|
||||
{% from 'macros.html' import pagination, siteheader %}
|
||||
{% block body %}
|
||||
{{ siteheader(site) }}
|
||||
|
||||
<label for="siteselector">Seite</label>
|
||||
<input id="siteselector" class="awesomplete" value="{{ site.url if not site.fallback }}"/>
|
||||
{% if not site.fallback %}
|
||||
|
||||
<a href="{{ url_for("index") }}">Clear filter</a>
|
||||
{% endif %}
|
||||
{{ pagination(pagearray, num_pages, page, True) }}
|
||||
{% for question in questions %}
|
||||
<div class="question"
|
||||
<div class="content question"
|
||||
style="border-right-color:{{ question.site.tag_foreground_color }};background-color:{{ question.site.tag_background_color }}">
|
||||
<div class="vote" data-id="{{ question.id }}">
|
||||
<div class="vote" data-id="{{ question.id }}" data-type="question">
|
||||
<a class="up"></a>
|
||||
<div>{{ question.upvotes - question.downvotes }}</div>
|
||||
<a class="down"></a>
|
||||
</div>
|
||||
<div class="questionbox">
|
||||
<div class="contentbox">
|
||||
<a href="https://{{ question.site.url }}" class="sitename" target="_blank" rel="noopener">
|
||||
{{ question.site.name }}
|
||||
</a>
|
||||
|
|
|
@ -42,3 +42,12 @@
|
|||
</div>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro siteheader(site) %}
|
||||
<header class="siteheader"
|
||||
style="background-color: {{ site.tag_background_color if site.tag_background_color!="#FFF" }};
|
||||
color: {{ site.link_color }}">
|
||||
<img src="{{ site.icon_url }}" width="30" height="30">
|
||||
<span>{{ site.name }}</span>
|
||||
<a class="gotolink" href="https://{{ site.url }}" target="_blank" rel="noopener">Go to site</a>
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
|
|
@ -74,7 +74,7 @@ def generate_text(chain: markovify.Text, model):
|
|||
if model == "Questions" or "Answers":
|
||||
paragraphs = []
|
||||
sentences = []
|
||||
count = int((random.randint(2, 6) * random.randint(2, 6) / 5))
|
||||
count = int((random.randint(2, 6) * random.randint(3, 6) / 5))
|
||||
for _ in range(count):
|
||||
sentences.append(chain.make_sentence())
|
||||
if random.random() < 0.4:
|
||||
|
|
1
todb.py
1
todb.py
|
@ -57,7 +57,6 @@ def add_question(site, count=100):
|
|||
num_answers = random.randint(1, 4)
|
||||
answers = Answer.select().where((Answer.site == site) & (Answer.question.is_null())).limit(num_answers)
|
||||
for answer in answers:
|
||||
print("question {} goes to answer {}".format(answer.id, question.id))
|
||||
answer.question = question
|
||||
answer.save()
|
||||
|
||||
|
|
19
utils.py
19
utils.py
|
@ -98,4 +98,21 @@ def create_pagination(num_pages, page, padding=2):
|
|||
|
||||
|
||||
def rand():
|
||||
return random.randint(-2**31, 2**31-1)
|
||||
return random.randint(-2 ** 31, 2 ** 31 - 1)
|
||||
|
||||
|
||||
rating_sql = """
|
||||
((upvotes + 1.9208) / (upvotes + downvotes) -
|
||||
1.96 * SQRT((upvotes * downvotes) / (upvotes + downvotes) + 0.9604) /
|
||||
(upvotes + downvotes)) / (1 + 3.8416 / (upvotes + downvotes))
|
||||
AS ci_lower_bound
|
||||
"""
|
||||
|
||||
|
||||
def get_fallback_site():
|
||||
return {
|
||||
"name": "Stack Exchange",
|
||||
"url": "stackexchange.com/",
|
||||
"icon_url": "https://cdn.sstatic.net/Sites/stackexchange/img/apple-touch-icon.png",
|
||||
"fallback": True
|
||||
}
|
||||
|
|
|
@ -3,12 +3,14 @@ document.addEventListener("DOMContentLoaded", function (event) {
|
|||
console.warn(vote);
|
||||
Array.prototype.forEach.call(vote, function (elvote) {
|
||||
var id = elvote.dataset.id;
|
||||
var type = elvote.dataset.type;
|
||||
Array.prototype.forEach.call(elvote.querySelectorAll("a"), function (el) {
|
||||
el.addEventListener("click", function (event) {
|
||||
var type = el.classList[0];
|
||||
console.info(id, type);
|
||||
console.info(elvote);
|
||||
var vote = el.classList[0];
|
||||
console.info(type, id, vote);
|
||||
var request = new XMLHttpRequest();
|
||||
request.open("POST", "/api/vote/" + id + "/" + type, true);
|
||||
request.open("POST", "/api/vote/" + type + "/" + id + "/" + vote, true);
|
||||
|
||||
request.onload = function () {
|
||||
if (this.status >= 200 && this.status < 400) {
|
||||
|
@ -28,5 +30,44 @@ document.addEventListener("DOMContentLoaded", function (event) {
|
|||
})
|
||||
});
|
||||
});
|
||||
var input = document.getElementById("siteselector");
|
||||
var request = new XMLHttpRequest();
|
||||
request.open("GET", "/api/sites", true);
|
||||
|
||||
request.onload = function () {
|
||||
if (this.status >= 200 && this.status < 400) {
|
||||
var resp = JSON.parse(this.response);
|
||||
var list = [];
|
||||
for (var key in resp) {
|
||||
if (resp.hasOwnProperty(key)) {
|
||||
var site, shortname;
|
||||
site = resp[key];
|
||||
shortname = site.url.replace(".stackexchange.com", ".SE");
|
||||
list.push({
|
||||
label: site.name + " (" + shortname + ")",
|
||||
value: site.url
|
||||
});
|
||||
}
|
||||
}
|
||||
new Awesomplete(input, {
|
||||
list: list
|
||||
});
|
||||
input.addEventListener("awesomplete-select", function (event) {
|
||||
if (!(event.text.value in resp)) { // shouldn't happen
|
||||
return false
|
||||
}
|
||||
var selectedSite=resp[event.text.value];
|
||||
window.location.href="/s/"+selectedSite.url
|
||||
|
||||
});
|
||||
} else {
|
||||
// We reached our target server, but it returned an error
|
||||
|
||||
}
|
||||
};
|
||||
request.onerror = function () {
|
||||
// There was a connection error of some sort
|
||||
};
|
||||
request.send();
|
||||
|
||||
});
|
||||
|
|
3
web/static/js/awesomplete.min.js
vendored
Normal file
3
web/static/js/awesomplete.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
33
web/static/sass/_awesomplete.base.scss
Normal file
33
web/static/sass/_awesomplete.base.scss
Normal file
|
@ -0,0 +1,33 @@
|
|||
.awesomplete [hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.awesomplete .visually-hidden {
|
||||
position: absolute;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.awesomplete {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.awesomplete > input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.awesomplete > ul {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
min-width: 100%;
|
||||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.awesomplete > ul:empty {
|
||||
display: none;
|
||||
}
|
|
@ -2,11 +2,12 @@ $link-color: #07C;
|
|||
$link-hover-color: #3af;
|
||||
|
||||
@import "../../milligram/src/milligram";
|
||||
|
||||
@import "awesomplete.base";
|
||||
@import "pagination";
|
||||
|
||||
body {
|
||||
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
|
||||
font-size: 15px;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
|
@ -14,15 +15,17 @@ pre > code {
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.question {
|
||||
.content {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 15px 5px 30px;
|
||||
border-bottom: solid lightgray 1px;
|
||||
padding: 15px 5px 15px;
|
||||
&:not(.question){
|
||||
border-bottom: solid lightgray 1px;
|
||||
}
|
||||
border-right: solid 10px transparent;
|
||||
.questionbox {
|
||||
.contentbox {
|
||||
margin-left: 10px;
|
||||
width: 100%;
|
||||
.sitename {
|
||||
|
@ -34,14 +37,14 @@ pre > code {
|
|||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.date{
|
||||
.date {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
font-size: 12px;
|
||||
color: grey;
|
||||
}
|
||||
.questionfooter {
|
||||
.contentfooter {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
margin: 0;
|
||||
|
@ -81,27 +84,28 @@ pre > code {
|
|||
border-width: 0 15px 15px 15px;
|
||||
border-color: transparent transparent #858c93 transparent;
|
||||
&:hover, &:focus, &.active {
|
||||
border-color: transparent transparent darken(#858c93,20%) transparent;
|
||||
border-color: transparent transparent darken(#858c93, 20%) transparent;
|
||||
}
|
||||
}
|
||||
.down {
|
||||
border-width: 15px 15px 0 15px;
|
||||
border-color: #858c93 transparent transparent transparent;
|
||||
&:hover, &:focus, &.active {
|
||||
border-color: darken(#858c93,20%) transparent transparent transparent;
|
||||
border-color: darken(#858c93, 20%) transparent transparent transparent;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1{
|
||||
h1, h2 {
|
||||
border-bottom: solid 1px lightgray;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 0;
|
||||
margin-top: 10px;
|
||||
color: inherit;
|
||||
}
|
||||
h2 {font-size: 18px}
|
||||
|
||||
.siteheader {
|
||||
background: #ebf2f5;
|
||||
|
|
Loading…
Reference in a new issue