Browse Source

basic

master
欧醚 3 years ago
commit
774f489f80
4 changed files with 595 additions and 0 deletions
  1. +149
    -0
      .gitignore
  2. +91
    -0
      app.py
  3. +4
    -0
      requirements.txt
  4. +351
    -0
      templates/list.html

+ 149
- 0
.gitignore View File

@ -0,0 +1,149 @@
# ---> Python
#sqlite
*.db
#config
config.py
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# custom generated files
tmp/

+ 91
- 0
app.py View File

@ -0,0 +1,91 @@
from flask import Flask, request, render_template, send_from_directory, abort, redirect, session
from flask_sqlalchemy import SQLAlchemy
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import ipfshttpclient
from datetime import date, datetime
from werkzeug.utils import secure_filename
import os
from config import C
app = Flask(__name__)
app.config.from_object('config.C')
app.secret_key = C.session_key
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["50 / minute"],
)
db = SQLAlchemy(app)
ipfs_client = ipfshttpclient.connect()
class Paper(db.Model):
id = db.Column(db.Integer, primary_key=True)
course = db.Column(db.String(30), index=True)
teacher = db.Column(db.String(10), index=True)
year = db.Column(db.Integer, index=True)
author = db.Column(db.String(30), index=True)
create_date = db.Column(db.Date)
like_num = db.Column(db.Integer, index=True, default=0)
file_hash = db.Column(db.String(64))
db.create_all()
# TODO 登陆
@app.route('/pastExam/')
def list():
ps = Paper.query
ps = ps.order_by(db.desc('like_num'))
pagination = ps.paginate(max_per_page=100)
curr_year = date.today().year
ipfs_base_url = C.ipfs_base_url
return render_template('list.html', **locals())
@app.route('/pastExam/upload', methods=['POST'])
@limiter.limit("10 / hour")
def upload():
username = 'guest' # TODO
name = request.form.get('name')
teacher = request.form.get('teacher')
year = request.form.get('year')
year = year and year.isdigit() and int(year) or 0
# TODO 检查长度
files = request.files.getlist('files[]')
dir_name = username + str(datetime.now())
base_path = os.path.join('/tmp', dir_name)
os.mkdir(base_path)
for f in files:
filename = f.filename.replace('/','_')
print(f, filename)
f.save(os.path.join(base_path, filename))
res = ipfs_client.add(base_path)
file_hash = ''
for r in res:
if r.get('Name') == dir_name:
file_hash = r.get('Hash')
if not file_hash:
abort(500)
paper = Paper(
course=name,
teacher=teacher,
year=year,
author=username,
create_date=date.today(),
file_hash=file_hash
)
db.session.add(paper)
db.session.commit()
return redirect('.')

+ 4
- 0
requirements.txt View File

@ -0,0 +1,4 @@
Flask==1.1.2
Flask_Limiter==1.3.1
Flask_SQLAlchemy==2.4.4
ipfshttpclient==0.7.0a1

+ 351
- 0
templates/list.html View File

