Compare commits
No commits in common. "master" and "v0.9.0" have entirely different histories.
68 changed files with 346 additions and 791 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -4,8 +4,8 @@ __pycache__/
|
||||||
uploads/
|
uploads/
|
||||||
*.pyc
|
*.pyc
|
||||||
**~
|
**~
|
||||||
.*.swp
|
**/.*.swp
|
||||||
__pycache__/
|
**/__pycache__/
|
||||||
venv
|
venv
|
||||||
.env
|
.env
|
||||||
.venv
|
.venv
|
||||||
|
|
@ -15,7 +15,3 @@ conf/
|
||||||
config/
|
config/
|
||||||
\#*\#
|
\#*\#
|
||||||
.\#*
|
.\#*
|
||||||
node_modules/
|
|
||||||
alembic.ini
|
|
||||||
**.egg-info
|
|
||||||
.vscode
|
|
||||||
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -1,17 +1,6 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.10.0
|
## 0.9-dev
|
||||||
+ Codebase refactor (with breaking changes!)
|
|
||||||
+ Dropped support for Python<=3.9
|
|
||||||
+ Switched database to PostgreSQL
|
|
||||||
+ Move ALL config to .env (config.py is NO MORE supported)
|
|
||||||
+ Config SITE_NAME replaced with APP_NAME
|
|
||||||
+ Add CSRF token and flask_WTF
|
|
||||||
+ Schema changes: biography and website moved to `User`; `UserProfile` table deprecated (and useless fields removed)
|
|
||||||
+ Posts can now be permanently deleted
|
|
||||||
+ Miscellaneous style changes
|
|
||||||
|
|
||||||
## 0.9.0
|
|
||||||
|
|
||||||
* Website redesign: added some material icons, implemented via a `inline_svg` function, injected by default in templates and defined in `utils.py`.
|
* Website redesign: added some material icons, implemented via a `inline_svg` function, injected by default in templates and defined in `utils.py`.
|
||||||
* Added positive feedback mechanism: now you can +1 a message. So, `score_message_add` and `score_message_remove` API endpoints were added, and `MessageUpvote` table was created.
|
* Added positive feedback mechanism: now you can +1 a message. So, `score_message_add` and `score_message_remove` API endpoints were added, and `MessageUpvote` table was created.
|
||||||
|
|
@ -37,7 +26,7 @@
|
||||||
* Changed default `robots.txt`, adding report and admin-related lines.
|
* Changed default `robots.txt`, adding report and admin-related lines.
|
||||||
* Released official [Android client](https://github.com/sakuragasaki46/coriplusapp/releases/tag/v0.8.0).
|
* Released official [Android client](https://github.com/sakuragasaki46/coriplusapp/releases/tag/v0.8.0).
|
||||||
|
|
||||||
## 0.7.1
|
## 0.7.1-dev
|
||||||
|
|
||||||
* Adding `messages_count`, `followers_count` and `following_count` to `profile_info` API endpoint (forgot to release).
|
* Adding `messages_count`, `followers_count` and `following_count` to `profile_info` API endpoint (forgot to release).
|
||||||
|
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -16,19 +16,10 @@ This is the server. For the client, see [coriplusapp](https://github.com/sakurag
|
||||||
* Add info to your profile
|
* Add info to your profile
|
||||||
* In-site notifications
|
* In-site notifications
|
||||||
* Public API
|
* Public API
|
||||||
* SQLite (or PostgreSQL)-based app
|
* SQLite-based app
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
* **Python 3.10+** with **pip**.
|
* **Python 3** only. We don't want to support Python 2.
|
||||||
* **Flask** web framework.
|
* **Flask** web framework (also required extension **Flask-Login**).
|
||||||
* **Peewee** ORM.
|
* **Peewee** ORM.
|
||||||
* A \*nix-based OS.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
* Install dependencies: `pip install .`
|
|
||||||
* Set the `DATABASE_URL` (must be SQLite or PostgreSQL)
|
|
||||||
* Run the migrations: `sh ./genmig.sh @`
|
|
||||||
* i forgor
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,37 +16,27 @@ For other, see `app.utils`.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Flask, g, jsonify, render_template, request,
|
Flask, abort, flash, g, jsonify, redirect, render_template, request,
|
||||||
send_from_directory, __version__ as flask_version)
|
send_from_directory, session, url_for, __version__ as flask_version)
|
||||||
import os, sys
|
import hashlib
|
||||||
|
import datetime, time, re, os, sys, string, json, html
|
||||||
|
from functools import wraps
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
from flask_wtf import CSRFProtect
|
|
||||||
import dotenv
|
|
||||||
import logging
|
|
||||||
|
|
||||||
__version__ = '0.10.0-dev50'
|
__version__ = '0.9.0'
|
||||||
|
|
||||||
# we want to support Python 3.10+ only.
|
# we want to support Python 3 only.
|
||||||
# Python 2 has too many caveats.
|
# Python 2 has too many caveats.
|
||||||
# Python <=3.9 has harder type support.
|
if sys.version_info[0] < 3:
|
||||||
if sys.version_info[0:2] < (3, 10):
|
raise RuntimeError('Python 3 required')
|
||||||
raise RuntimeError('Python 3.10+ required')
|
|
||||||
|
|
||||||
BASEDIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
os.chdir(os.path.dirname(os.path.dirname(__file__)))
|
||||||
os.chdir(BASEDIR)
|
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = os.environ['SECRET_KEY']
|
app.config.from_pyfile('../config.py')
|
||||||
|
|
||||||
login_manager = LoginManager(app)
|
login_manager = LoginManager(app)
|
||||||
|
|
||||||
CSRFProtect(app)
|
|
||||||
|
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
from .utils import *
|
from .utils import *
|
||||||
|
|
@ -63,20 +53,17 @@ def before_request():
|
||||||
try:
|
try:
|
||||||
g.db.connect()
|
g.db.connect()
|
||||||
except OperationalError:
|
except OperationalError:
|
||||||
logger.error('database connected twice')
|
sys.stderr.write('database connected twice.\n')
|
||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def after_request(response):
|
def after_request(response):
|
||||||
try:
|
|
||||||
g.db.close()
|
g.db.close()
|
||||||
except Exception:
|
|
||||||
logger.error('database closed twice')
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def _inject_variables():
|
def _inject_variables():
|
||||||
return {
|
return {
|
||||||
'site_name': os.environ.get('APP_NAME', 'Cori+'),
|
'site_name': app.config['SITE_NAME'],
|
||||||
'locations': locations,
|
'locations': locations,
|
||||||
'inline_svg': inline_svg
|
'inline_svg': inline_svg
|
||||||
}
|
}
|
||||||
|
|
@ -85,21 +72,17 @@ def _inject_variables():
|
||||||
def _inject_user(userid):
|
def _inject_user(userid):
|
||||||
return User[userid]
|
return User[userid]
|
||||||
|
|
||||||
@app.errorhandler(403)
|
|
||||||
def error_403(body):
|
|
||||||
return render_template('403.html'), 403
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def error_404(body):
|
def error_404(body):
|
||||||
return render_template('404.html'), 404
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
@app.route('/favicon.ico')
|
@app.route('/favicon.ico')
|
||||||
def favicon_ico():
|
def favicon_ico():
|
||||||
return send_from_directory(BASEDIR, 'src/favicon.ico')
|
return send_from_directory(os.getcwd(), 'favicon.ico')
|
||||||
|
|
||||||
@app.route('/robots.txt')
|
@app.route('/robots.txt')
|
||||||
def robots_txt():
|
def robots_txt():
|
||||||
return send_from_directory(BASEDIR, 'src/robots.txt')
|
return send_from_directory(os.getcwd(), 'robots.txt')
|
||||||
|
|
||||||
@app.route('/uploads/<id>.<type>')
|
@app.route('/uploads/<id>.<type>')
|
||||||
def uploads(id, type='jpg'):
|
def uploads(id, type='jpg'):
|
||||||
|
|
@ -4,8 +4,7 @@ Management of reports and the entire site.
|
||||||
New in 0.8.
|
New in 0.8.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from flask import Blueprint, abort, redirect, render_template, request, url_for
|
from flask import Blueprint, redirect, render_template, request, url_for
|
||||||
from flask_login import current_user
|
|
||||||
from .models import User, Message, Report, report_reasons, REPORT_STATUS_ACCEPTED, \
|
from .models import User, Message, Report, report_reasons, REPORT_STATUS_ACCEPTED, \
|
||||||
REPORT_MEDIA_USER, REPORT_MEDIA_MESSAGE
|
REPORT_MEDIA_USER, REPORT_MEDIA_MESSAGE
|
||||||
from .utils import pwdhash, object_list
|
from .utils import pwdhash, object_list
|
||||||
|
|
@ -13,17 +12,21 @@ from functools import wraps
|
||||||
|
|
||||||
bp = Blueprint('admin', __name__, url_prefix='/admin')
|
bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||||
|
|
||||||
def _check_auth(username) -> bool:
|
def check_auth(username, password):
|
||||||
try:
|
try:
|
||||||
return User.get((User.username == username)).is_admin
|
return User.get((User.username == username) & (User.password == pwdhash(password))
|
||||||
|
).is_admin
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def admin_required(f):
|
def admin_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapped_view(**kwargs):
|
def wrapped_view(**kwargs):
|
||||||
if not _check_auth(current_user.username):
|
auth = request.authorization
|
||||||
abort(403)
|
if not (auth and check_auth(auth.username, auth.password)):
|
||||||
|
return ('Unauthorized', 401, {
|
||||||
|
'WWW-Authenticate': 'Basic realm="Login Required"'
|
||||||
|
})
|
||||||
return f(**kwargs)
|
return f(**kwargs)
|
||||||
return wrapped_view
|
return wrapped_view
|
||||||
|
|
||||||
|
|
@ -5,9 +5,8 @@ Warning: this is not the public API.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from flask import Blueprint, jsonify
|
from flask import Blueprint, jsonify
|
||||||
from flask_login import current_user
|
|
||||||
from .models import User, Message, MessageUpvote
|
from .models import User, Message, MessageUpvote
|
||||||
from .utils import locations, is_username
|
from .utils import locations, get_current_user, is_username
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
bp = Blueprint('ajax', __name__, url_prefix='/ajax')
|
bp = Blueprint('ajax', __name__, url_prefix='/ajax')
|
||||||
|
|
@ -40,7 +39,7 @@ def location_search(name):
|
||||||
|
|
||||||
@bp.route('/score/<int:id>/toggle', methods=['POST'])
|
@bp.route('/score/<int:id>/toggle', methods=['POST'])
|
||||||
def score_toggle(id):
|
def score_toggle(id):
|
||||||
user = current_user
|
user = get_current_user()
|
||||||
message = Message[id]
|
message = Message[id]
|
||||||
upvoted_by_self = (MessageUpvote
|
upvoted_by_self = (MessageUpvote
|
||||||
.select()
|
.select()
|
||||||
|
|
@ -7,9 +7,6 @@ from .models import User, UserProfile, Message, Upload, Relationship, Notificati
|
||||||
MSGPRV_PUBLIC, MSGPRV_UNLISTED, MSGPRV_FRIENDS, MSGPRV_ONLYME, UPLOAD_DIRECTORY
|
MSGPRV_PUBLIC, MSGPRV_UNLISTED, MSGPRV_FRIENDS, MSGPRV_ONLYME, UPLOAD_DIRECTORY
|
||||||
from .utils import check_access_token, Visibility, push_notification, unpush_notification, \
|
from .utils import check_access_token, Visibility, push_notification, unpush_notification, \
|
||||||
create_mentions, is_username, generate_access_token, pwdhash
|
create_mentions, is_username, generate_access_token, pwdhash
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
bp = Blueprint('api', __name__, url_prefix='/api/V1')
|
bp = Blueprint('api', __name__, url_prefix='/api/V1')
|
||||||
|
|
||||||
|
|
@ -19,7 +16,7 @@ def get_message_info(message):
|
||||||
except IndexError:
|
except IndexError:
|
||||||
media = None
|
media = None
|
||||||
if media:
|
if media:
|
||||||
logger.debug(media)
|
print(media)
|
||||||
return {
|
return {
|
||||||
'id': message.id,
|
'id': message.id,
|
||||||
'user': {
|
'user': {
|
||||||
|
|
@ -125,7 +122,7 @@ def create2(self):
|
||||||
privacy=privacy)
|
privacy=privacy)
|
||||||
file = request.files.get('file')
|
file = request.files.get('file')
|
||||||
if file:
|
if file:
|
||||||
logger.info('Uploading', file.filename)
|
print('Uploading', file.filename)
|
||||||
ext = file.filename.split('.')[-1]
|
ext = file.filename.split('.')[-1]
|
||||||
upload = Upload.create(
|
upload = Upload.create(
|
||||||
type=ext,
|
type=ext,
|
||||||
|
|
@ -13,43 +13,32 @@ The tables are:
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from peewee import *
|
from peewee import *
|
||||||
from playhouse.db_url import connect
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from . import BASEDIR
|
|
||||||
# here should go `from .utils import get_current_user`, but it will cause
|
# here should go `from .utils import get_current_user`, but it will cause
|
||||||
# import errors. It's instead imported at function level.
|
# import errors. It's instead imported at function level.
|
||||||
|
|
||||||
database = connect(os.environ['DATABASE_URL'])
|
database = SqliteDatabase('coriplus.sqlite')
|
||||||
|
|
||||||
class BaseModel(Model):
|
class BaseModel(Model):
|
||||||
id = AutoField(primary_key=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
database = database
|
database = database
|
||||||
|
|
||||||
# A user. The user is separated from its page.
|
# A user. The user is separated from its page.
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
# The unique username.
|
# The unique username.
|
||||||
username = CharField(30, unique=True)
|
username = CharField(unique=True)
|
||||||
# The user's full name (here for better search since 0.8)
|
# The user's full name (here for better search since 0.8)
|
||||||
full_name = CharField(80)
|
full_name = TextField()
|
||||||
# The password hash.
|
# The password hash.
|
||||||
password = CharField(256)
|
password = CharField()
|
||||||
# An email address.
|
# An email address.
|
||||||
email = CharField(256)
|
email = CharField()
|
||||||
# The date of birth (required because of Terms of Service)
|
# The date of birth (required because of Terms of Service)
|
||||||
birthday = DateField()
|
birthday = DateField()
|
||||||
# The date joined
|
# The date joined
|
||||||
join_date = DateTimeField()
|
join_date = DateTimeField()
|
||||||
# A disabled flag. 0 = active, 1 = disabled by user, 2 = banned
|
# A disabled flag. 0 = active, 1 = disabled by user, 2 = banned
|
||||||
is_disabled = IntegerField(default=0)
|
is_disabled = IntegerField(default=0)
|
||||||
# Short description of user.
|
|
||||||
biography = CharField(256, default='')
|
|
||||||
# Personal website.
|
|
||||||
website = TextField(null=True)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Helpers for flask_login
|
# Helpers for flask_login
|
||||||
def get_id(self):
|
def get_id(self):
|
||||||
|
|
@ -118,12 +107,15 @@ class UserAdminship(BaseModel):
|
||||||
# User profile.
|
# User profile.
|
||||||
# Additional info for identifying users.
|
# Additional info for identifying users.
|
||||||
# New in 0.6
|
# New in 0.6
|
||||||
# Deprecated in 0.10 and merged with User
|
|
||||||
class UserProfile(BaseModel):
|
class UserProfile(BaseModel):
|
||||||
user = ForeignKeyField(User, primary_key=True)
|
user = ForeignKeyField(User, primary_key=True)
|
||||||
biography = TextField(default='')
|
biography = TextField(default='')
|
||||||
location = IntegerField(null=True)
|
location = IntegerField(null=True)
|
||||||
|
year = IntegerField(null=True)
|
||||||
website = TextField(null=True)
|
website = TextField(null=True)
|
||||||
|
instagram = TextField(null=True)
|
||||||
|
facebook = TextField(null=True)
|
||||||
|
telegram = TextField(null=True)
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self):
|
||||||
'''
|
'''
|
||||||
|
|
@ -197,7 +189,7 @@ class Relationship(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
UPLOAD_DIRECTORY = os.path.join(BASEDIR, 'uploads')
|
UPLOAD_DIRECTORY = os.path.join(os.path.split(os.path.dirname(__file__))[0], 'uploads')
|
||||||
|
|
||||||
class Upload(BaseModel):
|
class Upload(BaseModel):
|
||||||
# the extension of the media
|
# the extension of the media
|
||||||
|
|
@ -99,20 +99,12 @@ function showHideMessageOptions(id){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCsrfToken () {
|
|
||||||
var csrf_token = document.querySelector('meta[name="csrf_token"]');
|
|
||||||
return csrf_token?.getAttribute('content');
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleUpvote(id){
|
function toggleUpvote(id){
|
||||||
var msgElem = document.getElementById(id);
|
var msgElem = document.getElementById(id);
|
||||||
//var upvoteLink = msgElem.getElementsByClassName('message-upvote')[0];
|
var upvoteLink = msgElem.getElementsByClassName('message-upvote')[0];
|
||||||
var scoreCounter = msgElem.getElementsByClassName('message-score')[0];
|
var scoreCounter = msgElem.getElementsByClassName('message-score')[0];
|
||||||
var body = "csrf_token=" + getCsrfToken();
|
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.open("POST", "/ajax/score/" + id + "/toggle", true);
|
xhr.open("POST", "/ajax/score/" + id + "/toggle", true);
|
||||||
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
|
||||||
// TODO add csrf token somewhere
|
|
||||||
xhr.onreadystatechange = function(){
|
xhr.onreadystatechange = function(){
|
||||||
if(xhr.readyState == XMLHttpRequest.DONE){
|
if(xhr.readyState == XMLHttpRequest.DONE){
|
||||||
if(xhr.status == 200){
|
if(xhr.status == 200){
|
||||||
|
|
@ -122,5 +114,5 @@ function toggleUpvote(id){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
xhr.send(body);
|
xhr.send();
|
||||||
}
|
}
|
||||||
38
app/static/style.css
Normal file
38
app/static/style.css
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
body,button,input,select,textarea{font-family:Roboto,'Segoe UI',Arial,Helvetica,sans-serif}
|
||||||
|
body{margin:0}
|
||||||
|
a{text-decoration:none}
|
||||||
|
a:hover{text-decoration:underline}
|
||||||
|
@media (max-width:640px){
|
||||||
|
.mobile-collapse{display:none}
|
||||||
|
}
|
||||||
|
.header{padding:12px;color:white;background-color:#ff3018;box-shadow:0 0 3px 3px #ccc}
|
||||||
|
.content{padding:12px}
|
||||||
|
.header a{color:white}
|
||||||
|
.header a svg{fill:white}
|
||||||
|
.content a{color:#3399ff}
|
||||||
|
.content a svg{fill:#3399ff}
|
||||||
|
.content a.plus{color:#ff3018}
|
||||||
|
.metanav{float:right}
|
||||||
|
.metanav-divider{width:1px;background:white;display:inline-block}
|
||||||
|
.header h1{margin:0;display:inline-block}
|
||||||
|
.flash{background-color:#ff9;border:yellow 1px solid}
|
||||||
|
.infobox{padding:12px;border:#ccc 1px solid}
|
||||||
|
@media (min-width:640px) {
|
||||||
|
.infobox{float:right;width:320px}
|
||||||
|
}
|
||||||
|
.weak{opacity:.5}
|
||||||
|
.field_desc{display:block}
|
||||||
|
ul.timeline{padding:0;margin:auto;max-width:960px}
|
||||||
|
ul.timeline > li{list-style:none;border-bottom:#808080 1px solid}
|
||||||
|
.message-visual img{max-width:100%;margin:auto}
|
||||||
|
.message-options-showhide::before{content:'\2026'}
|
||||||
|
.message-options{display:none}
|
||||||
|
.create_text{width:100%;height:8em}
|
||||||
|
.biography_text{height:4em}
|
||||||
|
.before-toggle:not(:checked) + input{display:none}
|
||||||
|
.follow_button,input[type="submit"]{background-color:#ff3018;color:white;border-radius:3px;border:1px solid #ff3018}
|
||||||
|
.follow_button.following{background-color:transparent;color:#ff3018;border-color:#ff3018}
|
||||||
|
.copyright{font-size:smaller;text-align:center;color:#808080}
|
||||||
|
.copyright a:link,.copyright a:visited{color:#31559e}
|
||||||
|
.copyright ul{list-style:none;padding:0}
|
||||||
|
.copyright ul > li{padding:0 3px}
|
||||||
7
app/templates/404.html
Normal file
7
app/templates/404.html
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h2>Not Found</h2>
|
||||||
|
|
||||||
|
<p><a href="/">Back to homepage.</a></p>
|
||||||
|
{% endblock %}
|
||||||
34
app/templates/about.html
Normal file
34
app/templates/about.html
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>About {{ site_name }}</h1>
|
||||||
|
|
||||||
|
<p>{{ site_name }} {{ version }} – Python {{ python_version }} –
|
||||||
|
Flask {{ flask_version }}</p>
|
||||||
|
<p>Copyright © 2019 Sakuragasaki46.</p>
|
||||||
|
|
||||||
|
<h2>License</h2>
|
||||||
|
<p>Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files
|
||||||
|
(the "Software"), to deal in the Software without restriction,
|
||||||
|
including without limitation the rights to use, copy, modify, merge,
|
||||||
|
publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:</p>
|
||||||
|
|
||||||
|
<p>The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.</p>
|
||||||
|
|
||||||
|
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
|
||||||
|
|
||||||
|
<p>Source code for this site: <a
|
||||||
|
href="https://github.com/sakuragasaki46/coriplus/">
|
||||||
|
https://github.com/sakuragasaki46/coriplus/</a>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -7,19 +7,16 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1><a href="{{ url_for('admin.homepage') }}">{{ site_name }}: Admin</a></h1>
|
<h1><a href="{{ url_for('admin.homepage') }}">{{ site_name }} Admin</a></h1>
|
||||||
<div class="rightnav">
|
<div class="metanav">
|
||||||
<!-- what does it go here? -->
|
<!-- what does it go here? -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|
||||||
{% for message in get_flashed_messages() %}
|
{% for message in get_flashed_messages() %}
|
||||||
<div class="flash">{{ message }}</div>
|
<div class="flash">{{ message }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<main>
|
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p class="copyright">© 2019 Sakuragasaki46.
|
<p class="copyright">© 2019 Sakuragasaki46.
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2>Report detail #{{ report.id }}</h2>
|
<h2>Report detail #{{ report.id }}</h2>
|
||||||
<div class="card">
|
|
||||||
<p>Type: {{ [None, 'user', 'message'][report.media_type] }}</p>
|
<p>Type: {{ [None, 'user', 'message'][report.media_type] }}</p>
|
||||||
<p>Reason: <strong>{{ report_reasons[report.reason] }}</strong></p>
|
<p>Reason: <strong>{{ report_reasons[report.reason] }}</strong></p>
|
||||||
<p>Status: <strong>{{ ['Unreviewed', 'Accepted', 'Declined'][report.status] }}</strong></p>
|
<p>Status: <strong>{{ ['Unreviewed', 'Accepted', 'Declined'][report.status] }}</strong></p>
|
||||||
|
|
@ -22,9 +21,7 @@
|
||||||
{% include "includes/reported_message.html" %}
|
{% include "includes/reported_message.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
||||||
<input type="submit" name="take_down" value="Take down">
|
<input type="submit" name="take_down" value="Take down">
|
||||||
<input type="submit" name="discard" value="Discard">
|
<input type="submit" name="discard" value="Discard">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for report in report_list %}
|
{% for report in report_list %}
|
||||||
<li class="card {% if report.status > 0 %}done{% endif %}">
|
<li {% if report.status > 0 %}class="done"{% endif %}>
|
||||||
<p><strong>#{{ report.id }}</strong>
|
<p><strong>#{{ report.id }}</strong>
|
||||||
(<a href="{{ url_for('admin.reports_detail', id=report.id) }}">detail</a>)</p>
|
(<a href="{{ url_for('admin.reports_detail', id=report.id) }}">detail</a>)</p>
|
||||||
<p>Type: {{ [None, 'user', 'message'][report.media_type] }}</p>
|
<p>Type: {{ [None, 'user', 'message'][report.media_type] }}</p>
|
||||||
44
app/templates/base.html
Normal file
44
app/templates/base.html
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ site_name }}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/style.css">
|
||||||
|
<meta name="og:title" content="Cori+">
|
||||||
|
<meta name="og:description" content="A simple social network. Post text statuses, optionally with image.">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1><a href="{{ url_for('website.homepage') }}">{{ site_name }}</a></h1>
|
||||||
|
<div class="metanav">
|
||||||
|
{% if current_user.is_anonymous %}
|
||||||
|
<a href="{{ url_for('website.login', next=request.full_path) }}">{{ inline_svg('exit_to_app') }} <span class="mobile-collapse">log in</span></a>
|
||||||
|
<a href="{{ url_for('website.register', next=request.full_path) }}">{{ inline_svg('person_add') }} <span class="mobile-collapse">register</span></a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('website.user_detail', username=current_user.username) }}">{{ inline_svg('person') }} {{ current_user.username }}</a>
|
||||||
|
{% set notification_count = current_user.unseen_notification_count() %}
|
||||||
|
{% if notification_count > 0 %}
|
||||||
|
<a href="{{ url_for('website.notifications') }}">(<strong>{{ notification_count }}</strong>)</a>
|
||||||
|
{% endif %}
|
||||||
|
<span class="metanav-divider"></span>
|
||||||
|
<a href="{{ url_for('website.public_timeline') }}">{{ inline_svg('explore') }} <span class="mobile-collapse">explore</span></a>
|
||||||
|
<a href="{{ url_for('website.create') }}">{{ inline_svg('edit') }} <span class="mobile-collapse">create</span></a>
|
||||||
|
<a href="{{ url_for('website.logout') }}">{{ inline_svg('exit_to_app') }} <span class="mobile-collapse">log out</span></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
{% for message in get_flashed_messages() %}
|
||||||
|
<div class="flash">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p class="copyright">© 2019 Sakuragasaki46.
|
||||||
|
<a href="/about/">About</a> - <a href="/terms/">Terms</a> -
|
||||||
|
<a href="/privacy/">Privacy</a> -
|
||||||
|
<a href="https://github.com/sakuragasaki46/coriplus">GitHub</a></p>
|
||||||
|
</div>
|
||||||
|
<script src="/static/lib.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
app/templates/change_password.html
Normal file
17
app/templates/change_password.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h2>Change Password</h2>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<dl>
|
||||||
|
<dt>Old password:</dt>
|
||||||
|
<dd><input type="password" name="old_password"></dd>
|
||||||
|
<dt>New password:</dt>
|
||||||
|
<dd><input type="password" name="new_password"></dd>
|
||||||
|
<dt>New password, again:</dt>
|
||||||
|
<dd><input type="password" name="confirm_password"></dd>
|
||||||
|
<dd><input type="submit" value="Save"></dd>
|
||||||
|
</dl>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
22
app/templates/confirm_delete.html
Normal file
22
app/templates/confirm_delete.html
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h2>Confirm Deletion</h2>
|
||||||
|
|
||||||
|
<p>Are you sure you want to permanently delete this post?
|
||||||
|
Neither you nor others will be able to see it;
|
||||||
|
you cannot recover a post after it's deleted.</p>
|
||||||
|
|
||||||
|
<p>If you only want to hide it from the public,
|
||||||
|
you can <a href="/edit/{{ message.id }}">set its privacy</a> to "Only me".</p>
|
||||||
|
|
||||||
|
<p>Here's the content of the message for reference:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>{% include "includes/message.html" %}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<input type="submit" value="Delete">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="card">
|
|
||||||
<h2>Create</h2>
|
<h2>Create</h2>
|
||||||
<form action="{{ url_for('website.create') }}" method="POST" enctype="multipart/form-data">
|
<form action="{{ url_for('website.create') }}" method="POST" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Message:</dt>
|
<dt>Message:</dt>
|
||||||
<dd><textarea name="text" placeholder="What's happening?" class="create_text">{{ request.args['preload'] }}</textarea></dd>
|
<dd><textarea name="text" placeholder="What's happening?" class="create_text">{{ request.args['preload'] }}</textarea></dd>
|
||||||
|
|
@ -17,5 +15,4 @@
|
||||||
<dd><input type="submit" value="Create" /></dd>
|
<dd><input type="submit" value="Create" /></dd>
|
||||||
</dl>
|
</dl>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="card">
|
|
||||||
<h2>Edit</h2>
|
<h2>Edit</h2>
|
||||||
<form action="{{ url_for('website.edit', id=message.id) }}" method="POST" enctype="multipart/form-data">
|
<form action="{{ url_for('website.edit', id=message.id) }}" method="POST" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Message:</dt>
|
<dt>Message:</dt>
|
||||||
<dd><textarea name="text" required="" class="create_text">{{ message.text }}</textarea></dd>
|
<dd><textarea name="text" required="" class="create_text">{{ message.text }}</textarea></dd>
|
||||||
|
|
@ -16,5 +14,4 @@
|
||||||
<dd><input type="submit" value="Save" /></dd>
|
<dd><input type="submit" value="Save" /></dd>
|
||||||
</dl>
|
</dl>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="card">
|
|
||||||
<h2>Edit Profile</h2>
|
<h2>Edit Profile</h2>
|
||||||
|
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Username:</dt>
|
<dt>Username:</dt>
|
||||||
<dd><input type="text" class="username-input" name="username" required value="{{ current_user.username }}" autocomplete="off"></dd>
|
<dd><input type="text" class="username-input" name="username" required value="{{ current_user.username }}" autocomplete="off"></dd>
|
||||||
|
|
@ -34,5 +32,4 @@
|
||||||
<dd><input type="submit" value="Save"></dd>
|
<dd><input type="submit" value="Save"></dd>
|
||||||
</dl>
|
</dl>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% from "macros/message.html" import feed_message with context %}
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2>Explore</h2>
|
<h2>Explore</h2>
|
||||||
<ul class="timeline">
|
<ul class="timeline">
|
||||||
{% for message in message_list %}
|
{% for message in message_list %}
|
||||||
{{ feed_message(message) }}
|
<li id="{{ message.id }}">{% include "includes/message.html" %}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% include "includes/pagination.html" %}
|
{% include "includes/pagination.html" %}
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% from "macros/message.html" import feed_message with context %}
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2>Your Timeline</h2>
|
<h2>Your Timeline</h2>
|
||||||
<ul class="timeline">
|
<ul class="timeline">
|
||||||
{% for message in message_list %}
|
{% for message in message_list %}
|
||||||
{{ feed_message(message) }}
|
<li id="{{ message.id }}">{% include "includes/message.html" %}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% include "includes/pagination.html" %}
|
{% include "includes/pagination.html" %}
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="card">
|
|
||||||
<h2>Hello</h2>
|
<h2>Hello</h2>
|
||||||
|
|
||||||
<p>{{ site_name }} is made by people like you. <br/>
|
<p>{{ site_name }} is made by people like you. <br/>
|
||||||
<a href="{{ url_for('website.login') }}">Log in</a> or <a href="{{ url_for('website.register') }}">register</a> to see more.</p>
|
<a href="{{ url_for('website.login') }}">Log in</a> or <a href="{{ url_for('website.register') }}">register</a> to see more.</p>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
35
app/templates/includes/infobox_profile.html
Normal file
35
app/templates/includes/infobox_profile.html
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{% set profile = user.profile %}
|
||||||
|
<div class="infobox">
|
||||||
|
<h3>{{ profile.full_name }}</h3>
|
||||||
|
<p>{{ profile.biography|enrich }}</p>
|
||||||
|
{% if profile.location %}
|
||||||
|
<p><span class="weak">Location:</span> {{ profile.location|locationdata }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if profile.year %}
|
||||||
|
<p><span class="weak">Year:</span> {{ profile.year }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if profile.website %}
|
||||||
|
{% set website = profile.website %}
|
||||||
|
{% set website = website if website.startswith(('http://', 'https://')) else 'http://' + website %}
|
||||||
|
<p><span class="weak">Website:</span> {{ profile.website|urlize }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if profile.instagram %}
|
||||||
|
<p><span class="weak">Instagram:</span> <a href="https://www.instagram.com/{{ profile.instagram }}">{{ profile.instagram }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if profile.facebook %}
|
||||||
|
<p><span class="weak">Facebook:</span> <a href="https://facebook.com/{{ profile.facebook }}">{{ profile.facebook }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if profile.telegram %}
|
||||||
|
<p><span class="weak">Telegram:</span> <a href="https://t.me/{{ profile.facebook }}">{{ profile.telegram }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
<p>
|
||||||
|
<strong>{{ user.messages|count }}</strong> messages
|
||||||
|
-
|
||||||
|
<a href="{{ url_for('website.user_followers', username=user.username) }}"><strong>{{ user.followers()|count }}</strong></a> followers
|
||||||
|
-
|
||||||
|
<a href="{{ url_for('website.user_following', username=user.username) }}"><strong>{{ user.following()|count }}</strong></a> following
|
||||||
|
</p>
|
||||||
|
{% if user == current_user %}
|
||||||
|
<p><a href="/edit_profile/">{{ inline_svg('edit', 18) }} Edit profile</a></p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="card">
|
|
||||||
<h2>Join {{ site_name }}</h2>
|
<h2>Join {{ site_name }}</h2>
|
||||||
<form action="{{ url_for('website.register') }}" method="POST">
|
<form action="{{ url_for('website.register') }}" method="POST">
|
||||||
<dl>
|
<dl>
|
||||||
|
|
@ -33,5 +32,4 @@
|
||||||
<dd><input type="submit" value="Join">
|
<dd><input type="submit" value="Join">
|
||||||
</dl>
|
</dl>
|
||||||
</form>
|
</form>
|
||||||
<div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2>Login</h2>
|
<h2>Login</h2>
|
||||||
{% if error %}<p class="error"><strong>Error:</strong> {{ error }}</p>{% endif %}
|
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
|
||||||
<div class="card">
|
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Username or email:
|
<dt>Username or email:
|
||||||
<dd><input type="text" name="username">
|
<dd><input type="text" name="username">
|
||||||
|
|
@ -20,5 +18,4 @@
|
||||||
<dd><input type="submit" value="Login">
|
<dd><input type="submit" value="Login">
|
||||||
</dl>
|
</dl>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<h2>Notifications</h2>
|
<h2>Notifications</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{% for notification in notification_list %}
|
{% for notification in notification_list %}
|
||||||
<li class="card">{% include "includes/notification.html" %}</li>
|
<li>{% include "includes/notification.html" %}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% include "includes/pagination.html" %}
|
{% include "includes/pagination.html" %}
|
||||||
47
app/templates/privacy.html
Normal file
47
app/templates/privacy.html
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>Privacy Policy</h1>
|
||||||
|
|
||||||
|
<p>At {{ site_name }}, accessible from {{ request.host }}, one of our main priorities is the privacy of our visitors. This Privacy Policy document contains types of information that is collected and recorded by {{ site_name }} and how we use it.</p>
|
||||||
|
|
||||||
|
<p>If you have additional questions or require more information about our Privacy Policy, do not hesitate to contact us through email at sakuragasaki46@gmail.com</p>
|
||||||
|
|
||||||
|
<h2>Log Files</h2>
|
||||||
|
|
||||||
|
<p>{{ site_name }} follows a standard procedure of using log files. These files log visitors when they visit websites. All hosting companies do this and a part of hosting services' analytics. The information collected by log files include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit pages, and possibly the number of clicks. These are not linked to any information that is personally identifiable. The purpose of the information is for analyzing trends, administering the site, tracking users' movement on the website, and gathering demographic information.</p>
|
||||||
|
|
||||||
|
<h2>Cookies and Web Beacons</h2>
|
||||||
|
|
||||||
|
<p>Like any other website, {{ site_name }} uses 'cookies'. These cookies are used to store information including visitors' preferences, and the pages on the website that the visitor accessed or visited. The information is used to optimize the users' experience by customizing our web page content based on visitors' browser type and/or other information.</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<h2>Privacy Policies</h2>
|
||||||
|
|
||||||
|
<P>You may consult this list to find the Privacy Policy for each of the advertising partners of {{ site_name }}. Our Privacy Policy was created with the help of the <a href="https://www.privacypolicygenerator.info">Privacy Policy Generator</a> and the <a href="https://www.generateprivacypolicy.com">Generate Privacy Policy Generator</a>.</p>
|
||||||
|
|
||||||
|
<p>Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web Beacons that are used in their respective advertisements and links that appear on {{ site_name }}, which are sent directly to users' browser. They automatically receive your IP address when this occurs. These technologies are used to measure the effectiveness of their advertising campaigns and/or to personalize the advertising content that you see on websites that you visit.</p>
|
||||||
|
|
||||||
|
<p>Note that {{ site_name }} has no access to or control over these cookies that are used by third-party advertisers.</p>
|
||||||
|
|
||||||
|
<h2>Third Party Privacy Policies</h2>
|
||||||
|
|
||||||
|
<p>{{ site_name }}'s Privacy Policy does not apply to other advertisers or websites. Thus, we are advising you to consult the respective Privacy Policies of these third-party ad servers for more detailed information. It may include their practices and instructions about how to opt-out of certain options. You may find a complete list of these Privacy Policies and their links here: Privacy Policy Links.</p>
|
||||||
|
|
||||||
|
<p>You can choose to disable cookies through your individual browser options. To know more detailed information about cookie management with specific web browsers, it can be found at the browsers' respective websites. What Are Cookies?</p>
|
||||||
|
|
||||||
|
<h2>Children's Information</h2>
|
||||||
|
|
||||||
|
<p>Another part of our priority is adding protection for children while using the internet. We encourage parents and guardians to observe, participate in, and/or monitor and guide their online activity.</p>
|
||||||
|
|
||||||
|
<p>{{ site_name }} does not knowingly collect any Personal Identifiable Information from children under the age of 13. If you think that your child provided this kind of information on our website, we strongly encourage you to contact us immediately and we will do our best efforts to promptly remove such information from our records.</p>
|
||||||
|
|
||||||
|
<h2>Online Privacy Policy Only</h2>
|
||||||
|
|
||||||
|
<p>This Privacy Policy applies only to our online activities and is valid for visitors to our website with regards to the information that they shared and/or collect in {{ site_name }}. This policy is not applicable to any information collected offline or via channels other than this website.</p>
|
||||||
|
|
||||||
|
<h2>Consent</h2>
|
||||||
|
|
||||||
|
<p>By using our website, you hereby consent to our Privacy Policy and agree to its Terms and Conditions.</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% for reason in report_reasons %}
|
{% for reason in report_reasons %}
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
||||||
<div class="item" onclick="submitReport({{ reason[0] }})">
|
<div class="item" onclick="submitReport({{ reason[0] }})">
|
||||||
<h2>{{ reason[1] }}</h2>
|
<h2>{{ reason[1] }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% for reason in report_reasons %}
|
{% for reason in report_reasons %}
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
||||||
<div class="item" onclick="submitReport({{ reason[0] }})">
|
<div class="item" onclick="submitReport({{ reason[0] }})">
|
||||||
<h2>{{ reason[1] }}</h2>
|
<h2>{{ reason[1] }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="card">
|
|
||||||
<h1>Terms of Service</h1>
|
<h1>Terms of Service</h1>
|
||||||
|
|
||||||
<p>[decline to state]</p>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -1,18 +1,15 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% from "macros/message.html" import feed_message with context %}
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% include "includes/infobox_profile.html" %}
|
{% include "includes/infobox_profile.html" %}
|
||||||
<h2>Messages from {{ user.username }}</h2>
|
<h2>Messages from {{ user.username }}</h2>
|
||||||
{% if not current_user.is_anonymous %}
|
{% if not current_user.is_anonymous %}
|
||||||
{% if user.username != current_user.username %}
|
{% if user.username != current_user.username %}
|
||||||
{% if current_user|is_following(user) %}
|
{% if current_user|is_following(user) %}
|
||||||
<form action="{{ url_for('website.user_unfollow', username=user.username) }}" method="POST">
|
<form action="{{ url_for('website.user_unfollow', username=user.username) }}" method="post">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<input type="submit" class="follow_button following" value="- Un-follow" />
|
<input type="submit" class="follow_button following" value="- Un-follow" />
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form action="{{ url_for('website.user_follow', username=user.username) }}" method="POST">
|
<form action="{{ url_for('website.user_follow', username=user.username) }}" method="post">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<input type="submit" class="follow_button" value="+ Follow" />
|
<input type="submit" class="follow_button" value="+ Follow" />
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -23,7 +20,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ul class="timeline">
|
<ul class="timeline">
|
||||||
{% for message in message_list %}
|
{% for message in message_list %}
|
||||||
{{ feed_message(message) }}
|
<li id="{{ message.id }}">{% include "includes/message.html" %}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% include "includes/pagination.html" %}
|
{% include "includes/pagination.html" %}
|
||||||
|
|
@ -3,8 +3,6 @@ A list of utilities used across modules.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import datetime, re, base64, hashlib, string, sys, json
|
import datetime, re, base64, hashlib, string, sys, json
|
||||||
|
|
||||||
from flask_login import current_user
|
|
||||||
from .models import User, Message, Notification, MSGPRV_PUBLIC, MSGPRV_UNLISTED, \
|
from .models import User, Message, Notification, MSGPRV_PUBLIC, MSGPRV_UNLISTED, \
|
||||||
MSGPRV_FRIENDS, MSGPRV_ONLYME
|
MSGPRV_FRIENDS, MSGPRV_ONLYME
|
||||||
from flask import abort, render_template, request, session
|
from flask import abort, render_template, request, session
|
||||||
|
|
@ -104,14 +102,15 @@ except OSError:
|
||||||
|
|
||||||
# get the user from the session
|
# get the user from the session
|
||||||
# changed in 0.5 to comply with flask_login
|
# changed in 0.5 to comply with flask_login
|
||||||
# DEPRECATED in 0.10; use current_user instead
|
|
||||||
def get_current_user():
|
def get_current_user():
|
||||||
# new in 0.7; need a different method to get current user id
|
# new in 0.7; need a different method to get current user id
|
||||||
if request.path.startswith('/api/'):
|
if request.path.startswith('/api/'):
|
||||||
# assume token validation is already done
|
# assume token validation is already done
|
||||||
return User[request.args['access_token'].split(':')[0]]
|
return User[request.args['access_token'].split(':')[0]]
|
||||||
elif current_user.is_authenticated:
|
else:
|
||||||
return current_user
|
user_id = session.get('user_id')
|
||||||
|
if user_id:
|
||||||
|
return User[user_id]
|
||||||
|
|
||||||
def push_notification(type, target, **kwargs):
|
def push_notification(type, target, **kwargs):
|
||||||
try:
|
try:
|
||||||
|
|
@ -219,6 +218,12 @@ def create_mentions(cur_user, text, privacy):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# New in 0.9
|
# New in 0.9
|
||||||
# changed in 0.10
|
def inline_svg(name, width=None):
|
||||||
def inline_svg(name):
|
try:
|
||||||
return Markup('<span class="material-icons">{}</span>').format(name)
|
with open('icons/' + name + '-24px.svg') as f:
|
||||||
|
data = f.read()
|
||||||
|
if isinstance(width, int):
|
||||||
|
data = re.sub(r'( (?:height|width)=")\d+(")', lambda x:x.group(1) + str(width) + x.group(2), data)
|
||||||
|
return Markup(data)
|
||||||
|
except OSError:
|
||||||
|
return ''
|
||||||
|
|
@ -7,17 +7,14 @@ from .models import *
|
||||||
from . import __version__ as app_version
|
from . import __version__ as app_version
|
||||||
from sys import version as python_version
|
from sys import version as python_version
|
||||||
from flask import Blueprint, abort, flash, redirect, render_template, request, url_for, __version__ as flask_version
|
from flask import Blueprint, abort, flash, redirect, render_template, request, url_for, __version__ as flask_version
|
||||||
from flask_login import current_user, login_required, login_user, logout_user
|
from flask_login import login_required, login_user, logout_user
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
bp = Blueprint('website', __name__)
|
bp = Blueprint('website', __name__)
|
||||||
|
|
||||||
@bp.route('/')
|
@bp.route('/')
|
||||||
def homepage():
|
def homepage():
|
||||||
if current_user and current_user.is_authenticated:
|
if get_current_user():
|
||||||
return private_timeline()
|
return private_timeline()
|
||||||
else:
|
else:
|
||||||
return render_template('homepage.html')
|
return render_template('homepage.html')
|
||||||
|
|
@ -26,7 +23,7 @@ def private_timeline():
|
||||||
# the private timeline (aka feed) exemplifies the use of a subquery -- we are asking for
|
# the private timeline (aka feed) exemplifies the use of a subquery -- we are asking for
|
||||||
# messages where the person who created the message is someone the current
|
# messages where the person who created the message is someone the current
|
||||||
# user is following. these messages are then ordered newest-first.
|
# user is following. these messages are then ordered newest-first.
|
||||||
user = current_user
|
user = get_current_user()
|
||||||
messages = Visibility(Message
|
messages = Visibility(Message
|
||||||
.select()
|
.select()
|
||||||
.where((Message.user << user.following())
|
.where((Message.user << user.following())
|
||||||
|
|
@ -83,9 +80,6 @@ def register():
|
||||||
|
|
||||||
@bp.route('/login/', methods=['GET', 'POST'])
|
@bp.route('/login/', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
if current_user and current_user.is_authenticated:
|
|
||||||
flash('You are already logged in')
|
|
||||||
return redirect(request.args.get('next', '/'))
|
|
||||||
if request.method == 'POST' and request.form['username']:
|
if request.method == 'POST' and request.form['username']:
|
||||||
try:
|
try:
|
||||||
username = request.form['username']
|
username = request.form['username']
|
||||||
|
|
@ -137,12 +131,11 @@ def user_follow(username):
|
||||||
from_user=cur_user,
|
from_user=cur_user,
|
||||||
to_user=user,
|
to_user=user,
|
||||||
created_date=datetime.datetime.now())
|
created_date=datetime.datetime.now())
|
||||||
push_notification('follow', user, user=cur_user.id)
|
|
||||||
flash('You are now following %s' % user.username)
|
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
flash(f'Error following {user.username}')
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
flash('You are following %s' % user.username)
|
||||||
|
push_notification('follow', user, user=cur_user.id)
|
||||||
return redirect(url_for('website.user_detail', username=user.username))
|
return redirect(url_for('website.user_detail', username=user.username))
|
||||||
|
|
||||||
@bp.route('/+<username>/unfollow/', methods=['POST'])
|
@bp.route('/+<username>/unfollow/', methods=['POST'])
|
||||||
|
|
@ -188,7 +181,7 @@ def create():
|
||||||
privacy=privacy)
|
privacy=privacy)
|
||||||
file = request.files.get('file')
|
file = request.files.get('file')
|
||||||
if file:
|
if file:
|
||||||
logger.info('Uploading', file.filename)
|
print('Uploading', file.filename)
|
||||||
ext = file.filename.split('.')[-1]
|
ext = file.filename.split('.')[-1]
|
||||||
upload = Upload.create(
|
upload = Upload.create(
|
||||||
type=ext,
|
type=ext,
|
||||||
|
|
@ -239,15 +232,12 @@ def edit(id):
|
||||||
|
|
||||||
@bp.route('/delete/<int:id>', methods=['GET', 'POST'])
|
@bp.route('/delete/<int:id>', methods=['GET', 'POST'])
|
||||||
def confirm_delete(id):
|
def confirm_delete(id):
|
||||||
user: User = current_user
|
user = get_current_user()
|
||||||
message: Message = get_object_or_404(Message, Message.id == id)
|
message = get_object_or_404(Message, Message.id == id)
|
||||||
if message.user != user:
|
if message.user != user:
|
||||||
abort(404)
|
abort(404)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
if message.user == user:
|
abort(501, 'CSRF-Token missing.')
|
||||||
message.delete_instance()
|
|
||||||
flash('Your message has been deleted forever')
|
|
||||||
return redirect(request.args.get('next', '/'))
|
|
||||||
return render_template('confirm_delete.html', message=message)
|
return render_template('confirm_delete.html', message=message)
|
||||||
|
|
||||||
# Workaround for problems related to invalid data.
|
# Workaround for problems related to invalid data.
|
||||||
3
config.py
Normal file
3
config.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
DEBUG = True
|
||||||
|
SECRET_KEY = 'hin6bab8ge25*r=x&+5$0kn=-#log$pt^#@vrqjld!^2ci@g*b'
|
||||||
|
SITE_NAME = 'Cori+'
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
10
genmig.sh
10
genmig.sh
|
|
@ -1,10 +0,0 @@
|
||||||
#!/usr/bin/bash
|
|
||||||
# GENERATE MIGRATIONS
|
|
||||||
|
|
||||||
source venv/bin/activate && \
|
|
||||||
source .env && \
|
|
||||||
case "$1" in
|
|
||||||
("+") pw_migrate create --auto --auto-source=coriplus.models --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;;
|
|
||||||
("@") pw_migrate migrate --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;;
|
|
||||||
(\\) pw_migrate rollback --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;;
|
|
||||||
esac
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
[project]
|
|
||||||
name = "sakuragasaki46_coriplus"
|
|
||||||
authors = [
|
|
||||||
{ name = "Sakuragasaki46" }
|
|
||||||
]
|
|
||||||
dynamic = ["version"]
|
|
||||||
dependencies = [
|
|
||||||
"Python-Dotenv>=1.0.0",
|
|
||||||
"Flask",
|
|
||||||
"Flask-Login",
|
|
||||||
"Peewee",
|
|
||||||
"Flask-WTF",
|
|
||||||
"peewee-migrate",
|
|
||||||
"PsycoPG2"
|
|
||||||
]
|
|
||||||
requires-python = ">=3.10"
|
|
||||||
classifiers = [
|
|
||||||
"Private :: X"
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
|
||||||
version = { attr = "coriplus.__version__" }
|
|
||||||
|
|
||||||
|
|
||||||
16
run_example.py
Normal file
16
run_example.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '../..')
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('-p', '--port', type=int, default=5000,
|
||||||
|
help='An alternative port where to run the server.')
|
||||||
|
|
||||||
|
from app import app, create_tables
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
args = parser.parse_args()
|
||||||
|
create_tables()
|
||||||
|
app.run(port=args.port)
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
:root {
|
|
||||||
--accent: #f0372e;
|
|
||||||
--link: #3399ff;
|
|
||||||
}
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
body, button, input, select, textarea {
|
|
||||||
font-family: Inter, Roboto, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
background-color: #eeeeee;
|
|
||||||
}
|
|
||||||
#site-name, h1, h2, h3, h4, h5, h6 {
|
|
||||||
font-family: 'Funnel Sans', Roboto, sans-serif;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
body{margin:0}
|
|
||||||
main{margin:auto; max-width: 960px;}
|
|
||||||
a{text-decoration:none}
|
|
||||||
a:hover{text-decoration:underline}
|
|
||||||
.mobile-collapse {font-size: smaller;}
|
|
||||||
@media (max-width:640px){
|
|
||||||
.mobile-collapse{display:none}
|
|
||||||
}
|
|
||||||
.header{
|
|
||||||
padding:12px;color:white; background-color:var(--accent); box-shadow:0 0 3px 3px #ccc; display: flex; position: relative;
|
|
||||||
}
|
|
||||||
.centered{
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.content{padding:12px}
|
|
||||||
.header a{color:white; }
|
|
||||||
.header a svg{fill:white}
|
|
||||||
.content a{color:var(--link)}
|
|
||||||
.content a svg{fill:var(--link)}
|
|
||||||
.content a.plus{color:var(--accent)}
|
|
||||||
.leftnav, .rightnav{list-style: none; display: flex; padding: 0; margin: 0;position: absolute;}
|
|
||||||
.leftnav {left: 0}
|
|
||||||
.rightnav {right: 0}
|
|
||||||
.card {background-color: #fafafa; border-radius: 12px; padding: 12px; margin-bottom: 12px; border-bottom: 2px solid #999999;}
|
|
||||||
#site-name {text-align: center;flex: 1}
|
|
||||||
.header h1{margin:0;display:inline-block}
|
|
||||||
.flash{background-color:#ff9;border:yellow 1px solid}
|
|
||||||
.infobox{width: 50%; float: right;}
|
|
||||||
@media (max-width:639px) {
|
|
||||||
.infobox{width: 100%;}
|
|
||||||
}
|
|
||||||
.weak{opacity:.5}
|
|
||||||
.field_desc{display:block}
|
|
||||||
ul.timeline{padding:0;margin:auto;max-width:960px;clear: both}
|
|
||||||
ul.timeline > li{list-style:none;}
|
|
||||||
.message-visual img{max-width:100%;margin:auto}
|
|
||||||
.message-options-showhide::before{content:'\2026'}
|
|
||||||
.message-options{display:none}
|
|
||||||
.create_text{width:100%;height:8em}
|
|
||||||
.biography_text{height:4em}
|
|
||||||
.before-toggle:not(:checked) + input{display:none}
|
|
||||||
.follow_button,input[type="submit"]{background-color:var(--accent);color:white;border-radius:3px;border:1px solid var(--accent)}
|
|
||||||
.follow_button.following{background-color:transparent;color:var(--accent);border-color:var(--accent)}
|
|
||||||
.copyright{font-size:smaller;text-align:center;color:#808080}
|
|
||||||
.copyright a:link,.copyright a:visited{color:#31559e}
|
|
||||||
.copyright ul{list-style:none;padding:0}
|
|
||||||
.copyright ul > li{padding:0 3px}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<div class="centered">
|
|
||||||
<h2>Forbidden</h2>
|
|
||||||
|
|
||||||
<p><a href="/">Back to homepage.</a></p>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<div class="centered">
|
|
||||||
<h2>Not Found</h2>
|
|
||||||
|
|
||||||
<p><a href="/">Back to homepage.</a></p>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<div class="card">
|
|
||||||
<h1>About {{ site_name }}</h1>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>{{ site_name }} {{ version }}</li>
|
|
||||||
<li> Python {{ python_version }}</li>
|
|
||||||
<li>Flask {{ flask_version }}</li>
|
|
||||||
</ul>
|
|
||||||
<p>Copyright © 2019, 2025 Sakuragasaki46.</p>
|
|
||||||
|
|
||||||
<h2>License</h2>
|
|
||||||
<p>Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of this software and associated documentation files
|
|
||||||
(the "Software"), to deal in the Software without restriction,
|
|
||||||
including without limitation the rights to use, copy, modify, merge,
|
|
||||||
publish, distribute, sublicense, and/or sell copies of the Software,
|
|
||||||
and to permit persons to whom the Software is furnished to do so,
|
|
||||||
subject to the following conditions:</p>
|
|
||||||
|
|
||||||
<p>The above copyright notice and this permission notice shall be included
|
|
||||||
in all copies or substantial portions of the Software.</p>
|
|
||||||
|
|
||||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
||||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
||||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
||||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
|
|
||||||
|
|
||||||
<p>Source code for this site: <a
|
|
||||||
href="https://github.com/sakuragasaki46/coriplus/">
|
|
||||||
https://github.com/sakuragasaki46/coriplus/</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>{{ site_name }}</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/style.css">
|
|
||||||
<meta name="og:title" content="Cori+">
|
|
||||||
<meta name="og:description" content="A simple social network. Post text statuses, optionally with image.">
|
|
||||||
<meta name="csrf_token" content="{{ csrf_token() }}">
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Funnel+Sans:ital,wght@0,300..800;1,300..800&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="header">
|
|
||||||
<ul class="leftnav">
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
<h1 id="site-name"><a href="{{ url_for('website.homepage') }}">{{ site_name }}</a></h1>
|
|
||||||
<ul class="rightnav">
|
|
||||||
{% if current_user.is_anonymous %}
|
|
||||||
<li><a href="{{ url_for('website.login', next=request.full_path) }}">{{ inline_svg('exit_to_app') }} <span class="mobile-collapse">log in</span></a></li>
|
|
||||||
<li><a href="{{ url_for('website.register', next=request.full_path) }}">{{ inline_svg('person_add') }} <span class="mobile-collapse">register</span></a></li>
|
|
||||||
{% else %}
|
|
||||||
<li><a href="{{ url_for('website.user_detail', username=current_user.username) }}">{{ inline_svg('person') }} {{ current_user.username }}</a></li>
|
|
||||||
{% set notification_count = current_user.unseen_notification_count() %}
|
|
||||||
<li><a href="{{ url_for('website.notifications') }}">{{ inline_svg('notifications') }} (<strong>{{ notification_count }}</strong>)</a></li>
|
|
||||||
<li><a href="{{ url_for('website.public_timeline') }}">{{ inline_svg('explore') }} <span class="mobile-collapse">explore</span></a></li>
|
|
||||||
<li><a href="{{ url_for('website.create') }}">{{ inline_svg('edit') }} <span class="mobile-collapse">create</span></a></li>
|
|
||||||
<li><a href="{{ url_for('website.logout') }}">{{ inline_svg('exit_to_app') }} <span class="mobile-collapse">log out</span></a></li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
{% for message in get_flashed_messages() %}
|
|
||||||
<div class="flash">{{ message }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
<main>
|
|
||||||
{% block body %}{% endblock %}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
<p class="copyright">© 2019, 2025 Sakuragasaki46.
|
|
||||||
<a href="/about/">About</a> - <a href="/terms/">Terms</a> -
|
|
||||||
<a href="/privacy/">Privacy</a> -
|
|
||||||
<a href="https://github.com/sakuragasaki46/coriplus">GitHub</a></p>
|
|
||||||
</div>
|
|
||||||
<script src="/static/lib.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<div class="card">
|
|
||||||
<h2>Change Password</h2>
|
|
||||||
|
|
||||||
<form method="POST">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
||||||
<dl>
|
|
||||||
<dt>Old password:</dt>
|
|
||||||
<dd><input type="password" name="old_password"></dd>
|
|
||||||
<dt>New password:</dt>
|
|
||||||
<dd><input type="password" name="new_password"></dd>
|
|
||||||
<dt>New password, again:</dt>
|
|
||||||
<dd><input type="password" name="confirm_password"></dd>
|
|
||||||
<dd><input type="submit" value="Save"></dd>
|
|
||||||
</dl>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<div class="card">
|
|
||||||
<h2>Confirm Deletion</h2>
|
|
||||||
|
|
||||||
<p>Are you sure you want to <u>permanently delete</u> this post?
|
|
||||||
Neither you nor others will be able to see it;
|
|
||||||
you cannot recover a post after it's deleted.</p>
|
|
||||||
|
|
||||||
<p><small>Tip: If you only want to hide it from the public,
|
|
||||||
you can <a href="/edit/{{ message.id }}">set its privacy</a> to "Only me".</small></p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>{% include "includes/message.html" %}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<form method="POST">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
||||||
<input type="submit" value="Delete">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
|
|
||||||
<div class="card infobox">
|
|
||||||
<h3>{{ user.full_name }}</h3>
|
|
||||||
<p>{{ user.biography|enrich }}</p>
|
|
||||||
{% if user.website %}
|
|
||||||
{% set website = user.website %}
|
|
||||||
{% set website = website if website.startswith(('http://', 'https://')) else 'http://' + website %}
|
|
||||||
<p><span class="weak">Website:</span> {{ website|urlize }}</p>
|
|
||||||
{% endif %}
|
|
||||||
<p>
|
|
||||||
<strong>{{ user.messages|count }}</strong> messages
|
|
||||||
-
|
|
||||||
<a href="{{ url_for('website.user_followers', username=user.username) }}"><strong>{{ user.followers()|count }}</strong></a> followers
|
|
||||||
-
|
|
||||||
<a href="{{ url_for('website.user_following', username=user.username) }}"><strong>{{ user.following()|count }}</strong></a> following
|
|
||||||
</p>
|
|
||||||
{% if user == current_user %}
|
|
||||||
<p><a href="/edit_profile/">{{ inline_svg('edit') }} Edit profile</a></p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
{% macro feed_message(message) %}
|
|
||||||
<li class="card" id="{{ message.id }}">
|
|
||||||
<p class="message-content">{{ message.text|enrich }}</p>
|
|
||||||
{% if message.uploads %}
|
|
||||||
<div class="message-visual">
|
|
||||||
<img src="/uploads/{{ message.uploads[0].filename() }}">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<p class="message-footer">
|
|
||||||
<a href="javascript:void(0);" class="message-upvote" onclick="toggleUpvote({{ message.id }});">+</a>
|
|
||||||
<span class="message-score">{{ message.score }}</span>
|
|
||||||
-
|
|
||||||
<a href="{{ url_for('website.user_detail', username=message.user.username) }}">{{ message.user.username }}</a>
|
|
||||||
-
|
|
||||||
{% set message_privacy = message.privacy %}
|
|
||||||
{% if message_privacy == 0 %} Public
|
|
||||||
{% elif message_privacy == 1 %} Unlisted
|
|
||||||
{% elif message_privacy == 2 %} Friends
|
|
||||||
{% elif message_privacy == 3 %} Only me
|
|
||||||
{% endif %}
|
|
||||||
-
|
|
||||||
<time datetime="{{ message.pub_date.isoformat() }}" title="{{ message.pub_date.ctime() }}">{{ message.pub_date | human_date }}</time>
|
|
||||||
-
|
|
||||||
<a href="javascript:void(0);" onclick="showHideMessageOptions({{ message.id }});" class="message-options-showhide"></a>
|
|
||||||
</p>
|
|
||||||
<ul class="message-options">
|
|
||||||
{% if message.user == current_user %}
|
|
||||||
<li><a href="/edit/{{ message.id }}">Edit or change privacy</a></li>
|
|
||||||
<li><a href="/delete/{{ message.id }}">Delete permanently</a></li>
|
|
||||||
{% else %}
|
|
||||||
<li><a href="/report/message/{{ message.id }}" target="_blank">Report</a></li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<div class="card">
|
|
||||||
<h1>Privacy Policy</h1>
|
|
||||||
|
|
||||||
<p>At {{ site_name }}, accessible from {{ request.host }}, one of our main priorities is the privacy of our visitors. This Privacy Policy document contains types of information that is collected and recorded by {{ site_name }} and how we use it.</p>
|
|
||||||
|
|
||||||
<p>If you have additional questions or require more information about our Privacy Policy, do not hesitate to contact us through email at sakuragasaki46@gmail.com</p>
|
|
||||||
|
|
||||||
<h2>Log Files</h2>
|
|
||||||
|
|
||||||
<p>{{ site_name }} follows a standard procedure of using log files. These files log visitors when they visit websites. All hosting companies do this and a part of hosting services' analytics. The information collected by log files include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit pages, and possibly the number of clicks. These are not linked to any information that is personally identifiable. The purpose of the information is for analyzing trends, administering the site, tracking users' movement on the website, and gathering demographic information.</p>
|
|
||||||
|
|
||||||
<h2>Cookies and Web Beacons</h2>
|
|
||||||
|
|
||||||
<p>Like any other website, {{ site_name }} uses 'cookies'. These cookies are used to store information including visitors' preferences, and the pages on the website that the visitor accessed or visited. The information is used to optimize the users' experience by customizing our web page content based on visitors' browser type and/or other information.</p>
|
|
||||||
|
|
||||||
<p>You can choose to disable cookies through your individual browser options. This, however, can and will hurt Your usage of {{ site_name }}</p>
|
|
||||||
|
|
||||||
<h2>Privacy Policies</h2>
|
|
||||||
|
|
||||||
<P>You may consult this list to find the Privacy Policy for each of the advertising partners of {{ site_name }}. Our Privacy Policy was created with the help of the <a href="https://www.privacypolicygenerator.info">Privacy Policy Generator</a> and the <a href="https://www.generateprivacypolicy.com">Generate Privacy Policy Generator</a>.</p>
|
|
||||||
|
|
||||||
<p>Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web Beacons that are used in their respective advertisements and links that appear on {{ site_name }}, which are sent directly to users' browser. They automatically receive your IP address when this occurs. These technologies are used to measure the effectiveness of their advertising campaigns and/or to personalize the advertising content that you see on websites that you visit.</p>
|
|
||||||
|
|
||||||
<p>Note that {{ site_name }} has no access to or control over these cookies that are used by third-party advertisers.</p>
|
|
||||||
|
|
||||||
<h2>Third Party Privacy Policies</h2>
|
|
||||||
|
|
||||||
<p>{{ site_name }}'s Privacy Policy does not apply to other advertisers or websites. Thus, we are advising you to consult the respective Privacy Policies of these third-party ad servers for more detailed information. It may include their practices and instructions about how to opt-out of certain options. You may find a complete list of these Privacy Policies and their links here: Privacy Policy Links.</p>
|
|
||||||
|
|
||||||
<h2>Legal Basis</h2>
|
|
||||||
|
|
||||||
<p>Legal Basis for treatment is Legitimate Interest, except:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Transactional information, such as username, email and essential cookies, are treated according to Providing a Service.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Children's Information</h2>
|
|
||||||
|
|
||||||
<p>Another part of our priority is adding protection for children while using the internet. We encourage parents and guardians to observe, participate in, monitor, guide and/or exercise total control on their online activity.</p>
|
|
||||||
|
|
||||||
<p>{{ site_name }} does not knowingly collect any Personal Identifiable Information from children under the age of 13. If you think that your child provided this kind of information on our website, we strongly encourage you to contact us immediately and we will do our best efforts to promptly remove such information from our records.</p>
|
|
||||||
|
|
||||||
<h2>Online Privacy Policy Only</h2>
|
|
||||||
|
|
||||||
<p>This Privacy Policy applies only to our online activities and is valid for visitors to our website with regards to the information that they shared and/or collect in {{ site_name }}. This policy is not applicable to any information collected via channels other than this website.</p>
|
|
||||||
|
|
||||||
<h2>Consent</h2>
|
|
||||||
|
|
||||||
<p>By using our website, you hereby consent <u>irrevocably</u> to our Privacy Policy and agree to its Terms and Conditions.</p>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
"""Peewee migrations -- 001_0_9_to_0_10.py.
|
|
||||||
|
|
||||||
Some examples (model - class or model name)::
|
|
||||||
|
|
||||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
|
||||||
> Model = migrator.ModelClass # Return model in current state by name
|
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
|
||||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
|
||||||
> migrator.change_fields(model, **fields) # Change fields
|
|
||||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
|
||||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
|
||||||
> migrator.rename_table(model, new_table_name)
|
|
||||||
> migrator.add_index(model, *col_names, unique=False)
|
|
||||||
> migrator.add_not_null(model, *field_names)
|
|
||||||
> migrator.add_default(model, field_name, default)
|
|
||||||
> migrator.add_constraint(model, name, sql)
|
|
||||||
> migrator.drop_index(model, *col_names)
|
|
||||||
> migrator.drop_not_null(model, *field_names)
|
|
||||||
> migrator.drop_constraints(model, *constraints)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from contextlib import suppress
|
|
||||||
|
|
||||||
import peewee as pw
|
|
||||||
from peewee_migrate import Migrator
|
|
||||||
|
|
||||||
|
|
||||||
with suppress(ImportError):
|
|
||||||
import playhouse.postgres_ext as pw_pext
|
|
||||||
|
|
||||||
|
|
||||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
|
||||||
"""Write your migrations here."""
|
|
||||||
|
|
||||||
@migrator.create_model
|
|
||||||
class BaseModel(pw.Model):
|
|
||||||
id = pw.AutoField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table_name = "basemodel"
|
|
||||||
|
|
||||||
@migrator.create_model
|
|
||||||
class User(pw.Model):
|
|
||||||
id = pw.AutoField()
|
|
||||||
username = pw.CharField(max_length=255, unique=True)
|
|
||||||
full_name = pw.TextField()
|
|
||||||
password = pw.CharField(max_length=255)
|
|
||||||
email = pw.CharField(max_length=255)
|
|
||||||
birthday = pw.DateField()
|
|
||||||
join_date = pw.DateTimeField()
|
|
||||||
is_disabled = pw.IntegerField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table_name = "user"
|
|
||||||
|
|
||||||
@migrator.create_model
|
|
||||||
class Message(pw.Model):
|
|
||||||
id = pw.AutoField()
|
|
||||||
user = pw.ForeignKeyField(column_name='user_id', field='id', model=migrator.orm['user'])
|
|
||||||
text = pw.TextField()
|
|
||||||
pub_date = pw.DateTimeField()
|
|
||||||
privacy = pw.IntegerField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table_name = "message"
|
|
||||||
|
|
||||||
@migrator.create_model
|
|
||||||
class MessageUpvote(pw.Model):
|
|
||||||
id = pw.AutoField()
|
|
||||||
message = pw.ForeignKeyField(column_name='message_id', field='id', model=migrator.orm['message'])
|
|
||||||
user = pw.ForeignKeyField(column_name='user_id', field='id', model=migrator.orm['user'])
|
|
||||||
created_date = pw.DateTimeField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table_name = "messageupvote"
|
|
||||||
indexes = [(('message', 'user'), True)]
|
|
||||||
|
|
||||||
@migrator.create_model
|
|
||||||
class Notification(pw.Model):
|
|
||||||
id = pw.AutoField()
|
|
||||||
type = pw.TextField()
|
|
||||||
target = pw.ForeignKeyField(column_name='target_id', field='id', model=migrator.orm['user'])
|
|
||||||
detail = pw.TextField()
|
|
||||||
pub_date = pw.DateTimeField()
|
|
||||||
seen = pw.IntegerField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table_name = "notification"
|
|
||||||
|
|
||||||
@migrator.create_model
|
|
||||||
class Relationship(pw.Model):
|
|
||||||
id = pw.AutoField()
|
|
||||||
from_user = pw.ForeignKeyField(column_name='from_user_id', field='id', model=migrator.orm['user'])
|
|
||||||
to_user = pw.ForeignKeyField(column_name='to_user_id', field='id', model=migrator.orm['user'])
|
|
||||||
created_date = pw.DateTimeField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table_name = "relationship"
|
|
||||||
indexes = [(('from_user', 'to_user'), True)]
|
|
||||||
|
|
||||||
@migrator.create_model
|
|
||||||
class Report(pw.Model):
|
|
||||||
id = pw.AutoField()
|
|
||||||
media_type = pw.IntegerField()
|
|
||||||
media_id = pw.IntegerField()
|
|
||||||
sender = pw.ForeignKeyField(column_name='sender_id', field='id', model=migrator.orm['user'], null=True)
|
|
||||||
reason = pw.IntegerField()
|
|
||||||
status = pw.IntegerField()
|
|
||||||
created_date = pw.DateTimeField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table_name = "report"
|
|
||||||
|
|
||||||
@migrator.create_model
|
|
||||||
class Upload(pw.Model):
|
|
||||||
id = pw.AutoField()
|
|
||||||
type = pw.TextField()
|
|
||||||
message = pw.ForeignKeyField(column_name='message_id', field='id', model=migrator.orm['message'])
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table_name = "upload"
|
|
||||||
|
|
||||||
@migrator.create_model
|
|
||||||
class UserAdminship(pw.Model):
|
|
||||||
user = pw.ForeignKeyField(column_name='user_id', field='id', model=migrator.orm['user'], primary_key=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table_name = "useradminship"
|
|
||||||
|
|
||||||
@migrator.create_model
|
|
||||||
class UserProfile(pw.Model):
|
|
||||||
user = pw.ForeignKeyField(column_name='user_id', field='id', model=migrator.orm['user'], primary_key=True)
|
|
||||||
biography = pw.TextField()
|
|
||||||
location = pw.IntegerField(null=True)
|
|
||||||
year = pw.IntegerField(null=True)
|
|
||||||
website = pw.TextField(null=True)
|
|
||||||
instagram = pw.TextField(null=True)
|
|
||||||
facebook = pw.TextField(null=True)
|
|
||||||
telegram = pw.TextField(null=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table_name = "userprofile"
|
|
||||||
|
|
||||||
|
|
||||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
|
||||||
"""Write your rollback migrations here."""
|
|
||||||
|
|
||||||
migrator.remove_model('userprofile')
|
|
||||||
|
|
||||||
migrator.remove_model('useradminship')
|
|
||||||
|
|
||||||
migrator.remove_model('upload')
|
|
||||||
|
|
||||||
migrator.remove_model('report')
|
|
||||||
|
|
||||||
migrator.remove_model('relationship')
|
|
||||||
|
|
||||||
migrator.remove_model('notification')
|
|
||||||
|
|
||||||
migrator.remove_model('messageupvote')
|
|
||||||
|
|
||||||
migrator.remove_model('message')
|
|
||||||
|
|
||||||
migrator.remove_model('user')
|
|
||||||
|
|
||||||
migrator.remove_model('basemodel')
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
"""Peewee migrations -- 002_move_columns_from_userprofile.py.
|
|
||||||
|
|
||||||
Some examples (model - class or model name)::
|
|
||||||
|
|
||||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
|
||||||
> Model = migrator.ModelClass # Return model in current state by name
|
|
||||||
|
|
||||||
> migrator.sql(sql) # Run custom SQL
|
|
||||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
|
||||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
|
||||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
|
||||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
|
||||||
> migrator.change_fields(model, **fields) # Change fields
|
|
||||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
|
||||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
|
||||||
> migrator.rename_table(model, new_table_name)
|
|
||||||
> migrator.add_index(model, *col_names, unique=False)
|
|
||||||
> migrator.add_not_null(model, *field_names)
|
|
||||||
> migrator.add_default(model, field_name, default)
|
|
||||||
> migrator.add_constraint(model, name, sql)
|
|
||||||
> migrator.drop_index(model, *col_names)
|
|
||||||
> migrator.drop_not_null(model, *field_names)
|
|
||||||
> migrator.drop_constraints(model, *constraints)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from contextlib import suppress
|
|
||||||
|
|
||||||
import peewee as pw
|
|
||||||
from peewee_migrate import Migrator
|
|
||||||
|
|
||||||
|
|
||||||
with suppress(ImportError):
|
|
||||||
import playhouse.postgres_ext as pw_pext
|
|
||||||
|
|
||||||
|
|
||||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
|
||||||
"""Write your migrations here."""
|
|
||||||
|
|
||||||
migrator.add_fields(
|
|
||||||
'user',
|
|
||||||
|
|
||||||
biography=pw.CharField(max_length=256, default=""),
|
|
||||||
website=pw.TextField(null=True))
|
|
||||||
|
|
||||||
migrator.change_fields('user', username=pw.CharField(max_length=30, unique=True))
|
|
||||||
|
|
||||||
migrator.change_fields('user', full_name=pw.CharField(max_length=80))
|
|
||||||
|
|
||||||
migrator.change_fields('user', password=pw.CharField(max_length=256))
|
|
||||||
|
|
||||||
migrator.change_fields('user', email=pw.CharField(max_length=256))
|
|
||||||
|
|
||||||
migrator.sql("""
|
|
||||||
UPDATE "user" SET biography = (SELECT p.biography FROM userprofile p WHERE p.user_id = id LIMIT 1),
|
|
||||||
website = (SELECT p.website FROM userprofile p WHERE p.user_id = id LIMIT 1);
|
|
||||||
""")
|
|
||||||
|
|
||||||
migrator.remove_fields('userprofile', 'year', 'instagram', 'facebook', 'telegram')
|
|
||||||
|
|
||||||
|
|
||||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
|
||||||
"""Write your rollback migrations here."""
|
|
||||||
|
|
||||||
migrator.add_fields(
|
|
||||||
'userprofile',
|
|
||||||
|
|
||||||
year=pw.IntegerField(null=True),
|
|
||||||
instagram=pw.TextField(null=True),
|
|
||||||
facebook=pw.TextField(null=True),
|
|
||||||
telegram=pw.TextField(null=True))
|
|
||||||
|
|
||||||
migrator.sql("""
|
|
||||||
UPDATE "userprofile" SET biography = (SELECT p.biography FROM user p WHERE p.user_id = id LIMIT 1),
|
|
||||||
website = (SELECT p.website FROM user p WHERE p.user_id = id LIMIT 1);
|
|
||||||
""")
|
|
||||||
|
|
||||||
migrator.remove_fields('user', 'biography', 'website')
|
|
||||||
|
|
||||||
migrator.change_fields('user', username=pw.CharField(max_length=255, unique=True))
|
|
||||||
|
|
||||||
migrator.change_fields('user', full_name=pw.TextField())
|
|
||||||
|
|
||||||
migrator.change_fields('user', password=pw.CharField(max_length=255))
|
|
||||||
|
|
||||||
migrator.change_fields('user', email=pw.CharField(max_length=255))
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue