Initial code commit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
*.pyc
|
||||||
|
venv
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
blender-bfct/config.py
|
||||||
|
blender-bfct/runserver.wsgi
|
16
blender-bfct/application/__init__.py
Normal file
16
blender-bfct/application/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from flask import Flask
|
||||||
|
from flask.ext.sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
# Create app
|
||||||
|
app = Flask(__name__)
|
||||||
|
import config
|
||||||
|
app.config.from_object(config.Development)
|
||||||
|
|
||||||
|
# Create database connection object
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
from controllers import main
|
||||||
|
from controllers import admin
|
||||||
|
from controllers.applications import applications
|
||||||
|
|
||||||
|
app.register_blueprint(applications, url_prefix='/applications')
|
0
blender-bfct/application/controllers/__init__.py
Normal file
0
blender-bfct/application/controllers/__init__.py
Normal file
64
blender-bfct/application/controllers/admin.py
Normal file
64
blender-bfct/application/controllers/admin.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from application import app, db
|
||||||
|
from application.models.users import user_datastore
|
||||||
|
from application.models.applications import Application
|
||||||
|
|
||||||
|
from flask import render_template, redirect, url_for
|
||||||
|
from flask.ext.sqlalchemy import SQLAlchemy
|
||||||
|
from flask.ext import admin, login
|
||||||
|
from flask.ext.admin import Admin, expose, form
|
||||||
|
from flask.ext.admin.contrib import sqla
|
||||||
|
from flask.ext.admin.contrib.sqla import ModelView
|
||||||
|
from flask.ext.admin.base import BaseView
|
||||||
|
from flask.ext.security import login_required, current_user, roles_accepted
|
||||||
|
|
||||||
|
from werkzeug import secure_filename
|
||||||
|
from jinja2 import Markup
|
||||||
|
import os, hashlib, time
|
||||||
|
import os.path as op
|
||||||
|
|
||||||
|
|
||||||
|
# Create customized views with access restriction
|
||||||
|
class CustomModelView(ModelView):
|
||||||
|
def is_accessible(self):
|
||||||
|
default_role = user_datastore.find_role("admin")
|
||||||
|
#return login.current_user.is_authenticated()
|
||||||
|
return login.current_user.has_role(default_role)
|
||||||
|
|
||||||
|
class CustomBaseView(BaseView):
|
||||||
|
def is_accessible(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# Create customized index view class that handles login & registration
|
||||||
|
class MyAdminIndexView(admin.AdminIndexView):
|
||||||
|
|
||||||
|
@expose('/')
|
||||||
|
def index(self):
|
||||||
|
default_role = user_datastore.find_role("admin")
|
||||||
|
if login.current_user.is_authenticated() and login.current_user.has_role(default_role):
|
||||||
|
return super(MyAdminIndexView, self).index()
|
||||||
|
else:
|
||||||
|
return redirect(url_for('homepage'))
|
||||||
|
|
||||||
|
@expose('/logout/')
|
||||||
|
def logout_view(self):
|
||||||
|
login.logout_user()
|
||||||
|
return redirect(url_for('homepage'))
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationView(CustomModelView):
|
||||||
|
column_list = ('user.first_name', 'user.last_name', 'city_country', 'submission_date', 'status')
|
||||||
|
column_labels = {'user.first_name' : 'First Name', 'user.last_name' : 'Last Name'}
|
||||||
|
column_searchable_list = ('website', 'status')
|
||||||
|
can_create = False
|
||||||
|
|
||||||
|
|
||||||
|
# Create admin
|
||||||
|
backend = admin.Admin(
|
||||||
|
app,
|
||||||
|
'BFCT management',
|
||||||
|
index_view=MyAdminIndexView(),
|
||||||
|
base_template='admin/layout_admin.html'
|
||||||
|
)
|
||||||
|
|
||||||
|
backend.add_view(ApplicationView(Application, db.session, name='Applications', url='applications'))
|
94
blender-bfct/application/controllers/applications.py
Normal file
94
blender-bfct/application/controllers/applications.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import datetime
|
||||||
|
from application import db
|
||||||
|
from application.models.users import *
|
||||||
|
from application.models.applications import Application, Skill, ReviewersApplications
|
||||||
|
|
||||||
|
from flask import render_template, redirect, url_for, request, Blueprint
|
||||||
|
from flask.ext.security import login_required, roles_accepted
|
||||||
|
from flask.ext.security.core import current_user
|
||||||
|
from flask_wtf import Form
|
||||||
|
from wtforms import TextField, TextAreaField, BooleanField, SelectMultipleField
|
||||||
|
from wtforms.validators import DataRequired
|
||||||
|
from wtforms.fields.html5 import URLField
|
||||||
|
from wtforms.validators import url
|
||||||
|
|
||||||
|
applications = Blueprint('applications', __name__)
|
||||||
|
|
||||||
|
@applications.route('/')
|
||||||
|
@roles_accepted('bfct_manager', 'admin')
|
||||||
|
def index():
|
||||||
|
return render_template('applications/index.html',
|
||||||
|
title='applications',
|
||||||
|
applications=Application.query.all())
|
||||||
|
|
||||||
|
@applications.route('/view/<int:id>')
|
||||||
|
@roles_accepted('bfct_manager', 'admin')
|
||||||
|
def view(id):
|
||||||
|
review = ReviewersApplications.query.\
|
||||||
|
filter_by(application_id=id).\
|
||||||
|
filter_by(reviewer_blender_id=current_user.id).\
|
||||||
|
first()
|
||||||
|
|
||||||
|
reviews = ReviewersApplications.query.\
|
||||||
|
filter_by(application_id=id).\
|
||||||
|
all()
|
||||||
|
|
||||||
|
return render_template('applications/view.html',
|
||||||
|
title='applications',
|
||||||
|
application=Application.query.get_or_404(id),
|
||||||
|
review=review,
|
||||||
|
reviews=reviews)
|
||||||
|
|
||||||
|
@applications.route('/vote/<int:approved>/<int:id>')
|
||||||
|
@roles_accepted('bfct_manager', 'admin')
|
||||||
|
def vote(approved, id):
|
||||||
|
application = Application.query.get_or_404(id)
|
||||||
|
review = ReviewersApplications.query.\
|
||||||
|
filter_by(application_id=id).\
|
||||||
|
filter_by(reviewer_blender_id=current_user.id).\
|
||||||
|
first()
|
||||||
|
|
||||||
|
if approved:
|
||||||
|
if review:
|
||||||
|
application.reject -= 1
|
||||||
|
application.approve += 1
|
||||||
|
else:
|
||||||
|
if review:
|
||||||
|
application.approve -= 1
|
||||||
|
application.reject += 1
|
||||||
|
|
||||||
|
if application.status == 'submitted':
|
||||||
|
application.status = 'under_review'
|
||||||
|
application.review_start_date = datetime.datetime.now()
|
||||||
|
db.session.add(application)
|
||||||
|
|
||||||
|
if not review:
|
||||||
|
review = ReviewersApplications(
|
||||||
|
application_id=id,
|
||||||
|
reviewer_blender_id=current_user.id,
|
||||||
|
approved=approved)
|
||||||
|
else:
|
||||||
|
review.approved = approved
|
||||||
|
|
||||||
|
db.session.add(review)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for('.view', id=id))
|
||||||
|
|
||||||
|
@applications.route('/final-review/<int:approved>/<int:id>')
|
||||||
|
@roles_accepted('admin')
|
||||||
|
def final_review(approved, id):
|
||||||
|
application = Application.query.get_or_404(id)
|
||||||
|
|
||||||
|
if application.status != 'under_review':
|
||||||
|
return 'error'
|
||||||
|
else:
|
||||||
|
if approved:
|
||||||
|
application.status = 'approved'
|
||||||
|
else:
|
||||||
|
application.status = 'rejected'
|
||||||
|
application.review_end_date = datetime.datetime.now()
|
||||||
|
db.session.add(application)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for('.view', id=id))
|
79
blender-bfct/application/controllers/main.py
Normal file
79
blender-bfct/application/controllers/main.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
from application import app, db
|
||||||
|
from application.models.users import *
|
||||||
|
from application.models.applications import Application, Skill
|
||||||
|
|
||||||
|
from flask import render_template, redirect, url_for, request, flash
|
||||||
|
from flask.ext.security import login_required
|
||||||
|
from flask.ext.security.core import current_user
|
||||||
|
from sqlalchemy.orm.exc import MultipleResultsFound
|
||||||
|
from flask_wtf import Form
|
||||||
|
from wtforms import TextField, TextAreaField, BooleanField, SelectMultipleField
|
||||||
|
from wtforms.validators import DataRequired
|
||||||
|
from wtforms.fields.html5 import URLField
|
||||||
|
from wtforms.validators import url
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationForm(Form):
|
||||||
|
network_profile = TextField('Blender Network Profile', validators=[DataRequired()])
|
||||||
|
website = URLField(validators=[url()])
|
||||||
|
city_country = TextField('City and Country', validators=[DataRequired()])
|
||||||
|
teaching = BooleanField('Teaching')
|
||||||
|
skills = SelectMultipleField('Areas of expertise', coerce=int)
|
||||||
|
video_example = URLField(validators=[url()])
|
||||||
|
written_example = URLField(validators=[url()])
|
||||||
|
portfolio_cv = URLField(validators=[url()])
|
||||||
|
bio_message = TextAreaField('Your message for the board')
|
||||||
|
|
||||||
|
|
||||||
|
# Views
|
||||||
|
@app.route('/')
|
||||||
|
def homepage():
|
||||||
|
return render_template('index.html', title='home')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/apply', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def apply():
|
||||||
|
application = Application.query.filter_by(blender_id=current_user.id).first()
|
||||||
|
if application:
|
||||||
|
return redirect(url_for('my_application'))
|
||||||
|
else:
|
||||||
|
form = ApplicationForm()
|
||||||
|
form.skills.choices = [(s.id, s.name) for s in Skill.query.all()]
|
||||||
|
if form.validate_on_submit():
|
||||||
|
print 'validating'
|
||||||
|
application = Application(
|
||||||
|
blender_id=current_user.id,
|
||||||
|
website=form.website.data,
|
||||||
|
city_country=form.city_country.data,
|
||||||
|
teaching=form.teaching.data,
|
||||||
|
video_example=form.video_example.data,
|
||||||
|
written_example=form.written_example.data,
|
||||||
|
portfolio_cv=form.portfolio_cv.data,
|
||||||
|
bio_message=form.bio_message.data,
|
||||||
|
status='submitted')
|
||||||
|
for skill in form.skills.data:
|
||||||
|
s = Skill.query.get(skill)
|
||||||
|
application.skills.append(s)
|
||||||
|
db.session.add(application)
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for('my_application'))
|
||||||
|
# print form.errors
|
||||||
|
return render_template('apply.html',
|
||||||
|
title='apply',
|
||||||
|
form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/my-application')
|
||||||
|
@login_required
|
||||||
|
def my_application():
|
||||||
|
try:
|
||||||
|
application = Application.query.filter_by(blender_id=current_user.id).one()
|
||||||
|
except MultipleResultsFound:
|
||||||
|
flash('You have submitted more than one application. Get in touch with support@blendernetwork.org.')
|
||||||
|
return render_template('index.html')
|
||||||
|
if not application:
|
||||||
|
return redirect(url_for('apply'))
|
||||||
|
else:
|
||||||
|
return render_template('my_application.html',
|
||||||
|
application=application)
|
42
blender-bfct/application/helpers.py
Normal file
42
blender-bfct/application/helpers.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
def pretty_date(time=False):
|
||||||
|
"""
|
||||||
|
Get a datetime object or a int() Epoch timestamp and return a
|
||||||
|
pretty string like 'an hour ago', 'Yesterday', '3 months ago',
|
||||||
|
'just now', etc
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
now = datetime.now()
|
||||||
|
if type(time) is int:
|
||||||
|
diff = now - datetime.fromtimestamp(time)
|
||||||
|
elif isinstance(time,datetime):
|
||||||
|
diff = now - time
|
||||||
|
elif not time:
|
||||||
|
diff = now - now
|
||||||
|
second_diff = diff.seconds
|
||||||
|
day_diff = diff.days
|
||||||
|
|
||||||
|
if day_diff < 0:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
if day_diff == 0:
|
||||||
|
if second_diff < 10:
|
||||||
|
return "just now"
|
||||||
|
if second_diff < 60:
|
||||||
|
return str(second_diff) + " seconds ago"
|
||||||
|
if second_diff < 120:
|
||||||
|
return "a minute ago"
|
||||||
|
if second_diff < 3600:
|
||||||
|
return str( second_diff / 60 ) + " minutes ago"
|
||||||
|
if second_diff < 7200:
|
||||||
|
return "an hour ago"
|
||||||
|
if second_diff < 86400:
|
||||||
|
return str( second_diff / 3600 ) + " hours ago"
|
||||||
|
if day_diff == 1:
|
||||||
|
return "Yesterday"
|
||||||
|
if day_diff <= 7:
|
||||||
|
return str(day_diff) + " days ago"
|
||||||
|
if day_diff <= 31:
|
||||||
|
return str(day_diff/7) + " weeks ago"
|
||||||
|
if day_diff <= 365:
|
||||||
|
return str(day_diff/30) + " months ago"
|
||||||
|
return str(day_diff/365) + " years ago"
|
0
blender-bfct/application/models/__init__.py
Normal file
0
blender-bfct/application/models/__init__.py
Normal file
61
blender-bfct/application/models/applications.py
Normal file
61
blender-bfct/application/models/applications.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import datetime
|
||||||
|
from application import app
|
||||||
|
from application import db
|
||||||
|
from application.helpers import pretty_date
|
||||||
|
from users import User
|
||||||
|
|
||||||
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
|
|
||||||
|
|
||||||
|
class Application(db.Model):
|
||||||
|
__table_args__ = {'schema': 'blender-bfct'}
|
||||||
|
id = db.Column(db.Integer(), primary_key=True)
|
||||||
|
blender_id = db.Column(db.Integer(), db.ForeignKey(User.id), nullable=False)
|
||||||
|
user = db.relationship('User')
|
||||||
|
network_profile = db.Column(db.String(255))
|
||||||
|
website = db.Column(db.String(255))
|
||||||
|
city_country = db.Column(db.String(255))
|
||||||
|
teaching = db.Column(db.Boolean())
|
||||||
|
skills = db.relationship('Skill', secondary='skills_applications',
|
||||||
|
backref=db.backref('applications', lazy='dynamic'))
|
||||||
|
video_example = db.Column(db.String(255))
|
||||||
|
written_example = db.Column(db.String(255))
|
||||||
|
portfolio_cv = db.Column(db.String(255))
|
||||||
|
bio_message = db.Column(db.Text())
|
||||||
|
submission_date = db.Column(db.DateTime(), default=datetime.datetime.now)
|
||||||
|
status = db.Column(db.String(255))
|
||||||
|
approve = db.Column(db.Integer(), default=0)
|
||||||
|
reject = db.Column(db.Integer(), default=0)
|
||||||
|
review_start_date = db.Column(db.DateTime())
|
||||||
|
review_end_date = db.Column(db.DateTime())
|
||||||
|
renewal_date = db.Column(db.DateTime())
|
||||||
|
|
||||||
|
def show_pretty_date(self, stage_date):
|
||||||
|
if stage_date == 'submission':
|
||||||
|
return pretty_date(self.submission_date)
|
||||||
|
elif stage_date == 'review_start':
|
||||||
|
return pretty_date(self.review_start_date)
|
||||||
|
elif stage_date == 'review_end':
|
||||||
|
return pretty_date(self.review_end_date)
|
||||||
|
else:
|
||||||
|
return '--'
|
||||||
|
|
||||||
|
|
||||||
|
class Skill(db.Model):
|
||||||
|
id = db.Column(db.Integer(), primary_key=True)
|
||||||
|
name = db.Column(db.String(80), unique=True)
|
||||||
|
description = db.Column(db.String(255))
|
||||||
|
|
||||||
|
|
||||||
|
skills_applications = db.Table('skills_applications',
|
||||||
|
db.Column('application_id', db.Integer(), db.ForeignKey(Application.id)),
|
||||||
|
db.Column('skill_id', db.Integer(), db.ForeignKey('skill.id')))
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewersApplications(db.Model):
|
||||||
|
id = db.Column(db.Integer(), primary_key=True)
|
||||||
|
application_id = db.Column(db.Integer(), db.ForeignKey(Application.id), nullable=False)
|
||||||
|
application = db.relationship('Application')
|
||||||
|
reviewer_blender_id = db.Column(db.Integer(), db.ForeignKey(User.id), nullable=False)
|
||||||
|
reviewer = db.relationship('User')
|
||||||
|
approved = db.Column(db.Boolean(), nullable=False)
|
35
blender-bfct/application/models/users.py
Normal file
35
blender-bfct/application/models/users.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from flask.ext.security import Security, SQLAlchemyUserDatastore, \
|
||||||
|
UserMixin, RoleMixin
|
||||||
|
|
||||||
|
from application import app
|
||||||
|
from application import db
|
||||||
|
|
||||||
|
|
||||||
|
class User(db.Model, UserMixin):
|
||||||
|
__bind_key__ = 'users'
|
||||||
|
__table_args__ = {'schema': 'blender-id'}
|
||||||
|
id = db.Column(db.Integer(), primary_key=True)
|
||||||
|
first_name = db.Column(db.String(255))
|
||||||
|
last_name = db.Column(db.String(255))
|
||||||
|
email = db.Column(db.String(255), unique=True)
|
||||||
|
password = db.Column(db.String(255))
|
||||||
|
active = db.Column(db.Boolean())
|
||||||
|
confirmed_at = db.Column(db.DateTime())
|
||||||
|
roles = db.relationship('Role', secondary='roles_users',
|
||||||
|
backref=db.backref('users', lazy='dynamic'))
|
||||||
|
|
||||||
|
class Role(db.Model, RoleMixin):
|
||||||
|
__bind_key__ = 'users'
|
||||||
|
id = db.Column(db.Integer(), primary_key=True)
|
||||||
|
name = db.Column(db.String(80), unique=True)
|
||||||
|
description = db.Column(db.String(255))
|
||||||
|
|
||||||
|
# Define models
|
||||||
|
roles_users = db.Table('roles_users',
|
||||||
|
db.Column('user_id', db.Integer(), db.ForeignKey(User.id)),
|
||||||
|
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')),
|
||||||
|
info={'bind_key': 'users'})
|
||||||
|
|
||||||
|
# Setup Flask-Security
|
||||||
|
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
|
||||||
|
security = Security(app, user_datastore)
|
14
blender-bfct/application/templates/admin/layout_admin.html
Normal file
14
blender-bfct/application/templates/admin/layout_admin.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'admin/base.html' %}
|
||||||
|
|
||||||
|
{% block access_control %}
|
||||||
|
{% if current_user.is_authenticated() %}
|
||||||
|
<div class="btn-group pull-right">
|
||||||
|
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
|
||||||
|
<i class="icon-user"></i> {{ current_user.login }} <span class="caret"></span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="{{ url_for('admin.logout_view') }}">Log out</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
33
blender-bfct/application/templates/applications/index.html
Executable file
33
blender-bfct/application/templates/applications/index.html
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Applicant</th>
|
||||||
|
<th>Country</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Approvals</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for application in applications %}
|
||||||
|
<tr>
|
||||||
|
<td>{{application.user.first_name}} {{application.user.last_name}}</td>
|
||||||
|
<td>{{application.city_country}}</td>
|
||||||
|
<td>{{application.pretty_submission_date}}</td>
|
||||||
|
<td>{{application.status}}</td>
|
||||||
|
<td>{{application.approve}} / {{application.approve + application.reject}}</td>
|
||||||
|
<td><a href="{{url_for('applications.view', id=application.id)}}" class="btn btn-default btn-xs">View</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
77
blender-bfct/application/templates/applications/view.html
Executable file
77
blender-bfct/application/templates/applications/view.html
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h2>{{application.user.first_name}} {{application.user.last_name}} <small>{{application.city_country}}</small></h2>
|
||||||
|
<p>{{application.bio_message}}</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="{{application.website}}">{{application.website}}</a></li>
|
||||||
|
<li><a href="{{application.video_example}}">{{application.video_example}}</a></li>
|
||||||
|
<li><a href="{{application.written_example}}">{{application.written_example}}</a></li>
|
||||||
|
<li><a href="{{application.portfolio_cv}}">{{application.portfolio_cv}}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h2>Status: {{application.status}}</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Reviewer</th>
|
||||||
|
<th>Vote</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for review in reviews%}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% if review.reviewer.id == current_user.id %}
|
||||||
|
<strong>You</strong>
|
||||||
|
{% endif %}
|
||||||
|
{{review.reviewer.first_name}} {{review.reviewer.last_name}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if review.approved %}
|
||||||
|
√
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
{% if not review %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<a href="{{url_for('applications.vote', approved=1, id=application.id)}}" class="btn btn-default">Approve</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<a href="{{url_for('applications.vote', approved=0, id=application.id)}}" class="btn btn-default">Reject</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p>You {% if review.approved %} approved {% else %} rejected {% endif %} this candidate.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
{% if current_user.has_role('admin') %}
|
||||||
|
{% if not application.review_end_date %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<a href="{{url_for('applications.final_review', approved=1, id=application.id)}}" class="btn btn-default">Final Approve</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<a href="{{url_for('applications.final_review', approved=0, id=application.id)}}" class="btn btn-default">Final Reject</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
77
blender-bfct/application/templates/apply.html
Executable file
77
blender-bfct/application/templates/apply.html
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form class="form-horizontal" method="POST" action="{{url_for('apply')}}">
|
||||||
|
<fieldset>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<label class="control-label">
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<span class="input-group-addon">Blender Network Profile link</span>
|
||||||
|
{{ form.network_profile(class='form-control', placeholder='First Name') }}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="control-label">
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<span class="input-group-addon">Website</span>
|
||||||
|
{{ form.website(class='form-control', placeholder='Your website') }}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="control-label">
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<span class="input-group-addon">City and Country</span>
|
||||||
|
{{ form.city_country(class='form-control', placeholder='Your city and country') }}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="control-label">
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<span class="input-group-addon">Teaching at university</span>
|
||||||
|
{{ form.teaching(class='form-control') }}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="control-label">
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<span class="input-group-addon">Areas of expertise</span>
|
||||||
|
{{ form.skills(class='form-control') }}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="control-label">
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<span class="input-group-addon">Video Example</span>
|
||||||
|
{{ form.video_example(class='form-control', placeholder='Video Example') }}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="control-label">
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<span class="input-group-addon">Written Example</span>
|
||||||
|
{{ form.written_example(class='form-control', placeholder='Written Example') }}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="control-label">
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<span class="input-group-addon">Portfolio and CV</span>
|
||||||
|
{{ form.portfolio_cv(class='form-control', placeholder='Porftolio and CV') }}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="control-label">
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<span class="input-group-addon">Bio and message</span>
|
||||||
|
{{ form.bio_message(class='form-control', placeholder='Bio and message') }}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input class="btn btn-default btn-block" type="submit" value="Go">
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
18
blender-bfct/application/templates/index.html
Executable file
18
blender-bfct/application/templates/index.html
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h2>BFCT program</h2>
|
||||||
|
<p>BFCT is the Blender Foundation’s official certified trainers program.This membership is renewed annually, along with updated knowledge on new topics Blender might cover.</p>
|
||||||
|
|
||||||
|
<p>The goals of the BFCT program are:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Provide a standard for Certification for everyone who is interested in teaching Blender professionally</li>
|
||||||
|
<li>Help experienced Blender artists and developers to get into training business</li>
|
||||||
|
<li>Increase the quality and quantity of Blender training worldwide</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
104
blender-bfct/application/templates/layout.html
Normal file
104
blender-bfct/application/templates/layout.html
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Blender Cloud</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="author" content="">
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <link href='http://fonts.googleapis.com/css?family=Open+Sans:400,700,300' rel='stylesheet' type='text/css'> -->
|
||||||
|
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- <link rel="stylesheet" type="text/css" href="http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/css/jquery.dataTables.css"> -->
|
||||||
|
|
||||||
|
<!-- IE6-8 support of HTML5 elements -->
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
|
||||||
|
<!-- Le fav and touch icons -->
|
||||||
|
<link rel="shortcut icon" href="{{ url_for('static', filename='ico/favicon.ico') }}">
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/apple-touch-icon-144-precomposed.png">
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="/apple-touch-icon-114-precomposed.png">
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="/apple-touch-icon-72-precomposed.png">
|
||||||
|
<link rel="apple-touch-icon-precomposed" href="apple-touch-icon-57-precomposed.png">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{% block modal %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<header class="navbar navbar-default navbar-static-top" role="navigation">
|
||||||
|
<div class="container">
|
||||||
|
<!-- Brand and toggle get grouped for better mobile display -->
|
||||||
|
<div class="navbar-header">
|
||||||
|
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
|
||||||
|
<span class="sr-only">Toggle navigation</span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</button>
|
||||||
|
<a class="navbar-brand" href="{{ url_for('homepage') }}">blender-BFCT</a>
|
||||||
|
<ul class="nav navbar-nav">
|
||||||
|
<li><a href="http://blendernetwork.org/BFCT">Certified Trainers</a></li>
|
||||||
|
<li><a href="http://www.blender.org/certification/become-a-trainer/">Become a Trainer</a></li>
|
||||||
|
{% if current_user.has_role('admin') %}
|
||||||
|
<li><a href="{{url_for('applications.index')}}">Review Applications</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Collect the nav links, forms, and other content for toggling -->
|
||||||
|
<nav class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
|
||||||
|
<ul class="nav navbar-nav navbar-right">
|
||||||
|
{% if current_user.is_authenticated() %}
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">{{current_user.email}} <b class="caret"></b></a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li {% if title == 'home': %} class="active"{% endif %}>
|
||||||
|
<a href="{{ url_for('homepage') }}">Home</a>
|
||||||
|
</li>
|
||||||
|
{% if not current_user.has_role('bfct') %}
|
||||||
|
<li><a href="{{url_for('apply')}}">Apply/View application</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="divider"></li>
|
||||||
|
<li><a href="{{url_for_security('logout')}}">Log out</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="{{url_for_security('login')}}">Log in</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav><!-- /.navbar-collapse -->
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
{% for message in get_flashed_messages() %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<footer>
|
||||||
|
<p>Blender-BFCT is part of the blender.org project</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div> <!-- /container -->
|
||||||
|
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
|
||||||
|
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
|
||||||
|
{% block footer_scripts %}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
36
blender-bfct/application/templates/my_application.html
Executable file
36
blender-bfct/application/templates/my_application.html
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h2>Your BFCT status dashboard</h2>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Stage</th>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{{application.show_pretty_date('submission')}}</td>
|
||||||
|
<td>You submitted your application</td>
|
||||||
|
</tr>
|
||||||
|
{% if application.show_pretty_date('review_start') %}
|
||||||
|
<tr>
|
||||||
|
<td>{{application.show_pretty_date('review_start')}}</td>
|
||||||
|
<td>Application review started</td>
|
||||||
|
</tr>
|
||||||
|
{% endif%}
|
||||||
|
{% if application.show_pretty_date('review_end') %}
|
||||||
|
<tr>
|
||||||
|
<td>{{application.show_pretty_date('review_end')}}</td>
|
||||||
|
<td>Application review ended</td>
|
||||||
|
</tr>
|
||||||
|
{% endif%}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
46
blender-bfct/config.py.example
Normal file
46
blender-bfct/config.py.example
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
class Config(object):
|
||||||
|
DEBUG=False
|
||||||
|
|
||||||
|
# Configured for GMAIL
|
||||||
|
MAIL_SERVER = 'smtp.gmail.com'
|
||||||
|
MAIL_PORT = 465
|
||||||
|
MAIL_USE_SSL = True
|
||||||
|
MAIL_USERNAME = ''
|
||||||
|
MAIL_PASSWORD = ''
|
||||||
|
DEFAULT_MAIL_SENDER = ''
|
||||||
|
|
||||||
|
# Flask-Security setup
|
||||||
|
SECURITY_LOGIN_WITHOUT_CONFIRMATION = True
|
||||||
|
SECURITY_REGISTERABLE = True
|
||||||
|
SECURITY_RECOVERABLE = True
|
||||||
|
SECURITY_CHANGEABLE = True
|
||||||
|
SECUIRTY_POST_LOGIN = '/'
|
||||||
|
SECURITY_PASSWORD_HASH = 'bcrypt'
|
||||||
|
SECURITY_PASSWORD_SALT = 'YOURSALT'
|
||||||
|
SECURITY_EMAIL_SENDER = ''
|
||||||
|
SECURITY_POST_REGISTER_VIEW = '/welcome'
|
||||||
|
GOOGLE_ANALYTICS_TRACKING_ID = ''
|
||||||
|
GOOGLE_ANALYTICS_DOMAIN = ''
|
||||||
|
|
||||||
|
ADMIN_EMAIL = ['']
|
||||||
|
|
||||||
|
CDN_USE_URL_SIGNING = False
|
||||||
|
CDN_SERVICE_DOMAIN_PROTOCOL = 'http'
|
||||||
|
CDN_SERVICE_DOMAIN = ''
|
||||||
|
CDN_CONTENT_SUBFOLDER = ''
|
||||||
|
CDN_URL_SIGNING_KEY = ''
|
||||||
|
|
||||||
|
class Development(Config):
|
||||||
|
SECRET_KEY='YOURSECRET'
|
||||||
|
SERVER_NAME='cloud.blender.local'
|
||||||
|
DEBUG=True
|
||||||
|
SQLALCHEMY_DATABASE_URI='mysql://root:root@localhost/blender-bfct'
|
||||||
|
SQLALCHEMY_BINDS = {
|
||||||
|
'users': 'mysql://root:root@localhost/blender-id',
|
||||||
|
}
|
||||||
|
SECURITY_REGISTERABLE=True
|
||||||
|
SECURITY_LOGIN_USER_TEMPLATE = 'security/login_user.html'
|
||||||
|
ASSETS_DEBUG = False
|
||||||
|
CACHE_TYPE = ''
|
||||||
|
CACHE_DEFAULT_TIMEOUT = 60
|
||||||
|
CACHE_DIR = ''
|
5
blender-bfct/runserver.py
Normal file
5
blender-bfct/runserver.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from application import app
|
||||||
|
from application import db
|
||||||
|
db.create_all(bind=['users'])
|
||||||
|
db.create_all(bind=None)
|
||||||
|
app.run()
|
24
requirements.txt
Normal file
24
requirements.txt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Flask==0.10.1
|
||||||
|
Flask-Login==0.2.9
|
||||||
|
Flask-Mail==0.9.0
|
||||||
|
Flask-OAuth==0.12
|
||||||
|
Flask-Principal==0.4.0
|
||||||
|
Flask-SQLAlchemy==1.0
|
||||||
|
Flask-Security==1.7.1
|
||||||
|
Flask-Social==1.6.2
|
||||||
|
Flask-WTF==0.9.4
|
||||||
|
Jinja2==2.7.2
|
||||||
|
MarkupSafe==0.18
|
||||||
|
MySQL-python==1.2.5
|
||||||
|
SQLAlchemy==0.9.1
|
||||||
|
WTForms==1.0.5
|
||||||
|
Werkzeug==0.9.4
|
||||||
|
blinker==1.3
|
||||||
|
braintree==2.29.1
|
||||||
|
httplib2==0.8
|
||||||
|
itsdangerous==0.23
|
||||||
|
oauth2==1.5.211
|
||||||
|
passlib==1.6.2
|
||||||
|
py-bcrypt==0.4
|
||||||
|
requests==2.3.0
|
||||||
|
wsgiref==0.1.2
|
Reference in New Issue
Block a user