first_commit
75
.gitignore
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
# Python 相关忽略
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# 虚拟环境相关忽略(保留环境配置文件)
|
||||
*.env
|
||||
*.venv
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# 安装包
|
||||
*.egg
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
eggs/
|
||||
parts/
|
||||
var/
|
||||
*.log
|
||||
*.log.*
|
||||
*.swp
|
||||
*.swo
|
||||
*.pid
|
||||
|
||||
# PyCharm 相关忽略(保留项目配置文件)
|
||||
.idea/*
|
||||
!.idea/*.iml
|
||||
!.idea/modules.xml
|
||||
!.idea/workspace.xml
|
||||
!.idea/tasks.xml
|
||||
!.idea/inspectionProfiles
|
||||
|
||||
# Jupyter Notebook 忽略
|
||||
.ipynb_checkpoints/
|
||||
|
||||
# VSCode 相关忽略
|
||||
.vscode/
|
||||
!.vscode/settings.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# 操作系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 本地配置文件
|
||||
*.local
|
||||
*.dat
|
||||
*.db
|
||||
*.sqlite3
|
||||
|
||||
# 单元测试 / 覆盖率报告
|
||||
.coverage
|
||||
.tox/
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# MyPy
|
||||
.mypy_cache/
|
||||
|
||||
# 临时文件和目录
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
#深度学习模型
|
||||
models/
|
||||
|
||||
|
13
.idea/COVID-19-Detection-Flask-App-based-on-Chest-X-rays-and-CT-Scans-master.iml
generated
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.7 (covid_19_detector)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="TestRunnerService">
|
||||
<option name="PROJECT_TEST_RUNNER" value="pytest" />
|
||||
</component>
|
||||
</module>
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/COVID-19-Detection-Flask-App-based-on-Chest-X-rays-and-CT-Scans-master.iml" filepath="$PROJECT_DIR$/.idea/COVID-19-Detection-Flask-App-based-on-Chest-X-rays-and-CT-Scans-master.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
26
config.py
Normal file
@ -0,0 +1,26 @@
|
||||
import os
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
class Config: #公共配置
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
|
||||
FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
FLASKY_POSTS_PER_PAGE = 5
|
||||
FLASKY_PATIENT_PER_PAGE = 5
|
||||
@staticmethod
|
||||
def init_app(app):
|
||||
pass
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
DEBUG = 1
|
||||
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:123456@localhost/covid_detector'
|
||||
|
||||
class ProductionConfig(Config):
|
||||
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:123456@localhost/covid_detector'
|
||||
|
||||
config = {
|
||||
'development': DevelopmentConfig,
|
||||
'production': ProductionConfig,
|
||||
'default': DevelopmentConfig
|
||||
}
|
45
flask_app/__init__.py
Normal file
@ -0,0 +1,45 @@
|
||||
from flask import Flask,session
|
||||
from flask_mail import Mail
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager
|
||||
from config import config
|
||||
from flask_moment import Moment
|
||||
from flask_bootstrap import Bootstrap
|
||||
# 创建 Flask 应用对象 ;app
|
||||
# 通过配置文件或环境变量加载应用配置;
|
||||
# 初始化扩展对象,如数据库、邮件等;
|
||||
# 注册蓝图(Blueprint)对象,将不同模块的视图函数注册到应用中;
|
||||
# 返回 Flask 应用对象 。app
|
||||
mail = Mail()
|
||||
db = SQLAlchemy()
|
||||
moment = Moment()
|
||||
bootstrap=Bootstrap()
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = 'auth.login'
|
||||
def create_app(config_name):
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config[config_name])
|
||||
config[config_name].init_app(app)
|
||||
mail.init_app(app) #将 Flask 应用程序与 Flask-Mail 扩展库绑定
|
||||
db.init_app(app)
|
||||
moment.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
bootstrap.init_app(app)
|
||||
from .main import main as main_blueprint
|
||||
app.register_blueprint(main_blueprint)
|
||||
|
||||
from .auth import auth as auth_blueprint
|
||||
app.register_blueprint(auth_blueprint)
|
||||
|
||||
from .user import user as user_blueprint
|
||||
app.register_blueprint(user_blueprint)
|
||||
|
||||
from .appointment import appointment as appointment_blueprint
|
||||
app.register_blueprint(appointment_blueprint)
|
||||
|
||||
from .faqs import faqs as faqs_blueprint
|
||||
app.register_blueprint(faqs_blueprint)
|
||||
|
||||
# from .detect import detect as detect_blueprint
|
||||
# app.register_blueprint(detect_blueprint)
|
||||
return app
|
3
flask_app/appointment/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from flask import Blueprint
|
||||
appointment = Blueprint('appointment', __name__)
|
||||
from . import views
|
13
flask_app/appointment/forms.py
Normal file
@ -0,0 +1,13 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, FileField, SelectField, IntegerField
|
||||
from wtforms.validators import DataRequired, Length, Email
|
||||
from sqlalchemy import or_
|
||||
class PatientForm(FlaskForm):
|
||||
name = StringField('就诊人姓名', validators=[DataRequired(), Length(min=2, max=50)])
|
||||
id_number = StringField('证件号', validators=[DataRequired(), Length(min=18, max=18)])
|
||||
phone = StringField('手机号', validators=[DataRequired(), Length(min=11, max=11)])
|
||||
email = StringField('邮箱', validators=[DataRequired(), Email()])
|
||||
gender = SelectField('性别', choices=[('男', '男'), ('男', '女')], validators=[DataRequired()])
|
||||
age = IntegerField('年龄', validators=[DataRequired()])
|
||||
location = StringField('家庭地址', validators=[DataRequired()])
|
||||
submit = SubmitField('提交')
|
81
flask_app/appointment/views.py
Normal file
@ -0,0 +1,81 @@
|
||||
from datetime import datetime, date
|
||||
from flask import render_template, request, url_for, session, flash
|
||||
from werkzeug.utils import redirect
|
||||
|
||||
from flask_app.decorators import permission_required
|
||||
from . import appointment
|
||||
from .. import db
|
||||
from .forms import PatientForm
|
||||
from flask_login import current_user
|
||||
from ..models import User, DocComment, Workday, Appointment, Permission
|
||||
|
||||
|
||||
@appointment.route('/doctor_info/<int:docid>',methods=['GET','POST']) #咨询医生页
|
||||
def doctor_info(docid):
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
doctor = User.query.filter_by(id=docid).first()
|
||||
comments=DocComment.query.filter_by(doc_id=docid).all()
|
||||
authors = [User.query.get(comment.author_id) for comment in comments]
|
||||
comment_pairs = zip(comments, authors)
|
||||
empty_comments = (len(comments) == 0)
|
||||
return render_template('appointment/doctor_info.html', doctor=doctor,empty_comments=empty_comments,comment_pairs=comment_pairs)
|
||||
@appointment.route('/search_res')
|
||||
def search_res():
|
||||
stars = [4.50, 3.9, 4.30, 4, 3.5]
|
||||
counts = [155, 56, 100, 80, 56]
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
active_page = "date"
|
||||
q = request.args.get('q', '')
|
||||
doctors = User.query.filter_by(role_id=1).filter(User.name.ilike('%' + q + '%')).all()
|
||||
return render_template('main/date.html', active_page=active_page, doctors=doctors,stars=stars,counts=counts)
|
||||
|
||||
@appointment.route('/book_date/<int:chosen_docid>') #预约挂号
|
||||
def book_date(chosen_docid):
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
doctor=User.query.filter_by(id=chosen_docid).first()
|
||||
today = date.today()
|
||||
workdays = Workday.query.filter(Workday.doc_id == chosen_docid, Workday.date >= today).order_by(Workday.date).limit(6).all()
|
||||
return render_template('appointment/book_date.html',doctor=doctor,active_date="0",workdays=workdays,actworkdays=workdays)
|
||||
|
||||
@appointment.route('/<int:chosen_docid>/<formatted_date>') #预约挂号/具体日期
|
||||
def book_appointment(chosen_docid,formatted_date):
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
doctor=User.query.filter_by(id=chosen_docid).first()
|
||||
today = date.today()
|
||||
workdays = Workday.query.filter(Workday.doc_id == chosen_docid, Workday.date >= today).order_by(Workday.date).limit(6).all()
|
||||
date_obj = datetime.strptime(formatted_date, '%Y-%m-%d').date()
|
||||
workday=Workday.query.filter_by(doc_id=chosen_docid,date=date_obj).all()
|
||||
return render_template('appointment/book_date.html', doctor=doctor,active_date=formatted_date,workdays=workdays,actworkdays=workday)
|
||||
|
||||
|
||||
@appointment.route('/<int:chosen_docid>/<formatted_date>/<time>',methods=['GET','POST']) #申请挂号
|
||||
@permission_required(Permission.COMMENT)
|
||||
def submit_date(chosen_docid,formatted_date,time):
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
doctor = User.query.filter_by(id=chosen_docid).first()
|
||||
date_obj = datetime.strptime(formatted_date, '%Y-%m-%d').date()
|
||||
dateday=Workday.query.filter_by(doc_id=chosen_docid,date=date_obj).first()
|
||||
form = PatientForm()
|
||||
appointment = Appointment.query.filter_by(patient_id=current_user.id, doc_id=chosen_docid, date=formatted_date,
|
||||
time=time).first()
|
||||
if(appointment):
|
||||
flash("您已经预约过了")
|
||||
return render_template('appointment/submit_date.html',form=form,doctor=doctor,dateday=dateday,time=time)
|
||||
else:
|
||||
if request.method == 'POST' and form.validate_on_submit():
|
||||
if(time=="0"):
|
||||
num=dateday.afternoon_num-dateday.temp_afternoon+1
|
||||
dateday.temp_afternoon-=1
|
||||
else:
|
||||
num = dateday.morning_num - dateday.temp_morning + 1
|
||||
dateday.temp_morning -= 1
|
||||
appointment = Appointment(cost=dateday.cost,patient_id=current_user.id, doc_id=chosen_docid, date=date_obj,time=int(time),email=form.email.data,
|
||||
name=form.name.data, id_number=form.id_number.data, location=form.location.data, gender=form.gender.data,
|
||||
phone=form.phone.data, age=form.age.data,num=num)
|
||||
db.session.add(dateday)
|
||||
db.session.add(appointment)
|
||||
db.session.commit()
|
||||
flash("你已成功预约!")
|
||||
return redirect(url_for('appointment.book_date',chosen_docid=doctor.id))
|
||||
return render_template('appointment/submit_date.html',form=form,doctor=doctor,dateday=dateday,time=time)
|
||||
|
5
flask_app/auth/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
auth = Blueprint('auth', __name__)
|
||||
|
||||
from . import views
|
34
flask_app/auth/forms.py
Normal file
@ -0,0 +1,34 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
||||
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo
|
||||
from wtforms import ValidationError
|
||||
from ..models import User
|
||||
from sqlalchemy import or_
|
||||
class LoginForm(FlaskForm):
|
||||
email_or_username = StringField('邮箱/用户名', validators=[DataRequired(), Length(1, 64)])
|
||||
password = PasswordField('密码', validators=[DataRequired()])
|
||||
remember_me = BooleanField('保持登录信息') #用于让用户选择是否保持登录状态
|
||||
submit = SubmitField('登录')
|
||||
|
||||
|
||||
|
||||
class RegistrationForm(FlaskForm):
|
||||
email = StringField('邮箱', validators=[DataRequired(), Length(1, 64),
|
||||
Email()])
|
||||
username = StringField('用户名', validators=[
|
||||
DataRequired(), Length(1, 64),
|
||||
Regexp('^[A-Za-z0-9_\u4e00-\u9fa5\.]*$', 0,
|
||||
'Usernames must have only letters, numbers, dots or '
|
||||
'underscores')])
|
||||
password = PasswordField('密码', validators=[
|
||||
DataRequired(), EqualTo('password2', message='Passwords must match.')])
|
||||
password2 = PasswordField('确认密码', validators=[DataRequired()])
|
||||
submit = SubmitField('注册')
|
||||
|
||||
def validate_email(self, field): #用于验证用户输入的邮箱和用户名是否已经在数据库中被使用了
|
||||
if User.query.filter_by(email=field.data.lower()).first():
|
||||
raise ValidationError('邮箱已经注册')
|
||||
|
||||
def validate_username(self, field):
|
||||
if User.query.filter_by(username=field.data).first():
|
||||
raise ValidationError('用户名已被使用')
|
66
flask_app/auth/views.py
Normal file
@ -0,0 +1,66 @@
|
||||
from flask import render_template, redirect, request, url_for, flash, session
|
||||
from flask_login import login_user, logout_user, login_required, \
|
||||
current_user
|
||||
|
||||
from flask_app.decorators import permission_required
|
||||
from . import auth
|
||||
from .. import db
|
||||
from ..models import User, Permission
|
||||
from .forms import LoginForm, RegistrationForm
|
||||
from sqlalchemy import or_
|
||||
@auth.before_app_request
|
||||
def before_request():
|
||||
if current_user.is_authenticated:
|
||||
current_user.ping()
|
||||
|
||||
|
||||
@auth.app_errorhandler(403)
|
||||
def forbidden_error(error):
|
||||
return render_template('403.html')
|
||||
|
||||
@auth.route('/user/')
|
||||
@permission_required(Permission.COMMENT)
|
||||
def pleaselogin():
|
||||
flash("请先登录再访问该页面!")
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
@auth.route('/post/')
|
||||
@permission_required(Permission.COMMENT)
|
||||
def pleaselogin2():
|
||||
flash("请先登录再访问该页面!")
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
@auth.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter(or_(User.email == form.email_or_username.data.lower(), User.username == form.email_or_username.data)).first()
|
||||
if user is not None and user.verify_password(form.password.data): #验证成功
|
||||
login_user(user, form.remember_me.data)
|
||||
session['color'] = user.avatar_color
|
||||
next = session.pop('next', None)
|
||||
if next is None:
|
||||
next = url_for('main.root')
|
||||
return redirect(next)
|
||||
flash('账号或密码错误!')
|
||||
return render_template('auth/login.html', form=form)
|
||||
@auth.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
|
||||
@auth.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
form = RegistrationForm()
|
||||
if form.validate_on_submit():
|
||||
user = User(email=form.email.data.lower(),
|
||||
username=form.username.data,
|
||||
password=form.password.data)
|
||||
user.avatar_color = user.get_random_color()
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
flash('现在可以登录了!')
|
||||
return redirect(url_for('auth.login'))
|
||||
return render_template('auth/register.html', form=form)
|
25
flask_app/decorators.py
Normal file
@ -0,0 +1,25 @@
|
||||
from functools import wraps
|
||||
from flask import abort, render_template
|
||||
from flask_login import current_user
|
||||
from .models import Permission
|
||||
def permission_required(permission):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
abort(403)
|
||||
if not current_user.can(permission):
|
||||
abort(403)
|
||||
if 'username' in kwargs and kwargs['username'] != current_user.username:
|
||||
return render_template('error.html')
|
||||
if 'docid' in kwargs and kwargs['docid'] != str(current_user.id):
|
||||
return render_template('error.html')
|
||||
if 'uid' in kwargs and kwargs['uid'] != str(current_user.id):
|
||||
return render_template('error.html')
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
def doctor_required(f):
|
||||
return permission_required(Permission.DETECT)(f)
|
4
flask_app/detect/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from flask import Blueprint
|
||||
|
||||
detect = Blueprint('detect', __name__)
|
||||
from . import views
|
208
flask_app/detect/augmentations.py
Normal file
@ -0,0 +1,208 @@
|
||||
import math
|
||||
import tensorflow as tf
|
||||
|
||||
from data_utils import exterior_exclusion
|
||||
|
||||
|
||||
def random_rotation(image, max_degrees, bbox=None, prob=0.5):
|
||||
"""Applies random rotation to image and bbox"""
|
||||
def _rotation(image, bbox):
|
||||
# Get random angle
|
||||
degrees = tf.random.uniform([], minval=-max_degrees, maxval=max_degrees, dtype=tf.float32)
|
||||
radians = degrees * math.pi / 180.
|
||||
if bbox is not None:
|
||||
# Get offset from image center
|
||||
image_shape = tf.cast(tf.shape(image), tf.float32)
|
||||
image_height, image_width = image_shape[0], image_shape[1]
|
||||
bbox = tf.cast(bbox, tf.float32)
|
||||
center_x = image_width / 2.
|
||||
center_y = image_height / 2.
|
||||
bbox_center_x = (bbox[0] + bbox[2]) / 2.
|
||||
bbox_center_y = (bbox[1] + bbox[3]) / 2.
|
||||
trans_x = center_x - bbox_center_x
|
||||
trans_y = center_y - bbox_center_y
|
||||
|
||||
# Apply rotation
|
||||
image = _translate_image(image, trans_x, trans_y)
|
||||
bbox = _translate_bbox(bbox, image_height, image_width, trans_x, trans_y)
|
||||
image = tf.contrib.image.rotate(image, radians, interpolation='BILINEAR')
|
||||
bbox = _rotate_bbox(bbox, image_height, image_width, radians)
|
||||
image = _translate_image(image, -trans_x, -trans_y)
|
||||
bbox = _translate_bbox(bbox, image_height, image_width, -trans_x, -trans_y)
|
||||
bbox = tf.cast(bbox, tf.int32)
|
||||
|
||||
return image, bbox
|
||||
return tf.contrib.image.rotate(image, radians, interpolation='BILINEAR')
|
||||
|
||||
retval = image if bbox is None else (image, bbox)
|
||||
return tf.cond(_should_apply(prob), lambda: _rotation(image, bbox), lambda: retval)
|
||||
|
||||
|
||||
def random_bbox_jitter(bbox, image_height, image_width, max_fraction, prob=0.5):
|
||||
"""Randomly jitters bbox coordinates by +/- jitter_fraction of the width/height"""
|
||||
def _bbox_jitter(bbox):
|
||||
bbox = tf.cast(bbox, tf.float32)
|
||||
width_jitter = max_fraction*(bbox[2] - bbox[0])
|
||||
height_jitter = max_fraction*(bbox[3] - bbox[1])
|
||||
xmin = bbox[0] + tf.random.uniform([], minval=-width_jitter, maxval=width_jitter, dtype=tf.float32)
|
||||
ymin = bbox[1] + tf.random.uniform([], minval=-height_jitter, maxval=height_jitter, dtype=tf.float32)
|
||||
xmax = bbox[2] + tf.random.uniform([], minval=-width_jitter, maxval=width_jitter, dtype=tf.float32)
|
||||
ymax = bbox[3] + tf.random.uniform([], minval=-height_jitter, maxval=height_jitter, dtype=tf.float32)
|
||||
xmin, ymin, xmax, ymax = _clip_bbox(xmin, ymin, xmax, ymax, image_height, image_width)
|
||||
bbox = tf.cast(tf.stack([xmin, ymin, xmax, ymax]), tf.int32)
|
||||
return bbox
|
||||
|
||||
return tf.cond(_should_apply(prob), lambda: _bbox_jitter(bbox), lambda: bbox)
|
||||
|
||||
|
||||
def random_shift_and_scale(image, max_shift, max_scale_change, prob=0.5):
|
||||
"""Applies random shift and scale to pixel values"""
|
||||
def _shift_and_scale(image):
|
||||
shift = tf.cast(tf.random.uniform([], minval=-max_shift, maxval=max_shift, dtype=tf.int32), tf.float32)
|
||||
scale = tf.random.uniform([], minval=(1. - max_scale_change),
|
||||
maxval=(1. + max_scale_change), dtype=tf.float32)
|
||||
image = scale*(tf.cast(image, tf.float32) + shift)
|
||||
image = tf.cast(tf.clip_by_value(image, 0., 255.), tf.uint8)
|
||||
return image
|
||||
|
||||
return tf.cond(_should_apply(prob), lambda: _shift_and_scale(image), lambda: image)
|
||||
|
||||
|
||||
def random_shear(image, max_lambda, bbox=None, prob=0.5):
|
||||
"""Applies random shear in either the x or y direction"""
|
||||
shear_lambda = tf.random.uniform([], minval=-max_lambda, maxval=max_lambda, dtype=tf.float32)
|
||||
image_shape = tf.cast(tf.shape(image), tf.float32)
|
||||
image_height, image_width = image_shape[0], image_shape[1]
|
||||
|
||||
def _shear_x(image, bbox):
|
||||
image = _shear_x_image(image, shear_lambda)
|
||||
if bbox is not None:
|
||||
bbox = _shear_bbox(bbox, image_height, image_width, shear_lambda, horizontal=True)
|
||||
bbox = tf.cast(bbox, tf.int32)
|
||||
return image, bbox
|
||||
return image
|
||||
|
||||
def _shear_y(image, bbox):
|
||||
image = _shear_y_image(image, shear_lambda)
|
||||
if bbox is not None:
|
||||
bbox = _shear_bbox(bbox, image_height, image_width, shear_lambda, horizontal=False)
|
||||
bbox = tf.cast(bbox, tf.int32)
|
||||
return image, bbox
|
||||
return image
|
||||
|
||||
def _shear(image, bbox):
|
||||
return tf.cond(_should_apply(0.5), lambda: _shear_x(image, bbox), lambda: _shear_y(image, bbox))
|
||||
|
||||
retval = image if bbox is None else (image, bbox)
|
||||
return tf.cond(_should_apply(prob), lambda: _shear(image, bbox), lambda: retval)
|
||||
|
||||
|
||||
def random_exterior_exclusion(image, prob=0.5):
|
||||
"""Randomly removes visual features exterior to the patient's body"""
|
||||
def _exterior_exclusion(image):
|
||||
shape = image.get_shape()
|
||||
image = tf.py_func(exterior_exclusion, [image], tf.uint8)
|
||||
image.set_shape(shape)
|
||||
return image
|
||||
return tf.cond(_should_apply(prob), lambda: _exterior_exclusion(image), lambda: image)
|
||||
|
||||
|
||||
def _translate_image(image, delta_x, delta_y):
|
||||
"""Translate an image"""
|
||||
return tf.contrib.image.translate(image, [delta_x, delta_y], interpolation='BILINEAR')
|
||||
|
||||
|
||||
def _translate_bbox(bbox, image_height, image_width, delta_x, delta_y):
|
||||
"""Translate an bbox, ensuring coordinates lie in the image"""
|
||||
bbox = bbox + tf.stack([delta_x, delta_y, delta_x, delta_y])
|
||||
xmin, ymin, xmax, ymax = _clip_bbox(bbox[0], bbox[1], bbox[2], bbox[3], image_height, image_width)
|
||||
bbox = tf.stack([xmin, ymin, xmax, ymax])
|
||||
return bbox
|
||||
|
||||
|
||||
def _rotate_bbox(bbox, image_height, image_width, radians):
|
||||
"""Rotates the bbox by the given angle"""
|
||||
# Shift bbox to origin
|
||||
xmin, ymin, xmax, ymax = bbox[0], bbox[1], bbox[2], bbox[3]
|
||||
center_x = (xmin + xmax) / 2.
|
||||
center_y = (ymin + ymax) / 2.
|
||||
xmin = xmin - center_x
|
||||
xmax = xmax - center_x
|
||||
ymin = ymin - center_y
|
||||
ymax = ymax - center_y
|
||||
|
||||
# Rotate bbox coordinates
|
||||
radians = -radians # negate direction since y-axis is flipped
|
||||
coords = tf.stack([[xmin, ymin], [xmax, ymin], [xmin, ymax], [xmax, ymax]])
|
||||
coords = tf.transpose(tf.cast(coords, tf.float32))
|
||||
rotation_matrix = tf.stack(
|
||||
[[tf.cos(radians), -tf.sin(radians)],
|
||||
[tf.sin(radians), tf.cos(radians)]])
|
||||
new_coords = tf.matmul(rotation_matrix, coords)
|
||||
|
||||
# Find new bbox coordinates and clip to image size
|
||||
xmin = tf.reduce_min(new_coords[0, :]) + center_x
|
||||
ymin = tf.reduce_min(new_coords[1, :]) + center_y
|
||||
xmax = tf.reduce_max(new_coords[0, :]) + center_x
|
||||
ymax = tf.reduce_max(new_coords[1, :]) + center_y
|
||||
xmin, ymin, xmax, ymax = _clip_bbox(xmin, ymin, xmax, ymax, image_height, image_width)
|
||||
bbox = tf.stack([xmin, ymin, xmax, ymax])
|
||||
|
||||
return bbox
|
||||
|
||||
|
||||
def _shear_x_image(image, shear_lambda):
|
||||
"""Shear image in x-direction"""
|
||||
tform = tf.stack([1., shear_lambda, 0., 0., 1., 0., 0., 0.])
|
||||
image = tf.contrib.image.transform(
|
||||
image, tform, interpolation='BILINEAR')
|
||||
return image
|
||||
|
||||
|
||||
def _shear_y_image(image, shear_lambda):
|
||||
"""Shear image in y-direction"""
|
||||
tform = tf.stack([1., 0., 0., shear_lambda, 1., 0., 0., 0.])
|
||||
image = tf.contrib.image.transform(
|
||||
image, tform, interpolation='BILINEAR')
|
||||
return image
|
||||
|
||||
|
||||
def _shear_bbox(bbox, image_height, image_width, shear_lambda, horizontal=True):
|
||||
"""Shear bbox in x- or y-direction"""
|
||||
# Shear bbox coordinates
|
||||
xmin, ymin, xmax, ymax = bbox[0], bbox[1], bbox[2], bbox[3]
|
||||
coords = tf.stack([[xmin, ymin], [xmax, ymin], [xmin, ymax], [xmax, ymax]])
|
||||
coords = tf.transpose(tf.cast(coords, tf.float32))
|
||||
if horizontal:
|
||||
shear_matrix = tf.stack(
|
||||
[[1., -shear_lambda],
|
||||
[0., 1.]])
|
||||
else:
|
||||
shear_matrix = tf.stack(
|
||||
[[1., 0.],
|
||||
[-shear_lambda, 1.]])
|
||||
new_coords = tf.matmul(shear_matrix, coords)
|
||||
|
||||
# Find new bbox coordinates and clip to image size
|
||||
xmin = tf.reduce_min(new_coords[0, :])
|
||||
ymin = tf.reduce_min(new_coords[1, :])
|
||||
xmax = tf.reduce_max(new_coords[0, :])
|
||||
ymax = tf.reduce_max(new_coords[1, :])
|
||||
xmin, ymin, xmax, ymax = _clip_bbox(xmin, ymin, xmax, ymax, image_height, image_width)
|
||||
bbox = tf.stack([xmin, ymin, xmax, ymax])
|
||||
|
||||
return bbox
|
||||
|
||||
|
||||
def _clip_bbox(xmin, ymin, xmax, ymax, image_height, image_width):
|
||||
"""Clip bbox to valid image coordinates"""
|
||||
xmin = tf.clip_by_value(xmin, 0, image_width)
|
||||
ymin = tf.clip_by_value(ymin, 0, image_height)
|
||||
xmax = tf.clip_by_value(xmax, 0, image_width)
|
||||
ymax = tf.clip_by_value(ymax, 0, image_height)
|
||||
return xmin, ymin, xmax, ymax
|
||||
|
||||
|
||||
def _should_apply(prob):
|
||||
"""Helper function to create bool tensor with probability"""
|
||||
return tf.cast(tf.floor(tf.random_uniform([], dtype=tf.float32) + prob), tf.bool)
|
23
flask_app/detect/ct_dataset.py
Normal file
@ -0,0 +1,23 @@
|
||||
import os
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
|
||||
class COVIDxCTDataset:
|
||||
"""COVIDx CT dataset class, which handles construction of train/validation datasets"""
|
||||
def __init__(self, data_dir, image_height=512, image_width=512):
|
||||
# General parameters
|
||||
self.data_dir = data_dir
|
||||
self.image_height = image_height
|
||||
self.image_width = image_width
|
||||
|
||||
def _make_dataset(self, split_file, batch_size, is_training, balanced=True):
|
||||
"""Creates COVIDX-CT dataset for train or val split"""
|
||||
files, classes, bboxes = self._get_files(split_file)
|
||||
count = len(files)
|
||||
dataset = tf.data.Dataset.from_tensor_slices((files, classes, bboxes))
|
||||
dataset = dataset.batch(batch_size)
|
||||
return dataset, count, batch_size
|
||||
|
||||
|
||||
|
||||
|
94
flask_app/detect/predict_ct.py
Normal file
@ -0,0 +1,94 @@
|
||||
import os
|
||||
import cv2
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
from flask_app.detect.ct_dataset import COVIDxCTDataset
|
||||
IMAGE_INPUT_TENSOR = 'Placeholder:0'
|
||||
CLASS_PRED_TENSOR = 'ArgMax:0'
|
||||
CLASS_PROB_TENSOR = 'softmax_tensor:0'
|
||||
TRAINING_PH_TENSOR = 'is_training:0'
|
||||
CLASS_NAMES = ('Normal', 'Pneumonia', 'COVID-19')
|
||||
|
||||
def create_session():
|
||||
config = tf.ConfigProto()
|
||||
config.gpu_options.allow_growth = True #显存按需分配,避免预先分配固定大小的显存造成浪费
|
||||
sess = tf.Session(config=config)
|
||||
return sess
|
||||
|
||||
class COVIDNetCTRunner:
|
||||
"""Primary training/testing/inference class"""
|
||||
def __init__(self, meta_file, ckpt=None, data_dir=None, input_height=512, input_width=512):
|
||||
self.meta_file = meta_file
|
||||
self.ckpt = ckpt
|
||||
self.input_height = input_height
|
||||
self.input_width = input_width
|
||||
self.data_dir=data_dir
|
||||
if data_dir is None:
|
||||
self.dataset = None
|
||||
else:
|
||||
self.dataset = COVIDxCTDataset(
|
||||
data_dir,
|
||||
image_height=input_height,
|
||||
image_width=input_width,
|
||||
)
|
||||
|
||||
def load_graph(self):
|
||||
"""Creates new graph and session"""
|
||||
graph = tf.Graph()
|
||||
with graph.as_default():
|
||||
# Create session and load model
|
||||
sess = create_session()
|
||||
|
||||
# Load meta file
|
||||
print('Loading meta graph from ' + self.meta_file)
|
||||
saver = tf.train.import_meta_graph(self.meta_file, clear_devices=True)
|
||||
return graph, sess, saver
|
||||
|
||||
def load_ckpt(self, sess, saver):
|
||||
"""Helper for loading weights"""
|
||||
# Load weights
|
||||
if self.ckpt is not None:
|
||||
print('Loading weights from ' + self.ckpt)
|
||||
saver.restore(sess, self.ckpt)
|
||||
|
||||
|
||||
def infer(self, image_file, autocrop=False):
|
||||
image = cv2.imread(image_file, cv2.IMREAD_GRAYSCALE)
|
||||
image = cv2.resize(image, (self.input_width, self.input_height), cv2.INTER_CUBIC)
|
||||
image = image.astype(np.float32)/255.0
|
||||
image = np.expand_dims(np.stack((image, image, image), axis=-1), axis=0)
|
||||
|
||||
feed_dict = {IMAGE_INPUT_TENSOR: image, TRAINING_PH_TENSOR: False}
|
||||
graph, sess, saver = self.load_graph()
|
||||
with graph.as_default():
|
||||
self.load_ckpt(sess, saver)
|
||||
try:
|
||||
sess.graph.get_tensor_by_name(TRAINING_PH_TENSOR)
|
||||
feed_dict[TRAINING_PH_TENSOR] = False
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
class_, probs = sess.run([CLASS_PRED_TENSOR, CLASS_PROB_TENSOR], feed_dict=feed_dict)
|
||||
pred_type=CLASS_NAMES[class_[0]]
|
||||
pred_normal = round(probs[0][0],3)
|
||||
pred_pneu = round(probs[0][1],3)
|
||||
pred_covid = round(probs[0][2],3)
|
||||
return pred_type,pred_normal,pred_pneu,pred_covid
|
||||
|
||||
|
||||
def detectct(imagepath):
|
||||
model_dir="models/COVID-Net_CT-2_L"
|
||||
meta_name="model.meta"
|
||||
ckpt_name="model"
|
||||
input_height=512
|
||||
input_width=512
|
||||
meta_file = os.path.join(model_dir, meta_name)
|
||||
ckpt = os.path.join(model_dir, ckpt_name)
|
||||
runner = COVIDNetCTRunner(
|
||||
meta_file=meta_file,
|
||||
ckpt=ckpt,
|
||||
input_height=input_height,
|
||||
input_width=input_width
|
||||
)
|
||||
return runner.infer(imagepath)
|
||||
|
32
flask_app/detect/predict_sevxray.py
Normal file
@ -0,0 +1,32 @@
|
||||
import tensorflow as tf
|
||||
import os
|
||||
import numpy as np
|
||||
from .processImg import process_image_file
|
||||
def detectsev_xray(imagepath):
|
||||
weightspath = "models/COVIDNet-CXR-S"
|
||||
metaname = "model.meta"
|
||||
ckptname = "model"
|
||||
n_classes = "2"
|
||||
in_tensorname = "input_1:0"
|
||||
out_tensorname = "norm_dense_2/Softmax:0"
|
||||
input_size = 480
|
||||
top_percent = 0.08
|
||||
mapping = {'轻微': 0, '严重': 1}
|
||||
inv_mapping = {0: '轻微', 1: '严重'}
|
||||
mapping_keys = list(mapping.keys())
|
||||
sess = tf.Session()
|
||||
tf.get_default_graph()
|
||||
saver = tf.train.import_meta_graph(os.path.join(weightspath, metaname))
|
||||
saver.restore(sess, os.path.join(weightspath, ckptname))
|
||||
graph = tf.get_default_graph()
|
||||
image_tensor = graph.get_tensor_by_name(in_tensorname)
|
||||
pred_tensor = graph.get_tensor_by_name(out_tensorname)
|
||||
x = process_image_file(imagepath, input_size, top_percent=top_percent)
|
||||
x = x.astype('float32') / 255.0
|
||||
feed_dict = {image_tensor: np.expand_dims(x, axis=0)}
|
||||
pred = sess.run(pred_tensor, feed_dict=feed_dict)
|
||||
pred_type = inv_mapping[pred.argmax(axis=1)[0]]
|
||||
pred_mild = round(pred[0][mapping['轻微']], 3)
|
||||
pred_severe = round(pred[0][mapping['严重']], 3)
|
||||
return pred_type,pred_mild,pred_severe
|
||||
|
32
flask_app/detect/predict_xray.py
Normal file
@ -0,0 +1,32 @@
|
||||
import tensorflow as tf
|
||||
import os
|
||||
import numpy as np
|
||||
from .processImg import process_image_file
|
||||
def detectxray(imagepath):
|
||||
weightspath = "models/COVIDNet-CXR4-A"
|
||||
metaname = "model.meta"
|
||||
ckptname = "model-18540"
|
||||
n_classes = "3"
|
||||
in_tensorname = "input_1:0"
|
||||
out_tensorname = "norm_dense_1/Softmax:0"
|
||||
input_size = 480
|
||||
top_percent = 0.08
|
||||
mapping = {'normal': 0, 'pneumonia': 1, 'COVID-19': 2}
|
||||
inv_mapping = {0: 'normal', 1: 'pneumonia', 2: 'COVID-19'}
|
||||
mapping_keys = list(mapping.keys())
|
||||
sess = tf.Session()
|
||||
tf.get_default_graph()
|
||||
saver = tf.train.import_meta_graph(os.path.join(weightspath, metaname))
|
||||
saver.restore(sess, os.path.join(weightspath, ckptname))
|
||||
graph = tf.get_default_graph()
|
||||
image_tensor = graph.get_tensor_by_name(in_tensorname)
|
||||
pred_tensor = graph.get_tensor_by_name(out_tensorname)
|
||||
x = process_image_file(imagepath, input_size, top_percent=top_percent)
|
||||
x = x.astype('float32') / 255.0
|
||||
feed_dict = {image_tensor: np.expand_dims(x, axis=0)}
|
||||
pred = sess.run(pred_tensor, feed_dict=feed_dict)
|
||||
prediction = inv_mapping[pred.argmax(axis=1)[0]]
|
||||
pred_normal = round(pred[0][mapping['normal']], 3)
|
||||
pred_pneu = round(pred[0][mapping['pneumonia']], 3)
|
||||
pred_covid = round(pred[0][mapping['COVID-19']], 3)
|
||||
return prediction,pred_normal,pred_pneu,pred_covid
|
16
flask_app/detect/processImg.py
Normal file
@ -0,0 +1,16 @@
|
||||
import cv2
|
||||
def crop_top(img, percent=0.15): #对图像的上部进行裁剪,去除不必要信息
|
||||
offset = int(img.shape[0] * percent)
|
||||
return img[offset:]
|
||||
def central_crop(img): #裁剪为长宽相等且居中
|
||||
size = min(img.shape[0], img.shape[1])
|
||||
offset_h = int((img.shape[0] - size) / 2)
|
||||
offset_w = int((img.shape[1] - size) / 2)
|
||||
return img[offset_h:offset_h + size, offset_w:offset_w + size] #切片表示
|
||||
def process_image_file(filepath, size, top_percent=0.08, crop=True):
|
||||
img = cv2.imread(filepath) #返回三维列表 长 宽 通道
|
||||
img = crop_top(img, percent=top_percent)
|
||||
if crop:
|
||||
img = central_crop(img)
|
||||
img = cv2.resize(img, (size, size)) #默认双线性插值 调整长宽大小为size
|
||||
return img
|
107
flask_app/detect/views.py
Normal file
@ -0,0 +1,107 @@
|
||||
from flask import Flask, render_template, request, session, redirect, url_for, flash, jsonify
|
||||
from flask_login import current_user
|
||||
|
||||
from flask_app.decorators import permission_required
|
||||
from flask_app.models import Permission
|
||||
from .predict_xray import detectxray
|
||||
from .predict_ct import detectct
|
||||
from .predict_sevxray import detectsev_xray
|
||||
import os
|
||||
from . import detect
|
||||
@detect.route('/upload.html')
|
||||
@permission_required(Permission.COMMENT)
|
||||
def upload():
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
active_page = "index"
|
||||
return render_template('detect/upload.html',active_page=active_page)
|
||||
|
||||
@detect.route('/upload_chest.html')
|
||||
@permission_required(Permission.COMMENT)
|
||||
def upload_chest():
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
return render_template('detect/upload_chest.html')
|
||||
|
||||
@detect.route('/upload_ct.html')
|
||||
@permission_required(Permission.COMMENT)
|
||||
def upload_ct():
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
return render_template('detect/upload_ct.html')
|
||||
|
||||
|
||||
@detect.route('/uploaded_chest/<uid>', methods = ['POST', 'GET'])
|
||||
def uploaded_chest(uid):
|
||||
approot=detect.root_path
|
||||
dir_path = os.path.dirname(approot) #flask_app
|
||||
if request.method == 'POST':
|
||||
# check if the post request has the file part
|
||||
if 'file' not in request.files:
|
||||
flash('No file part')
|
||||
return redirect(request.url)
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
flash('No selected file')
|
||||
return redirect(request.url)
|
||||
if file:
|
||||
temp_dir = os.path.join(dir_path, 'static/images/upload_img', str(uid))
|
||||
if(not os.path.exists(temp_dir)):
|
||||
os.makedirs(temp_dir)
|
||||
file.save(os.path.join(temp_dir,'upload_chest.jpg')) #选择的图片保存到指定文件夹
|
||||
return render_template('detect/predicting.html',
|
||||
redirect_url=url_for('detect.result_chest'))
|
||||
|
||||
@detect.route('/result_chest')
|
||||
@permission_required(Permission.COMMENT)
|
||||
def result_chest():
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
approot = detect.root_path
|
||||
dir_path = os.path.dirname(approot)
|
||||
imagepath = os.path.join(dir_path, 'static/images/upload_img',str(current_user.id),'upload_chest.jpg')
|
||||
pred_type, pred_normal, pred_pneu, pred_covid = detectxray(imagepath)
|
||||
return render_template('detect/results_chest.html', pred_type=pred_type, pred_covid=pred_covid, pred_pneu=pred_pneu,
|
||||
pred_normal=pred_normal)
|
||||
|
||||
@detect.route('/uploaded_ct/<uid>', methods = ['POST', 'GET'])
|
||||
def uploaded_ct(uid):
|
||||
approot=detect.root_path
|
||||
dir_path = os.path.dirname(approot)
|
||||
if request.method == 'POST':
|
||||
# check if the post request has the file part
|
||||
if 'file' not in request.files:
|
||||
flash('No file part')
|
||||
return redirect(request.url)
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
flash('No selected file')
|
||||
return redirect(request.url)
|
||||
if file:
|
||||
temp_dir = os.path.join(dir_path, 'static/images/upload_img', str(uid))
|
||||
if (not os.path.exists(temp_dir)):
|
||||
os.makedirs(temp_dir)
|
||||
file.save(os.path.join(temp_dir, 'upload_ct.jpg')) # 选择的图片保存到指定文件夹
|
||||
return render_template('detect/predicting.html',
|
||||
redirect_url=url_for('detect.result_ct'))
|
||||
|
||||
@detect.route('/result_ct')
|
||||
@permission_required(Permission.COMMENT)
|
||||
def result_ct():
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
approot = detect.root_path
|
||||
dir_path = os.path.dirname(approot)
|
||||
imagepath = os.path.join(dir_path, 'static/images/upload_img',str(current_user.id),'upload_ct.jpg')
|
||||
pred_type,pred_normal,pred_pneu,pred_covid=detectct(imagepath)
|
||||
return render_template('detect/results_ct.html',pred_type=pred_type,pred_covid=pred_covid,pred_pneu=pred_pneu,pred_normal=pred_normal)
|
||||
|
||||
@detect.route('/sev_xray')
|
||||
def sev_xray():
|
||||
return render_template('detect/predicting.html',
|
||||
redirect_url=url_for('detect.result_sevxray'))
|
||||
|
||||
@detect.route('/result_sevxray')
|
||||
@permission_required(Permission.COMMENT)
|
||||
def result_sevxray():
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
approot = detect.root_path
|
||||
dir_path = os.path.dirname(approot)
|
||||
imagepath = os.path.join(dir_path, 'static/images/upload_img',str(current_user.id),'upload_chest.jpg')
|
||||
pred_type,pred_mild,pred_sev=detectsev_xray(imagepath)
|
||||
return render_template('detect/results_sevxray.html',pred_type=pred_type,pred_mild=pred_mild,pred_sev=pred_sev)
|
3
flask_app/faqs/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from flask import Blueprint
|
||||
faqs = Blueprint('faqs', __name__)
|
||||
from . import views
|
15
flask_app/faqs/forms.py
Normal file
@ -0,0 +1,15 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileAllowed, FileRequired
|
||||
from wtforms import StringField, SubmitField, FileField
|
||||
from wtforms.validators import DataRequired, Length
|
||||
from sqlalchemy import or_
|
||||
from flask_pagedown.fields import PageDownField
|
||||
class CommentForm(FlaskForm):
|
||||
body=StringField('',validators=[DataRequired()],render_kw={"placeholder": "我来说两句"})
|
||||
submit=SubmitField('提交')
|
||||
|
||||
class PostForm(FlaskForm):
|
||||
title = StringField('标题', validators=[DataRequired(), Length(min=5, max=50)],render_kw={"placeholder": "一句话概括你的问题"})
|
||||
content = PageDownField("正文", validators=[DataRequired()],render_kw={"placeholder": "详细描述你的问题"})
|
||||
images = FileField('插入图片', validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], '只能上传图片!'),
|
||||
FileRequired('未选择任何文件!')], render_kw={'multiple': True})
|
114
flask_app/faqs/views.py
Normal file
@ -0,0 +1,114 @@
|
||||
import os
|
||||
|
||||
from flask import render_template, request, url_for, flash, jsonify, session
|
||||
from werkzeug.utils import redirect
|
||||
|
||||
from flask_app.decorators import permission_required
|
||||
from . import faqs
|
||||
from .. import db
|
||||
from .forms import PostForm, CommentForm
|
||||
from flask_login import login_required,current_user
|
||||
from ..models import User, Post, Comment, Like, Collect, Permission
|
||||
|
||||
root_path=faqs.root_path
|
||||
root_dir=os.path.dirname(root_path)
|
||||
UPLOAD_FOLDER = os.path.join(root_dir,"static/images")
|
||||
|
||||
@faqs.route('/article/<int:id>',methods=['GET','POST'])
|
||||
@permission_required(Permission.COMMENT)
|
||||
def article(id):
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
post=Post.query.get_or_404(id)
|
||||
form=CommentForm()
|
||||
has_liked = Like.query.filter_by(user_id=current_user.id, post_id=id).first() is not None
|
||||
has_collected = Collect.query.filter_by(user_id=current_user.id, post_id=id).first() is not None
|
||||
if form.validate_on_submit():
|
||||
comment=Comment(body=form.body.data,post=post,author=current_user._get_current_object())
|
||||
post.comment_num+=1
|
||||
db.session.add(comment)
|
||||
db.session.add(post)
|
||||
db.session.commit()
|
||||
flash('评论发布成功!','success')
|
||||
form.body.data = ''
|
||||
return redirect(url_for('faqs.article',id=post.id))
|
||||
else:
|
||||
for field, errors in form.errors.items():
|
||||
for error in errors:
|
||||
flash(f'Error in {getattr(form, field).label.text}: {error}')
|
||||
comments = Comment.query.filter_by(post_id=id).all()
|
||||
authors = [User.query.get(comment.author_id) for comment in comments] #authors指发布评论的人
|
||||
comment_pairs = zip(comments, authors)
|
||||
empty_comments = (len(comments) == 0)
|
||||
return render_template('faqs/article.html',post=post,form=form,comment_pairs=comment_pairs,
|
||||
empty_comments=empty_comments,has_liked=has_liked,has_collected=has_collected)
|
||||
|
||||
|
||||
@faqs.route('/post/<username>',methods=['GET','POST'])
|
||||
@permission_required(Permission.COMMENT)
|
||||
def post(username):
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
form = PostForm()
|
||||
UPLOAD_FOLDER = os.path.join(root_dir, "static/images")
|
||||
if form.validate_on_submit():
|
||||
post = Post(title=form.title.data, content=form.content.data,
|
||||
author=current_user._get_current_object())
|
||||
img_count = 0
|
||||
filefolder = os.path.join(UPLOAD_FOLDER, "post", "none")
|
||||
if not os.path.exists(filefolder):
|
||||
os.makedirs(filefolder)
|
||||
for i, file in enumerate(request.files.getlist('images')):
|
||||
filepath = os.path.join(filefolder, str(i+1)+".jpg")
|
||||
file.save(filepath)
|
||||
img_count += 1
|
||||
post.img_count = img_count
|
||||
db.session.commit()
|
||||
new_dir_name=os.path.join(os.path.dirname(filefolder),str(post.id))
|
||||
os.rename(filefolder,new_dir_name)
|
||||
flash('您已成功发布一篇文章')
|
||||
return redirect(url_for('main.faqs'))
|
||||
return render_template('faqs/post.html', form=form)
|
||||
|
||||
@faqs.route('/like/<int:post_id>', methods=['POST', 'DELETE'])
|
||||
@login_required
|
||||
def like(post_id):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
user = current_user
|
||||
|
||||
if request.method == 'POST':
|
||||
# 处理点赞请求
|
||||
post.like_num += 1
|
||||
like = Like(post_id=post_id, user_id=user.id)
|
||||
db.session.add(like)
|
||||
db.session.add(post)
|
||||
db.session.commit()
|
||||
elif request.method == 'DELETE':
|
||||
# 处理取消点赞请求
|
||||
post.like_num -= 1
|
||||
like = Like.query.filter_by(post_id=post_id, user_id=user.id).first()
|
||||
db.session.delete(like)
|
||||
db.session.add(post)
|
||||
db.session.commit()
|
||||
# 返回点赞状态
|
||||
return jsonify()
|
||||
|
||||
@faqs.route('/collect/<int:post_id>', methods=['POST', 'DELETE'])
|
||||
@login_required
|
||||
def collect(post_id):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
user = current_user
|
||||
if request.method == 'POST':
|
||||
# 处理点赞请求
|
||||
post.collect_num += 1
|
||||
collect = Collect(post_id=post_id, user_id=user.id)
|
||||
db.session.add(collect)
|
||||
db.session.add(post)
|
||||
db.session.commit()
|
||||
elif request.method == 'DELETE':
|
||||
# 处理取消点赞请求
|
||||
post.collect_num -= 1
|
||||
collect = Like.query.filter_by(post_id=post_id, user_id=user.id).first()
|
||||
db.session.delete(collect)
|
||||
db.session.add(post)
|
||||
db.session.commit()
|
||||
# 返回点赞状态
|
||||
return jsonify()
|
4
flask_app/main/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from flask import Blueprint
|
||||
|
||||
main = Blueprint('main', __name__)
|
||||
from . import views
|
2
flask_app/main/forms.py
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
|
81
flask_app/main/views.py
Normal file
@ -0,0 +1,81 @@
|
||||
import string
|
||||
from flask import render_template, request, url_for, flash, current_app, session
|
||||
|
||||
from flask_app.decorators import permission_required
|
||||
from . import main
|
||||
from .. import db
|
||||
from flask_login import login_required,current_user
|
||||
from ..models import User, Post, Like, Collect, Feedback, Permission
|
||||
|
||||
letters = string.ascii_lowercase
|
||||
@main.route('/')
|
||||
def root():
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
active_page ="index"
|
||||
return render_template('main/index.html',active_page=active_page)
|
||||
@main.route('/index.html')
|
||||
def index():
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
active_page = "index"
|
||||
return render_template('main/index.html',active_page=active_page)
|
||||
|
||||
@main.route('/date.html')
|
||||
def date():
|
||||
stars=[4.50,4.40,4.30,4,3.5]
|
||||
counts=[155,206,100,80,56]
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
active_page="date"
|
||||
doctors = User.query.filter_by(role_id=1).all()
|
||||
return render_template('main/date.html',active_page=active_page,doctors=doctors,stars=stars,counts=counts)
|
||||
|
||||
@main.route('/medical.html')
|
||||
def medical():
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
active_page = "medical"
|
||||
should_do = ["洗手至少20秒", "出门记得戴口罩", "使用酒精消毒", "擤鼻涕时遮挡口鼻"]
|
||||
should_no=["惊慌","去人多的地方","与人物理接触","轻易听信谣言"]
|
||||
howtodo=["正确认识新冠病毒","在家抗原自测怎么做?","不同情况该怎么用药?",
|
||||
"如何居家隔离?","如何判断要不要去医院?","如何调整心态?","老人感染如何护理?","儿童感染如何护理?"]
|
||||
feedbacks=Feedback.query.all()
|
||||
return render_template('main/medical.html',should_do=should_do,should_no=should_no,howtodo=howtodo,feedbacks=feedbacks,active_page=active_page,User=User)
|
||||
|
||||
@main.route('/user/<username>')
|
||||
@permission_required(Permission.COMMENT)
|
||||
def user(username):
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
active_page ="user"
|
||||
active_page1="basic_info"
|
||||
user = User.query.filter_by(username=username).first_or_404()
|
||||
user_initial = user.username[0]
|
||||
return render_template('main/user.html', user=user,user_initial=user_initial,active_page=active_page,active_page1=active_page1)
|
||||
|
||||
@main.route('/doc/<docname>')
|
||||
@permission_required(Permission.DETECT)
|
||||
def doc_basic(docname):
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
active_page ="user"
|
||||
active_page1="basic_info"
|
||||
user = User.query.filter_by(username=docname).first_or_404()
|
||||
return render_template('main/doc_basic.html', user=user,active_page=active_page,active_page1=active_page1,user_initial=user.username[0])
|
||||
|
||||
@main.route('/faqs.html',methods=['GET','POST'])
|
||||
def faqs():
|
||||
# posts = Post.query.all()
|
||||
session['next'] = request.url # 将当前URL保存到session中
|
||||
active_page ="faqs"
|
||||
head_1=["新冠症状有哪些?","我们为什么应该居家隔离?","新冠病毒可以入侵你的肺部?","我们如何检测新冠?"]
|
||||
content_1=["一般症状为发热、乏力、干咳、味觉及嗅觉改变,部分患者起病症状轻微,甚至可恶明显发热","新冠病毒是一种高度传染性病毒,通过空气飞沫、接触传播等途径进行传播。居家隔离可避免医疗资源过度消耗和医疗系统崩溃,减少人员感染风险。",
|
||||
"新冠病毒是一种呼吸道病毒,主要通过空气飞沫传播,它会通过呼吸道向下移动,进入肺部并感染肺泡和支气管等部位。","1.RT-PCR检测:即核酸检测,这是目前最常用的检测方法 2.CT扫描和X光"]
|
||||
head_2=["盐蒸橙子/橘子能治疗感染吗?","转阴后为什么还一直咳嗽?","“阳了”后洗澡会加重病情?","“阳康”后,还要打疫苗吗?"]
|
||||
content_2=["盐蒸橙子/橘子可以补充维C,但不是药,不能发挥治疗效果","体内垃圾会变成痰液,通过咳嗽排出去,这是打扫“战场”、修复气道的康复过程。",
|
||||
"当处于急性高热严重时期,这时身体较虚弱,不建议洗澡;但一般而言,洗澡不会导致新冠症状加重。","在阳康后3个月,抗体和免疫记忆会消退至较低水平,需要用疫苗重新唤醒。"]
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = current_app.config['FLASKY_POSTS_PER_PAGE']
|
||||
pagination = Post.query.order_by(Post.timestamp.desc()).paginate(
|
||||
page, per_page, #每页最多显示记录数
|
||||
error_out=False)
|
||||
posts = pagination.items
|
||||
return render_template('main/faqs.html',posts=posts,pagination=pagination,Like=Like,Collect=Collect,
|
||||
head_1=head_1,head_2=head_2,content_1=content_1,content_2=content_2,active_page=active_page)
|
||||
|
||||
|
232
flask_app/models.py
Normal file
@ -0,0 +1,232 @@
|
||||
from datetime import datetime
|
||||
import random
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from flask_login import UserMixin,AnonymousUserMixin
|
||||
from . import db, login_manager
|
||||
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
|
||||
from flask import current_app
|
||||
|
||||
class Permission:
|
||||
FOLLOW = 1
|
||||
COMMENT = 2
|
||||
WRITE = 4
|
||||
DETECT=8
|
||||
|
||||
class Role(db.Model):
|
||||
__tablename__ = 'roles'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(64), unique=True)
|
||||
default = db.Column(db.Boolean, default=False, index=True)
|
||||
permissions = db.Column(db.Integer)
|
||||
users = db.relationship('User', backref='role', lazy='dynamic')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Role, self).__init__(**kwargs)
|
||||
if self.permissions is None:
|
||||
self.permissions = 0
|
||||
|
||||
@staticmethod
|
||||
def insert_roles():
|
||||
roles = {
|
||||
'User': [Permission.FOLLOW, Permission.COMMENT, Permission.WRITE],
|
||||
'Doctor': [Permission.FOLLOW, Permission.COMMENT,
|
||||
Permission.WRITE, Permission.DETECT,
|
||||
],
|
||||
}
|
||||
default_role = 'User'
|
||||
for r in roles:
|
||||
role = Role.query.filter_by(name=r).first()
|
||||
if role is None:
|
||||
role = Role(name=r)
|
||||
role.reset_permissions()
|
||||
for perm in roles[r]:
|
||||
role.add_permission(perm)
|
||||
role.default = (role.name == default_role)
|
||||
db.session.add(role)
|
||||
db.session.commit()
|
||||
|
||||
def add_permission(self, perm):
|
||||
if not self.has_permission(perm):
|
||||
self.permissions += perm
|
||||
|
||||
def remove_permission(self, perm):
|
||||
if self.has_permission(perm):
|
||||
self.permissions -= perm
|
||||
|
||||
def reset_permissions(self):
|
||||
self.permissions = 0
|
||||
|
||||
def has_permission(self, perm):
|
||||
return self.permissions & perm == perm
|
||||
|
||||
def __repr__(self):
|
||||
return '<Role %r>' % self.name
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
__tablename__ = 'users'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(64), unique=True, index=True)
|
||||
username = db.Column(db.String(64), unique=True, index=True)
|
||||
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
|
||||
password_hash = db.Column(db.String(128))
|
||||
name = db.Column(db.String(64))
|
||||
location = db.Column(db.String(256))
|
||||
about_me = db.Column(db.Text())
|
||||
last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
|
||||
avatar_color = db.Column(db.String(10))
|
||||
has_avatar = db.Column(db.Boolean, default=False)
|
||||
sex = db.Column(db.String(5))
|
||||
phone=db.Column(db.String(15))
|
||||
age=db.Column(db.Integer)
|
||||
department=db.Column(db.String(128))
|
||||
id_number= db.Column(db.String(64))
|
||||
posts=db.relationship('Post',backref='author',lazy='dynamic') #user表与comment为1对多, 即可通过一篇文章获得一个作者(用户)
|
||||
comments = db.relationship('Comment', backref='author', lazy='dynamic')
|
||||
def __init__(self, **kwargs):
|
||||
super(User, self).__init__(**kwargs)
|
||||
if self.role is None:
|
||||
default_role = Role.query.filter_by(default=True).first()
|
||||
self.role = default_role
|
||||
|
||||
def can(self, perm):
|
||||
return self.role is not None and self.role.has_permission(perm)
|
||||
|
||||
def is_administrator(self):
|
||||
return self.can(Permission.ADMIN)
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return True if self.id else False
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
raise AttributeError('password is not a readable attribute')
|
||||
|
||||
@password.setter
|
||||
def password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def verify_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
def get_random_color(self):
|
||||
colors = ['red', 'blue', 'orange','green']
|
||||
return random.choice(colors)
|
||||
|
||||
def ping(self):
|
||||
self.last_seen = datetime.utcnow()
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
def __repr__(self):
|
||||
return '<User %r>' % self.username
|
||||
|
||||
class Post(db.Model):
|
||||
__tablename__ = 'posts'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.Text, nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
img_count = db.Column(db.Integer, default=0)
|
||||
collect_num = db.Column(db.Integer, default=0)
|
||||
comment_num = db.Column(db.Integer, default=0)
|
||||
like_num = db.Column(db.Integer, default=0)
|
||||
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
|
||||
author_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) #一个人可以发多个文章,但每篇文章对应一个用户
|
||||
comments=db.relationship('Comment',backref='post',lazy='dynamic')
|
||||
|
||||
class AnonymousUser(AnonymousUserMixin):
|
||||
def can(self, permissions):
|
||||
return False
|
||||
|
||||
def is_administrator(self):
|
||||
return False
|
||||
|
||||
login_manager.anonymous_user = AnonymousUser
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
|
||||
class Comment(db.Model):
|
||||
__tablename__ = 'comments'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
body = db.Column(db.Text)
|
||||
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
|
||||
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))
|
||||
|
||||
class DocComment(db.Model):
|
||||
__tablename__ = 'doccomments'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
body = db.Column(db.Text)
|
||||
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
|
||||
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
doc_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
star_num=db.Column(db.Integer,default=5)
|
||||
class Like(db.Model):
|
||||
__tablename__ = 'likes'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))
|
||||
class Collect(db.Model):
|
||||
__tablename__ = 'collects'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))
|
||||
|
||||
class Feedback(db.Model):
|
||||
__tablename__='feedbacks'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
feedback=db.Column(db.Text, nullable=False)
|
||||
|
||||
class Workday(db.Model):
|
||||
__tablename__ = 'workday'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
doc_id=db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
date=db.Column(db.Date, index=True) #工作日期
|
||||
morning_num=db.Column(db.Integer, default=0)
|
||||
temp_morning=db.Column(db.Integer, default=0)
|
||||
afternoon_num=db.Column(db.Integer, default=0)
|
||||
temp_afternoon = db.Column(db.Integer, default=0)
|
||||
cost=db.Column(db.Integer, default=30)
|
||||
class Appointment(db.Model):
|
||||
__tablename__='appointment'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
patient_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
doc_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
date=db.Column(db.Date, index=True) #预约日期
|
||||
time=db.Column(db.Integer, default=-1)
|
||||
email = db.Column(db.String(256), index=True)
|
||||
id_number = db.Column(db.String(256))
|
||||
name = db.Column(db.String(256))
|
||||
gender = db.Column(db.String(256))
|
||||
location = db.Column(db.String(256))
|
||||
phone = db.Column(db.String(256))
|
||||
age = db.Column(db.Integer)
|
||||
cost=db.Column(db.Integer)
|
||||
num = db.Column(db.Integer)
|
||||
|
||||
class Call_number(db.Model):
|
||||
__tablename__ = 'call_numbers'
|
||||
id=db.Column(db.Integer, primary_key=True)
|
||||
appointment_id = db.Column(db.Integer, db.ForeignKey('appointment.id'), unique=True)
|
||||
notified = db.Column(db.Boolean, default=False)
|
||||
call_time=db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
class Report(db.Model):
|
||||
__tablename__='reports'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
appointment_id = db.Column(db.Integer, db.ForeignKey('appointment.id'), unique=True)
|
||||
diagnosis_result=db.Column(db.String(256))
|
||||
diagnosis_date=db.Column(db.String(64))
|
||||
diagnosis_advice=db.Column(db.Text, nullable=False)
|
||||
diagnosis_sign=db.Column(db.String(64))
|
||||
|
||||
class Private_message(db.Model):
|
||||
__tablename__ = 'private_messages'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
sender_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
recipient_id= db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
body=db.Column(db.Text)
|
||||
time = db.Column(db.DateTime, default=datetime.now)
|
3610
flask_app/static/css/animate.min.css
vendored
Normal file
6
flask_app/static/css/bootstrap-select.min.css
vendored
Normal file
7
flask_app/static/css/bootstrap.min.css
vendored
Normal file
157
flask_app/static/css/dark.css
Normal file
@ -0,0 +1,157 @@
|
||||
body {
|
||||
color: #9ea8c6;
|
||||
background-color: #051236;
|
||||
}
|
||||
|
||||
.blog-one__content h3,
|
||||
.funfact-one p,
|
||||
.block-title h3 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.about-one__icon-box p {
|
||||
color: #fefefe;
|
||||
}
|
||||
|
||||
.about-one__content > p {
|
||||
color: #9ea8c6;
|
||||
}
|
||||
|
||||
.blog-one__content,
|
||||
.about-one .inner-container {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.testimonials-one,
|
||||
.blog-one__home,
|
||||
.about-five,
|
||||
.service-one {
|
||||
background-color: #0a1c4f;
|
||||
}
|
||||
|
||||
.testimonials-one::before,
|
||||
.about-five::before,
|
||||
.blog-one__home::before,
|
||||
.service-one::before {
|
||||
background-image: url(../images/dark/shapes/virus-pattern-1-1.png);
|
||||
opacity: .15;
|
||||
background-blend-mode: overlay;
|
||||
background-color: #0a1c4f;
|
||||
}
|
||||
|
||||
.service-one__inner {
|
||||
background-color: #051236;
|
||||
}
|
||||
|
||||
.service-one__single:hover .service-one__inner {
|
||||
background-color: #040f2e;
|
||||
}
|
||||
|
||||
.faq-one .accrodion.active h4,
|
||||
.service-one__single h3 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.service-one__single p {
|
||||
color: #9ea8c6;
|
||||
}
|
||||
|
||||
.progress-one__box,
|
||||
.service-one__image {
|
||||
background-color: #040f2e;
|
||||
}
|
||||
|
||||
.blog-one__content,
|
||||
.service-one__single:hover .service-one__image {
|
||||
background-color: #051236;
|
||||
}
|
||||
|
||||
.prevention-one__box-top::before {
|
||||
background-image: url(../images/dark/shapes/prevent-header-shape-1-1.png);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.prevention-one__box-bottom {
|
||||
background-color: #040f2e;
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.brand-one__carousel,
|
||||
.faq-one .accrodion,
|
||||
.map-one .inner-container,
|
||||
.prevention-one__single + .prevention-one__single {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.prevention-one__icon-inner {
|
||||
background-color: #0a1c4f;
|
||||
}
|
||||
|
||||
.about-two__icon-text h3,
|
||||
.funfact-two__single i,
|
||||
.funfact-two__single h3,
|
||||
.team-one__single h3,
|
||||
.prevention-one__content h3 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.testimonials-one__text,
|
||||
.progress-one__box p,
|
||||
.prevention-one__content p {
|
||||
color: #9ea8c6;
|
||||
}
|
||||
|
||||
.faq-one .accrodion {
|
||||
background-color: #051236;
|
||||
}
|
||||
|
||||
.faq-one .accrodion.active .accrodion-title::before {
|
||||
color: var(--thm-primary);
|
||||
}
|
||||
|
||||
.team-one__sep,
|
||||
[class*="col"]:not(:last-of-type) .funfact-two__single::after, [class*="col"]:not(:last-of-type) .funfact-two__single::before {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.slider-one__wrapper::before {
|
||||
background-image: url(../images/dark/shapes/banner-2-bg-shape.png);
|
||||
}
|
||||
|
||||
.slider-one__video-btn {
|
||||
border-color: #051236;
|
||||
}
|
||||
|
||||
.symptomps-one__image {
|
||||
background-color: #0a1c4f;
|
||||
}
|
||||
|
||||
.symptomps-one__single h3 {
|
||||
background-color: #051236;
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.symptomps-one__single {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.funfact-two__single p,
|
||||
.team-one__single p,
|
||||
.about-five .about-one__list li,
|
||||
.about-five__content > p {
|
||||
color: #9ea8c6;
|
||||
}
|
||||
|
||||
.testimonials-one__title {
|
||||
color: #0c75d8;
|
||||
}
|
||||
|
||||
.prevention-one__icon::before {
|
||||
border-color: #040f2e;
|
||||
}
|
||||
|
||||
.testimonials-one__qoute {
|
||||
border-color: #0a1c4f;
|
||||
}
|
||||
/*# sourceMappingURL=dark.css.map */
|
9
flask_app/static/css/dark.css.map
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": 3,
|
||||
"mappings": "AAAA,AAAA,IAAI,CAAC;EACD,KAAK,EAAE,OAAO;EACd,gBAAgB,EAAE,OAAO;CAC5B;;AACD,AAAA,kBAAkB,CAAC,EAAE;AACrB,YAAY,CAAC,CAAC;AACd,YAAY,CAAC,EAAE,CAAC;EACZ,KAAK,EAAE,IAAI;CACd;;AACD,AAAA,oBAAoB,CAAC,CAAC,CAAA;EAClB,KAAK,EAAE,OAAO;CACjB;;AAED,AAAA,mBAAmB,GAAG,CAAC,CAAC;EACpB,KAAK,EAAE,OAAO;CACjB;;AACD,AAAA,kBAAkB;AAClB,UAAU,CAAC,gBAAgB,CAAC;EACxB,YAAY,EAAe,wBAAI;CAClC;;AACD,AAAA,iBAAiB;AACjB,eAAe;AACf,WAAW;AACX,YAAY,CAAC;EACT,gBAAgB,EAAE,OAAO;CAC5B;;AACD,AAAA,iBAAiB,AAAA,QAAQ;AACzB,WAAW,AAAA,QAAQ;AACnB,eAAe,AAAA,QAAQ;AACvB,YAAY,AAAA,QAAQ,CAAC;EACjB,gBAAgB,EAAE,gDAAgD;EAClE,OAAO,EAAE,GAAG;EACZ,qBAAqB,EAAE,OAAO;EAC9B,gBAAgB,EAAE,OAAO;CAC5B;;AAED,AAAA,mBAAmB,CAAC;EAChB,gBAAgB,EAAE,OAAO;CAC5B;;AAED,AAAA,oBAAoB,AAAA,MAAM,CAAC,mBAAmB,CAAC;EAC3C,gBAAgB,EAAE,OAAO;CAC5B;;AACD,AAAA,QAAQ,CAAC,UAAU,AAAA,OAAO,CAAC,EAAE;AAC7B,oBAAoB,CAAC,EAAE,CAAC;EACpB,KAAK,EAAE,IAAI;CACd;;AAED,AAAA,oBAAoB,CAAC,CAAC,CAAC;EACnB,KAAK,EAAE,OAAO;CACjB;;AACD,AAAA,kBAAkB;AAClB,mBAAmB,CAAC;EAChB,gBAAgB,EAAE,OAAO;CAC5B;;AACD,AAAA,kBAAkB;AAClB,oBAAoB,AAAA,MAAM,CAAC,mBAAmB,CAAC;EAC3C,gBAAgB,EAAE,OAAO;CAC5B;;AAED,AAAA,wBAAwB,AAAA,QAAQ,CAAC;EAC7B,gBAAgB,EAAE,uDAAuD;EACzE,YAAY,EAAe,wBAAI;CAClC;;AACD,AAAA,2BAA2B,CAAC;EACxB,gBAAgB,EAAE,OAAO;EACzB,YAAY,EAAe,wBAAI;CAClC;;AACD,AAAA,oBAAoB;AACpB,QAAQ,CAAC,UAAU;AACnB,QAAQ,CAAC,gBAAgB;AACzB,uBAAuB,GAAG,uBAAuB,CAAC;EAC9C,YAAY,EAAe,wBAAI;CAClC;;AACD,AAAA,2BAA2B,CAAC;EACxB,gBAAgB,EAAE,OAAO;CAC5B;;AACD,AAAA,qBAAqB,CAAC,EAAE;AACxB,oBAAoB,CAAC,CAAC;AACtB,oBAAoB,CAAC,EAAE;AACvB,iBAAiB,CAAC,EAAE;AACpB,wBAAwB,CAAC,EAAE,CAAC;EACxB,KAAK,EAAE,IAAI;CACd;;AACD,AAAA,uBAAuB;AACvB,kBAAkB,CAAC,CAAC;AACpB,wBAAwB,CAAC,CAAC,CAAC;EACvB,KAAK,EAAE,OAAO;CAEjB;;AAED,AAAA,QAAQ,CAAC,UAAU,CAAC;EAChB,gBAAgB,EAAE,OAAO;CAC5B;;AAED,AAAA,QAAQ,CAAC,UAAU,AAAA,OAAO,CAAC,gBAAgB,AAAA,QAAQ,CAAC;EAChD,KAAK,EAAE,kBAAkB;CAC5B;;AACD,AAAA,cAAc;CACd,AAAA,KAAC,EAAO,KAAK,AAAZ,CAAa,IAAK,CAAA,aAAa,EAAE,oBAAoB,AAAA,OAAO,GAAE,AAAA,KAAC,EAAO,KAAK,AAAZ,CAAa,IAAK,CAAA,aAAa,EAAE,oBAAoB,AAAA,QAAQ,CAAC;EAC1H,gBAAgB,EAAO,wBAAI;CAC9B;;AAED,AAAA,oBAAoB,AAAA,QAAQ,CAAC;EACzB,gBAAgB,EAAE,gDAAgD;CACrE;;AAED,AAAA,sBAAsB,CAAC;EACnB,YAAY,EAAE,OAAO;CACxB;;AAED,AAAA,qBAAqB,CAAC;EAClB,gBAAgB,EAAE,OAAO;CAC5B;;AAED,AAAA,sBAAsB,CAAC,EAAE,CAAC;EACtB,gBAAgB,EAAE,OAAO;EACzB,YAAY,EAAe,wBAAI;EAC/B,KAAK,EAAE,IAAI;CACd;;AAED,AAAA,sBAAsB,CAAC;EACnB,gBAAgB,EAAE,WAAW;CAChC;;AACD,AAAA,oBAAoB,CAAC,CAAC;AACtB,iBAAiB,CAAC,CAAC;AACnB,WAAW,CAAC,gBAAgB,CAAC,EAAE;AAC/B,oBAAoB,GAAG,CAAC,CAAC;EACrB,KAAK,EAAE,OAAO;CACjB;;AAED,AAAA,wBAAwB,CAAC;EACrB,KAAK,EAAE,OAAO;CACjB;;AAED,AAAA,qBAAqB,AAAA,QAAQ,CAAC;EAC1B,YAAY,EAAE,OAAO;CACxB;;AAED,AAAA,wBAAwB,CAAC;EACrB,YAAY,EAAE,OAAO;CACxB",
|
||||
"sources": [
|
||||
"../scss/dark.scss"
|
||||
],
|
||||
"names": [],
|
||||
"file": "dark.css"
|
||||
}
|
6203
flask_app/static/css/fontawesome-all.min.css
vendored
Normal file
2598
flask_app/static/css/main.css
Normal file
36
flask_app/static/css/main.css.map
Normal file
6
flask_app/static/css/owl.carousel.min.css
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Owl Carousel v2.3.4
|
||||
* Copyright 2013-2018 David Deutsch
|
||||
* Licensed under: SEE LICENSE IN https://github.com/OwlCarousel2/OwlCarousel2/blob/master/LICENSE
|
||||
*/
|
||||
.owl-carousel,.owl-carousel .owl-item{-webkit-tap-highlight-color:transparent;position:relative}.owl-carousel{display:none;width:100%;z-index:1}.owl-carousel .owl-stage{position:relative;-ms-touch-action:pan-Y;touch-action:manipulation;-moz-backface-visibility:hidden}.owl-carousel .owl-stage:after{content:".";display:block;clear:both;visibility:hidden;line-height:0;height:0}.owl-carousel .owl-stage-outer{position:relative;overflow:hidden;-webkit-transform:translate3d(0,0,0)}.owl-carousel .owl-item,.owl-carousel .owl-wrapper{-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0)}.owl-carousel .owl-item{min-height:1px;float:left;-webkit-backface-visibility:hidden;-webkit-touch-callout:none}.owl-carousel .owl-item img{display:block;width:100%}.owl-carousel .owl-dots.disabled,.owl-carousel .owl-nav.disabled{display:none}.no-js .owl-carousel,.owl-carousel.owl-loaded{display:block}.owl-carousel .owl-dot,.owl-carousel .owl-nav .owl-next,.owl-carousel .owl-nav .owl-prev{cursor:pointer;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.owl-carousel .owl-nav button.owl-next,.owl-carousel .owl-nav button.owl-prev,.owl-carousel button.owl-dot{background:0 0;color:inherit;border:none;padding:0!important;font:inherit}.owl-carousel.owl-loading{opacity:0;display:block}.owl-carousel.owl-hidden{opacity:0}.owl-carousel.owl-refresh .owl-item{visibility:hidden}.owl-carousel.owl-drag .owl-item{-ms-touch-action:pan-y;touch-action:pan-y;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.owl-carousel.owl-grab{cursor:move;cursor:grab}.owl-carousel.owl-rtl{direction:rtl}.owl-carousel.owl-rtl .owl-item{float:right}.owl-carousel .animated{animation-duration:1s;animation-fill-mode:both}.owl-carousel .owl-animated-in{z-index:0}.owl-carousel .owl-animated-out{z-index:1}.owl-carousel .fadeOut{animation-name:fadeOut}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.owl-height{transition:height .5s ease-in-out}.owl-carousel .owl-item .owl-lazy{opacity:0;transition:opacity .4s ease}.owl-carousel .owl-item .owl-lazy:not([src]),.owl-carousel .owl-item .owl-lazy[src^=""]{max-height:0}.owl-carousel .owl-item img.owl-lazy{transform-style:preserve-3d}.owl-carousel .owl-video-wrapper{position:relative;height:100%;background:#000}.owl-carousel .owl-video-play-icon{position:absolute;height:80px;width:80px;left:50%;top:50%;margin-left:-40px;margin-top:-40px;background:url(owl.video.play.png) no-repeat;cursor:pointer;z-index:1;-webkit-backface-visibility:hidden;transition:transform .1s ease}.owl-carousel .owl-video-play-icon:hover{-ms-transform:scale(1.3,1.3);transform:scale(1.3,1.3)}.owl-carousel .owl-video-playing .owl-video-play-icon,.owl-carousel .owl-video-playing .owl-video-tn{display:none}.owl-carousel .owl-video-tn{opacity:0;height:100%;background-position:center center;background-repeat:no-repeat;background-size:contain;transition:opacity .4s ease}.owl-carousel .owl-video-frame{position:relative;z-index:1;height:100%;width:100%}
|
6
flask_app/static/css/owl.theme.default.min.css
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Owl Carousel v2.3.4
|
||||
* Copyright 2013-2018 David Deutsch
|
||||
* Licensed under: SEE LICENSE IN https://github.com/OwlCarousel2/OwlCarousel2/blob/master/LICENSE
|
||||
*/
|
||||
.owl-theme .owl-dots,.owl-theme .owl-nav{text-align:center;-webkit-tap-highlight-color:transparent}.owl-theme .owl-nav{margin-top:10px}.owl-theme .owl-nav [class*=owl-]{color:#FFF;font-size:14px;margin:5px;padding:4px 7px;background:#D6D6D6;display:inline-block;cursor:pointer;border-radius:3px}.owl-theme .owl-nav [class*=owl-]:hover{background:#869791;color:#FFF;text-decoration:none}.owl-theme .owl-nav .disabled{opacity:.5;cursor:default}.owl-theme .owl-nav.disabled+.owl-dots{margin-top:10px}.owl-theme .owl-dots .owl-dot{display:inline-block;zoom:1}.owl-theme .owl-dots .owl-dot span{width:10px;height:10px;margin:5px 7px;background:#D6D6D6;display:block;-webkit-backface-visibility:visible;transition:opacity .2s ease;border-radius:30px}.owl-theme .owl-dots .owl-dot.active span,.owl-theme .owl-dots .owl-dot:hover span{background:#869791}
|
77
flask_app/static/css/vimns-icons.css
Normal file
@ -0,0 +1,77 @@
|
||||
@font-face {
|
||||
font-family: 'vimns-icons';
|
||||
src: url('../fonts/vimns-icons.eot?wz00rk');
|
||||
src: url('../fonts/vimns-icons.eot?wz00rk#iefix') format('embedded-opentype'),
|
||||
url('../fonts/vimns-icons.ttf?wz00rk') format('truetype'),
|
||||
url('../fonts/vimns-icons.woff?wz00rk') format('woff'),
|
||||
url('../fonts/vimns-icons.svg?wz00rk#vimns-icons') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
[class^="vimns-icon-"], [class*=" vimns-icon-"] {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'vimns-icons' !important;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.vimns-icon-phone:before {
|
||||
content: "\e900";
|
||||
}
|
||||
.vimns-icon-alert:before {
|
||||
content: "\e901";
|
||||
}
|
||||
.vimns-icon-virus:before {
|
||||
content: "\e902";
|
||||
}
|
||||
.vimns-icon-mask:before {
|
||||
content: "\e903";
|
||||
}
|
||||
.vimns-icon-tick:before {
|
||||
content: "\e904";
|
||||
}
|
||||
.vimns-icon-back:before {
|
||||
content: "\e905";
|
||||
}
|
||||
.vimns-icon-front:before {
|
||||
content: "\e909";
|
||||
}
|
||||
.vimns-icon-screw:before {
|
||||
content: "\e906";
|
||||
}
|
||||
.vimns-icon-work:before {
|
||||
content: "\e907";
|
||||
}
|
||||
.vimns-icon-mail:before {
|
||||
content: "\e908";
|
||||
}
|
||||
.vimns-icon-infected:before {
|
||||
content: "\e90a";
|
||||
}
|
||||
.vimns-icon-washing-hands:before {
|
||||
content: "\e90b";
|
||||
}
|
||||
.vimns-icon-shopping-online:before {
|
||||
content: "\e90c";
|
||||
}
|
||||
.vimns-icon-network:before {
|
||||
content: "\e90d";
|
||||
}
|
||||
.vimns-icon-worldwide:before {
|
||||
content: "\e90e";
|
||||
}
|
||||
.vimns-icon-family:before {
|
||||
content: "\e90f";
|
||||
}
|
||||
.vimns-icon-grave:before {
|
||||
content: "\e910";
|
||||
}
|
BIN
flask_app/static/fonts/fa-brands-400.eot
Normal file
3459
flask_app/static/fonts/fa-brands-400.svg
Normal file
After Width: | Height: | Size: 678 KiB |
BIN
flask_app/static/fonts/fa-brands-400.ttf
Normal file
BIN
flask_app/static/fonts/fa-brands-400.woff
Normal file
BIN
flask_app/static/fonts/fa-brands-400.woff2
Normal file
BIN
flask_app/static/fonts/fa-light-300.eot
Normal file
9901
flask_app/static/fonts/fa-light-300.svg
Normal file
After Width: | Height: | Size: 1.8 MiB |
BIN
flask_app/static/fonts/fa-light-300.ttf
Normal file
BIN
flask_app/static/fonts/fa-light-300.woff
Normal file
BIN
flask_app/static/fonts/fa-light-300.woff2
Normal file
BIN
flask_app/static/fonts/fa-regular-400.eot
Normal file
9103
flask_app/static/fonts/fa-regular-400.svg
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
flask_app/static/fonts/fa-regular-400.ttf
Normal file
BIN
flask_app/static/fonts/fa-regular-400.woff
Normal file
BIN
flask_app/static/fonts/fa-regular-400.woff2
Normal file
BIN
flask_app/static/fonts/fa-solid-900.eot
Normal file
7709
flask_app/static/fonts/fa-solid-900.svg
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
flask_app/static/fonts/fa-solid-900.ttf
Normal file
BIN
flask_app/static/fonts/fa-solid-900.woff
Normal file
BIN
flask_app/static/fonts/fa-solid-900.woff2
Normal file
BIN
flask_app/static/fonts/vimns-icons.eot
Normal file
27
flask_app/static/fonts/vimns-icons.svg
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
flask_app/static/fonts/vimns-icons.ttf
Normal file
BIN
flask_app/static/fonts/vimns-icons.woff
Normal file
BIN
flask_app/static/images/avatar/ChenDoc.jpg
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
flask_app/static/images/avatar/I月清风.jpg
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
flask_app/static/images/avatar/LeeCDoc.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
flask_app/static/images/avatar/Smile_先森.jpg
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
flask_app/static/images/avatar/WongDoc.jpg
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
flask_app/static/images/avatar/ZhangDoc - 副本.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
flask_app/static/images/avatar/ZhangDoc.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
flask_app/static/images/avatar/ZhangYu.jpg
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
flask_app/static/images/avatar/default.jpg
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
flask_app/static/images/avatar/小土豆.jpg
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
flask_app/static/images/avatar/豹子头林冲.jpg
Normal file
After Width: | Height: | Size: 356 KiB |
BIN
flask_app/static/images/background/banner-1--1-bg.jpg
Normal file
After Width: | Height: | Size: 233 KiB |
BIN
flask_app/static/images/background/blog-bg-1-1.jpg
Normal file
After Width: | Height: | Size: 179 KiB |
BIN
flask_app/static/images/background/cta-bg-1-1.jpg
Normal file
After Width: | Height: | Size: 124 KiB |
BIN
flask_app/static/images/background/footer-bg-1-1.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
flask_app/static/images/background/page-header-bg-1-1.jpg
Normal file
After Width: | Height: | Size: 536 KiB |
BIN
flask_app/static/images/ct-scan.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
flask_app/static/images/diagnosis/10.jpg
Normal file
After Width: | Height: | Size: 2.9 MiB |
BIN
flask_app/static/images/diagnosis/13.jpg
Normal file
After Width: | Height: | Size: 2.8 MiB |
BIN
flask_app/static/images/diagnosis/21.jpg
Normal file
After Width: | Height: | Size: 2.8 MiB |
BIN
flask_app/static/images/favicons/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
flask_app/static/images/favicons/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
flask_app/static/images/favicons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
flask_app/static/images/favicons/favicon-16x16.png
Normal file
After Width: | Height: | Size: 781 B |
BIN
flask_app/static/images/favicons/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
1
flask_app/static/images/favicons/site.webmanifest
Normal file
@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
BIN
flask_app/static/images/feedbacks/qoute.jpg
Normal file
After Width: | Height: | Size: 308 B |
BIN
flask_app/static/images/feedbacks/卢卡.jpg
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
flask_app/static/images/feedbacks/林樨.jpg
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
flask_app/static/images/feedbacks/洋洋.jpg
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
flask_app/static/images/icon/post.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
flask_app/static/images/icon/return.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
flask_app/static/images/loader.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
flask_app/static/images/logo-1-1.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
flask_app/static/images/post/12/1.jpg
Normal file
After Width: | Height: | Size: 302 KiB |
BIN
flask_app/static/images/post/12/2.jpg
Normal file
After Width: | Height: | Size: 387 KiB |
BIN
flask_app/static/images/post/12/3.jpg
Normal file
After Width: | Height: | Size: 313 KiB |