aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorrubenwardy <rw@rubenwardy.com>2018-03-18 17:43:30 +0000
committerrubenwardy <rw@rubenwardy.com>2018-03-18 17:43:30 +0000
commit366a2302d092c12f388f0eb8efb4faaa3acd3303 (patch)
tree070f3ef21b04bd716b9136a827d9fe7b90c32e2c
downloadcheatdb-366a2302d092c12f388f0eb8efb4faaa3acd3303.tar.xz
Initial commit
-rw-r--r--.gitignore172
-rw-r--r--app/__init__.py7
-rw-r--r--app/models.py72
-rw-r--r--app/static/screenshot.pngbin0 -> 228922 bytes
-rw-r--r--app/static/style.css245
-rw-r--r--app/templates/base.html74
-rw-r--r--app/templates/flask_user/login.html76
-rw-r--r--app/templates/flask_user/public_base.html10
-rw-r--r--app/templates/index.html35
-rw-r--r--app/views/__init__.py74
-rw-r--r--config.example.cfg6
-rw-r--r--requirements.txt5
-rw-r--r--rundebug.py3
-rw-r--r--setup.py17
14 files changed, 796 insertions, 0 deletions
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
--- /dev/null
+++ b/app/static/screenshot.png
Binary files 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 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
+ <link rel="stylesheet" type="text/css" href="/static/style.css">
+</head>
+
+<body>
+ <nav>
+ <ul class="nav navbar-nav">
+ <li><a href="/">{{ config.USER_APP_NAME }}</a></li>
+ {% for item in current_menu.children recursive %}
+ <li{% if item.children %} class="dropdown"{% endif %}>
+ <a href="{{ item.url }}"
+ {% if item.children %}
+ class="dropdown-toggle"
+ data-toggle="dropdown"
+ role="button"
+ aria-expanded="false"
+ {% endif %}>
+ {{ item.text }}
+ {% if item.children %}
+ <span class="caret"></span>
+ {% endif %}
+ </a>
+ {% if item.children %}
+ <ul class="dropdown-menu" role="menu">
+ {{ loop(item.children) }}
+ </ul>
+ {% endif %}
+ </li>
+ {% endfor %}
+ </ul>
+ <ul class="nav navbar-nav navbar-right">
+ {% if current_user.is_authenticated %}
+ <li><a href="{{ url_for('user_profile_page') }}">{{ current_user.first_name or current_user.username }}</a></li>
+ <li><a href="{{ url_for('user.logout') }}">Sign out</a></li>
+ {% else %}
+ <li><a href="{{ url_for('user.login') }}">Sign in</a></li>
+ {% endif %}
+ </ul>
+ <div style="clear:both;"></div>
+ </nav>
+
+
+ {% block flash_messages %}
+ {%- with messages = get_flashed_messages(with_categories=true) -%}
+ {% if messages %}
+ <ul id="alerts">
+ {% for category, message in messages %}
+ <li class="box box_grey alert alert-{{category}}">
+ <span class="icon_message"></span>
+
+ {{ message|safe }}
+
+ <div style="clear: both;"></div>
+ </li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {%- endwith %}
+ {% endblock %}
+
+ {% block container %}
+ <main>
+ {% block content %}
+ {% endblock %}
+ </main>
+ {% endblock %}
+</html>
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 %}
+<div class="sidebar_container">
+ <div class="left box box_grey">
+ {% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %}
+ <h2>{%trans%}Sign in{%endtrans%}</h2>
+
+ <form action="" method="POST" class="form" role="form">
+ {{ form.hidden_tag() }}
+
+ {# Username or Email field #}
+ {% set field = form.username if user_manager.enable_username else form.email %}
+ <div class="form-group {% if field.errors %}has-error{% endif %}">
+ {# Label on left, "New here? Register." on right #}
+ <div class="row">
+ <div class="col-xs-6">
+ <label for="{{ field.id }}" class="control-label">{{ field.label.text }}</label>
+ </div>
+ </div>
+ {{ field(class_='form-control', tabindex=110) }}
+ {% if field.errors %}
+ {% for e in field.errors %}
+ <p class="help-block">{{ e }}</p>
+ {% endfor %}
+ {% endif %}
+ </div>
+
+ {# Password field #}
+ {% set field = form.password %}
+ <div class="form-group {% if field.errors %}has-error{% endif %}">
+ {# Label on left, "Forgot your Password?" on right #}
+ <div class="row">
+ <div class="col-xs-6">
+ <label for="{{ field.id }}" class="control-label">{{ field.label.text }}</label>
+ </div>
+ <div class="col-xs-6 text-right">
+ {% if user_manager.enable_forgot_password %}
+ <a href="{{ url_for('user.forgot_password') }}" tabindex='195'>
+ {%trans%}Forgot your Password?{%endtrans%}</a>
+ {% endif %}
+ </div>
+ </div>
+ {{ field(class_='form-control', tabindex=120) }}
+ {% if field.errors %}
+ {% for e in field.errors %}
+ <p class="help-block">{{ e }}</p>
+ {% endfor %}
+ {% endif %}
+ </div>
+
+ {# Remember me #}
+ {% if user_manager.enable_remember_me %}
+ {{ render_checkbox_field(login_form.remember_me, tabindex=130) }}
+ {% endif %}
+
+ {# Submit button #}
+ {{ render_submit_field(form.submit, tabindex=180) }}
+ </form>
+ </div>
+
+ <div class="right">
+ <aside class="box box_grey">
+ <h2>New here?</h2>
+
+ {% if user_manager.enable_register and not user_manager.require_invitation %}
+ <a href="">{%trans%}Create an account{%endtrans%}</a>
+ {% endif %}
+ </aside>
+ </div>
+</div>
+{% 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 %}
+ <main>
+ <div class="box box_grey">
+ {% block content %}
+ {% endblock %}
+ </div>
+ </main>
+{% 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 %}
+ <div class="box box_grey">
+ <h2>{{ self.title() }}</h2>
+
+ {% if current_user.is_authenticated %}
+ <p>
+ Hello user!
+ </p>
+ {% else %}
+ <p>
+ Please login!
+ </p>
+ {% endif %}
+ </div>
+
+ <div class="2box">
+ <div class="box box_grey">
+ <h2>Top Mods</h2>
+ </div>
+ <div class="box box_grey">
+ <h2>Statistics</h2>
+ <ul>
+ <li>Total mods: 543</li>
+ <li>Missing mods: 1020</li>
+ <li>Downloads/day: 200</li>
+ </ul>
+ </div>
+ </div>
+{% 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/<path:path>')
+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/<username>/', 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")