commit 366a2302d092c12f388f0eb8efb4faaa3acd3303 Author: rubenwardy Date: Sun Mar 18 17:43:30 2018 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db3f938 --- /dev/null +++ b/.gitignore @@ -0,0 +1,172 @@ +config.cfg +*.sqlite +main.css + + +# Created by https://www.gitignore.io/api/linux,macos,python,windows + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +# End of https://www.gitignore.io/api/linux,macos,python,windows diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..fb0ef3a --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,7 @@ +from flask import * +from flask_user import * + +app = Flask(__name__) +app.config.from_pyfile('../config.cfg') + +import models, views diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..6129330 --- /dev/null +++ b/app/models.py @@ -0,0 +1,72 @@ +from flask import Flask, url_for +from flask.ext.sqlalchemy import SQLAlchemy +from app import app +from datetime import datetime +from sqlalchemy.orm import validates +from flask_user import login_required, UserManager, UserMixin, SQLAlchemyAdapter + +# Initialise database +db = SQLAlchemy(app) + +def title_to_url(title): + return title.lower().replace(" ", "_") + +def url_to_title(url): + return url.replace("_", " ") + +class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + + # User authentication information + username = db.Column(db.String(50), nullable=False, unique=True) + password = db.Column(db.String(255), nullable=False, server_default='') + reset_password_token = db.Column(db.String(100), nullable=False, server_default='') + + # User email information + email = db.Column(db.String(255), nullable=True, unique=True) + confirmed_at = db.Column(db.DateTime()) + + # User information + active = db.Column('is_active', db.Boolean, nullable=False, server_default='0') + display_name = db.Column(db.String(100), nullable=False, server_default='') + + # Content + mods = db.relationship('Mod', backref='author', lazy='dynamic') + + def __init__(self, username): + import datetime + + self.username = username + self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000) + + def isClaimed(self): + return self.password is not None and self.password != "" + +class Role(db.Model): + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(50), unique=True) + description = db.Column(db.String(255)) + +class UserRoles(db.Model): + id = db.Column(db.Integer(), primary_key=True) + user_id = db.Column(db.Integer(), db.ForeignKey('user.id', ondelete='CASCADE')) + role_id = db.Column(db.Integer(), db.ForeignKey('role.id', ondelete='CASCADE')) + +class Mod(db.Model): + id = db.Column(db.Integer, primary_key=True) + + # Basic details + author_id = db.Column(db.Integer, db.ForeignKey('user.id')) + name = db.Column(db.String(100), nullable=False) + title = db.Column(db.String(100), nullable=False) + desc = db.Column(db.Text, nullable=True) + + # Downloads + repo = db.Column(db.String(200), nullable=True) + website = db.Column(db.String(200), nullable=True) + issueTracker = db.Column(db.String(200), nullable=True) + forums = db.Column(db.String(200), nullable=False) + +# Setup Flask-User +db_adapter = SQLAlchemyAdapter(db, User) # Register the User model +user_manager = UserManager(db_adapter, app) # Initialize Flask-User diff --git a/app/static/screenshot.png b/app/static/screenshot.png new file mode 100644 index 0000000..3cbf879 Binary files /dev/null and b/app/static/screenshot.png differ diff --git a/app/static/style.css b/app/static/style.css new file mode 100644 index 0000000..c9c28d6 --- /dev/null +++ b/app/static/style.css @@ -0,0 +1,245 @@ +html, body { + font-family: "Arial", sans-serif; + background: #222; + color: #ddd; + padding: 0; + margin: 0; +} + +h1 { + text-align: center; +} + + +h2, h3 { + margin: 5px 0; +} + +a { + color: #0be; + font-weight: bold; + text-decoration: none; +} + +a:hover { + color: #0df; + text-decoration: underline; +} + +/* Containers */ + +.box { + border-radius: 5px; + margin: 15px 0; +} + +.box_grey { + padding: 10px; + background: #333; + border: 1px solid #444; +} + +.ul_boxes { + display: block; + margin: 0; + padding: 0; + list-style: none; +} + +.ul_boxes > li { + padding: 0; + list-style: none; +} + +.box_link { + display: block; + color: #ddd; + text-decoration: none; +} + +.box_link:hover{ + background: #3a3a3a; +} + +/* + buttonset +*/ + +.buttonset, .buttonset li { + display: block; + margin: 0; + padding: 0; + list-style: none; +} + +.buttonset { + margin: 15px 0; +} + +.buttonset li a { + text-align: center; + color: #ddd; + text-decoration: none; + margin: 5px 0 !important; +} + +.buttonset li a:hover { + background: #444; +} + +.btn_green { + background: #363 !important; + border: 1px solid #473; +} + +.btn_green:hover { + background: #474 !important; +} + +/* Alerts */ + +#alerts { + list-style: none; + position: fixed; + bottom: 15px; + left: 0; + right: 0; +} + +#alerts .alert { + margin: 5px 0; + vertical-align: middle; +} + +#alerts .close { + float: right; + color: white; +} + +#alerts .close:hover { + color: #fff; +} + +.alert-error { + background: #933; + border: 1px solid #c44; +} + +.alert-warning { + background: #963; + border: 1px solid #c96; +} + +/* Nav */ + +nav, main, #alerts { + width: 90%; + max-width: 960px; + margin: auto; + padding: 0; +} + +nav { + margin: 15px auto 5px auto; + list-style: none; + background: #333; + border-radius: 5px; + border: 1px solid #444; +} + +nav .navbar-nav { + float: left; +} + +nav .navbar-right { + float: right; +} + +nav ul { + margin: 0 auto 0 auto; + padding: 0; + list-style: none; +} + +nav li { + margin: 0; + padding: 0; + list-style: none; + display: inline-block; +} + +nav li a { + color: #ddd; + margin: 0; + padding: 10px 20px; + display: block; + border-left: 1px solid #444; +} + +nav a:hover { + color: #eee; + background: #444; + text-decoration: none; +} + + +/* Footer */ + +footer { + width: 80%; + max-width: 860px; + margin: auto; + padding: 50px 0 20px 0; +} + +footer a { + color: #666; +} + + +/* Mod */ + +.box_img { + position: relative; + background-position: center; + background-size: cover; + background-image: url("screenshot.png"); + min-height: 220px; + border-radius: 5px; + padding: 0; +} + +.box_img > h2 { + display: inline-block; + position: absolute; + bottom: 15px; + left: 15px; +} + +.sidebar_container { + display: block; + position: relative; + padding: 0; + margin: 0; +} + +.sidebar_container .right, .sidebar_container .left{ + position: absolute; + display: block; + top: 10px; + margin-top: 0; +} + +.sidebar_container .right { + right: 0; + width: 280px; +} + +.sidebar_container .left { + right: 295px; + left: 0; +} + +.sidebar_container .right > *:first-child, .sidebar_container .left > *:first-child { + margin-top: 0; +} diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..6e8c372 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,74 @@ + + + + + + + + {% block title %}title{% endblock %} - {{ config.USER_APP_NAME }} + + + + + + + + {% block flash_messages %} + {%- with messages = get_flashed_messages(with_categories=true) -%} + {% if messages %} + + {% endif %} + {%- endwith %} + {% endblock %} + + {% block container %} +
+ {% block content %} + {% endblock %} +
+ {% endblock %} + diff --git a/app/templates/flask_user/login.html b/app/templates/flask_user/login.html new file mode 100644 index 0000000..44274bb --- /dev/null +++ b/app/templates/flask_user/login.html @@ -0,0 +1,76 @@ +{% extends "base.html" %} + +{% block title %} +Sign in +{% endblock %} + +{% block content %} + +{% endblock %} diff --git a/app/templates/flask_user/public_base.html b/app/templates/flask_user/public_base.html new file mode 100644 index 0000000..1dddd67 --- /dev/null +++ b/app/templates/flask_user/public_base.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block container %} +
+
+ {% block content %} + {% endblock %} +
+
+{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..541b230 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block title %} +Dashboard +{% endblock %} + +{% block content %} +
+