@ -0,0 +1,351 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" type="image/png" href="/img/ord/icon-128.png" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<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">
<meta property="og:title" content="华清大学课程攻略" />
<meta property="og:description" content="说人话就是往年考题" />
<title>华清大学课程攻略</title>
<style>
body {
background: linear-gradient(-45deg,
#fff calc(50% - 1px),
#ddd calc(50%),
#fff calc(50% + 1px)
);
background-size: 8px 7px;
}
body,
pre {
font-family: 'Noto Sans SC', sans-serif;
}
h1,
h2,
h3,
h4,
h5,
h6,
.sort-by{
font-family: 'Noto Serif SC', serif;
font-weight: 300;
}
.sort-by b {
font-weight: 700;
}
a,
a:hover,
.btn-link,
.btn-link:hover {
color: inherit;
text-decoration: underline;
}
nav .page-link {
color: #000;
}
nav .page-item.active .page-link {
background-color: #000;
border-color: #000;
}
.btn-lg {
font-size: 2em;
font-family: 'Noto Serif SC', serif;
}
.part1 {
max-width: 500px;
float: left;
padding-right: 10px;
position: relative;
}
.input-form {
position: absolute;
top: 0;
overflow-y: auto;
overflow-x: hidden;
background-color: #0006;
height: 100%;
}
.part2 {
min-width: 200px;
overflow: hidden;
padding-left: 25px;
}
.card-header {
padding: .15em 1.25em;
}
.qbox {
border: 2px black solid;
background: #fffa;
padding: 5px 20px;
color: black;
margin: 5px 5px 40px;
}
.new .qbox {
background: white;
color: black;
border: none;
}
.new .qbox input {
border: none;
border-bottom: 1px solid black;
background: transparent;
color: black;
border-radius: 0;
}
.xnew .qbox .custom-select, .xnew .qbox .custom-file-label {
background-color: black;
color: white;
}
.qbox .inner {
margin: 15px 0 20px 15px;
}
.like {
fill: #fff;
}
.liked {
fill: #000;
}
.display_name {
margin: 0;
}
.card-body {
padding: 0.75em;
}
.new {
position: relative;
margin: 30px 20px 30px 0;
}
.footer {
background: black;
color: white;
text-align: center;
font-size: 80%;
border-top: solid 1px white;
}
.footer p {
margin: 10px 0 0;
float: bottom;
}
</style>
</head>
<body>
<div class="container" style="overflow: hidden;min-height: 100vh">
<div style='padding:15px'>
<h1>华清大学<br>&nbsp;课程攻略</h1>
</div>
<hr>
<div class="part1 new">
<form action="upload" method="post" enctype="multipart/form-data">
<div class="form-group qbox">
<h1 style="margin: -16px -26px 35px">上传</h1>
<p>欢迎分享你手上的往年考题,大家一起实现信息共享,消除信息不对等。课程名请与已有的保持一致,方便查询。</p>
<br>
<div class="form-group row">
<label class="col-sm-3 col-form-label mb-0">课程名</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="name" required="required" placeholder="课程名">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">任课教师</label>
<div class="col-sm-9">
<input type="text" class="form-control" name="teacher" required="required" placeholder="任课教师">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">年份</label>
<div class="col-sm-9">
<select class="custom-select" name="year">
<option selected>不详/不适用</option>
{% for y in range(curr_year, curr_year-10, -1) %}
<option value="{{y}}">{{y}}</option>
{% endfor %}
</select>
</div>
</div>
<br>
<div class="custom-file">
<input type="file" class="custom-file-input" name="files[]" multiple required>
<label class="custom-file-label" for="customFile" data-browse="浏览">选择文件,可以多选</label>
</div>
<p> TODO: 设置是否匿名展示 </p>
<button type="submit" class="btn btn-link btn-lg mt-4 mb-4">分享!</button>
<p><small>*发布内容的版权属于上传者/原作者/公有领域,基于上传前的版权情况。文件可以多选,电脑端一般是按住ctrl选择,移动端一般是直接勾选多个。对文件数量和大小没有限制,总大小不要太过分就行。时间请填写该学期开始时的年份。</small></p>
</div>
</form>
</div>
<div class="part2" id="part2">
<div id="accordion">
<div class="card">
<div class="card-header" id="headingOne">
<h5 class="mb-0">
<button class="btn btn-link" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
课程
</button>
</h5>
</div>
<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordion">
<div class="card-body">
#课程1 #课程2 TODO
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="headingTwo">
<h5 class="mb-0">
<button class="btn btn-link collapsed" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
教师
</button>
</h5>
</div>
<div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordion">
<div class="card-body">
#教师1 #教师2 TODO
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="headingThree">
<h5 class="mb-0">
<button class="btn btn-link collapsed" data-toggle="collapse" data-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
年份
</button>
</h5>
</div>
<div id="collapseThree" class="collapse" aria-labelledby="headingThree" data-parent="#accordion">
<div class="card-body">
#年份1 #年份2 TODO
</div>
</div>
</div>
</div>
{% for p in pagination.items %}
<div class="qbox">
{% if showPrivate %}
<form action="{{c.id}}/delete" method="post">
<input type="hidden" name="key" value="{{key}}">
<button type="submit" class="close" aria-label="Close">
<span>×</span>
</button>
</form>
{% endif %}
<p class="inner">
{{p.course}} - {{p.teacher}} - {{p.year or '/'}}
<small><a href="{{ipfs_base_url}}/{{p.file_hash}}" target="_black">查看</a></small>
</p>
<div style="text-align:right;margin: 27px 0 -5px">
<small>@{{p.author}} | {{p.create_date}}</small>
<a href="##" class="btn btn-link" id="like-{{p.id}}" onClick="like('{{p.id}}')" style="text-decoration: none;">
<svg viewBox="-20 0 552 512" height="16" class="{{'like'}}">
<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-38.358,0-73.391,15.781-98.645,44.435C13.267,101.605,0,138.213,0,177.351c0,42.603,16.633,82.228,52.345,124.7
c31.917,37.96,77.834,77.088,131.005,122.397c19.813,16.884,40.302,34.344,62.115,53.429l0.655,0.574
c2.828,2.476,6.354,3.713,9.88,3.713s7.052-1.238,9.88-3.713l0.655-0.574c21.813-19.085,42.302-36.544,62.118-53.431
c53.168-45.306,99.085-84.434,131.002-122.395C495.367,259.578,512,219.954,512,177.351
C512,138.213,498.733,101.605,474.644,74.27z" />
</svg>
<span>
{{p.like_num}}
</span>
</a>
</div>
</div>
{% endfor %}
<nav>
<ul class="pagination">
{%- for page in pagination.iter_pages() %}
{% if page %}
{% if page != pagination.page %}
<li class="page-item"><a class="page-link" href="{{ url_for('can_list', page=page, per_page=pagination.per_page, key=key, sort_by=sort_by) }}">{{ page }}</a></li>
{% else %}
<li class="page-item active">
<a class="page-link" href="#">{{ page }}<span class="sr-only">(current)</span></a>
</li>
{% endif %}
{% else %}
<li class="page-item"><span class=ellipsis></span></li>
{% endif %}
{%- endfor %}
</ul>
</nav>
</div>
</div>
<div class="footer">
<p>
<a href="//closed.social" target="_blank">闭社</a>提供技术支持,本系统开源于<a href="//git.closed.social/closed-social/pastExam">碧茶</a>
</p>
<p> 🄯 2020 Copyleft: closed.social</p>
</div>
</body>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bs-custom-file-input/dist/bs-custom-file-input.min.js" crossorigin="anonymous"></script>
<script>
function like(toot) {
if ($(`#like-${toot} svg`).hasClass("liked")) {
alert('赞都赞了,别撤回嘛');
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");
},
error: (xhr, status, error) => {
alert(error + ': ' + xhr.responseText);
}
});
}
$(document).ready(function () {
bsCustomFileInput.init()
})
</script>
</html>

Loading…
Cancel
Save