Browse Source

适配复旦版本

fudan
欧醚 4 years ago
parent
commit
e8b7033d19
Signed by: OmmyZhang GPG Key ID: 757D312E7C9D13F7
9 changed files with 34 additions and 180 deletions
  1. +2
    -0
      .gitignore
  2. +8
    -57
      app.py
  3. BIN
     
  4. BIN
     
  5. BIN
     
  6. BIN
     
  7. BIN
     
  8. BIN
     
  9. +24
    -123
      templates/list.html

+ 2
- 0
.gitignore View File

@ -5,6 +5,8 @@
#config #config
config.py config.py
config_fudan.py
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/

+ 8
- 57
app.py View File

@ -2,41 +2,30 @@ from flask import Flask, request, render_template, send_from_directory, abort, r
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_limiter import Limiter from flask_limiter import Limiter
from flask_limiter.util import get_remote_address from flask_limiter.util import get_remote_address
from mastodon import Mastodon
import re import re
import random import random
from datetime import datetime from datetime import datetime
from dateutil.tz import tzlocal from dateutil.tz import tzlocal
import html2text import html2text
from config import C
from config_fudan import C
app = Flask(__name__) app = Flask(__name__)
app.config.from_object('config.C')
app.config.from_object('config_fudan.C')
app.secret_key = C.session_key app.secret_key = C.session_key
th = Mastodon(
access_token = C.token,
api_base_url = 'https://' + C.domain
)
limiter = Limiter( limiter = Limiter(
app, app,
key_func=get_remote_address, key_func=get_remote_address,
default_limits=["50 / minute"], default_limits=["50 / minute"],
) )
h2t = html2text.HTML2Text()
h2t.ignore_links = True
db = SQLAlchemy(app) db = SQLAlchemy(app)
class Candidate(db.Model): class Candidate(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.String(4000)) content = db.Column(db.String(4000))
private = db.Column(db.String(1000)) private = db.Column(db.String(1000))
url = db.Column(db.String(50))
time = db.Column(db.DateTime) time = db.Column(db.DateTime)
toot = db.Column(db.BigInteger)
likeNum = db.Column(db.Integer, default=0) likeNum = db.Column(db.Integer, default=0)
# always increment 1 for the id of a new record # always increment 1 for the id of a new record
@ -87,7 +76,7 @@ def can_list():
} for name, ques, hint, ans in C.verify } for name, ques, hint, ans in C.verify
] ]
return render_template('list.html', pagination=pag, vs=vs, showPrivate=(key==C.key), sort_by=sort_by, key=key)
return render_template('list.html', pagination=pag, vs=vs, showPrivate=(key==C.key), sort_by=sort_by, key=key, text1=C.text1, text2=C.text2)
@app.route('/ordinary/new', methods=['POST']) @app.route('/ordinary/new', methods=['POST'])
@limiter.limit("5 / hour; 1 / 2 second") @limiter.limit("5 / hour; 1 / 2 second")
@ -95,7 +84,6 @@ def new_one():
content = request.form.get('text') content = request.form.get('text')
private = request.form.get('privateText') private = request.form.get('privateText')
url = request.form.get('url')
for name, ques, hint, ans in C.verify: for name, ques, hint, ans in C.verify:
if request.form.get(name) != ans: if request.form.get(name) != ans:
@ -114,19 +102,11 @@ def new_one():
if not content or len(content)>4000: abort(422) if not content or len(content)>4000: abort(422)
if private and len(private)>1000: 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(): if not Candidate.query.filter_by(content=content).first():
toot = th.status_post(
f"有新的自荐报名(大家可以直接在此处评论):\n\n{content}",
visibility=C.visibility
)
c = Candidate( c = Candidate(
content=content, content=content,
private=private, private=private,
url=url,
toot=toot.id,
time=datetime.now() time=datetime.now()
) )
db.session.add(c) db.session.add(c)
@ -134,48 +114,19 @@ def new_one():
return redirect(".") return redirect(".")
@limiter.limit("100 / hour; 2 / second")
@app.route('/ordinary/<int:toot>/comments')
def get_comments(toot):
c = Candidate.query.filter_by(toot=toot).first()
if not c:
abort(404)
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") @limiter.limit("100 / hour")
@app.route('/ordinary/<int:toot>/like', methods=['POST'])
def like(toot):
c = Candidate.query.filter_by(toot=toot).first()
@app.route('/ordinary/<int:id>/like', methods=['POST'])
def like(id):
c = Candidate.query.get(id)
if not c: if not c:
abort(404) abort(404)
uid = session['uid'] uid = session['uid']
if not uid: abort(401) if not uid: abort(401)
if Like.query.filter_by(uid=uid, cid=c.id).first():
if Like.query.filter_by(uid=uid, cid=id).first():
return '点赞过了', 403 return '点赞过了', 403
l = Like(uid=uid, cid=c.id)
l = Like(uid=uid, cid=id)
c.likeNum += 1 c.likeNum += 1
db.session.add(l) db.session.add(l)
db.session.commit() db.session.commit()

BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


+ 24
- 123
templates/list.html View File

@ -7,10 +7,10 @@
<link rel="icon" type="image/png" href="/img/ord/icon-128.png" /> <link rel="icon" type="image/png" href="/img/ord/icon-128.png" />
<link href="https://fonts.yecdn.com/css2?family=Noto+Serif+SC:wght@300;700&display=swap" rel="stylesheet"> <link href="https://fonts.yecdn.com/css2?family=Noto+Serif+SC:wght@300;700&display=swap" rel="stylesheet">
<link href="https://fonts.yecdn.com/css2?family=Noto+Sans+SC&display=swap" rel="stylesheet"> <link href="https://fonts.yecdn.com/css2?family=Noto+Sans+SC&display=swap" rel="stylesheet">
<meta property="og:title" content="华清大学特普通奖学金 报名页面" />
<meta property="og:description" content="华清大学特普通奖学金" />
<meta property="og:title" content="复旦大学特普通奖学金 报名页面" />
<meta property="og:description" content="复旦大学特普通奖学金" />
<link href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
<title>华清大学特普通奖 报名页面</title>
<title>复旦大学特普通奖 报名页面</title>
<style> <style>
body, body,
pre { pre {
@ -80,12 +80,6 @@
color: white; color: white;
} }
.judge .qbox {
background: white;
color: black;
padding: 5px 15px;
}
.twin.new .qbox input, .twin.new .qbox input,
.twin.new .qbox textarea { .twin.new .qbox textarea {
border-bottom: 2px solid; border-bottom: 2px solid;
@ -141,12 +135,13 @@
transition-duration: 0.5s; transition-duration: 0.5s;
} }
.judge {
.poster {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
margin: 0 0 30px 20px; margin: 0 0 30px 20px;
width: 90%; width: 90%;
border: 2px black solid;
} }
.twin { .twin {
overflow: hidden; overflow: hidden;
@ -187,10 +182,10 @@
</style> </style>
</head> </head>
<body style="background: #fff url('/img/ord/bg.jpg') repeat-x fixed center top / 650px">
<body style="background: #fff url('/img/ord_fudan/bg_sm.jpg') repeat-x fixed center top / 190px">
<div class="container" style="overflow: hidden;min-height: 100vh"> <div class="container" style="overflow: hidden;min-height: 100vh">
<div style='padding:15px'> <div style='padding:15px'>
<img src="/img/ord/logo.png" width="200px" />
<img src="/img/ord_fudan/logo_sm.png" width="200px" />
</div> </div>
<div class="part1"> <div class="part1">
@ -199,25 +194,14 @@
<form action="new" method="post"> <form action="new" method="post">
<div class="form-group qbox"> <div class="form-group qbox">
<h1 style="margin: -14px -13px 20px">自荐提名</h1> <h1 style="margin: -14px -13px 20px">自荐提名</h1>
<textarea class="form-control" name="text" rows="5" maxlength="4000" placeholder="
一段自我陈述,
向大家展示普通的自己。" required="required"></textarea>
<hr />
<textarea class="form-control" name="privateText" rows="4" maxlength="1000" placeholder="不想公开,但想被评委们看到的内容。
如有意正式提名,请于此留下自己的联系方式。"></textarea>
<hr />
<div class="form-group row">
<label for="url" class="col-sm-3 col-form-label">补充材料</label>
<div class="col-sm-9">
<input type="url" class="form-control" id="url" name="url" placeholder="清华云盘或safeShare链接,https://开头" pattern="https://(cloud\.tsinghua\.edu\.cn/f/[0-9a-z]+/(\?dl=1)?|closed\.social/safeShare/\d([a-zA-Z]+)?)">
</div>
</div>
<textarea class="form-control" name="text" rows="5" maxlength="4000" placeholder="{{text1}}" required="required"></textarea>
<hr /> <hr />
<textarea class="form-control" name="privateText" rows="4" maxlength="1000" placeholder="{{text2}}"></textarea>
<hr /> <hr />
{% for v in vs %} {% for v in vs %}
<div class="form-group row"> <div class="form-group row">
<label for="{{v.name}}" class="col-sm-8 col-form-label">{{v.ques}}</label>
<div class="col-sm-4">
<label for="{{v.name}}" class="col-sm-7 col-form-label">{{v.ques}}</label>
<div class="col-sm-5">
<input type="text" class="form-control" name="{{v.name}}" placeholder="{{v.hint}}" required="required"> <input type="text" class="form-control" name="{{v.name}}" placeholder="{{v.hint}}" required="required">
</div> </div>
</div> </div>
@ -226,62 +210,12 @@
</div> </div>
</form> </form>
<div style="font-size:80%;">
<ul>
<li>如需附上补充材料,请使用清华云盘。为了避免泄露姓名推荐使用<a href="/safeShare" target="_blank">safeShare</a>。其他云盘是不被接受的。</li>
<li>需要回答几个简单的问题以初步验证学生身份,入围后的线上答辩环节将于<a href="https://thu.closed.social/">闭社</a>平台进行(需清华邮箱注册)以正式验证身份。</li>
<li>如果出错(例如验证问题答错了),请回退,多数浏览器都会恢复之前填写的内容。</li>
<li>获得五个赞并留下联系方式(如微信号、手机号或邮箱)者被视为正式提名,会有工作人员与之联系并发放奖品。</li>
<li>点击倒三角按钮展开评论,发布评论请前往<a href="https://thu.closed.social/">闭社</a></li>
<li>如需删除报名,请联系工作人员(微信:ordinary_thuer)。</li>
</ul>
</div>
</div> </div>
<div class="judge twin-collapse twin behind">
<form action="judge" method="post">
<div class="form-group qbox">
<h1 style="text-align:right;margin:-12px -20px 20px">成为评委</h1>
<div style="font-size:80%">
<p>为了更好地选出十位普通人的代表,为了更好地展现大众的声音,我们希望招募更多的评委。</p>
<p>成为评委的条件:</p>
<ul>
<li>是华清大学在读学生</li>
<li>没有报名参选</li>
<li>愿意对所有报名者进行评分</li>
</ul>
<p>我们原则上不拒绝任何的评委报名。</p>
<p>目前在三个平台上建了三个评委群,分别是闭聊(Matrix),Telegram,微信。前两个消息自动同步,加入哪个都是等价的,微信群与另外两个群不连通。(注意Telegram墙内不可直接访问。闭聊由闭社提供,使用闭社帐号登陆,如使用其他服务器上的Matrix帐号加群需要验证华清身份。)</p>
<p>选择想加入的群,完成验证,进入评委群。如果不愿意加群,也可以私信主办方保持沟通和上传打分结果。</p>
</div>
<div style="text-align:center">
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="groupType" value="mx">
<label class="form-check-label" for="mx">闭聊</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="groupType" value="tg">
<label class="form-check-label" for="tg">Telegram</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="groupType" value="wx">
<label class="form-check-label" for="wx">微信</label>
</div>
</div>
<hr/>
{% for v in vs %}
<div class="form-group row">
<label for="{{v.name}}" class="col-sm-8 col-form-label">{{v.ques}}</label>
<div class="col-sm-4">
<input type="text" class="form-control" name="{{v.name}}" placeholder="{{v.hint}}" required="required">
</div>
</div>
{% endfor %}
<p>(火热开发中... 马上就可以点了)</p>
</div>
</form>
<div class="poster twin-collapse twin behind">
<a target="_blank">
<img src="/img/ord_fudan/poster_sm.jpg" width="100%">
</a>
</div> </div>
<a href="##" onclick="showNew()" class="show-mask"> <a href="##" onclick="showNew()" class="show-mask">
@ -327,7 +261,7 @@
{% endif %} {% endif %}
<div style="text-align:right;margin: 27px 0 -5px"> <div style="text-align:right;margin: 27px 0 -5px">
<time class="timeago" datetime="{{c.time}}"></time> <time class="timeago" datetime="{{c.time}}"></time>
<a href="##" class="btn btn-link" id="like-{{c.toot}}" onClick="like('{{c.toot}}')" style="text-decoration: none;">
<a href="##" class="btn btn-link" id="like-{{c.id}}" onClick="like('{{c.id}}')" style="text-decoration: none;">
<svg viewBox="-20 0 552 512" height="16" class="{{c.liked}}"> <svg viewBox="-20 0 552 512" height="16" class="{{c.liked}}">
<path stroke="#000" stroke-width="30" d="M474.644,74.27C449.391,45.616,414.358,29.836,376,29.836c-53.948,0-88.103,32.22-107.255,59.25 <path stroke="#000" stroke-width="30" d="M474.644,74.27C449.391,45.616,414.358,29.836,376,29.836c-53.948,0-88.103,32.22-107.255,59.25
c-4.969,7.014-9.196,14.047-12.745,20.665c-3.549-6.618-7.775-13.651-12.745-20.665c-19.152-27.03-53.307-59.25-107.255-59.25 c-4.969,7.014-9.196,14.047-12.745,20.665c-3.549-6.618-7.775-13.651-12.745-20.665c-19.152-27.03-53.307-59.25-107.255-59.25
@ -341,16 +275,6 @@
{{c.likeNum}} {{c.likeNum}}
</span> </span>
</a> </a>
<a class="btn btn-link request-answer" data-toggle="collapse" href="#collapse-{{c.toot}}" role="button" aria-expanded="false" aria-controls="collapse-{{c.toot}}">
<svg fill="#000" viewBox="0 24 24 20" height="16">
<path d="m0 24 l12 18 l12 -18 z"></path>
</svg>
</a>
</div>
<div class="collapse" id="collapse-{{c.toot}}">
<div class="card card-body">
加载中...
</div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
@ -378,7 +302,7 @@
</div> </div>
<div class="footer"> <div class="footer">
<p> <p>
<a href="//closed.social" target="_blank">闭社</a>提供技术支持
<a href="//closed.social" target="_blank">闭社</a>提供技术支持,本报名系统开源于<a href="//git.closed.social/closed-social/ordinary/src/branch/fudan">碧茶</a>
</p> </p>
<p> 🄯 2020 Copyleft: closed.social</p> <p> 🄯 2020 Copyleft: closed.social</p>
</div> </div>
@ -410,45 +334,22 @@
} }
$(document).ready(function(){ $(document).ready(function(){
if(window.innerWidth > 1000)
showNew();
$('.timeago').timeago(); $('.timeago').timeago();
$('.twin').click((e) => { $('.twin').click((e) => {
showNew();
if($(e.currentTarget).hasClass('front')) if($(e.currentTarget).hasClass('front'))
return; return;
let behind_box = $('.behind'); let behind_box = $('.behind');
let front_box = $('.front'); let front_box = $('.front');
behind_box.toggleClass('behind front'); behind_box.toggleClass('behind front');
front_box.toggleClass('front behind'); front_box.toggleClass('front behind');
});
$('.collapse').on('show.bs.collapse', (e) => {
let self = e.target;
let toot = self.id.split('-')[1];
$.ajax({
type: 'GET',
url: toot + '/comments',
success: (result, status, xhr) => {
if (result.replies.length) {
$(self).empty();
result.replies.forEach((rp) => {
$(self).append(`<div class="card card-body"><p class="display_name"><a href="${rp.url}">${rp.disp}</a>:</p><pre class="inner">${rp.content}</pre><time class="timeago" datetime="${rp.time}"></time></div>`)
});
$(self).find('.timeago').timeago();
} else {
$(self).empty();
$(self).append(`<div class="card card-body">(暂无评论)</div>`)
}
},
error: (xhr, status, error) => {
console.log(error, status, xhr.status, xhr.responseText);
$(self).empty();
$(self).append(`<div class="card card-body"><p style="color:red">${xhr.status} ${xhr.responseText}</p></div>`);
if (xhr.status == 404)
setTimeout(() => {
$(self).parent().remove()
}, 3000);
}
});
setTimeout(() => {
$('.poster.front a').attr('href', '/img/ord_fudan/poster.jpg');
$('.poster.behind a').removeAttr('href');
}, 300);
}); });
}); });
</script> </script>

Loading…
Cancel
Save