mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
community list and beginning of viewing community
This commit is contained in:
parent
a4f8791777
commit
82000c1095
29 changed files with 1530 additions and 22 deletions
182
INSTALL.md
Normal file
182
INSTALL.md
Normal file
|
@ -0,0 +1,182 @@
|
|||
Mac OS
|
||||
---
|
||||
Install Python Version Manager (pyenv)
|
||||
see this site: https://opensource.com/article/19/5/python-3-default-mac
|
||||
|
||||
brew install pyenv
|
||||
|
||||
Install Python3 version and set as default (with pyenv)
|
||||
|
||||
pyenv install 3.8.6
|
||||
pyenv global 3.7.3
|
||||
|
||||
Note..
|
||||
You may see this error when running `pip install -r requirements.txt` in regards to psycopg2:
|
||||
|
||||
ld: library not found for -lssl
|
||||
clang: error: linker command failed with exit code 1 (use -v to see invocation)
|
||||
error: command 'clang' failed with exit status 1
|
||||
|
||||
If this happens try installing openssl...
|
||||
Install openssl with brew install openssl if you don't have it already.
|
||||
|
||||
brew install openssl
|
||||
|
||||
Add openssl path to LIBRARY_PATH :
|
||||
|
||||
export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/opt/openssl/lib/
|
||||
|
||||
Linux
|
||||
---
|
||||
install these additional packages
|
||||
|
||||
```sudo apt install python3-psycopg2 libpq-dev python3-dev```
|
||||
|
||||
|
||||
Pip Package Management:
|
||||
---
|
||||
|
||||
make sure you have 'wheel' installed:
|
||||
```pip install wheel```
|
||||
|
||||
dump currently installed packages to file:
|
||||
```pip freeze > requirements.txt```
|
||||
|
||||
install packages from a file:
|
||||
```pip install -r requirements.txt```
|
||||
|
||||
upgrade a package:
|
||||
```pip install --upgrade <package_name>```
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
Postgresql Setup:
|
||||
---
|
||||
installing postgresql https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-18-04
|
||||
|
||||
|
||||
|
||||
Windows (WSL 2 - Ubuntu 22.04 LTS - Python 3.9.16)
|
||||
---
|
||||
**Important**
|
||||
Python 3.10+ or 3.11+ may cause some package or compatibility errors. If you are having issues installing packages from
|
||||
requirements.txt, try using Python 3.8 or 3.9 instead with pyenv (https://github.com/pyenv/pyenv).
|
||||
Follow all the setup instructions in the pyenv documentation and setup any version of either Python 3.8 or 3.9.
|
||||
If you are getting installation errors or missing packages with pyenv, run
|
||||
|
||||
sudo apt update
|
||||
sudo apt install build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev curl libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev llvm
|
||||
|
||||
Install Python 3, pip, and venv
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get upgrade
|
||||
sudo apt-get install python3 python3-pip ipython3 libpq-dev python3-psycopg2 python3-dev build-essential
|
||||
sudo apt-get install python3-venv
|
||||
|
||||
Setup venv first before installing other packages
|
||||
**Note**
|
||||
(Replace <3.9> with your version number if you are using another version of Python,
|
||||
e.g. 'sudo apt-get install python3.10-venv' for Python 3.10. Repeat for the rest of the instructions below.)
|
||||
|
||||
python3.9 -m venv ./venv
|
||||
source venv/bin/activate
|
||||
|
||||
Make sure that your venv is also running the correct version of pyenv. You may need to re-setup venv if you setup venv before pyenv.
|
||||
|
||||
Follow the package installation instructions above to get the packages
|
||||
|
||||
python3.9 -m pip install --upgrade pip setuptools wheel
|
||||
pip install -r requirements.txt
|
||||
|
||||
Continue with the .env setup and "Run API" sections below.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
.env setup
|
||||
---
|
||||
add something like this to .env
|
||||
|
||||
DATABASE_URL=postgresql+psycopg2://rimu:password@localhost/buddytree
|
||||
|
||||
other environment variables include:
|
||||
|
||||
API_KEY - used to control access. Set this to the same on both the frontend and backend
|
||||
MAIL_SERVER=email-smtp.us-east-2.amazonaws.com
|
||||
MAIL_PORT
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD
|
||||
MAIL_USE_TLS = False
|
||||
MAIL_USE_SSL = False
|
||||
EMAIL_FROM
|
||||
EMAIL_FROM_NAME=BuddyTree
|
||||
|
||||
|
||||
Virtual Env setup (inside the api root directory)
|
||||
---
|
||||
python -m venv ./venv
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
Database Setup
|
||||
---
|
||||
Inside api dir
|
||||
source venv/bin/activate (to set up virtual env if necessary)
|
||||
flask db upgrade
|
||||
flask drop-constraint file file_user_id_fkey
|
||||
flask init-db
|
||||
flask init-intentions
|
||||
flask init-interests
|
||||
flask init-ages
|
||||
flask init-locations
|
||||
flask init-roles
|
||||
flask init-topics
|
||||
flask init-topics2
|
||||
flask topic-files
|
||||
flask init-activity
|
||||
flask init-timezones
|
||||
flask init-private-hangout-topics
|
||||
flask tidy-private-hangout-topics
|
||||
flask init-hosted
|
||||
flask init-countries
|
||||
|
||||
In future if you use git pull and notice some new files in migrations/versions/*, you need to do
|
||||
|
||||
flask db upgrade
|
||||
|
||||
---
|
||||
|
||||
|
||||
Run development server
|
||||
---
|
||||
|
||||
export FLASK_APP=pyfedi.py
|
||||
flask run
|
||||
|
||||
To enable debug mode and hot reloading, set the environment variable FLASK_ENV=development
|
||||
|
||||
export FLASK_ENV=development
|
||||
export FLASK_APP=pyfedi.py
|
||||
flask run
|
||||
|
||||
Make sure you have activated the venv by running
|
||||
|
||||
source venv/bin/activate
|
||||
first!
|
||||
|
||||
|
||||
Database Changes
|
||||
---
|
||||
create a migration based on recent changes to app/models.py:
|
||||
|
||||
flask db migrate -m "users table"
|
||||
|
||||
run migrations
|
||||
|
||||
flask db upgrade
|
|
@ -12,6 +12,8 @@ from flask_bootstrap import Bootstrap5
|
|||
from flask_mail import Mail
|
||||
from flask_moment import Moment
|
||||
from flask_babel import Babel, lazy_gettext as _l
|
||||
from sqlalchemy_searchable import make_searchable
|
||||
|
||||
from config import Config
|
||||
|
||||
|
||||
|
@ -36,6 +38,7 @@ def create_app(config_class=Config):
|
|||
mail.init_app(app)
|
||||
bootstrap.init_app(app)
|
||||
moment.init_app(app)
|
||||
make_searchable(db.metadata)
|
||||
babel.init_app(app, locale_selector=get_locale)
|
||||
|
||||
from app.main import bp as main_bp
|
||||
|
@ -53,6 +56,9 @@ def create_app(config_class=Config):
|
|||
from app.auth import bp as auth_bp
|
||||
app.register_blueprint(auth_bp, url_prefix='/auth')
|
||||
|
||||
from app.community import bp as community_bp
|
||||
app.register_blueprint(community_bp, url_prefix='/community')
|
||||
|
||||
def get_resource_as_string(name, charset='utf-8'):
|
||||
with app.open_resource(name) as f:
|
||||
return f.read().decode(charset)
|
||||
|
|
|
@ -3,6 +3,8 @@ from sqlalchemy import text
|
|||
from app import db
|
||||
from app.activitypub import bp
|
||||
from flask import request, Response, render_template, current_app, abort, jsonify
|
||||
|
||||
from app.community.routes import show_community
|
||||
from app.models import User, Community
|
||||
from app.activitypub.util import public_key, users_total, active_half_year, active_month, local_posts, local_comments, \
|
||||
post_to_activity
|
||||
|
@ -141,9 +143,15 @@ def community_profile(actor):
|
|||
""" Requests to this endpoint can be for a JSON representation of the community, or a HTML rendering of it.
|
||||
The two types of requests are differentiated by the header """
|
||||
actor = actor.strip()
|
||||
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
|
||||
if '@' in actor:
|
||||
# don't provide activitypub info for remote communities
|
||||
if 'application/ld+json' in request.headers.get('Accept', ''):
|
||||
abort(404)
|
||||
community = Community.query.filter_by(ap_id=actor, banned=False).first()
|
||||
else:
|
||||
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
|
||||
if community is not None:
|
||||
if 'application/ld+json' in request.headers.get('Accept', '') or request.accept_mimetypes.accept_json:
|
||||
if 'application/ld+json' in request.headers.get('Accept', ''):
|
||||
server = current_app.config['SERVER_NAME']
|
||||
actor_data = {"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
|
@ -188,8 +196,10 @@ def community_profile(actor):
|
|||
resp = jsonify(actor_data)
|
||||
resp.content_type = 'application/activity+json'
|
||||
return resp
|
||||
else:
|
||||
return render_template('user_profile.html', user=community)
|
||||
else: # browser request - return html
|
||||
return show_community(community)
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
|
||||
@bp.route('/c/<actor>/outbox', methods=['GET'])
|
||||
|
|
|
@ -39,7 +39,7 @@ import arrow
|
|||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
||||
from flask import Request
|
||||
from flask import Request, current_app
|
||||
from datetime import datetime
|
||||
from dateutil import parser
|
||||
from pyld import jsonld
|
||||
|
@ -313,7 +313,7 @@ class HttpSignature:
|
|||
)
|
||||
except requests.exceptions.SSLError as invalid_cert:
|
||||
# Not our problem if the other end doesn't have proper SSL
|
||||
print(f"{uri} {invalid_cert}")
|
||||
current_app.logger.info(f"{uri} {invalid_cert}")
|
||||
raise requests.exceptions.SSLError from invalid_cert
|
||||
except ValueError as ex:
|
||||
# Convert to a more generic error we handle
|
||||
|
|
|
@ -9,7 +9,7 @@ from app.auth.forms import LoginForm, RegistrationForm, ResetPasswordRequestForm
|
|||
from app.auth.util import random_token
|
||||
from app.models import User
|
||||
from app.auth.email import send_password_reset_email, send_welcome_email, send_verification_email
|
||||
from sqlalchemy import text
|
||||
from app.activitypub.signature import RsaKeys
|
||||
|
||||
|
||||
@bp.route('/login', methods=['GET', 'POST'])
|
||||
|
@ -132,10 +132,15 @@ def verify_email(token):
|
|||
if token != '':
|
||||
user = User.query.filter_by(verification_token=token).first()
|
||||
if user is not None:
|
||||
if user.verified: # guard against users double-clicking the link in the email
|
||||
return redirect(url_for('main.index'))
|
||||
user.verified = True
|
||||
user.last_seen = datetime.utcnow()
|
||||
private_key, public_key = RsaKeys.generate_keypair()
|
||||
user.private_key = private_key
|
||||
user.public_key = public_key
|
||||
db.session.commit()
|
||||
flash(_('Thanks for verifying your email address.'))
|
||||
flash(_('Thank you for verifying your email address. You can now post content and vote.'))
|
||||
else:
|
||||
flash(_('Email address validation failed.'), 'error')
|
||||
return redirect(url_for('main.index'))
|
14
app/cli.py
14
app/cli.py
|
@ -1,3 +1,8 @@
|
|||
# if commands in this file are not working (e.g. 'flask translate') make sure you set the FLASK_APP environment variable.
|
||||
# e.g. export FLASK_APP=pyfedi.py
|
||||
|
||||
|
||||
from app import db
|
||||
import click
|
||||
import os
|
||||
|
||||
|
@ -33,3 +38,12 @@ def register(app):
|
|||
"""Compile all languages."""
|
||||
if os.system('pybabel compile -d app/translations'):
|
||||
raise RuntimeError('compile command failed')
|
||||
|
||||
@app.cli.command("init-db")
|
||||
def init_db():
|
||||
with app.app_context():
|
||||
db.drop_all()
|
||||
db.configure_mappers()
|
||||
db.create_all()
|
||||
db.session.commit()
|
||||
print("Done")
|
||||
|
|
5
app/community/__init__.py
Normal file
5
app/community/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('community', __name__)
|
||||
|
||||
from app.community import routes
|
18
app/community/forms.py
Normal file
18
app/community/forms.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, TextAreaField, BooleanField
|
||||
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length
|
||||
from flask_babel import _, lazy_gettext as _l
|
||||
|
||||
|
||||
class AddLocalCommunity():
|
||||
name = StringField(_l('Name'), validators=[DataRequired()])
|
||||
url = StringField(_l('Url'))
|
||||
description = TextAreaField(_l('Description'))
|
||||
rules = TextAreaField(_l('Rules'))
|
||||
nsfw = BooleanField('18+ NSFW')
|
||||
submit = SubmitField(_l('Create'))
|
||||
|
||||
|
||||
class SearchRemoteCommunity(FlaskForm):
|
||||
address = StringField(_l('Server address'), validators=[DataRequired()])
|
||||
submit = SubmitField(_l('Search'))
|
44
app/community/routes.py
Normal file
44
app/community/routes.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
from datetime import date, datetime, timedelta
|
||||
from flask import render_template, redirect, url_for, flash, request, make_response, session, Markup, current_app
|
||||
from flask_login import login_user, logout_user, current_user
|
||||
from flask_babel import _
|
||||
from app import db
|
||||
from app.community.forms import SearchRemoteCommunity
|
||||
from app.community.util import search_for_community
|
||||
from app.constants import SUBSCRIPTION_MEMBER
|
||||
from app.models import User, Community
|
||||
from app.community import bp
|
||||
|
||||
|
||||
@bp.route('/add_local', methods=['GET', 'POST'])
|
||||
def add_local():
|
||||
form = AddLocalCommunity()
|
||||
if form.validate_on_submit():
|
||||
...
|
||||
|
||||
|
||||
@bp.route('/add_remote', methods=['GET', 'POST'])
|
||||
def add_remote():
|
||||
form = SearchRemoteCommunity()
|
||||
new_community = None
|
||||
if form.validate_on_submit():
|
||||
address = form.address.data.strip()
|
||||
if address.startswith('!') and '@' in address:
|
||||
new_community = search_for_community(address)
|
||||
elif address.startswith('@') and '@' in address[1:]:
|
||||
# todo: the user is searching for a person instead
|
||||
...
|
||||
elif '@' in address:
|
||||
new_community = search_for_community('!' + address)
|
||||
else:
|
||||
message = Markup('Type address in the format !community@server.name. Search on <a href="https://lemmyverse.net/communities">Lemmyverse.net</a> to find some.')
|
||||
flash(message, 'error')
|
||||
|
||||
return render_template('community/add_remote.html',
|
||||
title=_('Add remote community'), form=form, new_community=new_community,
|
||||
subscribed=current_user.subscribed(new_community) >= SUBSCRIPTION_MEMBER)
|
||||
|
||||
|
||||
# @bp.route('/c/<actor>', methods=['GET']) - defined in activitypub/routes.py, which calls this function for user requests. A bit weird.
|
||||
def show_community(community: Community):
|
||||
return render_template('community/community.html', community=community, title=community.title)
|
66
app/community/util.py
Normal file
66
app/community/util.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
from datetime import datetime
|
||||
|
||||
from app import db
|
||||
from app.models import Community, File, BannedInstances
|
||||
from app.utils import get_request
|
||||
|
||||
|
||||
def search_for_community(address: str):
|
||||
if address.startswith('!'):
|
||||
name, server = address[1:].split('@')
|
||||
|
||||
banned = BannedInstances.query.filter_by(domain=server).first()
|
||||
if banned:
|
||||
reason = f" Reason: {banned.reason}" if banned.reason is not None else ''
|
||||
raise Exception(f"{server} is blocked.{reason}") # todo: create custom exception class hierarchy
|
||||
|
||||
already_exists = Community.query.filter_by(ap_id=address[1:]).first()
|
||||
if already_exists:
|
||||
return already_exists
|
||||
|
||||
# Look up the profile address of the community using WebFinger
|
||||
# todo: try, except block around every get_request
|
||||
webfinger_data = get_request(f"https://{server}/.well-known/webfinger",
|
||||
params={'resource': f"acct:{address[1:]}"})
|
||||
if webfinger_data.status_code == 200:
|
||||
webfinger_json = webfinger_data.json()
|
||||
for links in webfinger_json['links']:
|
||||
if 'rel' in links and links['rel'] == 'self': # this contains the URL of the activitypub profile
|
||||
type = links['type'] if 'type' in links else 'application/activity+json'
|
||||
# retrieve the activitypub profile
|
||||
community_data = get_request(links['href'], headers={'Accept': type})
|
||||
# to see the structure of the json contained in community_data, do a GET to https://lemmy.world/c/technology with header Accept: application/activity+json
|
||||
if community_data.status_code == 200:
|
||||
community_json = community_data.json()
|
||||
if community_json['type'] == 'Group':
|
||||
community = Community(name=community_json['preferredUsername'],
|
||||
title=community_json['name'],
|
||||
description=community_json['summary'],
|
||||
nsfw=community_json['sensitive'],
|
||||
restricted_to_mods=community_json['postingRestrictedToMods'],
|
||||
created_at=community_json['published'],
|
||||
last_active=community_json['updated'],
|
||||
ap_id=f"{address[1:]}",
|
||||
ap_public_url=community_json['id'],
|
||||
ap_profile_id=community_json['id'],
|
||||
ap_followers_url=community_json['followers'],
|
||||
ap_inbox_url=community_json['endpoints']['sharedInbox'],
|
||||
ap_fetched_at=datetime.utcnow(),
|
||||
ap_domain=server,
|
||||
public_key=community_json['publicKey']['publicKeyPem'],
|
||||
# language=community_json['language'][0]['identifier'] # todo: language
|
||||
)
|
||||
if 'icon' in community_json:
|
||||
# todo: retrieve icon, save to disk, save more complete File record
|
||||
icon = File(source_url=community_json['icon']['url'])
|
||||
community.icon = icon
|
||||
db.session.add(icon)
|
||||
if 'image' in community_json:
|
||||
# todo: retrieve image, save to disk, save more complete File record
|
||||
image = File(source_url=community_json['image']['url'])
|
||||
community.image = image
|
||||
db.session.add(image)
|
||||
db.session.add(community)
|
||||
db.session.commit()
|
||||
return community
|
||||
return None
|
|
@ -6,3 +6,8 @@ POST_TYPE_IMAGE = 3
|
|||
POST_TYPE_VIDEO = 4
|
||||
|
||||
DATETIME_MS_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||
|
||||
SUBSCRIPTION_OWNER = 3
|
||||
SUBSCRIPTION_MODERATOR = 2
|
||||
SUBSCRIPTION_MEMBER = 1
|
||||
SUBSCRIPTION_NONMEMBER = 0
|
||||
|
|
|
@ -9,11 +9,6 @@ from flask_babel import _, get_locale
|
|||
from app.models import Community
|
||||
|
||||
|
||||
@bp.before_app_request
|
||||
def before_request():
|
||||
g.locale = str(get_locale())
|
||||
|
||||
|
||||
@bp.route('/', methods=['GET', 'POST'])
|
||||
@bp.route('/index', methods=['GET', 'POST'])
|
||||
def index():
|
||||
|
@ -25,4 +20,10 @@ def index():
|
|||
@bp.route('/communities', methods=['GET'])
|
||||
def list_communities():
|
||||
communities = Community.query.all()
|
||||
return render_template('list_communities.html', communities=communities)
|
||||
return render_template('list_communities.html', communities=communities)
|
||||
|
||||
|
||||
|
||||
@bp.before_app_request
|
||||
def before_request():
|
||||
g.locale = str(get_locale())
|
|
@ -10,6 +10,8 @@ from sqlalchemy_utils.types import TSVectorType # https://sqlalchemy-searchable.
|
|||
from app import db, login
|
||||
import jwt
|
||||
|
||||
from app.constants import SUBSCRIPTION_NONMEMBER, SUBSCRIPTION_MEMBER, SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER
|
||||
|
||||
|
||||
class File(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
@ -24,6 +26,7 @@ class File(db.Model):
|
|||
class Community(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
icon_id = db.Column(db.Integer, db.ForeignKey('file.id'))
|
||||
image_id = db.Column(db.Integer, db.ForeignKey('file.id'))
|
||||
name = db.Column(db.String(256), index=True)
|
||||
title = db.Column(db.String(256))
|
||||
description = db.Column(db.Text)
|
||||
|
@ -53,10 +56,40 @@ class Community(db.Model):
|
|||
restricted_to_mods = db.Column(db.Boolean, default=False)
|
||||
searchable = db.Column(db.Boolean, default=True)
|
||||
|
||||
search_vector = db.Column(TSVectorType('name', 'title', 'description'))
|
||||
search_vector = db.Column(TSVectorType('name', 'title', 'description', 'rules'))
|
||||
|
||||
posts = db.relationship('Post', backref='community', lazy='dynamic', cascade="all, delete-orphan")
|
||||
replies = db.relationship('PostReply', backref='community', lazy='dynamic', cascade="all, delete-orphan")
|
||||
icon = db.relationship('File', foreign_keys=[icon_id], single_parent=True, backref='community', cascade="all, delete-orphan")
|
||||
image = db.relationship('File', foreign_keys=[image_id], single_parent=True, cascade="all, delete-orphan")
|
||||
|
||||
def icon_image(self) -> str:
|
||||
if self.icon_id is not None:
|
||||
if self.icon.file_path is not None:
|
||||
return self.icon.file_path
|
||||
if self.icon.source_url is not None:
|
||||
return self.icon.source_url
|
||||
return ''
|
||||
|
||||
def header_image(self) -> str:
|
||||
if self.image_id is not None:
|
||||
if self.image.file_path is not None:
|
||||
return self.image.file_path
|
||||
if self.image.source_url is not None:
|
||||
return self.image.source_url
|
||||
return ''
|
||||
|
||||
def display_name(self) -> str:
|
||||
if self.ap_id is None:
|
||||
return self.title
|
||||
else:
|
||||
return f"{self.title}@{self.ap_domain}"
|
||||
|
||||
def link(self) -> str:
|
||||
if self.ap_id is None:
|
||||
return self.name
|
||||
else:
|
||||
return self.ap_id
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
|
@ -147,6 +180,20 @@ class User(UserMixin, db.Model):
|
|||
return True
|
||||
return self.expires < datetime(2019, 9, 1)
|
||||
|
||||
def subscribed(self, community) -> int:
|
||||
if community is None:
|
||||
return False
|
||||
subscription:CommunityMember = CommunityMember.query.filter_by(user_id=self.id, community_id=community.id).first()
|
||||
if subscription:
|
||||
if subscription.is_owner:
|
||||
return SUBSCRIPTION_OWNER
|
||||
elif subscription.is_moderator:
|
||||
return SUBSCRIPTION_MODERATOR
|
||||
else:
|
||||
return SUBSCRIPTION_MEMBER
|
||||
else:
|
||||
return SUBSCRIPTION_NONMEMBER
|
||||
|
||||
@staticmethod
|
||||
def verify_reset_password_token(token):
|
||||
try:
|
||||
|
@ -282,7 +329,7 @@ class BannedInstances(db.Model):
|
|||
|
||||
class Instance(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
domain = db.Column(db.String(256))
|
||||
domain = db.Column(db.String(256), index=True)
|
||||
inbox = db.Column(db.String(256))
|
||||
shared_inbox = db.Column(db.String(256))
|
||||
outbox = db.Column(db.String(256))
|
||||
|
|
41
app/static/js/markdown/demo.html
Normal file
41
app/static/js/markdown/demo.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
<link rel="stylesheet" href="https://netdna.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" href="https://netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
|
||||
<link href="editor.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<header></header>
|
||||
<main>
|
||||
|
||||
<textarea rows="10" class="form-control" id="editor"></textarea>
|
||||
</div>
|
||||
<script src="index.min.js"></script>
|
||||
<script>
|
||||
easyMarkdown(document.getElementById('editor'),{
|
||||
btnClass:'btn btn-danger',
|
||||
framework:'bootstrap',
|
||||
icons : {
|
||||
bold: 'fa fa-bold fa-fw',
|
||||
italic: 'fa fa-italic fa-fw',
|
||||
header: 'fa fa-header fa-fw',
|
||||
image: 'fa fa-picture-o fa-fw',
|
||||
link: 'fa fa-link fa-fw',
|
||||
ol: 'fa fa-list-ol fa-fw',
|
||||
ul: 'fa fa-list-ul fa-fw',
|
||||
comment: 'fa fa-comment fa-fw',
|
||||
code: 'fa fa-code fa-fw',
|
||||
preview: 'fa fa-search fa-fw',
|
||||
back: 'fa fa-backward fa-fw'
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
</main>
|
||||
<footer></footer>
|
||||
</body>
|
||||
</html>
|
31
app/static/js/markdown/editor.css
Normal file
31
app/static/js/markdown/editor.css
Normal file
|
@ -0,0 +1,31 @@
|
|||
.easy-markdown{
|
||||
position: absolute;
|
||||
display: block;
|
||||
left:0;
|
||||
top:0;
|
||||
width: 100%;
|
||||
webkit-transition: left 0.7s ease-in-out 0.1s;
|
||||
-moz-transition : left 0.7s ease-in-out 0.1s;
|
||||
-ms-transition : left 0.7s ease-in-out 0.1s;
|
||||
-o-transition : left 0.7s ease-in-out 0.1s;
|
||||
transition : left 0.7s ease-in-out 0.1s;
|
||||
}
|
||||
.easy-preview{
|
||||
position: absolute;
|
||||
display: block;
|
||||
top:0;
|
||||
left: 100%;
|
||||
width: 100%;
|
||||
|
||||
webkit-transition: left 0.7s ease-in-out 0.1s;
|
||||
-moz-transition : left 0.7s ease-in-out 0.1s;
|
||||
-ms-transition : left 0.7s ease-in-out 0.1s;
|
||||
-o-transition : left 0.7s ease-in-out 0.1s;
|
||||
transition : left 0.7s ease-in-out 0.1s;
|
||||
}
|
||||
.easy-markdown.is-hidden{
|
||||
left:-105%;
|
||||
}
|
||||
.easy-preview.is-visible{
|
||||
left:0;
|
||||
}
|
725
app/static/js/markdown/editor.js
Normal file
725
app/static/js/markdown/editor.js
Normal file
|
@ -0,0 +1,725 @@
|
|||
// MIT license
|
||||
|
||||
var easyMarkdown = (function() {
|
||||
'use strict';
|
||||
var regexplink = new RegExp(
|
||||
'^' +
|
||||
// protocol identifier
|
||||
'(?:(?:https?|ftp)://)' +
|
||||
// user:pass authentication
|
||||
'(?:\\S+(?::\\S*)?@)?' +
|
||||
'(?:' +
|
||||
// IP address exclusion
|
||||
// private & local networks
|
||||
'(?!(?:10|127)(?:\\.\\d{1,3}){3})' +
|
||||
'(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' +
|
||||
'(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' +
|
||||
// IP address dotted notation octets
|
||||
// excludes loopback network 0.0.0.0
|
||||
// excludes reserved space >= 224.0.0.0
|
||||
// excludes network & broacast addresses
|
||||
// (first & last IP address of each class)
|
||||
'(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' +
|
||||
'(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' +
|
||||
'(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' +
|
||||
'|' +
|
||||
// host name
|
||||
'(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)' +
|
||||
// domain name
|
||||
'(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*' +
|
||||
// TLD identifier
|
||||
'(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))' +
|
||||
')' +
|
||||
// port number
|
||||
'(?::\\d{2,5})?' +
|
||||
// resource path
|
||||
'(?:/\\S*)?' +
|
||||
'$', 'i'
|
||||
);
|
||||
var regexppic = new RegExp(
|
||||
'^' +
|
||||
// protocol identifier
|
||||
'(?:(?:https?|ftp)://)' +
|
||||
// user:pass authentication
|
||||
'(?:\\S+(?::\\S*)?@)?' +
|
||||
'(?:' +
|
||||
// IP address exclusion
|
||||
// private & local networks
|
||||
'(?!(?:10|127)(?:\\.\\d{1,3}){3})' +
|
||||
'(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' +
|
||||
'(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' +
|
||||
// IP address dotted notation octets
|
||||
// excludes loopback network 0.0.0.0
|
||||
// excludes reserved space >= 224.0.0.0
|
||||
// excludes network & broacast addresses
|
||||
// (first & last IP address of each class)
|
||||
'(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' +
|
||||
'(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' +
|
||||
'(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' +
|
||||
'|' +
|
||||
// host name
|
||||
'(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)' +
|
||||
// domain name
|
||||
'(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*' +
|
||||
// TLD identifier
|
||||
'(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))' +
|
||||
')' +
|
||||
// port number
|
||||
'(?::\\d{2,5})?' +
|
||||
// resource path
|
||||
'(?:/\\S*)?' +
|
||||
// image
|
||||
'(?:jpg|gif|png)'+
|
||||
'$', 'i'
|
||||
);
|
||||
function createDom(obj){
|
||||
var nodeArray = [];
|
||||
for ( var i in obj){
|
||||
var node = document.createElement(obj[i].type);
|
||||
for ( var j in obj[i].attrs)
|
||||
node.setAttribute( j, obj[i].attrs[j]);
|
||||
if (obj[i].text)
|
||||
node.appendChild(document.createTextNode(obj[i].text));
|
||||
nodeArray[i] = node;
|
||||
if (typeof(obj[i].childrenOf) !== 'undefined')
|
||||
nodeArray[obj[i].childrenOf].appendChild(node);
|
||||
}
|
||||
return nodeArray[0];
|
||||
}
|
||||
function createNode(el,attrs,text){
|
||||
var node = document.createElement(el);
|
||||
for(var key in attrs)
|
||||
node.setAttribute(key, attrs[key]);
|
||||
if (text)
|
||||
node.appendChild(document.createTextNode(text));
|
||||
return node;
|
||||
}
|
||||
function applyStyle(el,attrs){
|
||||
for(var key in attrs)
|
||||
el.style[key] = attrs[key];
|
||||
}
|
||||
function merge(obj) {
|
||||
var i = 1,target, key;
|
||||
for (; i < arguments.length; i += 1) {
|
||||
target = arguments[i];
|
||||
for (key in target)
|
||||
if (Object.prototype.hasOwnProperty.call(target, key))
|
||||
obj[key] = target[key];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
function getStyle (el,styleProp){
|
||||
var y;
|
||||
if (el.currentStyle)
|
||||
y = el.currentStyle[styleProp];
|
||||
else if (window.getComputedStyle)
|
||||
y = document.defaultView.getComputedStyle(el,null).getPropertyValue(styleProp);
|
||||
return y;
|
||||
}
|
||||
function easyMarkdown(node, options) {
|
||||
return new Editor(node, options);
|
||||
}
|
||||
/*========== BUTTONS ==========*/
|
||||
function Buttons(element,options,buttons) {
|
||||
this.element = element;
|
||||
this.options = options;
|
||||
this.locale = merge({}, easyMarkdown.locale, easyMarkdown.locale[options.locale] || {});
|
||||
this.buttons = {
|
||||
header: {
|
||||
title : this.locale.header.title,
|
||||
text : 'header',
|
||||
group : 0,
|
||||
callback : function(e) {
|
||||
// Append/remove ### surround the selection
|
||||
var chunk, cursor, selected = e.getSelection(),
|
||||
content = e.getContent();
|
||||
if (selected.length === 0) {
|
||||
// Give extra word
|
||||
chunk = e.locale.header.description + '\n';
|
||||
} else {
|
||||
chunk = selected.text + '\n';
|
||||
}
|
||||
var key = 0,
|
||||
hash='',
|
||||
start = selected.start-1,
|
||||
end = selected.start,
|
||||
prevChr = content.substring(start,end);
|
||||
while (/^\s+$|^#+$/.test(prevChr)){
|
||||
if (/^#+$/.test(prevChr))
|
||||
hash = hash+'#';
|
||||
key +=1;
|
||||
prevChr = content.substring(start-key,end-key);
|
||||
}
|
||||
|
||||
if (hash.length > 0){
|
||||
// already a title
|
||||
var startLinePos,
|
||||
endLinePos = content.indexOf('\n', selected.start);
|
||||
|
||||
// more than ### -> #
|
||||
if (hash.length > 2){
|
||||
hash = '#';
|
||||
startLinePos = content.indexOf('\n', selected.start - 5);
|
||||
e.setSelection(startLinePos, endLinePos+1);
|
||||
e.replaceSelection('\n'+hash+' '+chunk);
|
||||
cursor = startLinePos+3;
|
||||
}else{
|
||||
hash = hash +'#';
|
||||
startLinePos = content.indexOf('\n', selected.start - (hash.length + 1));
|
||||
e.setSelection(startLinePos, endLinePos+1);
|
||||
e.replaceSelection('\n'+hash+' '+chunk);
|
||||
cursor = selected.start + 1;
|
||||
}
|
||||
}else{
|
||||
|
||||
// new title
|
||||
hash= '#';
|
||||
e.replaceSelection('\n'+hash+' '+ chunk);
|
||||
cursor = selected.start + 3;
|
||||
}
|
||||
e.setSelection(cursor, cursor + chunk.length-1);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
bold: {
|
||||
title : this.locale.bold.title,
|
||||
text : 'bold',
|
||||
group : 0,
|
||||
callback : function(e) {
|
||||
// Give/remove ** surround the selection
|
||||
var chunk, cursor, selected = e.getSelection(),
|
||||
content = e.getContent();
|
||||
if (selected.length === 0) {
|
||||
// Give extra word
|
||||
chunk = e.locale.bold.description;
|
||||
} else {
|
||||
chunk = selected.text;
|
||||
}
|
||||
// transform selection and set the cursor into chunked text
|
||||
if (content.substr(selected.start - 2, 2) === '**' && content.substr(selected.end, 2) === '**') {
|
||||
e.setSelection(selected.start - 2, selected.end + 2);
|
||||
e.replaceSelection(chunk);
|
||||
cursor = selected.start - 2;
|
||||
} else {
|
||||
e.replaceSelection('**' + chunk + '**');
|
||||
cursor = selected.start + 2;
|
||||
}
|
||||
// Set the cursor
|
||||
e.setSelection(cursor, cursor + chunk.length);
|
||||
}
|
||||
},
|
||||
italic: {
|
||||
title : this.locale.italic.title,
|
||||
text : 'italic',
|
||||
group : 0,
|
||||
callback : function(e) {
|
||||
// Give/remove * surround the selection
|
||||
var chunk, cursor, selected = e.getSelection(),
|
||||
content = e.getContent();
|
||||
if (selected.length === 0) {
|
||||
// Give extra word
|
||||
chunk = e.locale.italic.description;
|
||||
} else {
|
||||
chunk = selected.text;
|
||||
}
|
||||
// transform selection and set the cursor into chunked text
|
||||
if (content.substr(selected.start - 1, 1) === '_' && content.substr(selected.end, 1) === '_') {
|
||||
e.setSelection(selected.start - 1, selected.end + 1);
|
||||
e.replaceSelection(chunk);
|
||||
cursor = selected.start - 1;
|
||||
} else {
|
||||
e.replaceSelection('_' + chunk + '_');
|
||||
cursor = selected.start + 1;
|
||||
}
|
||||
// Set the cursor
|
||||
e.setSelection(cursor, cursor + chunk.length);
|
||||
}
|
||||
},
|
||||
image: {
|
||||
title : this.locale.image.title,
|
||||
text : 'image',
|
||||
group : 1,
|
||||
callback : function(e) {
|
||||
// Give ![] surround the selection and prepend the image link
|
||||
var chunk, cursor, selected = e.getSelection(),
|
||||
link;
|
||||
if (selected.length === 0) {
|
||||
// Give extra word
|
||||
chunk = e.locale.image.description;
|
||||
} else {
|
||||
chunk = selected.text;
|
||||
}
|
||||
link = prompt(e.locale.image.title, 'http://');
|
||||
if (regexppic.test(link)) {
|
||||
e.replaceSelection('![' + chunk + '](' + link + ' "' + e.locale.image.description + '")');
|
||||
cursor = selected.start + 2;
|
||||
e.setSelection(cursor, cursor + chunk.length);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
link: {
|
||||
title : this.locale.link.title,
|
||||
text : 'link',
|
||||
group : 1,
|
||||
callback : function(e) {
|
||||
// Give [] surround the selection and prepend the link
|
||||
var chunk, cursor, selected = e.getSelection(),
|
||||
link;
|
||||
if (selected.length === 0) {
|
||||
// Give extra word
|
||||
chunk = e.locale.link.description;
|
||||
} else {
|
||||
chunk = selected.text;
|
||||
}
|
||||
link = prompt(e.locale.link.title, 'http://');
|
||||
if (regexplink.test(link)) {
|
||||
e.replaceSelection('[' + chunk + '](' + link + ')');
|
||||
cursor = selected.start + 1;
|
||||
// Set the cursor
|
||||
e.setSelection(cursor, cursor + chunk.length);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
ol: {
|
||||
title : this.locale.ol.title,
|
||||
text : 'ol',
|
||||
group : 2,
|
||||
callback : function(e) {
|
||||
// Prepend/Give - surround the selection
|
||||
var chunk, cursor, selected = e.getSelection();
|
||||
// transform selection and set the cursor into chunked text
|
||||
if (selected.length === 0) {
|
||||
// Give extra word
|
||||
chunk = e.locale.ol.description;
|
||||
e.replaceSelection('1. ' + chunk);
|
||||
// Set the cursor
|
||||
cursor = selected.start + 3;
|
||||
} else {
|
||||
if (selected.text.indexOf('\n') < 0) {
|
||||
chunk = selected.text;
|
||||
e.replaceSelection('1. ' + chunk);
|
||||
// Set the cursor
|
||||
cursor = selected.start + 3;
|
||||
} else {
|
||||
var list = [];
|
||||
list = selected.text.split('\n');
|
||||
chunk = list[0];
|
||||
for (var key in list) {
|
||||
var index = parseInt(key) + parseInt(1);
|
||||
list[key] = index + '. ' + list[key];
|
||||
}
|
||||
e.replaceSelection('\n\n' + list.join('\n'));
|
||||
// Set the cursor
|
||||
cursor = selected.start + 5;
|
||||
}
|
||||
}
|
||||
// Set the cursor
|
||||
e.setSelection(cursor, cursor + chunk.length);
|
||||
}
|
||||
},
|
||||
ul: {
|
||||
title : this.locale.ul.title,
|
||||
text : 'ul',
|
||||
group : 2,
|
||||
callback : function(e) {
|
||||
// Prepend/Give - surround the selection
|
||||
var chunk, cursor, selected = e.getSelection();
|
||||
// transform selection and set the cursor into chunked text
|
||||
if (selected.length === 0) {
|
||||
// Give extra word
|
||||
chunk = e.locale.ul.description;
|
||||
e.replaceSelection('- ' + chunk);
|
||||
// Set the cursor
|
||||
cursor = selected.start + 2;
|
||||
} else {
|
||||
if (selected.text.indexOf('\n') < 0) {
|
||||
chunk = selected.text;
|
||||
e.replaceSelection('- ' + chunk);
|
||||
// Set the cursor
|
||||
cursor = selected.start + 2;
|
||||
} else {
|
||||
var list = [];
|
||||
list = selected.text.split('\n');
|
||||
chunk = list[0];
|
||||
for (var key in list) {
|
||||
list[key] = '- ' + list[key];
|
||||
}
|
||||
e.replaceSelection('\n\n' + list.join('\n'));
|
||||
// Set the cursor
|
||||
cursor = selected.start + 4;
|
||||
}
|
||||
}
|
||||
// Set the cursor
|
||||
e.setSelection(cursor, cursor + chunk.length);
|
||||
}
|
||||
},
|
||||
comment: {
|
||||
title : this.locale.comment.title,
|
||||
text : 'comment',
|
||||
group : 3,
|
||||
callback : function(e) {
|
||||
// Prepend/Give - surround the selection
|
||||
var chunk, cursor, selected = e.getSelection(),
|
||||
content = e.getContent();
|
||||
// transform selection and set the cursor into chunked text
|
||||
if (selected.length === 0) {
|
||||
// Give extra word
|
||||
chunk = e.locale.comment.description;
|
||||
e.replaceSelection('> ' + chunk);
|
||||
// Set the cursor
|
||||
cursor = selected.start + 2;
|
||||
} else {
|
||||
if (selected.text.indexOf('\n') < 0) {
|
||||
chunk = selected.text;
|
||||
e.replaceSelection('> ' + chunk);
|
||||
// Set the cursor
|
||||
cursor = selected.start + 2;
|
||||
} else {
|
||||
var list = [];
|
||||
list = selected.text.split('\n');
|
||||
chunk = list[0];
|
||||
for (var key in list)
|
||||
list[key] = '> ' + list[key];
|
||||
e.replaceSelection('\n\n' + list.join('\n'));
|
||||
// Set the cursor
|
||||
cursor = selected.start + 4;
|
||||
}
|
||||
}
|
||||
// Set the cursor
|
||||
e.setSelection(cursor, cursor + chunk.length);
|
||||
}
|
||||
},
|
||||
code: {
|
||||
title : this.locale.code.title,
|
||||
text : 'code',
|
||||
group : 3,
|
||||
callback : function(e) {
|
||||
// Give/remove ** surround the selection
|
||||
var chunk, cursor, selected = e.getSelection(),
|
||||
content = e.getContent();
|
||||
if (selected.length === 0) {
|
||||
// Give extra word
|
||||
chunk = e.locale.code.description;
|
||||
} else {
|
||||
chunk = selected.text;
|
||||
}
|
||||
// transform selection and set the cursor into chunked text
|
||||
if (content.substr(selected.start - 4, 4) === '```\n' && content.substr(selected.end, 4) === '\n```') {
|
||||
e.setSelection(selected.start - 4, selected.end + 4);
|
||||
e.replaceSelection(chunk);
|
||||
cursor = selected.start - 4;
|
||||
} else if (content.substr(selected.start - 1, 1) === '`' && content.substr(selected.end, 1) === '`') {
|
||||
e.setSelection(selected.start - 1, selected.end + 1);
|
||||
e.replaceSelection(chunk);
|
||||
cursor = selected.start - 1;
|
||||
} else if (content.indexOf('\n') > -1) {
|
||||
e.replaceSelection('```\n' + chunk + '\n```');
|
||||
cursor = selected.start + 4;
|
||||
} else {
|
||||
e.replaceSelection('`' + chunk + '`');
|
||||
cursor = selected.start + 1;
|
||||
}
|
||||
// Set the cursor
|
||||
e.setSelection(cursor, cursor + chunk.length);
|
||||
}
|
||||
},
|
||||
preview: {
|
||||
title : this.locale.preview.title,
|
||||
text : 'preview',
|
||||
group : 4,
|
||||
callback : function(e) {
|
||||
var txteditor = document.getElementById('easy-markdown');
|
||||
var preview = document.getElementById('easy-preview');
|
||||
var button = document.getElementById('easy-preview-close');
|
||||
button.removeAttribute('disabled');
|
||||
//preview.childNodes[1].childNodes[0].innerHTML = markdown.toHTML(e.element.value);
|
||||
var md = window.markdownit();
|
||||
|
||||
preview.childNodes[1].innerHTML = md.render(e.element.value);
|
||||
txteditor.classList.add('is-hidden');
|
||||
preview.classList.add('is-visible');
|
||||
}
|
||||
}
|
||||
};
|
||||
if (this.options.framework === 'bootstrap' || this.options.framework === 'foundation'){
|
||||
return this[this.options.framework]();
|
||||
}else{
|
||||
return this.none();
|
||||
}
|
||||
}
|
||||
Buttons.prototype = {
|
||||
getContent: function() {
|
||||
return this.element.value;
|
||||
},
|
||||
findSelection: function(chunk) {
|
||||
var content = this.getContent(),
|
||||
startChunkPosition;
|
||||
if (startChunkPosition = content.indexOf(chunk), startChunkPosition >= 0 && chunk.length > 0) {
|
||||
var oldSelection = this.getSelection(),
|
||||
selection;
|
||||
this.setSelection(startChunkPosition, startChunkPosition + chunk.length);
|
||||
selection = this.getSelection();
|
||||
this.setSelection(oldSelection.start, oldSelection.end);
|
||||
return selection;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getSelection: function() {
|
||||
var e = this.element;
|
||||
return (
|
||||
('selectionStart' in e && function() {
|
||||
var l = e.selectionEnd - e.selectionStart;
|
||||
return {
|
||||
start: e.selectionStart,
|
||||
end: e.selectionEnd,
|
||||
length: l,
|
||||
text: e.value.substr(e.selectionStart, l)
|
||||
};
|
||||
}) ||
|
||||
/* browser not supported */
|
||||
function() {
|
||||
return null;
|
||||
}
|
||||
)();
|
||||
},
|
||||
setIcons: function(element,button){
|
||||
if (typeof(this.options.icons) === 'string'){
|
||||
var t = document.createTextNode(element.title);
|
||||
button.appendChild(t);
|
||||
|
||||
}else{
|
||||
var i = document.createElement('I');
|
||||
i.setAttribute('class', this.options.icons[element.text]);
|
||||
button.appendChild(i);
|
||||
}
|
||||
},
|
||||
setListener: function(node) {
|
||||
var that = this;
|
||||
node.addEventListener('click', function(e) {
|
||||
var element = e.target,
|
||||
target = (element.nodeName === 'I') ? element.parentNode : element;
|
||||
that.buttons[target.getAttribute('data-md')].callback(that);
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
return node;
|
||||
},
|
||||
setSelection: function(start, end) {
|
||||
var e = this.element;
|
||||
return (
|
||||
('selectionStart' in e && function() {
|
||||
e.selectionStart = start;
|
||||
e.selectionEnd = end;
|
||||
return;
|
||||
}) ||
|
||||
/* browser not supported */
|
||||
function() {
|
||||
return null;
|
||||
}
|
||||
)();
|
||||
},
|
||||
replaceSelection: function(text) {
|
||||
var e = this.element;
|
||||
return (
|
||||
('selectionStart' in e && function() {
|
||||
e.value = e.value.substr(0, e.selectionStart) + text + e.value.substr(e.selectionEnd, e.value.length);
|
||||
// Set cursor to the last replacement end
|
||||
e.selectionStart = e.value.length;
|
||||
return this;
|
||||
})
|
||||
)();
|
||||
}
|
||||
};
|
||||
Buttons.prototype.bootstrap = function(){
|
||||
var button_groups = createDom ({
|
||||
0 : {'type':'div'},
|
||||
1 : {'type':'div','attrs': {'class':'btn-group','role':'group','style':'margin:5px;'},'childrenOf': 0},
|
||||
2 : {'type':'div','attrs': {'class':'btn-group','role':'group','style':'margin:5px;'},'childrenOf': 0},
|
||||
3 : {'type':'div','attrs': {'class':'btn-group','role':'group','style':'margin:5px;'},'childrenOf': 0},
|
||||
4 : {'type':'div','attrs': {'class':'btn-group','role':'group','style':'margin:5px;'},'childrenOf': 0},
|
||||
5 : {'type':'div','attrs': {'class':'btn-group','role':'group','style':'margin:5px;'},'childrenOf': 0}
|
||||
});
|
||||
for (var i in this.buttons) {
|
||||
var obj = this.buttons[i];
|
||||
if (this.options.disabled[obj.text] !== true) {
|
||||
var button = createNode('BUTTON',{'class':this.options.btnClass,'data-md':obj.text,'title':obj.title });
|
||||
this.setIcons(obj,button);
|
||||
button_groups.childNodes[obj.group].appendChild(button);
|
||||
}
|
||||
}
|
||||
this.setListener(button_groups);
|
||||
return button_groups;
|
||||
};
|
||||
Buttons.prototype.foundation = function(){
|
||||
var container = createNode('UL',{'class': 'button-group','style':'margin: 0 0 10px 0;'});
|
||||
for (var i in this.buttons) {
|
||||
var obj = this.buttons[i];
|
||||
if (this.options.disabled[obj.text] !== true) {
|
||||
var li = createNode('LI');
|
||||
var a = createNode('A',{'class':this.options.btnClass,'data-md':obj.text,'title':obj.title });
|
||||
this.setIcons(obj,a);
|
||||
container.appendChild(li).appendChild(a);
|
||||
}
|
||||
}
|
||||
this.setListener(container);
|
||||
return container;
|
||||
};
|
||||
Buttons.prototype.none = function(){
|
||||
var container = document.createElement('DIV');
|
||||
for (var key in this.buttons) {
|
||||
var obj = this.buttons[key];
|
||||
if (this.options.disabled[obj.text] !== true) {
|
||||
var button = createNode('BUTTON',{'class':this.options.btnClass,'data-md':obj.text,'title':obj.title });
|
||||
this.setIcons(obj,button);
|
||||
container.appendChild(button);
|
||||
}
|
||||
}
|
||||
this.setListener(container);
|
||||
return container;
|
||||
};
|
||||
/*========== SKELETON ==========*/
|
||||
|
||||
function Skeleton(options,textarea,buttons) {
|
||||
this.element = textarea;
|
||||
this.options = options;
|
||||
this.buttons = buttons;
|
||||
return this.build();
|
||||
}
|
||||
Skeleton.prototype = {
|
||||
build : function(){
|
||||
var buttons = new Buttons(this.element,this.options,this.buttons);
|
||||
var dom = createDom ({
|
||||
0 : {'type':'div','attrs': {'class':'easy-markdown','id':'easy-markdown'}},
|
||||
1 : {'type':'div','attrs': {'id':'easy-markdown-buttons'},'childrenOf': 0},
|
||||
2 : {'type':'div','attrs': {'id':'easy-markdown-textarea'},'childrenOf': 0}
|
||||
});
|
||||
dom.childNodes[0].appendChild(buttons);
|
||||
dom.childNodes[1].appendChild(this.element);
|
||||
return dom;
|
||||
}
|
||||
};
|
||||
/*========== PREVIEW ==========*/
|
||||
function Preview(options,parent) {
|
||||
this.parent = parent;
|
||||
this.options = options;
|
||||
this.locale = merge({}, easyMarkdown.locale, easyMarkdown.locale[options.locale] || {});
|
||||
return this.build();
|
||||
}
|
||||
Preview.prototype = {
|
||||
build: function() {
|
||||
|
||||
var dom = createDom ({
|
||||
0 : {'type':'div','attrs': {'class':'easy-preview','id':'easy-preview','style':'height:'+this.parent.clientHeight +'px;'}},
|
||||
1 : {'type':'div','attrs': {'id':'easy-preview-buttons'},'childrenOf': 0},
|
||||
2 : {'type':'div','attrs': {'id':'easy-preview-html','style':'overflow:auto;'},'childrenOf': 0},
|
||||
3 : {'type':'button','attrs':{'class':this.options.btnClass,'disabled':'disabled','id':'easy-preview-close'},'text':this.locale.getback.title,'childrenOf': 1},
|
||||
4 : {'type':'HR','childrenOf':1}
|
||||
});
|
||||
this.setListener(dom.childNodes[0].childNodes[0]);
|
||||
this.parent.appendChild(dom);
|
||||
dom.childNodes[1].style.height = this.parent.clientHeight - dom.childNodes[0].clientHeight - 20 +'px';
|
||||
|
||||
},
|
||||
setListener : function(node){
|
||||
node.addEventListener('click', function(e) {
|
||||
var txteditor = document.getElementById('easy-markdown');
|
||||
var preview = document.getElementById('easy-preview');
|
||||
var button = document.getElementById('easy-preview-close');
|
||||
button.setAttribute('disabled','disabled');
|
||||
txteditor.classList.remove('is-hidden');
|
||||
preview.classList.remove('is-visible');
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
return node;
|
||||
}
|
||||
};
|
||||
/*========== EDITOR ==========*/
|
||||
function Editor(node, options) {
|
||||
this.element = node;
|
||||
this.parent = node.parentNode;
|
||||
this.parent.innerHTML = '';
|
||||
this.options = merge({}, Editor.defaults, options || {});
|
||||
// test if markdown.js is missing
|
||||
if (typeof(markdownit) === 'undefined')
|
||||
this.options.disabled.preview = true;
|
||||
this.preview = 'off';
|
||||
var skeleton = new Skeleton(this.options,this.element);
|
||||
node.style.width = this.options.width;
|
||||
this.parent.appendChild(skeleton);
|
||||
|
||||
applyStyle(this.parent,{position:'relative',height:skeleton.clientHeight+'px',overflow:'hidden'});
|
||||
new Preview(this.options,this.parent);
|
||||
}
|
||||
easyMarkdown.Preview = Preview;
|
||||
easyMarkdown.Buttons = Buttons;
|
||||
easyMarkdown.Skeleton = Skeleton;
|
||||
easyMarkdown.Editor = Editor;
|
||||
easyMarkdown.locale = {
|
||||
bold: {
|
||||
title:'Bold',
|
||||
description:'Strong Text'
|
||||
},
|
||||
italic: {
|
||||
title:'Italic' ,
|
||||
description: 'Emphasized text'
|
||||
},
|
||||
header: {
|
||||
title:'Header',
|
||||
description: 'Heading text'
|
||||
},
|
||||
image: {
|
||||
title:'Image',
|
||||
description:'Image description'
|
||||
},
|
||||
link: {
|
||||
title:'Link',
|
||||
description:'Link description'
|
||||
},
|
||||
ol: {
|
||||
title:'Numbered',
|
||||
description:'Numbered list'
|
||||
},
|
||||
ul: {
|
||||
title:'Bullet',
|
||||
description:'Bulleted list'
|
||||
},
|
||||
comment: {
|
||||
title:'Comment',
|
||||
description: 'Comment'
|
||||
},
|
||||
code: {
|
||||
title:'Code',
|
||||
description: 'code text'
|
||||
},
|
||||
preview: {
|
||||
title:'Preview'
|
||||
},
|
||||
getback: {
|
||||
title: 'Get back'
|
||||
}
|
||||
};
|
||||
Editor.defaults = {
|
||||
width: '100%',
|
||||
btnClass: '',
|
||||
framework: 'none',
|
||||
locale:'',
|
||||
icons: '',
|
||||
disabled: {
|
||||
bold: false,
|
||||
italic: false,
|
||||
header: false,
|
||||
image: false,
|
||||
link: false,
|
||||
ol: false,
|
||||
ul: false,
|
||||
comment: false,
|
||||
code: false,
|
||||
preview: false
|
||||
}
|
||||
};
|
||||
|
||||
return easyMarkdown;
|
||||
|
||||
})();
|
1
app/static/js/markdown/index.min.js
vendored
Normal file
1
app/static/js/markdown/index.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -265,4 +265,16 @@ fieldset legend {
|
|||
margin-right: auto;
|
||||
}
|
||||
|
||||
.community_header {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.community_header_no_background .community_icon, .community_header .community_icon {
|
||||
width: 120px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=structure.css.map */
|
||||
|
|
|
@ -50,3 +50,17 @@ nav, etc which are used site-wide */
|
|||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.community_header {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.community_header_no_background, .community_header {
|
||||
.community_icon {
|
||||
width: 120px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -247,10 +247,37 @@ nav.navbar {
|
|||
font-size: 140%;
|
||||
}
|
||||
|
||||
.alert-message {
|
||||
.alert-info, .alert-message, .alert-error {
|
||||
color: #0c5460;
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.alert-info, .alert-message, .alert-error {
|
||||
padding-left: 1.8rem;
|
||||
padding-right: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
color: #722047;
|
||||
background-color: #f8d7da;
|
||||
}
|
||||
|
||||
.community_icon {
|
||||
width: 30px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.community_icon_big {
|
||||
width: 120px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.bump_up {
|
||||
position: absolute;
|
||||
top: 104px;
|
||||
left: 26px;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=styles.css.map */
|
||||
|
|
|
@ -45,8 +45,34 @@ nav.navbar {
|
|||
font-size: 140%;
|
||||
}
|
||||
|
||||
.alert-message {
|
||||
.alert-info, .alert-message, .alert-error {
|
||||
color: #0c5460;
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
@include breakpoint(tablet) {
|
||||
padding-left: 1.8rem;
|
||||
padding-right: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
color: #722047;
|
||||
background-color: #f8d7da;
|
||||
}
|
||||
|
||||
.community_icon {
|
||||
width: 30px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.community_icon_big {
|
||||
width: 120px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.bump_up {
|
||||
position: absolute;
|
||||
top: 104px;
|
||||
left: 26px;
|
||||
|
||||
}
|
|
@ -32,7 +32,7 @@
|
|||
<!-- Page content -->
|
||||
{% block navbar %}
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand" href="/" target="_blank">Logo</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
|
@ -43,10 +43,12 @@
|
|||
<ul class="nav navbar-nav ml-md-4">
|
||||
{% if current_user.is_anonymous %}
|
||||
<li class="nav-item"><a class="nav-link" href="/">{{ _('Home') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/communities">{{ _('Communities') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/auth/login">{{ _('Log in') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/auth/register">{{ _('Register') }}</a></li>
|
||||
{% else %}
|
||||
<li class="nav-item"><a class="nav-link" href="/">{{ _('Home') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/communities">{{ _('Communities') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/auth/logout">{{ _('Log out') }}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
@ -57,7 +59,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="outer_container" class="container-fluid flex-shrink-0 mt-4">
|
||||
<div id="outer_container" class="container-lg flex-shrink-0 mt-4 pt-1">
|
||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
|
|
38
app/templates/community/add_remote.html
Normal file
38
app/templates/community/add_remote.html
Normal file
|
@ -0,0 +1,38 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col mx-auto">
|
||||
<div class="card mt-5">
|
||||
<div class="card-body p-6">
|
||||
<div class="card-title">{{ _('Search') }}</div>
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if new_community %}
|
||||
<div class="row">
|
||||
<div class="col mx-auto">
|
||||
<div class="card mt-5">
|
||||
<div class="card-body p-6">
|
||||
<div class="card-title">{{ _('Found a community:') }}</div>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
<a href="/c/{{ new_community.link() }}"><img src="{{ new_community.icon_image()}}" class="community_icon rounded-circle" /></a>
|
||||
<a href="/c/{{ new_community.link() }}">{{ new_community.title }}@{{ new_community.ap_domain }}</a>
|
||||
{% if subscribed %}
|
||||
<a class="btn btn-primary mt-4" href="/c/{{ new_community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
|
||||
{% else %}
|
||||
<a class="btn btn-primary mt-4" href="/c/{{ new_community.link() }}/subscribe">{{ _('Subscribe') }}</a>
|
||||
{% endif %}
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
58
app/templates/community/community.html
Normal file
58
app/templates/community/community.html
Normal file
|
@ -0,0 +1,58 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-8 position-relative">
|
||||
{% if community.header_image() != '' %}
|
||||
<div class="community_header" style="height: 240px; background-image: url({{ community.header_image() }});"></div>
|
||||
<img class="community_icon_big bump_up rounded-circle" src="{{ community.icon_image() }}" />
|
||||
<h1 class="mt-2">{{ community.title }}</h1>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<img class="community_icon_big rounded-circle" src="{{ community.icon_image() }}" />
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<h1 class="mt-3">{{ community.title }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
{% if current_user.subscribed(community) %}
|
||||
<a class="w-100 btn btn-primary" href="/c/{{ community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
|
||||
{% else %}
|
||||
<a class="w-100 btn btn-primary" href="/c/{{ community.link() }}/subscribe">{{ _('Subscribe') }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<a class="w-100 btn btn-primary" href="#">{{ _('Create a post') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<form method="get">
|
||||
<input type="search" name="search" class="form-control mt-2" placeholder="Search this community" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>About community</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{ community.description|safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -2,5 +2,56 @@
|
|||
{% from 'bootstrap5/form.html' import render_form %}
|
||||
|
||||
{% block app_content %}
|
||||
<p>Community list goes here.</p>
|
||||
<div class="row g-2 justify-content-between">
|
||||
<div class="col-auto">
|
||||
<div class="btn-group">
|
||||
<a href="#" class="btn btn-outline-secondary">{{ _('All') }}</a>
|
||||
<a href="#" class="btn btn-outline-secondary">{{ _('Local') }}</a>
|
||||
<a href="#" class="btn btn-outline-secondary">{{ _('Subscribed') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
|
||||
<div class="btn-group">
|
||||
|
||||
<a href="#" class="btn btn-outline-secondary">{{ _('Add local') }}</a>
|
||||
<a href="{{ url_for('community.add_remote') }}" class="btn btn-outline-secondary">{{ _('Add remote') }}</a>
|
||||
</div>
|
||||
<input type="search" placeholder="Find a community" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
{% if len(communities) > 0 %}
|
||||
<div class="table-responsive-md">
|
||||
<table class="table table-striped table-hover w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" colspan="2">{{ _('Name') }}</th>
|
||||
<th scope="col">{{ _('Posts') }}</th>
|
||||
<th scope="col">{{ _('Comments') }}</th>
|
||||
<th scope="col">{{ _('Active') }}</th>
|
||||
<th scope="col">{{ _('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for community in communities %}
|
||||
<tr class="">
|
||||
<td><a href="/c/{{ community.link() }}"><img src="{{ community.icon_image() }}" class="community_icon rounded-circle" /></a></td>
|
||||
<th scope="row"><a href="/c/{{ community.link() }}">{{ community.display_name() }}</a></th>
|
||||
<td>{{ community.post_count }}</td>
|
||||
<td>{{ community.post_reply_count }}</td>
|
||||
<td>{{ moment(community.last_active).fromNow(refresh=True) }}</td>
|
||||
<td>{% if current_user.subscribed(community) %}
|
||||
<a class="btn btn-primary btn-sm" href="/c/{{ community.link() }}/unsubscribe">Unsubscribe</a>
|
||||
{% else %}
|
||||
<a class="btn btn-primary btn-sm" href="/c/{{ community.link() }}/subscribe">Subscribe</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>{{ _('There are no communities yet.') }}</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
25
app/utils.py
Normal file
25
app/utils.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
import requests
|
||||
import os
|
||||
|
||||
from flask import current_app
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Jinja: when a file was modified. Useful for cache-busting
|
||||
def getmtime(filename):
|
||||
return os.path.getmtime('static/' + filename)
|
||||
|
||||
|
||||
# do a GET request to a uri, return the result
|
||||
def get_request(uri, params=None, headers=None) -> requests.Response:
|
||||
try:
|
||||
response = requests.get(uri, params=params, headers=headers, timeout=1, allow_redirects=True)
|
||||
except requests.exceptions.SSLError as invalid_cert:
|
||||
# Not our problem if the other end doesn't have proper SSL
|
||||
current_app.logger.info(f"{uri} {invalid_cert}")
|
||||
raise requests.exceptions.SSLError from invalid_cert
|
||||
except ValueError as ex:
|
||||
# Convert to a more generic error we handle
|
||||
raise requests.exceptions.RequestException(f"InvalidCodepoint: {str(ex)}") from None
|
||||
|
||||
return response
|
|
@ -20,3 +20,4 @@ class Config(object):
|
|||
RECAPTCHA3_PUBLIC_KEY = os.environ.get("RECAPTCHA3_PUBLIC_KEY")
|
||||
RECAPTCHA3_PRIVATE_KEY = os.environ.get("RECAPTCHA3_PRIVATE_KEY")
|
||||
MODE = os.environ.get('MODE') or 'development'
|
||||
LANGUAGES = ['en']
|
||||
|
|
46
migrations/versions/feef49234599_community_image.py
Normal file
46
migrations/versions/feef49234599_community_image.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
"""community image
|
||||
|
||||
Revision ID: feef49234599
|
||||
Revises: 93e4cef1698c
|
||||
Create Date: 2023-08-27 19:04:44.591324
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'feef49234599'
|
||||
down_revision = '93e4cef1698c'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('community', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('image_id', sa.Integer(), nullable=True))
|
||||
batch_op.create_foreign_key(None, 'file', ['image_id'], ['id'])
|
||||
|
||||
with op.batch_alter_table('instance', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_instance_domain'), ['domain'], unique=False)
|
||||
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_user_verification_token'), ['verification_token'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_user_verification_token'))
|
||||
|
||||
with op.batch_alter_table('instance', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_instance_domain'))
|
||||
|
||||
with op.batch_alter_table('community', schema=None) as batch_op:
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.drop_column('image_id')
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
|
||||
from app import create_app, db, cli
|
||||
import os
|
||||
import os, click
|
||||
|
||||
from app.utils import getmtime
|
||||
|
||||
app = create_app()
|
||||
cli.register(app)
|
||||
|
@ -19,3 +21,8 @@ def app_context_processor(): # NB there needs to be an identical function in cb
|
|||
@app.shell_context_processor
|
||||
def make_shell_context():
|
||||
return {'db': db}
|
||||
|
||||
|
||||
with app.app_context():
|
||||
app.jinja_env.globals['getmtime'] = getmtime
|
||||
app.jinja_env.globals['len'] = len
|
Loading…
Reference in a new issue