Browse Source

feat: 列表与点赞

tree
欧醚 2 years ago
parent
commit
cfdaf3b727
4 changed files with 129 additions and 134 deletions
  1. +72
    -105
      app.py
  2. +2
    -3
      init_data.py
  3. +12
    -3
      static/css/ordinary.css
  4. +43
    -23
      templates/story.html

+ 72
- 105
app.py View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from functools import wraps
from flask import (Flask, request, render_template, send_from_directory, abort,
redirect, session, Blueprint, url_for)
@ -57,12 +57,12 @@ class Story(db.Model):
class Paragraph(db.Model):
id = db.Column(db.Integer, primary_key=True)
parent_id = db.Column(db.Integer, default=0, index=True)
story_id = db.Column(db.Integer, default=0, index=True)
story_id = db.Column(db.Integer, index=True)
is_hidden = db.Column(db.Boolean, default=False)
is_chosen = db.Column(db.Boolean, default=False)
text = db.Column(db.Text)
author = db.Column(db.String(30))
time = db.Column(db.DateTime)
time = db.Column(db.DateTime, default=datetime.now)
like_num = db.Column(db.Integer, default=0, index=True)
angry_num = db.Column(db.Integer, default=0)
fun_num = db.Column(db.Integer, default=0)
@ -72,13 +72,22 @@ class Paragraph(db.Model):
def create_at(self):
return self.time.strftime("%m-%d %H:%M")
# always increment 1 for the id of a new record
__table_args__ = {'sqlite_autoincrement': True}
def reaction_status(self):
user = session.get('username')
return list(zip(
'👍😡🤣😅👎',
[self.like_num, self.angry_num, self.fun_num, self.sweat_num, 0],
[
user and bool(Reaction.query.filter_by(pid=self.id, user=user, kind=i).first())
for i in range(1, 6)
],
range(1, 6)
))
class Reaction(db.Model):
id = db.Column(db.Integer, primary_key=True)
kind = db.Column(db.SmallInteger) # 1: like 2: angry 3: funny 4: sweat
kind = db.Column(db.SmallInteger) # 1: like 2: angry 3: funny 4: sweat 5:dislike
pid = db.Column(db.Integer, index=True) # id of paragraph
user = db.Column(db.String(30)) # username of user
@ -180,7 +189,7 @@ def story(story_id):
sort_by = request.args.get('sort_by', 'time')
q = Paragraph.query.filter_by(parent_id=story.tail, is_hidden=False)
q = q.order_by(Paragraph.like_num if sort_by == 'like' else
q = q.order_by(Paragraph.like_num.desc() if sort_by == 'like' else
Paragraph.id.desc())
pagination = q.paginate(max_per_page=100)
@ -192,110 +201,68 @@ def story(story_id):
return render_template('story.html', **locals())
'''
key = request.args.get('key')
sort_by = request.args.get('sort_by', 'time')
final_list = request.args.get('final_list', '')
if 'uid' not in session:
return redirect('set_session')
uid = session.get('uid')
q = Candidate.query
if final_list and C.step2.get('final_list'):
q = q.filter(Candidate.id.in_(C.step2['final_list']))
q = q.order_by(db.desc('likeNum')) if sort_by=='likeNum' else q.order_by(db.desc('id'))
pag = q.paginate(max_per_page=100)
def check_like(c):
c.liked = 'liked' if Like.query.filter_by(uid=uid, cid=c.id).count() else 'like'
return c
pag.items = map(check_like, pag.items)
vs = [{
'name': name,
'ques': ques,
'hint': hint
} for name, ques, hint, ans in C.verify
]
return render_template('list.html', pagination=pag, vs=vs, verified=session.get('verified'), showPrivate=(key==C.key), sort_by=sort_by, key=key, final_list=final_list,base_toot_url='https://%s/web/statuses/' % C.domain, step2=C.step2, text1=C.text1, text2=C.text2)
@app.route('/ordinary/new', methods=['POST'])
@limiter.limit("5 / hour; 1 / 2 second")
@need_verify
def new_one():
content = request.form.get('text')
private = request.form.get('privateText')
url = request.form.get('url')
if not content or len(content)>4000: abort(422)
if private and len(private)>1000: abort(422)
if url and not re.match('https://(cloud\.tsinghua\.edu\.cn/f/[0-9a-z]+/(\?dl=1)?|closed\.social/safeShare/\d([a-zA-Z]+)?)', url): abort(422)
if not Candidate.query.filter_by(content=content).first():
toot = th.status_post(
f"有新的自荐报名(大家可以直接在此处评论):\n\n{content}",
visibility=C.visibility
)
c = Candidate(
content=content,
private=private,
url=url,
toot=toot.id,
time=datetime.now()
)
db.session.add(c)
db.session.commit()
@bp.route('/create', methods=['POST'])
@login_required
@limiter.limit("15 / hour")
def create():
story_id = request.form.get('story-id')
text = request.form.get('text')
if not text or len(text) > 140:
abort(422)
story = Story.query.get_or_404(story_id)
return redirect(".")
context = th.status_context(toot)
replies = [
{
'disp': (t.account.display_name or t.account.acct),
'url': t.account.url,
'content': h2t.handle(t.content).replace(C.bot_name,'').strip(),
'time': str(t.created_at)
}
for t in context.descendants
]
d = list(filter(
lambda r: r['content'] == '删除' and r['url'].split('/@')[1] in C.admins,
replies
))
if d:
db.session.delete(c)
db.session.commit()
th.status_delete(toot)
return '该内容已被删除', 404
return {'replies': replies}
@limiter.limit("100 / hour")
@app.route('/ordinary/<int:toot>/like', methods=['POST'])
def like(toot):
c = Candidate.query.filter_by(toot=toot).first()
if not c:
abort(404)
uid = session['uid']
if not uid: abort(401)
if Like.query.filter_by(uid=uid, cid=c.id).first():
return '点赞过了', 403
l = Like(uid=uid, cid=c.id)
c.likeNum += 1
db.session.add(l)
p = Paragraph(
parent_id=story.tail,
story_id=story_id,
text=text,
author=session['username']
)
db.session.add(p)
db.session.commit()
return str(c.likeNum)
return redirect(story_id)
@bp.route('/react', methods=['POST'])
@login_required
@limiter.limit("4 / minute")
def react():
kind = request.form.get('kind', type=int)
pid = request.form.get('pid', type=int)
if kind not in range(1, 6):
abort(422)
p = Paragraph.query.get_or_404(pid)
d = dict(kind=kind, user=session['username'], pid=pid)
if Reaction.query.filter_by(**d).first():
return ''
db.session.add(Reaction(**d))
n = ''
if kind == 1:
p.like_num += 1
n = p.like_num
elif kind == 2:
p.angry_num += 1
n = p.angry_num
elif kind == 3:
p.fun_num += 1
n = p.fun_num
elif kind == 4:
p.sweat_num += 1
n = p.sweat_num
db.session.commit()
return str(n)
'''
@bp.route('/choose')
def choose_next():
min_like = request.args.get('min_like', type=int)
choose_new_next(min_like)
return 'ok'
app.register_blueprint(bp)

+ 2
- 3
init_data.py View File

@ -1,5 +1,4 @@
from app import Story, Paragraph, db
from datetime import datetime
db.drop_all()
db.create_all()
@ -57,11 +56,11 @@ BEGIN_WORDS = [
),
]
for idx, (title, text, avatar) in zip(range(10), BEGIN_WORDS):
for idx, (title, text, avatar) in zip(range(1, 11), BEGIN_WORDS):
s = Story(id=idx, title=title, text=text, tail=idx,
avatar="/ordinary/static/img/" + avatar)
p = Paragraph(id=idx, text=text, story_id=idx, is_chosen=True,
author="初始设定", time=datetime.now())
author="初始设定")
db.session.add(s)
db.session.add(p)

+ 12
- 3
static/css/ordinary.css View File

@ -27,6 +27,11 @@ body {
vertical-align: top;
}
.story-text {
font-size: 90%;
text-indent: 2rem;
}
.card-img-top {
margin: 5% 25%;
width: 50%;
@ -55,27 +60,31 @@ a:focus, .btn-link:focus {
}
.part1 {
width: 100%;
max-width: 500px;
float: left;
padding-right: 10px;
position: relative;
}
.part2 {
min-width: 250px;
max-width: 500px;
overflow: hidden;
padding-left: 25px;
position: relative;
}
.part-box {
min-width: 250px;
min-height: 450px;
}
.qbox {
border: 2px black solid;
background: white;
padding: 5px;
color: black;
margin: 5px 5px 40px;
min-width: 250px;
min-height: 300px;
}
#answerLogin input {

+ 43
- 23
templates/story.html View File

@ -35,18 +35,18 @@
</div>
</div>
<div class="part1">
<div class="qbox">
<div class="part1 col-md-6">
<div class="qbox part-box">
<h1 style="margin: -8px -5px 20px">{{story.title}}</h1>
<hr>
{% for p in paragraph_part %}
<p title="{{p.author}}, {{p.create_at}}, {{p.like_num}}">{{p.text}}</p>
<p class="story-text" title="{{p.author}}, {{p.create_at}}, {{p.like_num}}">{{p.text}}</p>
{% endfor %}
</div>
</div>
<div class="part2" id="part2">
<div class="paragraph-list twin front qbox">
<div class="paragraph-list twin front qbox part-box">
<h1 style="margin: -8px -5px 20px">备选后续</h1>
<span style="margin:8px" class="sort-by">
{% if sort_by == 'like' %}
@ -55,13 +55,29 @@
<b>按时间</b> | <a href="?sort_by=like#part2">按赞数</a>
{% endif %}
</span>
{% for p in pagination.items %}
<div class="qbox">
<small>No. {{p.id}}</small>
<p class="story-text">{{p.text}}</p>
<div class="text-end" title="{{p.create_at}}">
{% for emoji, num, reacted, kind in p.reaction_status() %}
<button class="btn {{'btn-secondary' if reacted else 'btn-outline-secondary can-react'}}
btn-sm" data-kind="{{kind}}" data-pid="{{p.id}}">{{emoji}} <span>{{num or ''}}</span></button>
{% endfor %}
<br>
<time class="timeago" datetime="{{p.time}}"></time>
</div>
</div>
{% endfor %}
</div>
<div class="create-paragraph twin behind qbox">
<h1 style="text-align:right;margin:-8px -15px 16px">续!</h1>
<div class="create-paragraph twin behind qbox part-box">
<h1 style="text-align:right;margin:-8px -8px 16px">续!</h1>
<form action="create" method="post">
<input type="hidden" name="story-id" value={{story_id}}>
<div class="form-group">
<textarea class="form-control" rows="7" placeholder="来续写吧,140字以内" required></textarea>
<textarea class="form-control" name="text" rows="11" placeholder="来续写吧,140字以内" required maxLength="140"></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn btn-link btn-lg">续!</button>
@ -73,7 +89,7 @@
<div class="footer">
<p>
<a href="//closed.social" target="_blank">闭社</a>提供技术支持,本报名系统开源于<a href="//git.closed.social/closed-social/">碧茶</a>
<a href="//closed.social" target="_blank">闭社</a>提供技术支持,本报名系统开源于<a href="//git.closed.social/closed-social/">碧茶</a>
</p>
<p> 🄯 2021 Copyleft: closed.social</p>
</div>
@ -121,24 +137,28 @@
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-timeago/1.6.7/locales/jquery.timeago.zh-CN.js"></script>
<script>
function like(toot) {
if ($(`#like-${toot} svg`).hasClass("liked")) {
alert('赞都赞了,别撤回嘛');
$('.can-react').click(function (e) {
if (check_login())
return;
}
$.ajax({
type: 'POST',
url: toot + '/like',
success: (result, status, xhr) => {
console.log(result + ' : ' + status);
$(`#like-${toot} span`).text(result);
$(`#like-${toot} svg`).toggleClass("like liked");
$this = $(this);
$.post(
'react',
{
'pid': $this.data('pid'),
'kind': $this.data('kind')
},
error: (xhr, status, error) => {
alert(error + ': ' + xhr.responseText);
(n) => {
$this.addClass('btn-secondary').removeClass('btn-outline-secondary can-react');
console.log(n);
if (n) {
$this.find('span').text(n);
}
}
});
}
).fail(function(xhr, status, error) {
console.log(xhr, status, error);
alert(error);
});;
});
function check_login() {
if (! "{{username}}") {

Loading…
Cancel
Save