1
0
Fork 0
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:
Lukas Winkler 2018-03-26 21:56:25 +02:00
parent 18fc22de4d
commit 8d89773539
13 changed files with 226 additions and 77 deletions

View file

@ -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
View file

@ -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)

View file

@ -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>

View file

@ -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 %}

View file

@ -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>

View file

@ -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 %}

View file

@ -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:

View file

@ -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()

View file

@ -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
}

View file

@ -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

File diff suppressed because one or more lines are too long

View 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;
}

View file

@ -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;