Initial commit

This commit is contained in:
rubenwardy 2018-03-18 17:43:30 +00:00
commit 366a2302d0
14 changed files with 796 additions and 0 deletions

172
.gitignore vendored Normal file
View File

@ -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

7
app/__init__.py Normal file
View File

@ -0,0 +1,7 @@
from flask import *
from flask_user import *
app = Flask(__name__)
app.config.from_pyfile('../config.cfg')
import models, views

72
app/models.py Normal file
View File

@ -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

BIN
app/static/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

245
app/static/style.css Normal file
View File

@ -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;
}

74
app/templates/base.html Normal file
View File

@ -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>

View File

@ -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 %}

View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block container %}
<main>
<div class="box box_grey">
{% block content %}
{% endblock %}
</div>
</main>
{% endblock %}

35
app/templates/index.html Normal file
View File

@ -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 %}

74
app/views/__init__.py Normal file
View File

@ -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)

6
config.example.cfg Normal file
View File

@ -0,0 +1,6 @@
USER_APP_NAME="Content DB"
SECRET_KEY=""
WTF_CSRF_SECRET_KEY=""
SQLALCHEMY_DATABASE_URI = "sqlite:///../db.sqlite"

5
requirements.txt Normal file
View File

@ -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

3
rundebug.py Normal file
View File

@ -0,0 +1,3 @@
from app import app
app.run(host='0.0.0.0', port=5000, debug=True)

17
setup.py Normal file
View File

@ -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")