@ -1,36 +0,0 @@ | |||
from datetime import datetime | |||
from app import db, login | |||
from flask_login import UserMixin | |||
from werkzeug.security import generate_password_hash, check_password_hash | |||
class User(UserMixin, db.Model): | |||
id = db.Column(db.Integer, primary_key=True) | |||
username = db.Column(db.String(64), index=True, unique=True) | |||
email = db.Column(db.String(120), index=True, unique=True) | |||
password_hash = db.Column(db.String(128)) | |||
posts = db.relationship('Post', backref='author', lazy='dynamic') | |||
def __repr__(self): | |||
return '<User {}>'.format(self.username) | |||
def set_password(self, password): | |||
self.password_hash = generate_password_hash(password) | |||
def check_password(self, password): | |||
return check_password_hash(self.password_hash, password) | |||
@login.user_loader | |||
def load_user(id): | |||
return User.query.get(int(id)) | |||
class Post(db.Model): | |||
id = db.Column(db.Integer, primary_key=True) | |||
body = db.Column(db.String(140)) | |||
timestamp = db.Column(db.DateTime, index=True, default=datetime.now) | |||
user_id = db.Column(db.Integer, db.ForeignKey('user.id')) | |||
def __repr__(self): | |||
return '<Post {}>'.format(self.body) |
@ -0,0 +1,68 @@ | |||
# User management | |||
User information: | |||
Name Unique, no space, at least one letter | |||
Avatar Stored in a server folder. Size limit? | |||
Password Hashed, of course | |||
Email Mail address, used to send newsletters | |||
Points Participation measure (mainly number of posts) | |||
Innovation points A different kind of participation measure | |||
Settings... | |||
Relations: | |||
Notifications 1 to many | |||
Groups 1 to many | |||
Sent PMs 1 to many | |||
Received PMs 1 to many | |||
Trophies/Titles 1 to many | |||
All posts many to many (tutorials can have several authors) | |||
Privileges many to many | |||
Rest API for users: | |||
Requests where "Search" is set to "Yes" accept search patterns. The syntax | |||
needs to be chosen, it could be something like "/users[name~=/*storm*/i]". | |||
Method URL Search Description | |||
----------------------------------------------------------------------------- | |||
GET /users Yes Query users | |||
POST /users - Create new user | |||
----------------------------------------------------------------------------- | |||
GET /users/<id> - Get user information/settings | |||
PATCH /users/<id> - Update user information/settings | |||
DELETE /users/<id> - Delete user account | |||
----------------------------------------------------------------------------- | |||
GET /users/<id>/trophies - Get unlocked trophies | |||
----------------------------------------------------------------------------- | |||
GET /users/<id>/messages Yes Query private messages | |||
POST /users/<id>/messages - Send PM (<id> is sender) | |||
DELETE /users/<id>/messages Required Delete PMs matching pattern | |||
----------------------------------------------------------------------------- | |||
GET /users/<id>/groups - Get user groups | |||
PATCH /users/<id>/groups - Add or remove group memberships (*) | |||
----------------------------------------------------------------------------- | |||
GET /users/<id>/privs - Get user privileges | |||
PATCH /users/<id>/privs - Grant/revoke special privileges (*) | |||
----------------------------------------------------------------------------- | |||
Updating the participation scores is not a request, it's tied to posting | |||
contents, so it has nothing to do in the API. | |||
(*) Not sure if this is relevant, since these are administrator duties. | |||
Rest API for groups: | |||
Method URL Search Description | |||
----------------------------------------------------------------------------- | |||
GET /groups - Get list of groups | |||
POST /groups - Create group | |||
----------------------------------------------------------------------------- | |||
GET /groups/<id> - Get list of users in groups | |||
----------------------------------------------------------------------------- | |||
There are no methods to change the privileges associated to each groups | |||
because this task is clearly for administrators, not API users. |
@ -0,0 +1 @@ | |||
Generic single-database configuration. |
@ -0,0 +1,45 @@ | |||
# A generic, single database configuration. | |||
[alembic] | |||
# template used to generate migration files | |||
# file_template = %%(rev)s_%%(slug)s | |||
# set to 'true' to run the environment during | |||
# the 'revision' command, regardless of autogenerate | |||
# revision_environment = false | |||
# Logging configuration | |||
[loggers] | |||
keys = root,sqlalchemy,alembic | |||
[handlers] | |||
keys = console | |||
[formatters] | |||
keys = generic | |||
[logger_root] | |||
level = WARN | |||
handlers = console | |||
qualname = | |||
[logger_sqlalchemy] | |||
level = WARN | |||
handlers = | |||
qualname = sqlalchemy.engine | |||
[logger_alembic] | |||
level = INFO | |||
handlers = | |||
qualname = alembic | |||
[handler_console] | |||
class = StreamHandler | |||
args = (sys.stderr,) | |||
level = NOTSET | |||
formatter = generic | |||
[formatter_generic] | |||
format = %(levelname)-5.5s [%(name)s] %(message)s | |||
datefmt = %H:%M:%S |
@ -0,0 +1,90 @@ | |||
from __future__ import with_statement | |||
from alembic import context | |||
from sqlalchemy import engine_from_config, pool | |||
from logging.config import fileConfig | |||
import logging | |||
# this is the Alembic Config object, which provides | |||
# access to the values within the .ini file in use. | |||
config = context.config | |||
# Interpret the config file for Python logging. | |||
# This line sets up loggers basically. | |||
fileConfig(config.config_file_name) | |||
logger = logging.getLogger('alembic.env') | |||
# add your model's MetaData object here | |||
# for 'autogenerate' support | |||
# from myapp import mymodel | |||
# target_metadata = mymodel.Base.metadata | |||
from flask import current_app | |||
config.set_main_option('sqlalchemy.url', | |||
current_app.config.get('SQLALCHEMY_DATABASE_URI')) | |||
target_metadata = current_app.extensions['migrate'].db.metadata | |||
# other values from the config, defined by the needs of env.py, | |||
# can be acquired: | |||
# my_important_option = config.get_main_option("my_important_option") | |||
# ... etc. | |||
def run_migrations_offline(): | |||
"""Run migrations in 'offline' mode. | |||
This configures the context with just a URL | |||
and not an Engine, though an Engine is acceptable | |||
here as well. By skipping the Engine creation | |||
we don't even need a DBAPI to be available. | |||
Calls to context.execute() here emit the given string to the | |||
script output. | |||
""" | |||
url = config.get_main_option("sqlalchemy.url") | |||
context.configure(url=url) | |||
with context.begin_transaction(): | |||
context.run_migrations() | |||
def run_migrations_online(): | |||
"""Run migrations in 'online' mode. | |||
In this scenario we need to create an Engine | |||
and associate a connection with the context. | |||
""" | |||
# this callback is used to prevent an auto-migration from being generated | |||
# when there are no changes to the schema | |||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html | |||
def process_revision_directives(context, revision, directives): | |||
if getattr(config.cmd_opts, 'autogenerate', False): | |||
script = directives[0] | |||
if script.upgrade_ops.is_empty(): | |||
directives[:] = [] | |||
logger.info('No changes in schema detected.') | |||
engine = engine_from_config(config.get_section(config.config_ini_section), | |||
prefix='sqlalchemy.', | |||
poolclass=pool.NullPool) | |||
connection = engine.connect() | |||
context.configure(connection=connection, | |||
target_metadata=target_metadata, | |||
process_revision_directives=process_revision_directives, | |||
**current_app.extensions['migrate'].configure_args) | |||
try: | |||
with context.begin_transaction(): | |||
context.run_migrations() | |||
except Exception as exception: | |||
logger.error(exception) | |||
raise exception | |||
finally: | |||
connection.close() | |||
if context.is_offline_mode(): | |||
run_migrations_offline() | |||
else: | |||
run_migrations_online() |
@ -0,0 +1,24 @@ | |||
"""${message} | |||
Revision ID: ${up_revision} | |||
Revises: ${down_revision | comma,n} | |||
Create Date: ${create_date} | |||
""" | |||
from alembic import op | |||
import sqlalchemy as sa | |||
${imports if imports else ""} | |||
# revision identifiers, used by Alembic. | |||
revision = ${repr(up_revision)} | |||
down_revision = ${repr(down_revision)} | |||
branch_labels = ${repr(branch_labels)} | |||
depends_on = ${repr(depends_on)} | |||
def upgrade(): | |||
${upgrades if upgrades else "pass"} | |||
def downgrade(): | |||
${downgrades if downgrades else "pass"} |
@ -0,0 +1,73 @@ | |||
"""empty message | |||
Revision ID: 27c0fff58193 | |||
Revises: | |||
Create Date: 2019-02-02 16:06:49.372395 | |||
""" | |||
from alembic import op | |||
import sqlalchemy as sa | |||
# revision identifiers, used by Alembic. | |||
revision = '27c0fff58193' | |||
down_revision = None | |||
branch_labels = None | |||
depends_on = None | |||
def upgrade(): | |||
# ### commands auto generated by Alembic - please adjust! ### | |||
op.create_table('user', | |||
sa.Column('id', sa.Integer(), nullable=False), | |||
sa.Column('type', sa.String(length=20), nullable=True), | |||
sa.PrimaryKeyConstraint('id') | |||
) | |||
op.create_table('content', | |||
sa.Column('id', sa.Integer(), nullable=False), | |||
sa.Column('type', sa.String(length=20), nullable=True), | |||
sa.Column('data', sa.Text(convert_unicode=True), nullable=True), | |||
sa.Column('date_created', sa.DateTime(), nullable=True), | |||
sa.Column('date_modified', sa.DateTime(), nullable=True), | |||
sa.Column('author_id', sa.Integer(), nullable=True), | |||
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ), | |||
sa.PrimaryKeyConstraint('id') | |||
) | |||
op.create_table('guest', | |||
sa.Column('id', sa.Integer(), nullable=False), | |||
sa.Column('username', sa.Unicode(length=64), nullable=True), | |||
sa.Column('last_post', sa.DateTime(), nullable=True), | |||
sa.ForeignKeyConstraint(['id'], ['user.id'], ), | |||
sa.PrimaryKeyConstraint('id') | |||
) | |||
op.create_index(op.f('ix_guest_username'), 'guest', ['username'], unique=False) | |||
op.create_table('member', | |||
sa.Column('id', sa.Integer(), nullable=False), | |||
sa.Column('username', sa.Unicode(length=64), nullable=True), | |||
sa.Column('email', sa.String(length=120), nullable=True), | |||
sa.Column('password_hash', sa.String(length=255), nullable=True), | |||
sa.Column('xp_points', sa.Integer(), nullable=True), | |||
sa.Column('innovation_points', sa.Integer(), nullable=True), | |||
sa.Column('biography', sa.Text(convert_unicode=True), nullable=True), | |||
sa.Column('signature', sa.Text(convert_unicode=True), nullable=True), | |||
sa.Column('birthday', sa.Date(), nullable=True), | |||
sa.Column('register_date', sa.Date(), nullable=True), | |||
sa.Column('receive_newsletter', sa.Boolean(), nullable=True), | |||
sa.ForeignKeyConstraint(['id'], ['user.id'], ), | |||
sa.PrimaryKeyConstraint('id') | |||
) | |||
op.create_index(op.f('ix_member_email'), 'member', ['email'], unique=True) | |||
op.create_index(op.f('ix_member_username'), 'member', ['username'], unique=True) | |||
# ### end Alembic commands ### | |||
def downgrade(): | |||
# ### commands auto generated by Alembic - please adjust! ### | |||
op.drop_index(op.f('ix_member_username'), table_name='member') | |||
op.drop_index(op.f('ix_member_email'), table_name='member') | |||
op.drop_table('member') | |||
op.drop_index(op.f('ix_guest_username'), table_name='guest') | |||
op.drop_table('guest') | |||
op.drop_table('content') | |||
op.drop_table('user') | |||
# ### end Alembic commands ### |
@ -0,0 +1,28 @@ | |||
"""Fix a typo in a field name | |||
Revision ID: 7dfd5e3aa1fb | |||
Revises: 27c0fff58193 | |||
Create Date: 2019-02-02 16:13:13.229250 | |||
""" | |||
from alembic import op | |||
import sqlalchemy as sa | |||
# revision identifiers, used by Alembic. | |||
revision = '7dfd5e3aa1fb' | |||
down_revision = '27c0fff58193' | |||
branch_labels = None | |||
depends_on = None | |||
def upgrade(): | |||
# ### commands auto generated by Alembic - please adjust! ### | |||
op.add_column('guest', sa.Column('ip', sa.String(length=47), nullable=True)) | |||
# ### end Alembic commands ### | |||
def downgrade(): | |||
# ### commands auto generated by Alembic - please adjust! ### | |||
op.drop_column('guest', 'ip') | |||
# ### end Alembic commands ### |