{{ self.title() }}

+ + {% if current_user.is_authenticated %} +

+ Hello user! +

+ {% else %} +

+ Please login! +

+ {% endif %} +
+ +
+
+

Top Mods

+
+
+

Statistics

+
    +
  • Total mods: 543
  • +
  • Missing mods: 1020
  • +
  • Downloads/day: 200
  • +
+
+
+{% endblock %} diff --git a/app/views/__init__.py b/app/views/__init__.py new file mode 100644 index 0000000..deb5ac5 --- /dev/null +++ b/app/views/__init__.py @@ -0,0 +1,74 @@ +from app import app +from flask import * +from flask_user import * +from flask_login import login_user, logout_user +from app.models import * +from flask.ext import menu, markdown +from sqlalchemy import func +from werkzeug.contrib.cache import SimpleCache +cache = SimpleCache() + +menu.Menu(app=app) +markdown.Markdown(app, extensions=['fenced_code']) + +# TODO: remove on production! +@app.route('/static/') +def send_static(path): + return send_from_directory('static', path) + +@app.route('/') +@menu.register_menu(app, '.', 'Home') +def home_page(): + return render_template('index.html') + +# Define the User registration form +# It augments the Flask-User RegisterForm with additional fields +from flask_user.forms import RegisterForm +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, validators +class MyRegisterForm(RegisterForm): + first_name = StringField('First name', validators=[ + validators.DataRequired('First name is required')]) + last_name = StringField('Last name', validators=[ + validators.DataRequired('Last name is required')]) + +# Define the User profile form +class UserProfileForm(FlaskForm): + first_name = StringField('First name', validators=[ + validators.DataRequired('First name is required')]) + last_name = StringField('Last name', validators=[ + validators.DataRequired('Last name is required')]) + submit = SubmitField('Save') + +@app.route('/user/', methods=['GET', 'POST']) +@app.route('/user//', methods=['GET']) +def user_profile_page(username=None): + user = None + form = None + if username is None: + if not current_user.is_authenticated: + return current_app.login_manager.unauthorized() + user = current_user + else: + user = User.query.filter_by(username=username).first() + if not user: + abort(404) + + if user == current_user: + # Initialize form + form = UserProfileForm(request.form, current_user) + + # Process valid POST + if request.method=='POST' and form.validate(): + # Copy form fields to user_profile fields + form.populate_obj(current_user) + + # Save user_profile + db.session.commit() + + # Redirect to home page + return redirect(url_for('home_page')) + + # Process GET or invalid POST + return render_template('users/user_profile_page.html', + user=user, form=form) diff --git a/config.example.cfg b/config.example.cfg new file mode 100644 index 0000000..7fffe2c --- /dev/null +++ b/config.example.cfg @@ -0,0 +1,6 @@ +USER_APP_NAME="Content DB" + +SECRET_KEY="" +WTF_CSRF_SECRET_KEY="" + +SQLALCHEMY_DATABASE_URI = "sqlite:///../db.sqlite" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4e52acf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +Flask>=0.10.1 +Flask-SQLAlchemy>=2.1 +Flask-User>=0.6.9 +Flask-Menu>=0.5.0 +Flask-Markdown>=0.3 diff --git a/rundebug.py b/rundebug.py new file mode 100644 index 0000000..e4592d2 --- /dev/null +++ b/rundebug.py @@ -0,0 +1,3 @@ +from app import app + +app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0ba2b5a --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +import os, datetime + +delete_db = False + +if delete_db and os.path.isfile("app/data.sqlite"): + os.remove("app/data.sqlite") + +if not os.path.isfile("app/data.sqlite"): + from app import models + + print("Creating database tables...") + models.db.create_all() + + print("Filling database...") + models.db.session.commit() +else: + print("Database already exists")